|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
"""Implements geometric objects used in the graph representation."""
|
|
|
from __future__ import annotations
|
|
|
from collections import defaultdict
|
|
|
from typing import Any, Type
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Node:
|
|
|
r"""Node in the proof state graph.
|
|
|
|
|
|
Can be Point, Line, Circle, etc.
|
|
|
|
|
|
Each node maintains a merge history to
|
|
|
other nodes if they are (found out to be) equivalent
|
|
|
|
|
|
a -> b -
|
|
|
\
|
|
|
c -> d -> e -> f -> g
|
|
|
|
|
|
d.merged_to = e
|
|
|
d.rep = g
|
|
|
d.merged_from = {a, b, c, d}
|
|
|
d.equivs = {a, b, c, d, e, f, g}
|
|
|
"""
|
|
|
|
|
|
def __init__(self, name: str = '', graph: Any = None):
|
|
|
self.name = name or str(self)
|
|
|
self.graph = graph
|
|
|
|
|
|
self.edge_graph = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.merge_graph = {}
|
|
|
|
|
|
|
|
|
|
|
|
self.rep_by = None
|
|
|
self.members = {self}
|
|
|
|
|
|
self._val = None
|
|
|
self._obj = None
|
|
|
|
|
|
self.deps = []
|
|
|
|
|
|
|
|
|
self.num = None
|
|
|
self.change = set()
|
|
|
|
|
|
def set_rep(self, node: Node) -> None:
|
|
|
if node == self:
|
|
|
return
|
|
|
self.rep_by = node
|
|
|
node.merge_edge_graph(self.edge_graph)
|
|
|
node.members.update(self.members)
|
|
|
|
|
|
def rep(self) -> Node:
|
|
|
x = self
|
|
|
while x.rep_by:
|
|
|
x = x.rep_by
|
|
|
return x
|
|
|
|
|
|
def why_rep(self) -> list[Any]:
|
|
|
return self.why_equal([self.rep()], None)
|
|
|
|
|
|
def rep_and_why(self) -> tuple[Node, list[Any]]:
|
|
|
rep = self.rep()
|
|
|
return rep, self.why_equal([rep], None)
|
|
|
|
|
|
def neighbors(
|
|
|
self, oftype: Type[Node], return_set: bool = False, do_rep: bool = True
|
|
|
) -> list[Node]:
|
|
|
"""Neighbors of this node in the proof state graph."""
|
|
|
if do_rep:
|
|
|
rep = self.rep()
|
|
|
else:
|
|
|
rep = self
|
|
|
result = set()
|
|
|
|
|
|
for n in rep.edge_graph:
|
|
|
if oftype is None or oftype and isinstance(n, oftype):
|
|
|
if do_rep:
|
|
|
result.add(n.rep())
|
|
|
else:
|
|
|
result.add(n)
|
|
|
|
|
|
if return_set:
|
|
|
return result
|
|
|
return list(result)
|
|
|
|
|
|
def merge_edge_graph(
|
|
|
self, new_edge_graph: dict[Node, dict[Node, list[Node]]]
|
|
|
) -> None:
|
|
|
for x, xdict in new_edge_graph.items():
|
|
|
if x in self.edge_graph:
|
|
|
self.edge_graph[x].update(dict(xdict))
|
|
|
else:
|
|
|
self.edge_graph[x] = dict(xdict)
|
|
|
|
|
|
def merge(self, nodes: list[Node], deps: list[Any]) -> None:
|
|
|
for node in nodes:
|
|
|
self.merge_one(node, deps)
|
|
|
|
|
|
def merge_one(self, node: Node, deps: list[Any]) -> None:
|
|
|
node.rep().set_rep(self.rep())
|
|
|
|
|
|
if node in self.merge_graph:
|
|
|
return
|
|
|
|
|
|
self.merge_graph[node] = deps
|
|
|
node.merge_graph[self] = deps
|
|
|
|
|
|
def is_val(self, node: Node) -> bool:
|
|
|
return (
|
|
|
isinstance(self, Line)
|
|
|
and isinstance(node, Direction)
|
|
|
or isinstance(self, Segment)
|
|
|
and isinstance(node, Length)
|
|
|
or isinstance(self, Angle)
|
|
|
and isinstance(node, Measure)
|
|
|
or isinstance(self, Ratio)
|
|
|
and isinstance(node, Value)
|
|
|
)
|
|
|
|
|
|
def set_val(self, node: Node) -> None:
|
|
|
self._val = node
|
|
|
|
|
|
def set_obj(self, node: Node) -> None:
|
|
|
self._obj = node
|
|
|
|
|
|
@property
|
|
|
def val(self) -> Node:
|
|
|
if self._val is None:
|
|
|
return None
|
|
|
return self._val.rep()
|
|
|
|
|
|
@property
|
|
|
def obj(self) -> Node:
|
|
|
if self._obj is None:
|
|
|
return None
|
|
|
return self._obj.rep()
|
|
|
|
|
|
def equivs(self) -> set[Node]:
|
|
|
return self.rep().members
|
|
|
|
|
|
def connect_to(self, node: Node, deps: list[Any] = None) -> None:
|
|
|
rep = self.rep()
|
|
|
|
|
|
if node in rep.edge_graph:
|
|
|
rep.edge_graph[node].update({self: deps})
|
|
|
else:
|
|
|
rep.edge_graph[node] = {self: deps}
|
|
|
|
|
|
if self.is_val(node):
|
|
|
self.set_val(node)
|
|
|
node.set_obj(self)
|
|
|
|
|
|
def equivs_upto(self, level: int) -> dict[Node, Node]:
|
|
|
"""What are the equivalent nodes up to a certain level."""
|
|
|
parent = {self: None}
|
|
|
visited = set()
|
|
|
queue = [self]
|
|
|
i = 0
|
|
|
|
|
|
while i < len(queue):
|
|
|
current = queue[i]
|
|
|
i += 1
|
|
|
visited.add(current)
|
|
|
|
|
|
for neighbor in current.merge_graph:
|
|
|
if (
|
|
|
level is not None
|
|
|
and current.merge_graph[neighbor].level is not None
|
|
|
and current.merge_graph[neighbor].level >= level
|
|
|
):
|
|
|
continue
|
|
|
if neighbor not in visited:
|
|
|
queue.append(neighbor)
|
|
|
parent[neighbor] = current
|
|
|
|
|
|
return parent
|
|
|
|
|
|
def why_equal(self, others: list[Node], level: int) -> list[Any]:
|
|
|
"""BFS why this node is equal to other nodes."""
|
|
|
others = set(others)
|
|
|
found = 0
|
|
|
|
|
|
parent = {}
|
|
|
queue = [self]
|
|
|
i = 0
|
|
|
|
|
|
while i < len(queue):
|
|
|
current = queue[i]
|
|
|
if current in others:
|
|
|
found += 1
|
|
|
if found == len(others):
|
|
|
break
|
|
|
|
|
|
i += 1
|
|
|
|
|
|
for neighbor in current.merge_graph:
|
|
|
if (
|
|
|
level is not None
|
|
|
and current.merge_graph[neighbor].level is not None
|
|
|
and current.merge_graph[neighbor].level >= level
|
|
|
):
|
|
|
continue
|
|
|
if neighbor not in parent:
|
|
|
queue.append(neighbor)
|
|
|
parent[neighbor] = current
|
|
|
|
|
|
return bfs_backtrack(self, others, parent)
|
|
|
|
|
|
def why_equal_groups(
|
|
|
self, groups: list[list[Node]], level: int
|
|
|
) -> tuple[list[Any], list[Node]]:
|
|
|
"""BFS for why self is equal to at least one member of each group."""
|
|
|
others = [None for _ in groups]
|
|
|
found = 0
|
|
|
|
|
|
parent = {}
|
|
|
queue = [self]
|
|
|
i = 0
|
|
|
|
|
|
while i < len(queue):
|
|
|
current = queue[i]
|
|
|
|
|
|
for j, grp in enumerate(groups):
|
|
|
if others[j] is None and current in grp:
|
|
|
others[j] = current
|
|
|
found += 1
|
|
|
|
|
|
if found == len(others):
|
|
|
break
|
|
|
|
|
|
i += 1
|
|
|
|
|
|
for neighbor in current.merge_graph:
|
|
|
if (
|
|
|
level is not None
|
|
|
and current.merge_graph[neighbor].level is not None
|
|
|
and current.merge_graph[neighbor].level >= level
|
|
|
):
|
|
|
continue
|
|
|
if neighbor not in parent:
|
|
|
queue.append(neighbor)
|
|
|
parent[neighbor] = current
|
|
|
|
|
|
return bfs_backtrack(self, others, parent), others
|
|
|
|
|
|
def why_val(self, level: int) -> list[Any]:
|
|
|
return self._val.why_equal([self.val], level)
|
|
|
|
|
|
def why_connect(self, node: Node, level: int = None) -> list[Any]:
|
|
|
rep = self.rep()
|
|
|
equivs = list(rep.edge_graph[node].keys())
|
|
|
if not equivs:
|
|
|
return None
|
|
|
equiv = equivs[0]
|
|
|
dep = rep.edge_graph[node][equiv]
|
|
|
return [dep] + self.why_equal(equiv, level)
|
|
|
|
|
|
|
|
|
def why_connect(*pairs: list[tuple[Node, Node]]) -> list[Any]:
|
|
|
result = []
|
|
|
for node1, node2 in pairs:
|
|
|
result += node1.why_connect(node2)
|
|
|
return result
|
|
|
|
|
|
|
|
|
def is_equiv(x: Node, y: Node, level: int = None) -> bool:
|
|
|
level = level or float('inf')
|
|
|
return x.why_equal([y], level) is not None
|
|
|
|
|
|
|
|
|
def is_equal(x: Node, y: Node, level: int = None) -> bool:
|
|
|
if x == y:
|
|
|
return True
|
|
|
if x._val is None or y._val is None:
|
|
|
return False
|
|
|
if x.val != y.val:
|
|
|
return False
|
|
|
return is_equiv(x._val, y._val, level)
|
|
|
|
|
|
|
|
|
def bfs_backtrack(
|
|
|
root: Node, leafs: list[Node], parent: dict[Node, Node]
|
|
|
) -> list[Any]:
|
|
|
"""Return the path given BFS trace of parent nodes."""
|
|
|
backtracked = {root}
|
|
|
deps = []
|
|
|
for node in leafs:
|
|
|
if node is None:
|
|
|
return None
|
|
|
if node in backtracked:
|
|
|
continue
|
|
|
if node not in parent:
|
|
|
return None
|
|
|
while node not in backtracked:
|
|
|
backtracked.add(node)
|
|
|
deps.append(node.merge_graph[parent[node]])
|
|
|
node = parent[node]
|
|
|
|
|
|
return deps
|
|
|
|
|
|
|
|
|
class Point(Node):
|
|
|
pass
|
|
|
|
|
|
|
|
|
class Line(Node):
|
|
|
"""Node of type Line."""
|
|
|
|
|
|
def new_val(self) -> Direction:
|
|
|
return Direction()
|
|
|
|
|
|
def why_coll(self, points: list[Point], level: int = None) -> list[Any]:
|
|
|
"""Why points are connected to self."""
|
|
|
level = level or float('inf')
|
|
|
|
|
|
groups = []
|
|
|
for p in points:
|
|
|
group = [
|
|
|
l
|
|
|
for l, d in self.edge_graph[p].items()
|
|
|
if d is None or d.level < level
|
|
|
]
|
|
|
if not group:
|
|
|
return None
|
|
|
groups.append(group)
|
|
|
|
|
|
min_deps = None
|
|
|
for line in groups[0]:
|
|
|
deps, others = line.why_equal_groups(groups[1:], level)
|
|
|
if deps is None:
|
|
|
continue
|
|
|
for p, o in zip(points, [line] + others):
|
|
|
deps.append(self.edge_graph[p][o])
|
|
|
if min_deps is None or len(deps) < len(min_deps):
|
|
|
min_deps = deps
|
|
|
|
|
|
if min_deps is None:
|
|
|
return None
|
|
|
return [d for d in min_deps if d is not None]
|
|
|
|
|
|
|
|
|
class Segment(Node):
|
|
|
|
|
|
def new_val(self) -> Length:
|
|
|
return Length()
|
|
|
|
|
|
|
|
|
class Circle(Node):
|
|
|
"""Node of type Circle."""
|
|
|
|
|
|
def why_cyclic(self, points: list[Point], level: int = None) -> list[Any]:
|
|
|
"""Why points are connected to self."""
|
|
|
level = level or float('inf')
|
|
|
|
|
|
groups = []
|
|
|
for p in points:
|
|
|
group = [
|
|
|
c
|
|
|
for c, d in self.edge_graph[p].items()
|
|
|
if d is None or d.level < level
|
|
|
]
|
|
|
if not group:
|
|
|
return None
|
|
|
groups.append(group)
|
|
|
|
|
|
min_deps = None
|
|
|
for circle in groups[0]:
|
|
|
deps, others = circle.why_equal_groups(groups[1:], level)
|
|
|
if deps is None:
|
|
|
continue
|
|
|
for p, o in zip(points, [circle] + others):
|
|
|
deps.append(self.edge_graph[p][o])
|
|
|
|
|
|
if min_deps is None or len(deps) < len(min_deps):
|
|
|
min_deps = deps
|
|
|
|
|
|
if min_deps is None:
|
|
|
return None
|
|
|
return [d for d in min_deps if d is not None]
|
|
|
|
|
|
|
|
|
def why_equal(x: Node, y: Node, level: int = None) -> list[Any]:
|
|
|
if x == y:
|
|
|
return []
|
|
|
if not x._val or not y._val:
|
|
|
return None
|
|
|
if x._val == y._val:
|
|
|
return []
|
|
|
return x._val.why_equal([y._val], level)
|
|
|
|
|
|
|
|
|
class Direction(Node):
|
|
|
pass
|
|
|
|
|
|
|
|
|
def get_lines_thru_all(*points: list[Point]) -> list[Line]:
|
|
|
line2count = defaultdict(lambda: 0)
|
|
|
points = set(points)
|
|
|
for p in points:
|
|
|
for l in p.neighbors(Line):
|
|
|
line2count[l] += 1
|
|
|
return [l for l, count in line2count.items() if count == len(points)]
|
|
|
|
|
|
|
|
|
def line_of_and_why(
|
|
|
points: list[Point], level: int = None
|
|
|
) -> tuple[Line, list[Any]]:
|
|
|
"""Why points are collinear."""
|
|
|
for l0 in get_lines_thru_all(*points):
|
|
|
for l in l0.equivs():
|
|
|
if all([p in l.edge_graph for p in points]):
|
|
|
x, y = l.points
|
|
|
colls = list({x, y} | set(points))
|
|
|
|
|
|
|
|
|
why = l.why_coll(colls, level)
|
|
|
if why is not None:
|
|
|
return l, why
|
|
|
|
|
|
return None, None
|
|
|
|
|
|
|
|
|
def get_circles_thru_all(*points: list[Point]) -> list[Circle]:
|
|
|
circle2count = defaultdict(lambda: 0)
|
|
|
points = set(points)
|
|
|
for p in points:
|
|
|
for c in p.neighbors(Circle):
|
|
|
circle2count[c] += 1
|
|
|
return [c for c, count in circle2count.items() if count == len(points)]
|
|
|
|
|
|
|
|
|
def circle_of_and_why(
|
|
|
points: list[Point], level: int = None
|
|
|
) -> tuple[Circle, list[Any]]:
|
|
|
"""Why points are concyclic."""
|
|
|
for c0 in get_circles_thru_all(*points):
|
|
|
for c in c0.equivs():
|
|
|
if all([p in c.edge_graph for p in points]):
|
|
|
cycls = list(set(points))
|
|
|
why = c.why_cyclic(cycls, level)
|
|
|
if why is not None:
|
|
|
return c, why
|
|
|
|
|
|
return None, None
|
|
|
|
|
|
|
|
|
def name_map(struct: Any) -> Any:
|
|
|
if isinstance(struct, list):
|
|
|
return [name_map(x) for x in struct]
|
|
|
elif isinstance(struct, tuple):
|
|
|
return tuple([name_map(x) for x in struct])
|
|
|
elif isinstance(struct, set):
|
|
|
return set([name_map(x) for x in struct])
|
|
|
elif isinstance(struct, dict):
|
|
|
return {name_map(x): name_map(y) for x, y in struct.items()}
|
|
|
else:
|
|
|
return getattr(struct, 'name', '')
|
|
|
|
|
|
|
|
|
class Angle(Node):
|
|
|
"""Node of type Angle."""
|
|
|
|
|
|
def new_val(self) -> Measure:
|
|
|
return Measure()
|
|
|
|
|
|
def set_directions(self, d1: Direction, d2: Direction) -> None:
|
|
|
self._d = d1, d2
|
|
|
|
|
|
@property
|
|
|
def directions(self) -> tuple[Direction, Direction]:
|
|
|
d1, d2 = self._d
|
|
|
if d1 is None or d2 is None:
|
|
|
return d1, d2
|
|
|
return d1.rep(), d2.rep()
|
|
|
|
|
|
|
|
|
class Measure(Node):
|
|
|
pass
|
|
|
|
|
|
|
|
|
class Length(Node):
|
|
|
pass
|
|
|
|
|
|
|
|
|
class Ratio(Node):
|
|
|
"""Node of type Ratio."""
|
|
|
|
|
|
def new_val(self) -> Value:
|
|
|
return Value()
|
|
|
|
|
|
def set_lengths(self, l1: Length, l2: Length) -> None:
|
|
|
self._l = l1, l2
|
|
|
|
|
|
@property
|
|
|
def lengths(self) -> tuple[Length, Length]:
|
|
|
l1, l2 = self._l
|
|
|
if l1 is None or l2 is None:
|
|
|
return l1, l2
|
|
|
return l1.rep(), l2.rep()
|
|
|
|
|
|
|
|
|
class Value(Node):
|
|
|
pass
|
|
|
|
|
|
|
|
|
def all_angles(
|
|
|
d1: Direction, d2: Direction, level: int = None
|
|
|
) -> tuple[Angle, list[Direction], list[Direction]]:
|
|
|
level = level or float('inf')
|
|
|
d1s = d1.equivs_upto(level)
|
|
|
d2s = d2.equivs_upto(level)
|
|
|
|
|
|
for ang in d1.rep().neighbors(Angle):
|
|
|
d1_, d2_ = ang._d
|
|
|
if d1_ in d1s and d2_ in d2s:
|
|
|
yield ang, d1s, d2s
|
|
|
|
|
|
|
|
|
def all_ratios(
|
|
|
d1, d2, level=None
|
|
|
) -> tuple[Angle, list[Direction], list[Direction]]:
|
|
|
level = level or float('inf')
|
|
|
d1s = d1.equivs_upto(level)
|
|
|
d2s = d2.equivs_upto(level)
|
|
|
|
|
|
for ang in d1.rep().neighbors(Ratio):
|
|
|
d1_, d2_ = ang._l
|
|
|
if d1_ in d1s and d2_ in d2s:
|
|
|
yield ang, d1s, d2s
|
|
|
|
|
|
|
|
|
RANKING = {
|
|
|
Point: 0,
|
|
|
Line: 1,
|
|
|
Segment: 2,
|
|
|
Circle: 3,
|
|
|
Direction: 4,
|
|
|
Length: 5,
|
|
|
Angle: 6,
|
|
|
Ratio: 7,
|
|
|
Measure: 8,
|
|
|
Value: 9,
|
|
|
}
|
|
|
|
|
|
|
|
|
def val_type(x: Node) -> Type[Node]:
|
|
|
if isinstance(x, Line):
|
|
|
return Direction
|
|
|
if isinstance(x, Segment):
|
|
|
return Length
|
|
|
if isinstance(x, Angle):
|
|
|
return Measure
|
|
|
if isinstance(x, Ratio):
|
|
|
return Value
|
|
|
|