diff --git a/ChangeLog.rst b/ChangeLog.rst index 781ddb8..9f6fe10 100644 --- a/ChangeLog.rst +++ b/ChangeLog.rst @@ -15,6 +15,13 @@ New Features :ref:`rule-constraints`. +Bugs Fixed +---------- + +- Fix adjacency constraint when in term mode and multiple labels in the constraint + matches the same label in the candidate graph. + + v0.15.0 (2024-01-26) ==================== diff --git a/libs/libmod/src/mod/lib/GraphMorphism/Constraints/VertexAdjacency.hpp b/libs/libmod/src/mod/lib/GraphMorphism/Constraints/VertexAdjacency.hpp index 1900d68..0e05563 100644 --- a/libs/libmod/src/mod/lib/GraphMorphism/Constraints/VertexAdjacency.hpp +++ b/libs/libmod/src/mod/lib/GraphMorphism/Constraints/VertexAdjacency.hpp @@ -9,6 +9,13 @@ #include #include +//#define DEBUG_ADJACENCY_CONSTRAINT +#ifdef DEBUG_ADJACENCY_CONSTRAINT + +#include + +#endif + namespace mod::lib::GraphMorphism::Constraints { template @@ -19,7 +26,7 @@ struct VertexAdjacency : Constraint { VertexAdjacency(Vertex vConstrained, Operator op, int count) : vConstrained(vConstrained), op(op), count(count), vertexLabels(1), edgeLabels(1) {} - virtual std::unique_ptr> clone() const override { + virtual std::unique_ptr> clone() const override { auto c = std::make_unique(vConstrained, op, count); c->vertexLabels = vertexLabels; c->edgeLabels = edgeLabels; @@ -32,7 +39,7 @@ struct VertexAdjacency : Constraint { private: template int matchesImpl(Visitor &vis, const Graph &gDom, const LabelledGraphCodom &lgCodom, VertexMap &m, - const LabelSettings ls, std::false_type) const { + const LabelSettings ls, std::false_type) const { assert(ls.type == LabelType::String); // otherwise someone forgot to add the TermData prop using GraphCodom = typename LabelledGraphTraits::GraphType; const GraphCodom &gCodom = get_graph(lgCodom); @@ -55,7 +62,7 @@ struct VertexAdjacency : Constraint { template int matchesImpl(Visitor &vis, const Graph &gDom, const LabelledGraphCodom &lgCodom, VertexMap &m, - const LabelSettings ls, std::true_type) const { + const LabelSettings ls, std::true_type) const { assert(ls.type == LabelType::Term); // otherwise someone did something very strange using GraphCodom = typename LabelledGraphTraits::GraphType; const GraphCodom &gCodom = get_graph(lgCodom); @@ -66,14 +73,17 @@ struct VertexAdjacency : Constraint { const auto countPerVertexTerms = [&](const auto h) { if(vertexTerms.empty()) { ++count; + return true; } else { for(const auto t: vertexTerms) { lib::Term::MGU mgu = machine.unifyHeapTemp(h, t); + machine.revert(mgu); if(mgu.status == lib::Term::MGU::Status::Exists) { ++count; + return true; } - machine.revert(mgu); } + return false; } }; const auto countPerEdgeTerms = [&](const auto hEdge, const auto hVertex) { @@ -83,9 +93,13 @@ struct VertexAdjacency : Constraint { for(const auto t: edgeTerms) { lib::Term::MGU mgu = machine.unifyHeapTemp(hEdge, t); if(mgu.status == lib::Term::MGU::Status::Exists) { - countPerVertexTerms(hVertex); + if(countPerVertexTerms(hVertex)) { + machine.revert(mgu); + return; + } + } else { + machine.revert(mgu); } - machine.revert(mgu); } } }; @@ -98,10 +112,11 @@ struct VertexAdjacency : Constraint { public: template bool matches(Visitor &vis, const Graph &gDom, const LabelledGraphCodom &lgCodom, VertexMap &m, - const LabelSettings ls) const { + const LabelSettings ls) const { using GraphCodom = typename LabelledGraphTraits::GraphType; - static_assert(std::is_same::GraphDom>::value, - ""); + static_assert( + std::is_same::GraphDom>::value, + ""); static_assert( std::is_same::GraphCodom>::value, ""); @@ -118,6 +133,30 @@ struct VertexAdjacency : Constraint { using HasTerm = GraphMorphism::HasTermData; const int count = matchesImpl(vis, gDom, lgCodom, m, ls, HasTerm()); +#ifdef DEBUG_ADJACENCY_CONSTRAINT + { + std::cout << "AdjacencyConstraint eval: {"; + for(const auto &s: vertexLabels) std::cout << " " << s; + std::cout << " } {"; + for(const auto &s: edgeLabels) std::cout << " " << s; + std::cout << " } = " << count << " "; + [this]() -> std::ostream & { + switch(op) { + case Operator::EQ: + return std::cout << "="; + case Operator::LT: + return std::cout << "<"; + case Operator::GT: + return std::cout << ">"; + case Operator::LEQ: + return std::cout << "<="; + case Operator::GEQ: + return std::cout << ">="; + } + return std::cout; + }() << " " << this->count << " hasTerm=" << std::boolalpha << HasTerm::value << std::endl; + } +#endif switch(op) { case Operator::EQ: return count == this->count; diff --git a/test/py/matchConstraints/adjacency/10_const.py b/test/py/matchConstraints/adjacency/10_const.py new file mode 100644 index 0000000..06c93f1 --- /dev/null +++ b/test/py/matchConstraints/adjacency/10_const.py @@ -0,0 +1,86 @@ +post.disableInvokeMake() + +lString = LabelSettings(LabelType.String, LabelRelation.Specialisation) +lTerm = LabelSettings(LabelType.Term, LabelRelation.Specialisation) + +Graph.fromDFS("[Q]({a}[A])({b}[B])({a}[C])({b}[C])({c}[C])({c}[C])") +ruleTemplate = """rule [ + ruleID "{}" + left [ node [ id 0 label "Q" ] ] + right [ node [ id 0 label "Q({})" ] ] + constrainAdj [ + id 0 op "{}" + count {} + {} + ] +]""" +ops = {'lt': '<', 'leq': '<=', 'eq': '=', 'geq': '>=', 'gt': '>'} +def evalOp(a, op, b): + if op == '<': return a < b + if op == '<=': return a <= b + if op == '=': return a == b + if op == '>=': return a >= b + if op == '>': return a > b + assert False +nodeLabels = { + '': '', + 'A': 'nodeLabels [ label "A" ]', + 'B': 'nodeLabels [ label "B" label "B" ]', # make sure duplicates don't do anything + 'C': 'nodeLabels [ label "C" ]', + 'AB': 'nodeLabels [ label "A" label "B" ]', + 'AC': 'nodeLabels [ label "A" label "C" ]', + 'BC': 'nodeLabels [ label "B" label "C" ]', + 'ABC': 'nodeLabels [ label "A" label "B" label "C" ]', +} +edgeLabels = { + '': '', + 'a': 'edgeLabels [ label "a" ]', + 'b': 'edgeLabels [ label "b" label "b" ]', + 'c': 'edgeLabels [ label "c" ]', + 'ab': 'edgeLabels [ label "a" label "b" ]', + 'ac': 'edgeLabels [ label "a" label "c" ]', + 'bc': 'edgeLabels [ label "b" label "c" ]', + 'abc': 'edgeLabels [ label "a" label "b" label "c" ]', +} +for count in range(10): + for opName, op in ops.items(): + for nlName, nl in nodeLabels.items(): + for elName, el in edgeLabels.items(): + name = ','.join([opName, str(count), f"V{nlName}", f"E{elName}"]) + labels = f"{nl}\n{el}\n" + Rule.fromGMLString(ruleTemplate.format(name, name, op, count, labels)) + +err = False + +def doDG(name, lSettings): + print(name + "\n" + "="*50) + dg = DG(graphDatabase=inputGraphs, labelSettings=lSettings) + dg.build().execute(addSubset(inputGraphs) >> inputRules) + found = set() + for vDG in dg.vertices: + g = vDG.graph + try: + v = next(a for a in g.vertices if a.stringLabel.startswith("Q(")) + except StopIteration: + continue + l = v.stringLabel + l = l[2:-1] + op, count, nl, el = l.split(',') + op, count, nl, el = ops[op], int(count), nl.strip()[1:], el.strip()[1:] + candCount = 0 + for e in v.incidentEdges: + if len(el) != 0 and e.stringLabel not in el: + continue + u = e.target + if len(nl) != 0 and e.target.stringLabel not in nl: + continue + candCount += 1 + if(not evalOp(candCount, op, count)): + print(f"Error: '{nl}' '{el}' = {candConut} {op} {count}") + err = True + +doDG("String", lString) +doDG("Term", lTerm) + +if err: + assert False diff --git a/test/py/matchConstraints/vertexAdjacency/coreInsteadOfComponent.py b/test/py/matchConstraints/adjacency/20_rc_coreInsteadOfComponent.py similarity index 77% rename from test/py/matchConstraints/vertexAdjacency/coreInsteadOfComponent.py rename to test/py/matchConstraints/adjacency/20_rc_coreInsteadOfComponent.py index caf5242..7c8177e 100644 --- a/test/py/matchConstraints/vertexAdjacency/coreInsteadOfComponent.py +++ b/test/py/matchConstraints/adjacency/20_rc_coreInsteadOfComponent.py @@ -1,6 +1,8 @@ +post.disableInvokeMake() + # test case for the bug where the constraint is checked in the core graph # instead of the component graph -rLeft = ruleGMLString("""rule [ +rLeft = Rule.fromGMLString("""rule [ left [ edge [ source 1 target 2 label "-" ] ] @@ -9,16 +11,16 @@ node [ id 2 label "O" ] ] ]""") -rRight = ruleGMLString("""rule [ +rRight = Rule.fromGMLString("""rule [ context [ node [ id 1 label "C" ] ] constrainAdj [ - id 1 count 0 op "=" + id 1 nodeLabels [ label "O" ] + count 0 op "=" ] ]""") rc = rcEvaluator(inputRules) exp = rc.eval(rLeft *rcSuper(enforceConstraints=True)* rRight) assert len(exp) > 0 -for a in exp: a.print() diff --git a/test/py/matchConstraints/adjacency/51_term_dup_match.py b/test/py/matchConstraints/adjacency/51_term_dup_match.py new file mode 100644 index 0000000..f6114f8 --- /dev/null +++ b/test/py/matchConstraints/adjacency/51_term_dup_match.py @@ -0,0 +1,75 @@ +post.disableInvokeMake() + +lTerm = LabelSettings(LabelType.Term, LabelRelation.Specialisation) + +# test that multiple different matches of constraints don't increase the count + +Graph.fromDFS("[Q]({a}[A])({b}[B])") +ruleTemplate = """rule [ + ruleID "{}" + left [ node [ id 0 label "Q" ] ] + right [ node [ id 0 label "Q({})" ] ] + constrainAdj [ + id 0 op "{}" + count {} + {} + ] +]""" +ops = {'lt': '<', 'leq': '<=', 'eq': '=', 'geq': '>=', 'gt': '>'} +def evalOp(a, op, b): + if op == '<': return a < b + if op == '<=': return a <= b + if op == '=': return a == b + if op == '>=': return a >= b + if op == '>': return a > b + assert False +nodeLabels = { + '': '', + 'A': 'nodeLabels [ label "_A" ]', + 'AA': 'nodeLabels [ label "_A" label "_A" ]', + 'AB': 'nodeLabels [ label "_A" label "_B" ]', +} +edgeLabels = { + '': '', + 'a': 'edgeLabels [ label "_a" ]', + 'aa': 'edgeLabels [ label "_a" label "_a" ]', + 'ab': 'edgeLabels [ label "_a" label "_b" ]', +} +valid = set() +for count in range(0, 4): + for opName, op in ops.items(): + for nlName, nl in nodeLabels.items(): + for elName, el in edgeLabels.items(): + if evalOp(2, op, count): + valid.add((nlName, elName, op, count)) + name = ','.join([opName, str(count), f"V{nlName}", f"E{elName}"]) + labels = f"{nl}\n{el}\n" + Rule.fromGMLString(ruleTemplate.format(name, name, op, count, labels)) + + +dg = DG(graphDatabase=inputGraphs, labelSettings=lTerm) +dg.build().execute(addSubset(inputGraphs) >> inputRules) +dg.print() +found = set() +for vDG in dg.vertices: + g = vDG.graph + try: + v = next(a for a in g.vertices if a.stringLabel.startswith("Q(")) + except StopIteration: + continue + l = v.stringLabel + l = l[2:-1] + op, count, nl, el = l.split(',') + op, count, nl, el = ops[op], int(count), nl.strip()[1:], el.strip()[1:] + found.add((nl, el, op, count)) +err = False +for e in sorted(found): + if e not in valid: + print("Invalid:", e) + err = True +for e in sorted(valid): + if e not in found: + print("Missing valid:", e) + err = True +if err: + assert False diff --git a/test/py/matchConstraints/adjacency/52_term_dependent.py b/test/py/matchConstraints/adjacency/52_term_dependent.py new file mode 100644 index 0000000..cabde0b --- /dev/null +++ b/test/py/matchConstraints/adjacency/52_term_dependent.py @@ -0,0 +1,18 @@ +post.disableInvokeMake() + +# check that the node and edge label unifications must happen at the same time +Graph.fromDFS("[_x]{_x}[C]") +Rule.fromGMLString("""rule [ + left [ node [ id 0 label "C" ] ] + right [ node [ id 0 label "U" ] ] + constrainAdj [ + id 0 + nodeLabels [ label "a" ] + edgeLabels [ label "b" ] + op "=" count 1 + ] +]""") +dg = DG(graphDatabase=inputGraphs, + labelSettings=LabelSettings(LabelType.Term, LabelRelation.Specialisation)) +dg.build().execute(addSubset(inputGraphs) >> inputRules) +assert dg.numEdges == 0, f"|E| = {dg.numEdges}" diff --git a/test/py/matchConstraints/vertexAdjacency/main.py b/test/py/matchConstraints/vertexAdjacency/main.py deleted file mode 100644 index 0b22126..0000000 --- a/test/py/matchConstraints/vertexAdjacency/main.py +++ /dev/null @@ -1,107 +0,0 @@ -post.disableInvokeMake() - -lString = LabelSettings(LabelType.String, LabelRelation.Unification) -lTerm = LabelSettings(LabelType.Term, LabelRelation.Unification) - -graphGMLString("""graph [ - node [ id 0 label "C" ] - edge [ source 0 target 1 label "a" ] - node [ id 1 label "A" ] - edge [ source 0 target 2 label "b" ] - node [ id 2 label "B" ] -]""") -ruleTemplate = """rule [ - ruleID "%s" - left [ - node [ id 0 label "C" ] - ] - right [ - node [ id 0 label "C(%s)" ] - ] - constrainAdj [ - id 0 op "%s" - count %d - %s - ] -]""" -ops = {'lt': '<', 'leq': '<=', 'eq': '=', 'geq': '>=', 'gt': '>'} -def evalOp(a, op, b): - if op == '<': return a < b - if op == '<=': return a <= b - if op == '=': return a == b - if op == '>=': return a >= b - if op == '>': return a > b - assert False -nodeLabels = { - '': '', - 'A': 'nodeLabels [ label "A" ]', - 'AB': 'nodeLabels [ label "A" label "B" ]', -} -edgeLabels = { - '': '', - 'a': 'edgeLabels [ label "a" ]', - 'ab': 'edgeLabels [ label "a" label "b" ]', -} -trueCounts = { - ('', ''): 2, ('', 'a'): 1, ('', 'ab'): 2, - ('A', ''): 1, ('A', 'a'): 1, ('A', 'ab'): 1, - ('AB', ''): 2, ('AB', 'a'): 1, ('AB', 'ab'): 2 -} -valid = set() -for count in range(0, 4): - for opName, op in ops.items(): - for nlName, nl in nodeLabels.items(): - for elName, el in edgeLabels.items(): - if evalOp(trueCounts[(nlName, elName)], op, count): - valid.add((nlName, elName, opName, count)) - name = ','.join(a for a in [opName, str(count), nlName, elName] if len(a) > 0) - labels = nl + "\n" + el + "\n" - ruleGMLString(ruleTemplate % (name, name, op, count, labels)) -def doDG(name, lSettings): - print(name + "\n" + "="*50) - dg = dgRuleComp(inputGraphs, addSubset(inputGraphs) >> inputRules, labelSettings=lSettings) - dg.calc() - found = set() - for vDG in dg.vertices: - g = vDG.graph - v = next(a for a in g.vertices if a.stringLabel.startswith("C")) - l = v.stringLabel - if l == "C": continue - l = l[2:-1] - if l.find(', ') != -1: - ls = l.split(', ') - else: - ls = l.split(',') - if len(ls[2:]) == 0: - nl = '' - el = '' - elif len(ls[2:]) == 1: - if ls[2] in nodeLabels: - nl = ls[2] - el = '' - else: - assert ls[2] in edgeLabels - nl = '' - el = ls[2] - else: - assert len(ls[2:]) == 2 - nl = ls[2] - el = ls[3] - t = (nl, el, ls[0], int(ls[1])) - found.add(t) - print(ls[2:], ops[ls[0]], ls[1]) - err = False - for f in found: - if f not in valid: - print("Too much:", f) - err = True - for v in valid: - if v not in found: - print("Too little:", v) - err = True - if err: - print(sorted(found)) - print(sorted(valid)) - assert False -doDG("String", lString) -doDG("Term", lTerm) diff --git a/test/py/matchConstraints/vertexAdjacency/termConversion.py b/test/py/matchConstraints/vertexAdjacency/termConversion.py deleted file mode 100644 index b723df2..0000000 --- a/test/py/matchConstraints/vertexAdjacency/termConversion.py +++ /dev/null @@ -1,47 +0,0 @@ -ls = LabelSettings(LabelType.Term, LabelRelation.Unification) -rId = ruleGMLString("""rule [ - context [ - node [ id 0 label "C" ] - ] - right [ - node [ id 1 label "R" ] - edge [ source 0 target 1 label "-" ] - ] -]""") -rIdInv = rId.makeInverse() -rStr = """rule [ - context [ - node [ id 0 label "C" ] - node [ id 1 label "Q" ] - edge [ source 0 target 1 label "-" ] - ] - constrainAdj [ - id 1 op "=" count 1 - nodeLabels [ label "a" label "_x" label "b(c)" label "d(_y)" ] - edgeLabels [ label "g" label "_p" label "h(i)" label "j(_q)" ] - ] -]""" -a = ruleGMLString(rStr) -post.summaryChapter("TermState") -a.printTermState() -#b = a.makeInverse() - -post.summaryChapter("Compose first") -rc = rcEvaluator([], ls) -res = rc.eval(a *rcSuper* rId) -for b in res: - b.print() - b.printTermState() - -post.summaryChapter("Compose second") -rc = rcEvaluator([], ls) -res = rc.eval(rIdInv *rcSub* a) -for b in res: - b.print() - b.printTermState() - -post.summaryChapter("DGRuleComp") -graphDFS("C[Q]") -dg = dgRuleComp([], addSubset(inputGraphs) >> a, ls) -dg.calc() -dg.print() diff --git a/test/py/matchConstraints/vertexAdjacency/vertexAndEdge.py b/test/py/matchConstraints/vertexAdjacency/vertexAndEdge.py deleted file mode 100644 index 010b4a9..0000000 --- a/test/py/matchConstraints/vertexAdjacency/vertexAndEdge.py +++ /dev/null @@ -1,18 +0,0 @@ -graphDFS("[_x]{_x}[C]") -ruleGMLString("""rule [ - left [ - node [ id 0 label "C" ] - ] - right [ - node [ id 0 label "U" ] - ] - constrainAdj [ - id 0 op "=" count 1 - nodeLabels [ label "a" ] - edgeLabels [ label "b" ] - ] -]""") -dg = dgRuleComp(inputGraphs, addSubset(inputGraphs) >> inputRules, - labelSettings=LabelSettings(LabelType.Term, LabelRelation.Specialisation)) -dg.calc() -dg.print()