Skip to content

Commit

Permalink
Fix adjacency constraint in term mode
Browse files Browse the repository at this point in the history
  • Loading branch information
jakobandersen committed Feb 25, 2024
1 parent ea7a05e commit 0bd0e8a
Show file tree
Hide file tree
Showing 9 changed files with 240 additions and 185 deletions.
7 changes: 7 additions & 0 deletions ChangeLog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
====================

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
#include <jla_boost/graph/morphism/Concepts.hpp>
#include <jla_boost/graph/morphism/models/PropertyVertexMap.hpp>

//#define DEBUG_ADJACENCY_CONSTRAINT
#ifdef DEBUG_ADJACENCY_CONSTRAINT

#include <iostream>

#endif

namespace mod::lib::GraphMorphism::Constraints {

template<typename Graph>
Expand All @@ -19,7 +26,7 @@ struct VertexAdjacency : Constraint<Graph> {
VertexAdjacency(Vertex vConstrained, Operator op, int count)
: vConstrained(vConstrained), op(op), count(count), vertexLabels(1), edgeLabels(1) {}

virtual std::unique_ptr<Constraint < Graph>> clone() const override {
virtual std::unique_ptr<Constraint<Graph>> clone() const override {
auto c = std::make_unique<VertexAdjacency>(vConstrained, op, count);
c->vertexLabels = vertexLabels;
c->edgeLabels = edgeLabels;
Expand All @@ -32,7 +39,7 @@ struct VertexAdjacency : Constraint<Graph> {
private:
template<typename Visitor, typename LabelledGraphCodom, typename VertexMap>
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<LabelledGraphCodom>::GraphType;
const GraphCodom &gCodom = get_graph(lgCodom);
Expand All @@ -55,7 +62,7 @@ struct VertexAdjacency : Constraint<Graph> {

template<typename Visitor, typename LabelledGraphCodom, typename VertexMap>
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<LabelledGraphCodom>::GraphType;
const GraphCodom &gCodom = get_graph(lgCodom);
Expand All @@ -66,14 +73,17 @@ struct VertexAdjacency : Constraint<Graph> {
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) {
Expand All @@ -83,9 +93,13 @@ struct VertexAdjacency : Constraint<Graph> {
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);
}
}
};
Expand All @@ -98,10 +112,11 @@ struct VertexAdjacency : Constraint<Graph> {
public:
template<typename Visitor, typename LabelledGraphCodom, typename VertexMap>
bool matches(Visitor &vis, const Graph &gDom, const LabelledGraphCodom &lgCodom, VertexMap &m,
const LabelSettings ls) const {
const LabelSettings ls) const {
using GraphCodom = typename LabelledGraphTraits<LabelledGraphCodom>::GraphType;
static_assert(std::is_same<Graph, typename jla_boost::GraphMorphism::VertexMapTraits<VertexMap>::GraphDom>::value,
"");
static_assert(
std::is_same<Graph, typename jla_boost::GraphMorphism::VertexMapTraits<VertexMap>::GraphDom>::value,
"");
static_assert(
std::is_same<GraphCodom, typename jla_boost::GraphMorphism::VertexMapTraits<VertexMap>::GraphCodom>::value,
"");
Expand All @@ -118,6 +133,30 @@ struct VertexAdjacency : Constraint<Graph> {

using HasTerm = GraphMorphism::HasTermData<VertexMap>;
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;
Expand Down
86 changes: 86 additions & 0 deletions test/py/matchConstraints/adjacency/10_const.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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 "-" ]
]
Expand All @@ -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()
75 changes: 75 additions & 0 deletions test/py/matchConstraints/adjacency/51_term_dup_match.py
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions test/py/matchConstraints/adjacency/52_term_dependent.py
Original file line number Diff line number Diff line change
@@ -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}"
Loading

0 comments on commit 0bd0e8a

Please sign in to comment.