Skip to content

Commit

Permalink
Fix bug in check_hypergraph_acyclicity
Browse files Browse the repository at this point in the history
  • Loading branch information
gfrances committed Sep 29, 2020
1 parent 2d57d46 commit c16ef4a
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 30 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed
### Deprecated
### Fixed
- Fixed a bug in `check_hypergraph_acyclicity` reported by @abcorrea.


## [0.6.0]- 2020-09-18
Expand Down
44 changes: 34 additions & 10 deletions src/tarski/analysis/csp.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,44 @@ def compute_primal_graph_from_hypergraph(hypergraph):


def _remove_ear_if_exists(edges, node_counts):
""" A helper to find a so-called ear of the hypergraph """
for e1, e2 in itertools.product(edges, edges):
diff = e1-e2
if diff and all(node_counts[node] == 1 for node in diff):
# e1 is an ear and can be removed. We update node count as well
for node in e1:
node_counts[node] -= 1
edges.remove(e1)
""" Find and remove from the given set of edges a so-called "ear" and return True.
If no such ear exists, return False.
From Abiteboul, S., Hull, R. and Vianu, V (1995). Foundations of Databases:
An ear of hypergraph F = (V, F) is an edge f ∈ F such that for some distinct f' ∈ F,
no vertex of f − f' is in any other edge [...]. In this case, f' is called a witness that f is an ear.
As a special case, if there is an edge f of F that intersects no other edge, then f is also considered an ear.
Note that this implementation is possibly not the most efficient, but it should work fine for the kind of small
hypergraphs we're interested in.
"""
def remove_edge(edge):
for node in edge:
node_counts[node] -= 1
edges.remove(edge)

for f in edges:
# Check first the "special case"
if all(node_counts[node] == 1 for node in f):
remove_edge(f)
# print(f'Edge {f} removed because it intersects no other edge')
return True

for f, fprime in itertools.permutations(edges, 2):

This comment has been minimized.

Copy link
@abcorrea

abcorrea Oct 5, 2020

Collaborator

I think that this is correct to identify acyclicity or not, however, I think that in order to extract the join tree, this algorithm would need some "tweaks". We don't need to change it now, but just to keep it in mind.

This comment has been minimized.

Copy link
@gfrances

gfrances Oct 5, 2020

Author Member

It would be great to have a (unit-tested) method to compute the join tree :-)

This comment has been minimized.

Copy link
@abcorrea

abcorrea Oct 5, 2020

Collaborator

Oh sorry, maybe I am wrong and it is straightforward with the while loop in the check_hypergraph_acyclicity function.

# Note that this includes the case where f \subseteq f'.
# Abiteboul et al. assume that hypergraph is "reduced" and f has thus been compiled into f' at preprocessing;
# we simply compile it away here on the fly by assuming it is indeed an ear and can be removed.
if all(node_counts[node] == 1 for node in f-fprime):
remove_edge(f)
# print(f'Edge {f} removed in favor of {fprime}')
return True
# print(f'No more ears found in {edges}')
return False


def check_hypergraph_acyclicity(hypergraph):
""" Check whether the given hypergraph is acyclic by applying the GYO reduction as described in
Jeffrey D. Ullman, Principles of Database and Knowledge-Base Systems, Vol. II
Abiteboul, S., Hull, R. and Vianu, V (1995). Foundations of Databases, pp.131-132.
"""
nodes = set(itertools.chain.from_iterable(hypergraph))
edges = set(frozenset(x) for x in hypergraph) # simply convert the tuple into frozensets
Expand All @@ -64,10 +87,11 @@ def check_hypergraph_acyclicity(hypergraph):
for n in edge:
node_counts[n] += 1

# This is essentially the GYO algorithm:
while _remove_ear_if_exists(edges, node_counts):
pass

return len(edges) <= 1
return len(edges) == 0


def _collect_hyperedges(phi, edges):
Expand Down
51 changes: 31 additions & 20 deletions tests/analysis/test_csp.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,34 @@
from tests.io.common import collect_strips_benchmarks, reader


def test_hypergraph_generation():
def compute_acyclicity(instance):
instance_file, domain_file = collect_strips_benchmarks([instance])[0]
problem = reader(case_insensitive=True).read_problem(domain_file, instance_file)
hgraphs = {a.name: compute_schema_constraint_hypergraph(a) for a in problem.actions.values()}
return {name: check_hypergraph_acyclicity(h) for name, h in hgraphs.items()}

acyclicity = compute_acyclicity("pipesworld-notankage:p01-net1-b6-g2.pddl")
assert acyclicity == {'PUSH-START': False, 'PUSH-END': True, 'POP-START': False,
'POP-END': True, 'PUSH-UNITARYPIPE': False, 'POP-UNITARYPIPE': False}

acyclicity = compute_acyclicity("ged-opt14-strips:d-1-2.pddl")
assert all(x is True for x in acyclicity.values())


def check_acyclicity(domain_file, instance_file):
problem = reader(case_insensitive=True, strict_with_requirements=False).read_problem(domain_file, instance_file)
for a in problem.actions.values():
acyclic = check_hypergraph_acyclicity(compute_schema_constraint_hypergraph(a))
print(f'Action {a.name} is {"acyclic" if acyclic else "cyclic"}.')
def test_acyclicity_detection():
# assert check_hypergraph_acyclicity({('room',), ('obj', 'room'), ('gripper',), ('obj',)}) is True
# assert check_hypergraph_acyclicity({('gripper', 'obj'), ('gripper',), ('obj',), ('room',)}) is True

# A hyperedge contained in another hyperedge is correctly marked as acyclic
assert check_hypergraph_acyclicity({('X', ), ('X', 'Y')}) is True

# Two repeated hyperedges are dealt with adequately
assert check_hypergraph_acyclicity({('X', ), ('X', 'Y'), ('Y', 'X')}) is True

# Two non-intersecting hyperedges are acyclic
assert check_hypergraph_acyclicity({('S', 'T'), ('X', 'Y')}) is True

# A simple cyclic hypergraph
assert check_hypergraph_acyclicity({('X', 'Y'), ('Y', 'Z'), ('Z', 'X')}) is False

# Now compute acyclicity over a few domain schemas
assert compute_acyclicity("pipesworld-notankage:p01-net1-b6-g2.pddl") ==\

This comment has been minimized.

Copy link
@abcorrea

abcorrea Oct 5, 2020

Collaborator

There are two pipesworld-notankage domain files available. Should we add a comment that this is the 'split' domain so we avoid confusion in the future?

This comment has been minimized.

Copy link
@gfrances

gfrances Oct 5, 2020

Author Member

Sure, I don't see why not

{'PUSH-START': False, 'PUSH-END': True, 'POP-START': False,
'POP-END': True, 'PUSH-UNITARYPIPE': False, 'POP-UNITARYPIPE': False}

assert all(x is True for x in compute_acyclicity("ged-opt14-strips:d-1-2.pddl").values())

assert compute_acyclicity("gripper:prob01.pddl") == {'move': True, 'pick': True, 'drop': True}


def compute_acyclicity(instance):
instance_file, domain_file = collect_strips_benchmarks([instance])[0]
problem = reader(case_insensitive=True).read_problem(domain_file, instance_file)
hgraphs = {a.name: compute_schema_constraint_hypergraph(a) for a in problem.actions.values()}
return {name: check_hypergraph_acyclicity(h) for name, h in hgraphs.items()}

0 comments on commit c16ef4a

Please sign in to comment.