HugoVoxx's picture
Upload 96 files
be3b34d verified
raw
history blame
90 kB
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Implements the graph representation of the proof state."""
# pylint: disable=g-multiple-import
from __future__ import annotations
from collections import defaultdict # pylint: disable=g-importing-member
from typing import Callable, Generator, Optional, Type, Union
from absl import logging
import ar
import geometry as gm
from geometry import Angle, Direction, Length, Ratio
from geometry import Circle, Line, Point, Segment
from geometry import Measure, Value
import graph_utils as utils
import numericals as nm
import problem
from problem import Dependency, EmptyDependency
np = nm.np
FREE = [
'free',
'segment',
'r_triangle',
'risos',
'triangle',
'triangle12',
'ieq_triangle',
'eq_quadrangle',
'eq_trapezoid',
'eqdia_quadrangle',
'quadrangle',
'r_trapezoid',
'rectangle',
'isquare',
'trapezoid',
'pentagon',
'iso_triangle',
]
INTERSECT = [
'angle_bisector',
'angle_mirror',
'eqdistance',
'lc_tangent',
'on_aline',
'on_bline',
'on_circle',
'on_line',
'on_pline',
'on_tline',
'on_dia',
's_angle',
'on_opline',
'eqangle3',
]
# pylint: disable=protected-access
# pylint: disable=unused-argument
class DepCheckFailError(Exception):
pass
class PointTooCloseError(Exception):
pass
class PointTooFarError(Exception):
pass
class Graph:
"""Graph data structure representing proof state."""
def __init__(self):
self.type2nodes = {
Point: [],
Line: [],
Segment: [],
Circle: [],
Direction: [],
Length: [],
Angle: [],
Ratio: [],
Measure: [],
Value: [],
}
self._name2point = {}
self._name2node = {}
self.rconst = {} # contains all constant ratios
self.aconst = {} # contains all constant angles.
self.halfpi, _ = self.get_or_create_const_ang(1, 2)
self.vhalfpi = self.halfpi.val
self.atable = ar.AngleTable()
self.dtable = ar.DistanceTable()
self.rtable = ar.RatioTable()
# to quick access deps.
self.cache = {}
self._pair2line = {}
self._triplet2circle = {}
def copy(self) -> Graph:
"""Make a copy of self."""
p, definitions = self.build_def
p = p.copy()
for clause in p.clauses:
clause.nums = []
for pname in clause.points:
clause.nums.append(self._name2node[pname].num)
g, _ = Graph.build_problem(p, definitions, verbose=False, init_copy=False)
g.build_clauses = list(getattr(self, 'build_clauses', []))
return g
def _create_const_ang(self, n: int, d: int) -> None:
n, d = ar.simplify(n, d)
ang = self.aconst[(n, d)] = self.new_node(Angle, f'{n}pi/{d}')
ang.set_directions(None, None)
self.connect_val(ang, deps=None)
def _create_const_rat(self, n: int, d: int) -> None:
n, d = ar.simplify(n, d)
rat = self.rconst[(n, d)] = self.new_node(Ratio, f'{n}/{d}')
rat.set_lengths(None, None)
self.connect_val(rat, deps=None)
def get_or_create_const_ang(self, n: int, d: int) -> None:
n, d = ar.simplify(n, d)
if (n, d) not in self.aconst:
self._create_const_ang(n, d)
ang1 = self.aconst[(n, d)]
n, d = ar.simplify(d - n, d)
if (n, d) not in self.aconst:
self._create_const_ang(n, d)
ang2 = self.aconst[(n, d)]
return ang1, ang2
def get_or_create_const_rat(self, n: int, d: int) -> None:
n, d = ar.simplify(n, d)
if (n, d) not in self.rconst:
self._create_const_rat(n, d)
rat1 = self.rconst[(n, d)]
if (d, n) not in self.rconst:
self._create_const_rat(d, n) # pylint: disable=arguments-out-of-order
rat2 = self.rconst[(d, n)]
return rat1, rat2
def add_algebra(self, dep: Dependency, level: int) -> None:
"""Add new algebraic predicates."""
_ = level
if dep.name not in [
'para',
'perp',
'eqangle',
'eqratio',
'aconst',
'rconst',
'cong',
]:
return
name, args = dep.name, dep.args
if name == 'para':
ab, cd = dep.algebra
self.atable.add_para(ab, cd, dep)
if name == 'perp':
ab, cd = dep.algebra
self.atable.add_const_angle(ab, cd, 90, dep)
if name == 'eqangle':
ab, cd, mn, pq = dep.algebra
if (ab, cd) == (pq, mn):
self.atable.add_const_angle(ab, cd, 90, dep)
else:
self.atable.add_eqangle(ab, cd, mn, pq, dep)
if name == 'eqratio':
ab, cd, mn, pq = dep.algebra
if (ab, cd) == (pq, mn):
self.rtable.add_eq(ab, cd, dep)
else:
self.rtable.add_eqratio(ab, cd, mn, pq, dep)
if name == 'aconst':
bx, ab, y = dep.algebra
self.atable.add_const_angle(bx, ab, y, dep)
if name == 'rconst':
l1, l2, m, n = dep.algebra
self.rtable.add_const_ratio(l1, l2, m, n, dep)
if name == 'cong':
a, b, c, d = args
ab, _ = self.get_line_thru_pair_why(a, b)
cd, _ = self.get_line_thru_pair_why(c, d)
self.dtable.add_cong(ab, cd, a, b, c, d, dep)
ab, cd = dep.algebra
self.rtable.add_eq(ab, cd, dep)
def add_eqrat_const(
self, args: list[Point], deps: EmptyDependency
) -> list[Dependency]:
"""Add new algebraic predicates of type eqratio-constant."""
a, b, c, d, num, den = args
nd, dn = self.get_or_create_const_rat(num, den)
if num == den:
return self.add_cong([a, b, c, d], deps)
ab = self._get_or_create_segment(a, b, deps=None)
cd = self._get_or_create_segment(c, d, deps=None)
self.connect_val(ab, deps=None)
self.connect_val(cd, deps=None)
if ab.val == cd.val:
raise ValueError(f'{ab.name} and {cd.name} cannot be equal')
args = [a, b, c, d, nd]
i = 0
for x, y, xy in [(a, b, ab), (c, d, cd)]:
i += 1
x_, y_ = list(xy._val._obj.points)
if {x, y} == {x_, y_}:
continue
if deps:
deps = deps.extend(self, 'rconst', list(args), 'cong', [x, y, x_, y_])
args[2 * i - 2] = x_
args[2 * i - 1] = y_
ab_cd, cd_ab, why = self._get_or_create_ratio(ab, cd, deps=None)
if why:
dep0 = deps.populate('rconst', [a, b, c, d, nd])
deps = EmptyDependency(level=deps.level, rule_name=None)
deps.why = [dep0] + why
lab, lcd = ab_cd._l
a, b = list(lab._obj.points)
c, d = list(lcd._obj.points)
add = []
if not self.is_equal(ab_cd, nd):
args = [a, b, c, d, nd]
dep1 = deps.populate('rconst', args)
dep1.algebra = ab._val, cd._val, num, den
self.make_equal(nd, ab_cd, deps=dep1)
self.cache_dep('rconst', [a, b, c, d, nd], dep1)
add += [dep1]
if not self.is_equal(cd_ab, dn):
args = [c, d, a, b, dn]
dep2 = deps.populate('rconst', args)
dep2.algebra = cd._val, ab._val, den, num
self.make_equal(dn, cd_ab, deps=dep2)
self.cache_dep('rconst', [c, d, a, b, dn], dep2)
add += [dep2]
return add
def do_algebra(self, name: str, args: list[Point]) -> list[Dependency]:
"""Derive (but not add) new algebraic predicates."""
if name == 'para':
a, b, dep = args
if gm.is_equiv(a, b):
return []
(x, y), (m, n) = a._obj.points, b._obj.points
return self.add_para([x, y, m, n], dep)
if name == 'aconst':
a, b, n, d, dep = args
ab, ba, why = self.get_or_create_angle_d(a, b, deps=None)
nd, dn = self.get_or_create_const_ang(n, d)
(x, y), (m, n) = a._obj.points, b._obj.points
if why:
dep0 = dep.populate('aconst', [x, y, m, n, nd])
dep = EmptyDependency(level=dep.level, rule_name=None)
dep.why = [dep0] + why
a, b = ab._d
(x, y), (m, n) = a._obj.points, b._obj.points
added = []
if not self.is_equal(ab, nd):
if nd == self.halfpi:
added += self.add_perp([x, y, m, n], dep)
# else:
name = 'aconst'
args = [x, y, m, n, nd]
dep1 = dep.populate(name, args)
self.cache_dep(name, args, dep1)
self.make_equal(nd, ab, deps=dep1)
added += [dep1]
if not self.is_equal(ba, dn):
if dn == self.halfpi:
added += self.add_perp([m, n, x, y], dep)
name = 'aconst'
args = [m, n, x, y, dn]
dep2 = dep.populate(name, args)
self.cache_dep(name, args, dep2)
self.make_equal(dn, ba, deps=dep2)
added += [dep2]
return added
if name == 'rconst':
a, b, c, d, num, den, dep = args
return self.add_eqrat_const([a, b, c, d, num, den], dep)
if name == 'eqangle':
d1, d2, d3, d4, dep = args
a, b = d1._obj.points
c, d = d2._obj.points
e, f = d3._obj.points
g, h = d4._obj.points
return self.add_eqangle([a, b, c, d, e, f, g, h], dep)
if name == 'eqratio':
d1, d2, d3, d4, dep = args
a, b = d1._obj.points
c, d = d2._obj.points
e, f = d3._obj.points
g, h = d4._obj.points
return self.add_eqratio([a, b, c, d, e, f, g, h], dep)
if name in ['cong', 'cong2']:
a, b, c, d, dep = args
if not (a != b and c != d and (a != c or b != d)):
return []
return self.add_cong([a, b, c, d], dep)
return []
def derive_algebra(
self, level: int, verbose: bool = False
) -> tuple[
dict[str, list[tuple[Point, ...]]], dict[str, [tuple[Point, ...]]]
]:
"""Derive new algebraic predicates."""
derives = {}
ang_derives = self.derive_angle_algebra(level, verbose=verbose)
dist_derives = self.derive_distance_algebra(level, verbose=verbose)
rat_derives = self.derive_ratio_algebra(level, verbose=verbose)
derives.update(ang_derives)
derives.update(dist_derives)
derives.update(rat_derives)
# Separate eqangle and eqratio derivations
# As they are too numerous => slow down DD+AR.
# & reserve them only for last effort.
eqs = {'eqangle': derives.pop('eqangle'), 'eqratio': derives.pop('eqratio')}
return derives, eqs
def derive_ratio_algebra(
self, level: int, verbose: bool = False
) -> dict[str, list[tuple[Point, ...]]]:
"""Derive new eqratio predicates."""
added = {'cong2': [], 'eqratio': []}
for x in self.rtable.get_all_eqs_and_why():
x, why = x[:-1], x[-1]
dep = EmptyDependency(level=level, rule_name='a01')
dep.why = why
if len(x) == 2:
a, b = x
if gm.is_equiv(a, b):
continue
(m, n), (p, q) = a._obj.points, b._obj.points
added['cong2'].append((m, n, p, q, dep))
if len(x) == 4:
a, b, c, d = x
added['eqratio'].append((a, b, c, d, dep))
return added
def derive_angle_algebra(
self, level: int, verbose: bool = False
) -> dict[str, list[tuple[Point, ...]]]:
"""Derive new eqangles predicates."""
added = {'eqangle': [], 'aconst': [], 'para': []}
for x in self.atable.get_all_eqs_and_why():
x, why = x[:-1], x[-1]
dep = EmptyDependency(level=level, rule_name='a02')
dep.why = why
if len(x) == 2:
a, b = x
if gm.is_equiv(a, b):
continue
(e, f), (p, q) = a._obj.points, b._obj.points
if not nm.check('para', [e, f, p, q]):
continue
added['para'].append((a, b, dep))
if len(x) == 3:
a, b, (n, d) = x
(e, f), (p, q) = a._obj.points, b._obj.points
if not nm.check('aconst', [e, f, p, q, n, d]):
continue
added['aconst'].append((a, b, n, d, dep))
if len(x) == 4:
a, b, c, d = x
added['eqangle'].append((a, b, c, d, dep))
return added
def derive_distance_algebra(
self, level: int, verbose: bool = False
) -> dict[str, list[tuple[Point, ...]]]:
"""Derive new cong predicates."""
added = {'inci': [], 'cong': [], 'rconst': []}
for x in self.dtable.get_all_eqs_and_why():
x, why = x[:-1], x[-1]
dep = EmptyDependency(level=level, rule_name='a00')
dep.why = why
if len(x) == 2:
a, b = x
if a == b:
continue
dep.name = f'inci {a.name} {b.name}'
added['inci'].append((x, dep))
if len(x) == 4:
a, b, c, d = x
if not (a != b and c != d and (a != c or b != d)):
continue
added['cong'].append((a, b, c, d, dep))
if len(x) == 6:
a, b, c, d, num, den = x
if not (a != b and c != d and (a != c or b != d)):
continue
added['rconst'].append((a, b, c, d, num, den, dep))
return added
@classmethod
def build_problem(
cls,
pr: problem.Problem,
definitions: dict[str, problem.Definition],
verbose: bool = True,
init_copy: bool = True,
) -> tuple[Graph, list[Dependency]]:
"""Build a problem into a gr.Graph object."""
check = False
g = None
added = None
if verbose:
logging.info(pr.url)
logging.info(pr.txt())
while not check:
try:
g = Graph()
added = []
plevel = 0
for clause in pr.clauses:
adds, plevel = g.add_clause(
clause, plevel, definitions, verbose=verbose
)
added += adds
g.plevel = plevel
except (nm.InvalidLineIntersectError, nm.InvalidQuadSolveError):
continue
except DepCheckFailError:
continue
except (PointTooCloseError, PointTooFarError):
continue
if not pr.goal:
break
args = list(map(lambda x: g.get(x, lambda: int(x)), pr.goal.args))
check = nm.check(pr.goal.name, args)
g.url = pr.url
g.build_def = (pr, definitions)
for add in added:
g.add_algebra(add, level=0)
return g, added
def all_points(self) -> list[Point]:
"""Return all nodes of type Point."""
return list(self.type2nodes[Point])
def all_nodes(self) -> list[gm.Node]:
"""Return all nodes."""
return list(self._name2node.values())
def add_points(self, pnames: list[str]) -> list[Point]:
"""Add new points with given names in list pnames."""
result = [self.new_node(Point, name) for name in pnames]
self._name2point.update(zip(pnames, result))
return result
def names2nodes(self, pnames: list[str]) -> list[gm.Node]:
return [self._name2node[name] for name in pnames]
def names2points(
self, pnames: list[str], create_new_point: bool = False
) -> list[Point]:
"""Return Point objects given names."""
result = []
for name in pnames:
if name not in self._name2node and not create_new_point:
raise ValueError(f'Cannot find point {name} in graph')
elif name in self._name2node:
obj = self._name2node[name]
else:
obj = self.new_node(Point, name)
result.append(obj)
return result
def names2points_or_int(self, pnames: list[str]) -> list[Point]:
"""Return Point objects given names."""
result = []
for name in pnames:
if name.isdigit():
result += [int(name)]
elif 'pi/' in name:
n, d = name.split('pi/')
ang, _ = self.get_or_create_const_ang(int(n), int(d))
result += [ang]
elif '/' in name:
n, d = name.split('/')
rat, _ = self.get_or_create_const_rat(int(n), int(d))
result += [rat]
else:
result += [self._name2point[name]]
return result
def get(self, pointname: str, default_fn: Callable[str, Point]) -> Point:
if pointname in self._name2point:
return self._name2point[pointname]
if pointname in self._name2node:
return self._name2node[pointname]
return default_fn()
def new_node(self, oftype: Type[gm.Node], name: str = '') -> gm.Node:
node = oftype(name, self)
self.type2nodes[oftype].append(node)
self._name2node[name] = node
if isinstance(node, Point):
self._name2point[name] = node
return node
def merge(self, nodes: list[gm.Node], deps: Dependency) -> gm.Node:
"""Merge all nodes."""
if len(nodes) < 2:
return
node0, *nodes1 = nodes
all_nodes = self.type2nodes[type(node0)]
# find node0 that exists in all_nodes to be the rep
# and merge all other nodes into node0
for node in nodes:
if node in all_nodes:
node0 = node
nodes1 = [n for n in nodes if n != node0]
break
return self.merge_into(node0, nodes1, deps)
def merge_into(
self, node0: gm.Node, nodes1: list[gm.Node], deps: Dependency
) -> gm.Node:
"""Merge nodes1 into a single node0."""
node0.merge(nodes1, deps)
for n in nodes1:
if n.rep() != n:
self.remove([n])
nodes = [node0] + nodes1
if any([node._val for node in nodes]):
for node in nodes:
self.connect_val(node, deps=None)
vals1 = [n._val for n in nodes1]
node0._val.merge(vals1, deps)
for v in vals1:
if v.rep() != v:
self.remove([v])
return node0
def remove(self, nodes: list[gm.Node]) -> None:
"""Remove nodes out of self because they are merged."""
if not nodes:
return
for node in nodes:
all_nodes = self.type2nodes[type(nodes[0])]
if node in all_nodes:
all_nodes.remove(node)
if node.name in self._name2node.values():
self._name2node.pop(node.name)
def connect(self, a: gm.Node, b: gm.Node, deps: Dependency) -> None:
a.connect_to(b, deps)
b.connect_to(a, deps)
def connect_val(self, node: gm.Node, deps: Dependency) -> gm.Node:
"""Connect a node into its value (equality) node."""
if node._val:
return node._val
name = None
if isinstance(node, Line):
name = 'd(' + node.name + ')'
if isinstance(node, Angle):
name = 'm(' + node.name + ')'
if isinstance(node, Segment):
name = 'l(' + node.name + ')'
if isinstance(node, Ratio):
name = 'r(' + node.name + ')'
v = self.new_node(gm.val_type(node), name)
self.connect(node, v, deps=deps)
return v
def is_equal(self, x: gm.Node, y: gm.Node, level: int = None) -> bool:
return gm.is_equal(x, y, level)
def add_piece(
self, name: str, args: list[Point], deps: EmptyDependency
) -> list[Dependency]:
"""Add a new predicate."""
if name in ['coll', 'collx']:
return self.add_coll(args, deps)
elif name == 'para':
return self.add_para(args, deps)
elif name == 'perp':
return self.add_perp(args, deps)
elif name == 'midp':
return self.add_midp(args, deps)
elif name == 'cong':
return self.add_cong(args, deps)
elif name == 'circle':
return self.add_circle(args, deps)
elif name == 'cyclic':
return self.add_cyclic(args, deps)
elif name in ['eqangle', 'eqangle6']:
return self.add_eqangle(args, deps)
elif name in ['eqratio', 'eqratio6']:
return self.add_eqratio(args, deps)
# numerical!
elif name == 's_angle':
return self.add_s_angle(args, deps)
elif name == 'aconst':
a, b, c, d, ang = args
if isinstance(ang, str):
name = ang
else:
name = ang.name
num, den = name.split('pi/')
num, den = int(num), int(den)
return self.add_aconst([a, b, c, d, num, den], deps)
elif name == 's_angle':
b, x, a, b, ang = ( # pylint: disable=redeclared-assigned-name,unused-variable
args
)
if isinstance(ang, str):
name = ang
else:
name = ang.name
n, d = name.split('pi/')
ang = int(n) * 180 / int(d)
return self.add_s_angle([a, b, x, ang], deps)
elif name == 'rconst':
a, b, c, d, rat = args
if isinstance(rat, str):
name = rat
else:
name = rat.name
num, den = name.split('/')
num, den = int(num), int(den)
return self.add_eqrat_const([a, b, c, d, num, den], deps)
# composite pieces:
elif name == 'cong2':
return self.add_cong2(args, deps)
elif name == 'eqratio3':
return self.add_eqratio3(args, deps)
elif name == 'eqratio4':
return self.add_eqratio4(args, deps)
elif name == 'simtri':
return self.add_simtri(args, deps)
elif name == 'contri':
return self.add_contri(args, deps)
elif name == 'simtri2':
return self.add_simtri2(args, deps)
elif name == 'contri2':
return self.add_contri2(args, deps)
elif name == 'simtri*':
return self.add_simtri_check(args, deps)
elif name == 'contri*':
return self.add_contri_check(args, deps)
elif name in ['acompute', 'rcompute']:
dep = deps.populate(name, args)
self.cache_dep(name, args, dep)
return [dep]
elif name in ['fixl', 'fixc', 'fixb', 'fixt', 'fixp']:
dep = deps.populate(name, args)
self.cache_dep(name, args, dep)
return [dep]
elif name in ['ind']:
return []
raise ValueError(f'Not recognize {name}')
def check(self, name: str, args: list[Point]) -> bool:
"""Symbolically check if a predicate is True."""
if name == 'ncoll':
return self.check_ncoll(args)
if name == 'npara':
return self.check_npara(args)
if name == 'nperp':
return self.check_nperp(args)
if name == 'midp':
return self.check_midp(args)
if name == 'cong':
return self.check_cong(args)
if name == 'perp':
return self.check_perp(args)
if name == 'para':
return self.check_para(args)
if name == 'coll':
return self.check_coll(args)
if name == 'cyclic':
return self.check_cyclic(args)
if name == 'circle':
return self.check_circle(args)
if name == 'aconst':
return self.check_aconst(args)
if name == 'rconst':
return self.check_rconst(args)
if name == 'acompute':
return self.check_acompute(args)
if name == 'rcompute':
return self.check_rcompute(args)
if name in ['eqangle', 'eqangle6']:
if len(args) == 5:
return self.check_aconst(args)
return self.check_eqangle(args)
if name in ['eqratio', 'eqratio6']:
if len(args) == 5:
return self.check_rconst(args)
return self.check_eqratio(args)
if name in ['simtri', 'simtri2', 'simtri*']:
return self.check_simtri(args)
if name in ['contri', 'contri2', 'contri*']:
return self.check_contri(args)
if name == 'sameside':
return self.check_sameside(args)
if name in 'diff':
a, b = args
return not a.num.close(b.num)
if name in ['fixl', 'fixc', 'fixb', 'fixt', 'fixp']:
return self.in_cache(name, args)
if name in ['ind']:
return True
raise ValueError(f'Not recognize {name}')
def get_lines_thru_all(self, *points: list[gm.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 _get_line(self, a: Point, b: Point) -> Optional[Line]:
linesa = a.neighbors(Line)
for l in b.neighbors(Line):
if l in linesa:
return l
return None
def _get_line_all(self, a: Point, b: Point) -> Generator[Line, None, None]:
linesa = a.neighbors(Line, do_rep=False)
linesb = b.neighbors(Line, do_rep=False)
for l in linesb:
if l in linesa:
yield l
def _get_lines(self, *points: list[Point]) -> list[Line]:
"""Return all lines that connect to >= 2 points."""
line2count = defaultdict(lambda: 0)
for p in points:
for l in p.neighbors(Line):
line2count[l] += 1
return [l for l, count in line2count.items() if count >= 2]
def get_circle_thru_triplet(self, p1: Point, p2: Point, p3: Point) -> Circle:
p1, p2, p3 = sorted([p1, p2, p3], key=lambda x: x.name)
if (p1, p2, p3) in self._triplet2circle:
return self._triplet2circle[(p1, p2, p3)]
return self.get_new_circle_thru_triplet(p1, p2, p3)
def get_new_circle_thru_triplet(
self, p1: Point, p2: Point, p3: Point
) -> Circle:
"""Get a new Circle that goes thru three given Points."""
p1, p2, p3 = sorted([p1, p2, p3], key=lambda x: x.name)
name = p1.name.lower() + p2.name.lower() + p3.name.lower()
circle = self.new_node(Circle, f'({name})')
circle.num = nm.Circle(p1=p1.num, p2=p2.num, p3=p3.num)
circle.points = p1, p2, p3
self.connect(p1, circle, deps=None)
self.connect(p2, circle, deps=None)
self.connect(p3, circle, deps=None)
self._triplet2circle[(p1, p2, p3)] = circle
return circle
def get_line_thru_pair(self, p1: Point, p2: Point) -> Line:
if (p1, p2) in self._pair2line:
return self._pair2line[(p1, p2)]
if (p2, p1) in self._pair2line:
return self._pair2line[(p2, p1)]
return self.get_new_line_thru_pair(p1, p2)
def get_new_line_thru_pair(self, p1: Point, p2: Point) -> Line:
if p1.name.lower() > p2.name.lower():
p1, p2 = p2, p1
name = p1.name.lower() + p2.name.lower()
line = self.new_node(Line, name)
line.num = nm.Line(p1.num, p2.num)
line.points = p1, p2
self.connect(p1, line, deps=None)
self.connect(p2, line, deps=None)
self._pair2line[(p1, p2)] = line
return line
def get_line_thru_pair_why(
self, p1: Point, p2: Point
) -> tuple[Line, list[Dependency]]:
"""Get one line thru two given points and the corresponding dependency list."""
if p1.name.lower() > p2.name.lower():
p1, p2 = p2, p1
if (p1, p2) in self._pair2line:
return self._pair2line[(p1, p2)].rep_and_why()
l, why = gm.line_of_and_why([p1, p2])
if l is None:
l = self.get_new_line_thru_pair(p1, p2)
why = []
return l, why
def coll_dep(self, points: list[Point], p: Point) -> list[Dependency]:
"""Return the dep(.why) explaining why p is coll with points."""
for p1, p2 in utils.comb2(points):
if self.check_coll([p1, p2, p]):
dep = Dependency('coll', [p1, p2, p], None, None)
return dep.why_me_or_cache(self, None)
def add_coll(
self, points: list[Point], deps: EmptyDependency
) -> list[Dependency]:
"""Add a predicate that `points` are collinear."""
points = list(set(points))
og_points = list(points)
all_lines = []
for p1, p2 in utils.comb2(points):
all_lines.append(self.get_line_thru_pair(p1, p2))
points = sum([l.neighbors(Point) for l in all_lines], [])
points = list(set(points))
existed = set()
new = set()
for p1, p2 in utils.comb2(points):
if p1.name > p2.name:
p1, p2 = p2, p1
if (p1, p2) in self._pair2line:
line = self._pair2line[(p1, p2)]
existed.add(line)
else:
line = self.get_new_line_thru_pair(p1, p2)
new.add(line)
existed = sorted(existed, key=lambda l: l.name)
new = sorted(new, key=lambda l: l.name)
existed, new = list(existed), list(new)
if not existed:
line0, *lines = new
else:
line0, lines = existed[0], existed[1:] + new
add = []
line0, why0 = line0.rep_and_why()
a, b = line0.points
for line in lines:
c, d = line.points
args = list({a, b, c, d})
if len(args) < 3:
continue
whys = []
for x in args:
if x not in og_points:
whys.append(self.coll_dep(og_points, x))
abcd_deps = deps
if whys + why0:
dep0 = deps.populate('coll', og_points)
abcd_deps = EmptyDependency(level=deps.level, rule_name=None)
abcd_deps.why = [dep0] + whys
is_coll = self.check_coll(args)
dep = abcd_deps.populate('coll', args)
self.cache_dep('coll', args, dep)
self.merge_into(line0, [line], dep)
if not is_coll:
add += [dep]
return add
def check_coll(self, points: list[Point]) -> bool:
points = list(set(points))
if len(points) < 3:
return True
line2count = defaultdict(lambda: 0)
for p in points:
for l in p.neighbors(Line):
line2count[l] += 1
return any([count == len(points) for _, count in line2count.items()])
def why_coll(self, args: tuple[Line, list[Point]]) -> list[Dependency]:
line, points = args
return line.why_coll(points)
def check_ncoll(self, points: list[Point]) -> bool:
if self.check_coll(points):
return False
return not nm.check_coll([p.num for p in points])
def check_sameside(self, points: list[Point]) -> bool:
return nm.check_sameside([p.num for p in points])
def make_equal(self, x: gm.Node, y: gm.Node, deps: Dependency) -> None:
"""Make that two nodes x and y are equal, i.e. merge their value node."""
if x.val is None:
x, y = y, x
self.connect_val(x, deps=None)
self.connect_val(y, deps=None)
vx = x._val
vy = y._val
if vx == vy:
return
merges = [vx, vy]
if (
isinstance(x, Angle)
and x not in self.aconst.values()
and y not in self.aconst.values()
and x.directions == y.directions[::-1]
and x.directions[0] != x.directions[1]
):
merges = [self.vhalfpi, vx, vy]
self.merge(merges, deps)
def merge_vals(self, vx: gm.Node, vy: gm.Node, deps: Dependency) -> None:
if vx == vy:
return
merges = [vx, vy]
self.merge(merges, deps)
def why_equal(self, x: gm.Node, y: gm.Node, level: int) -> list[Dependency]:
return gm.why_equal(x, y, level)
def _why_coll4(
self,
a: Point,
b: Point,
ab: Line,
c: Point,
d: Point,
cd: Line,
level: int,
) -> list[Dependency]:
return self._why_coll2(a, b, ab, level) + self._why_coll2(c, d, cd, level)
def _why_coll8(
self,
a: Point,
b: Point,
ab: Line,
c: Point,
d: Point,
cd: Line,
m: Point,
n: Point,
mn: Line,
p: Point,
q: Point,
pq: Line,
level: int,
) -> list[Dependency]:
"""Dependency list of why 8 points are collinear."""
why8 = self._why_coll4(a, b, ab, c, d, cd, level)
why8 += self._why_coll4(m, n, mn, p, q, pq, level)
return why8
def add_para(
self, points: list[Point], deps: EmptyDependency
) -> list[Dependency]:
"""Add a new predicate that 4 points (2 lines) are parallel."""
a, b, c, d = points
ab, why1 = self.get_line_thru_pair_why(a, b)
cd, why2 = self.get_line_thru_pair_why(c, d)
is_equal = self.is_equal(ab, cd)
(a, b), (c, d) = ab.points, cd.points
dep0 = deps.populate('para', points)
deps = EmptyDependency(level=deps.level, rule_name=None)
deps = deps.populate('para', [a, b, c, d])
deps.why = [dep0] + why1 + why2
self.make_equal(ab, cd, deps)
deps.algebra = ab._val, cd._val
self.cache_dep('para', [a, b, c, d], deps)
if not is_equal:
return [deps]
return []
def why_para(self, args: list[Point]) -> list[Dependency]:
ab, cd, lvl = args
return self.why_equal(ab, cd, lvl)
def check_para_or_coll(self, points: list[Point]) -> bool:
return self.check_para(points) or self.check_coll(points)
def check_para(self, points: list[Point]) -> bool:
a, b, c, d = points
if (a == b) or (c == d):
return False
ab = self._get_line(a, b)
cd = self._get_line(c, d)
if not ab or not cd:
return False
return self.is_equal(ab, cd)
def check_npara(self, points: list[Point]) -> bool:
if self.check_para(points):
return False
return not nm.check_para([p.num for p in points])
def _get_angle(
self, d1: Direction, d2: Direction
) -> tuple[Angle, Optional[Angle]]:
for a in self.type2nodes[Angle]:
if a.directions == (d1, d2):
return a, a.opposite
return None, None
def get_first_angle(
self, l1: Line, l2: Line
) -> tuple[Angle, list[Dependency]]:
"""Get a first angle between line l1 and line l2."""
d1, d2 = l1._val, l2._val
d1s = d1.all_reps()
d2s = d2.all_reps()
found = d1.first_angle(d2s)
if found is None:
found = d2.first_angle(d1s)
if found is None:
return None, []
ang, x2, x1 = found
found = ang.opposite, x1, x2
ang, x1, x2 = found
return ang, d1.deps_upto(x1) + d2.deps_upto(x2)
def _get_or_create_angle(
self, l1: Line, l2: Line, deps: Dependency
) -> tuple[Angle, Angle, list[Dependency]]:
return self.get_or_create_angle_d(l1._val, l2._val, deps)
def get_or_create_angle_d(
self, d1: Direction, d2: Direction, deps: Dependency
) -> tuple[Angle, Angle, list[Dependency]]:
"""Get or create an angle between two Direction d1 and d2."""
for a in self.type2nodes[Angle]:
if a.directions == (d1.rep(), d2.rep()): # directions = _d.rep()
d1_, d2_ = a._d
why1 = d1.why_equal([d1_], None) + d1_.why_rep()
why2 = d2.why_equal([d2_], None) + d2_.why_rep()
return a, a.opposite, why1 + why2
d1, why1 = d1.rep_and_why()
d2, why2 = d2.rep_and_why()
a12 = self.new_node(Angle, f'{d1.name}-{d2.name}')
a21 = self.new_node(Angle, f'{d2.name}-{d1.name}')
self.connect(d1, a12, deps)
self.connect(d2, a21, deps)
self.connect(a12, a21, deps)
a12.set_directions(d1, d2)
a21.set_directions(d2, d1)
a12.opposite = a21
a21.opposite = a12
return a12, a21, why1 + why2
def _add_para_or_coll(
self,
a: Point,
b: Point,
c: Point,
d: Point,
x: Point,
y: Point,
m: Point,
n: Point,
deps: EmptyDependency,
) -> list[Dependency]:
"""Add a new parallel or collinear predicate."""
extends = [('perp', [x, y, m, n])]
if {a, b} == {x, y}:
pass
elif self.check_para([a, b, x, y]):
extends.append(('para', [a, b, x, y]))
elif self.check_coll([a, b, x, y]):
extends.append(('coll', set(list([a, b, x, y]))))
else:
return None
if m in [c, d] or n in [c, d] or c in [m, n] or d in [m, n]:
pass
elif self.check_coll([c, d, m]):
extends.append(('coll', [c, d, m]))
elif self.check_coll([c, d, n]):
extends.append(('coll', [c, d, n]))
elif self.check_coll([c, m, n]):
extends.append(('coll', [c, m, n]))
elif self.check_coll([d, m, n]):
extends.append(('coll', [d, m, n]))
else:
deps = deps.extend_many(self, 'perp', [a, b, c, d], extends)
return self.add_para([c, d, m, n], deps)
deps = deps.extend_many(self, 'perp', [a, b, c, d], extends)
return self.add_coll(list(set([c, d, m, n])), deps)
def maybe_make_para_from_perp(
self, points: list[Point], deps: EmptyDependency
) -> Optional[list[Dependency]]:
"""Maybe add a new parallel predicate from perp predicate."""
a, b, c, d = points
halfpi = self.aconst[(1, 2)]
for ang in halfpi.val.neighbors(Angle):
if ang == halfpi:
continue
d1, d2 = ang.directions
x, y = d1._obj.points
m, n = d2._obj.points
for args in [
(a, b, c, d, x, y, m, n),
(a, b, c, d, m, n, x, y),
(c, d, a, b, x, y, m, n),
(c, d, a, b, m, n, x, y),
]:
args = args + (deps,)
add = self._add_para_or_coll(*args)
if add:
return add
return None
def add_perp(
self, points: list[Point], deps: EmptyDependency
) -> list[Dependency]:
"""Add a new perpendicular predicate from 4 points (2 lines)."""
add = self.maybe_make_para_from_perp(points, deps)
if add is not None:
return add
a, b, c, d = points
ab, why1 = self.get_line_thru_pair_why(a, b)
cd, why2 = self.get_line_thru_pair_why(c, d)
(a, b), (c, d) = ab.points, cd.points
if why1 + why2:
dep0 = deps.populate('perp', points)
deps = EmptyDependency(level=deps.level, rule_name=None)
deps.why = [dep0] + why1 + why2
self.connect_val(ab, deps=None)
self.connect_val(cd, deps=None)
if ab.val == cd.val:
raise ValueError(f'{ab.name} and {cd.name} Cannot be perp.')
args = [a, b, c, d]
i = 0
for x, y, xy in [(a, b, ab), (c, d, cd)]:
i += 1
x_, y_ = xy._val._obj.points
if {x, y} == {x_, y_}:
continue
if deps:
deps = deps.extend(self, 'perp', list(args), 'para', [x, y, x_, y_])
args[2 * i - 2] = x_
args[2 * i - 1] = y_
a12, a21, why = self._get_or_create_angle(ab, cd, deps=None)
if why:
dep0 = deps.populate('perp', [a, b, c, d])
deps = EmptyDependency(level=deps.level, rule_name=None)
deps.why = [dep0] + why
dab, dcd = a12._d
a, b = dab._obj.points
c, d = dcd._obj.points
is_equal = self.is_equal(a12, a21)
deps = deps.populate('perp', [a, b, c, d])
deps.algebra = [dab, dcd]
self.make_equal(a12, a21, deps=deps)
self.cache_dep('perp', [a, b, c, d], deps)
self.cache_dep('eqangle', [a, b, c, d, c, d, a, b], deps)
if not is_equal:
return [deps]
return []
def why_perp(
self, args: list[Union[Point, list[Dependency]]]
) -> list[Dependency]:
a, b, deps = args
return deps + self.why_equal(a, b, None)
def check_perpl(self, ab: Line, cd: Line) -> bool:
if ab.val is None or cd.val is None:
return False
if ab.val == cd.val:
return False
a12, a21 = self._get_angle(ab.val, cd.val)
if a12 is None or a21 is None:
return False
return self.is_equal(a12, a21)
def check_perp(self, points: list[Point]) -> bool:
a, b, c, d = points
ab = self._get_line(a, b)
cd = self._get_line(c, d)
if not ab or not cd:
return False
return self.check_perpl(ab, cd)
def check_nperp(self, points: list[Point]) -> bool:
if self.check_perp(points):
return False
return not nm.check_perp([p.num for p in points])
def _get_segment(self, p1: Point, p2: Point) -> Optional[Segment]:
for s in self.type2nodes[Segment]:
if s.points == {p1, p2}:
return s
return None
def _get_or_create_segment(
self, p1: Point, p2: Point, deps: Dependency
) -> Segment:
"""Get or create a Segment object between two Points p1 and p2."""
if p1 == p2:
raise ValueError(f'Creating same 0-length segment {p1.name}')
for s in self.type2nodes[Segment]:
if s.points == {p1, p2}:
return s
if p1.name > p2.name:
p1, p2 = p2, p1
s = self.new_node(Segment, name=f'{p1.name.upper()}{p2.name.upper()}')
self.connect(p1, s, deps=deps)
self.connect(p2, s, deps=deps)
s.points = {p1, p2}
return s
def add_cong(
self, points: list[Point], deps: EmptyDependency
) -> list[Dependency]:
"""Add that two segments (4 points) are congruent."""
a, b, c, d = points
ab = self._get_or_create_segment(a, b, deps=None)
cd = self._get_or_create_segment(c, d, deps=None)
is_equal = self.is_equal(ab, cd)
dep = deps.populate('cong', [a, b, c, d])
self.make_equal(ab, cd, deps=dep)
dep.algebra = ab._val, cd._val
self.cache_dep('cong', [a, b, c, d], dep)
result = []
if not is_equal:
result += [dep]
if a not in [c, d] and b not in [c, d]:
return result
if b in [c, d]:
a, b = b, a
if a == d:
c, d = d, c # pylint: disable=unused-variable
result += self._maybe_add_cyclic_from_cong(a, b, d, dep)
return result
def _maybe_add_cyclic_from_cong(
self, a: Point, b: Point, c: Point, cong_ab_ac: Dependency
) -> list[Dependency]:
"""Maybe add a new cyclic predicate from given congruent segments."""
ab = self._get_or_create_segment(a, b, deps=None)
# all eq segs with one end being a.
segs = [s for s in ab.val.neighbors(Segment) if a in s.points]
# all points on circle (a, b)
points = []
for s in segs:
x, y = list(s.points)
points.append(x if y == a else y)
# for sure both b and c are in points
points = [p for p in points if p not in [b, c]]
if len(points) < 2:
return []
x, y = points[:2]
if self.check_cyclic([b, c, x, y]):
return []
ax = self._get_or_create_segment(a, x, deps=None)
ay = self._get_or_create_segment(a, y, deps=None)
why = ab._val.why_equal([ax._val, ay._val], level=None)
why += [cong_ab_ac]
deps = EmptyDependency(cong_ab_ac.level, '')
deps.why = why
return self.add_cyclic([b, c, x, y], deps)
def check_cong(self, points: list[Point]) -> bool:
a, b, c, d = points
if {a, b} == {c, d}:
return True
ab = self._get_segment(a, b)
cd = self._get_segment(c, d)
if ab is None or cd is None:
return False
return self.is_equal(ab, cd)
def why_cong(self, args: tuple[Segment, Segment]) -> list[Dependency]:
ab, cd = args
return self.why_equal(ab, cd, None)
def add_midp(
self, points: list[Point], deps: EmptyDependency
) -> list[Dependency]:
m, a, b = points
add = self.add_coll(points, deps=deps)
add += self.add_cong([m, a, m, b], deps)
return add
def why_midp(
self, args: tuple[Line, list[Point], Segment, Segment]
) -> list[Dependency]:
line, points, ma, mb = args
return self.why_coll([line, points]) + self.why_cong([ma, mb])
def check_midp(self, points: list[Point]) -> bool:
if not self.check_coll(points):
return False
m, a, b = points
return self.check_cong([m, a, m, b])
def add_circle(
self, points: list[Point], deps: EmptyDependency
) -> list[Dependency]:
o, a, b, c = points
add = self.add_cong([o, a, o, b], deps=deps)
add += self.add_cong([o, a, o, c], deps=deps)
return add
def why_circle(
self, args: tuple[Segment, Segment, Segment]
) -> list[Dependency]:
oa, ob, oc = args
return self.why_equal(oa, ob, None) and self.why_equal(oa, oc, None)
def check_circle(self, points: list[Point]) -> bool:
o, a, b, c = points
return self.check_cong([o, a, o, b]) and self.check_cong([o, a, o, c])
def get_circles_thru_all(self, *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 _get_circles(self, *points: list[Point]) -> list[Circle]:
circle2count = defaultdict(lambda: 0)
for p in points:
for c in p.neighbors(Circle):
circle2count[c] += 1
return [c for c, count in circle2count.items() if count >= 3]
def cyclic_dep(self, points: list[Point], p: Point) -> list[Dependency]:
for p1, p2, p3 in utils.comb3(points):
if self.check_cyclic([p1, p2, p3, p]):
dep = Dependency('cyclic', [p1, p2, p3, p], None, None)
return dep.why_me_or_cache(self, None)
def add_cyclic(
self, points: list[Point], deps: EmptyDependency
) -> list[Dependency]:
"""Add a new cyclic predicate that 4 points are concyclic."""
points = list(set(points))
og_points = list(points)
all_circles = []
for p1, p2, p3 in utils.comb3(points):
all_circles.append(self.get_circle_thru_triplet(p1, p2, p3))
points = sum([c.neighbors(Point) for c in all_circles], [])
points = list(set(points))
existed = set()
new = set()
for p1, p2, p3 in utils.comb3(points):
p1, p2, p3 = sorted([p1, p2, p3], key=lambda x: x.name)
if (p1, p2, p3) in self._triplet2circle:
circle = self._triplet2circle[(p1, p2, p3)]
existed.add(circle)
else:
circle = self.get_new_circle_thru_triplet(p1, p2, p3)
new.add(circle)
existed = sorted(existed, key=lambda l: l.name)
new = sorted(new, key=lambda l: l.name)
existed, new = list(existed), list(new)
if not existed:
circle0, *circles = new
else:
circle0, circles = existed[0], existed[1:] + new
add = []
circle0, why0 = circle0.rep_and_why()
a, b, c = circle0.points
for circle in circles:
d, e, f = circle.points
args = list({a, b, c, d, e, f})
if len(args) < 4:
continue
whys = []
for x in [a, b, c, d, e, f]:
if x not in og_points:
whys.append(self.cyclic_dep(og_points, x))
abcdef_deps = deps
if whys + why0:
dep0 = deps.populate('cyclic', og_points)
abcdef_deps = EmptyDependency(level=deps.level, rule_name=None)
abcdef_deps.why = [dep0] + whys
is_cyclic = self.check_cyclic(args)
dep = abcdef_deps.populate('cyclic', args)
self.cache_dep('cyclic', args, dep)
self.merge_into(circle0, [circle], dep)
if not is_cyclic:
add += [dep]
return add
def check_cyclic(self, points: list[Point]) -> bool:
points = list(set(points))
if len(points) < 4:
return True
circle2count = defaultdict(lambda: 0)
for p in points:
for c in p.neighbors(Circle):
circle2count[c] += 1
return any([count == len(points) for _, count in circle2count.items()])
def make_equal_pairs(
self,
a: Point,
b: Point,
c: Point,
d: Point,
m: Point,
n: Point,
p: Point,
q: Point,
ab: Line,
cd: Line,
mn: Line,
pq: Line,
deps: EmptyDependency,
) -> list[Dependency]:
"""Add ab/cd = mn/pq in case either two of (ab,cd,mn,pq) are equal."""
depname = 'eqratio' if isinstance(ab, Segment) else 'eqangle'
eqname = 'cong' if isinstance(ab, Segment) else 'para'
is_equal = self.is_equal(mn, pq)
if ab != cd:
dep0 = deps.populate(depname, [a, b, c, d, m, n, p, q])
deps = EmptyDependency(level=deps.level, rule_name=None)
dep = Dependency(eqname, [a, b, c, d], None, deps.level)
deps.why = [dep0, dep.why_me_or_cache(self, None)]
elif eqname == 'para': # ab == cd.
colls = [a, b, c, d]
if len(set(colls)) > 2:
dep0 = deps.populate(depname, [a, b, c, d, m, n, p, q])
deps = EmptyDependency(level=deps.level, rule_name=None)
dep = Dependency('collx', colls, None, deps.level)
deps.why = [dep0, dep.why_me_or_cache(self, None)]
deps = deps.populate(eqname, [m, n, p, q])
self.make_equal(mn, pq, deps=deps)
deps.algebra = mn._val, pq._val
self.cache_dep(eqname, [m, n, p, q], deps)
if is_equal:
return []
return [deps]
def maybe_make_equal_pairs(
self,
a: Point,
b: Point,
c: Point,
d: Point,
m: Point,
n: Point,
p: Point,
q: Point,
ab: Line,
cd: Line,
mn: Line,
pq: Line,
deps: EmptyDependency,
) -> Optional[list[Dependency]]:
"""Add ab/cd = mn/pq in case maybe either two of (ab,cd,mn,pq) are equal."""
level = deps.level
if self.is_equal(ab, cd, level):
return self.make_equal_pairs(a, b, c, d, m, n, p, q, ab, cd, mn, pq, deps)
elif self.is_equal(mn, pq, level):
return self.make_equal_pairs( # pylint: disable=arguments-out-of-order
m,
n,
p,
q,
a,
b,
c,
d,
mn,
pq,
ab,
cd,
deps,
)
elif self.is_equal(ab, mn, level):
return self.make_equal_pairs( # pylint: disable=arguments-out-of-order
a,
b,
m,
n,
c,
d,
p,
q,
ab,
mn,
cd,
pq,
deps,
)
elif self.is_equal(cd, pq, level):
return self.make_equal_pairs( # pylint: disable=arguments-out-of-order
c,
d,
p,
q,
a,
b,
m,
n,
cd,
pq,
ab,
mn,
deps,
)
else:
return None
def _add_eqangle(
self,
a: Point,
b: Point,
c: Point,
d: Point,
m: Point,
n: Point,
p: Point,
q: Point,
ab: Line,
cd: Line,
mn: Line,
pq: Line,
deps: EmptyDependency,
) -> list[Dependency]:
"""Add eqangle core."""
if deps:
deps = deps.copy()
args = [a, b, c, d, m, n, p, q]
i = 0
for x, y, xy in [(a, b, ab), (c, d, cd), (m, n, mn), (p, q, pq)]:
i += 1
x_, y_ = xy._val._obj.points
if {x, y} == {x_, y_}:
continue
if deps:
deps = deps.extend(self, 'eqangle', list(args), 'para', [x, y, x_, y_])
args[2 * i - 2] = x_
args[2 * i - 1] = y_
add = []
ab_cd, cd_ab, why1 = self._get_or_create_angle(ab, cd, deps=None)
mn_pq, pq_mn, why2 = self._get_or_create_angle(mn, pq, deps=None)
why = why1 + why2
if why:
dep0 = deps.populate('eqangle', args)
deps = EmptyDependency(level=deps.level, rule_name=None)
deps.why = [dep0] + why
dab, dcd = ab_cd._d
dmn, dpq = mn_pq._d
a, b = dab._obj.points
c, d = dcd._obj.points
m, n = dmn._obj.points
p, q = dpq._obj.points
is_eq1 = self.is_equal(ab_cd, mn_pq)
deps1 = None
if deps:
deps1 = deps.populate('eqangle', [a, b, c, d, m, n, p, q])
deps1.algebra = [dab, dcd, dmn, dpq]
if not is_eq1:
add += [deps1]
self.cache_dep('eqangle', [a, b, c, d, m, n, p, q], deps1)
self.make_equal(ab_cd, mn_pq, deps=deps1)
is_eq2 = self.is_equal(cd_ab, pq_mn)
deps2 = None
if deps:
deps2 = deps.populate('eqangle', [c, d, a, b, p, q, m, n])
deps2.algebra = [dcd, dab, dpq, dmn]
if not is_eq2:
add += [deps2]
self.cache_dep('eqangle', [c, d, a, b, p, q, m, n], deps2)
self.make_equal(cd_ab, pq_mn, deps=deps2)
return add
def add_eqangle(
self, points: list[Point], deps: EmptyDependency
) -> list[Dependency]:
"""Add eqangle made by 8 points in `points`."""
if deps:
deps = deps.copy()
a, b, c, d, m, n, p, q = points
ab, why1 = self.get_line_thru_pair_why(a, b)
cd, why2 = self.get_line_thru_pair_why(c, d)
mn, why3 = self.get_line_thru_pair_why(m, n)
pq, why4 = self.get_line_thru_pair_why(p, q)
a, b = ab.points
c, d = cd.points
m, n = mn.points
p, q = pq.points
if deps and why1 + why2 + why3 + why4:
dep0 = deps.populate('eqangle', points)
deps = EmptyDependency(level=deps.level, rule_name=None)
deps.why = [dep0] + why1 + why2 + why3 + why4
add = self.maybe_make_equal_pairs(
a, b, c, d, m, n, p, q, ab, cd, mn, pq, deps
)
if add is not None:
return add
self.connect_val(ab, deps=None)
self.connect_val(cd, deps=None)
self.connect_val(mn, deps=None)
self.connect_val(pq, deps=None)
add = []
if (
ab.val != cd.val
and mn.val != pq.val
and (ab.val != mn.val or cd.val != pq.val)
):
add += self._add_eqangle(a, b, c, d, m, n, p, q, ab, cd, mn, pq, deps)
if (
ab.val != mn.val
and cd.val != pq.val
and (ab.val != cd.val or mn.val != pq.val)
):
add += self._add_eqangle( # pylint: disable=arguments-out-of-order
a,
b,
m,
n,
c,
d,
p,
q,
ab,
mn,
cd,
pq,
deps,
)
return add
def add_aconst(
self, points: list[Point], deps: EmptyDependency
) -> list[Dependency]:
"""Add that an angle is equal to some constant."""
a, b, c, d, num, den = points
nd, dn = self.get_or_create_const_ang(num, den)
if nd == self.halfpi:
return self.add_perp([a, b, c, d], deps)
ab, why1 = self.get_line_thru_pair_why(a, b)
cd, why2 = self.get_line_thru_pair_why(c, d)
(a, b), (c, d) = ab.points, cd.points
if why1 + why2:
args = points[:-2] + [nd]
dep0 = deps.populate('aconst', args)
deps = EmptyDependency(level=deps.level, rule_name=None)
deps.why = [dep0] + why1 + why2
self.connect_val(ab, deps=None)
self.connect_val(cd, deps=None)
if ab.val == cd.val:
raise ValueError(f'{ab.name} - {cd.name} cannot be {nd.name}')
args = [a, b, c, d, nd]
i = 0
for x, y, xy in [(a, b, ab), (c, d, cd)]:
i += 1
x_, y_ = xy._val._obj.points
if {x, y} == {x_, y_}:
continue
if deps:
deps = deps.extend(self, 'aconst', list(args), 'para', [x, y, x_, y_])
args[2 * i - 2] = x_
args[2 * i - 1] = y_
ab_cd, cd_ab, why = self._get_or_create_angle(ab, cd, deps=None)
if why:
dep0 = deps.populate('aconst', [a, b, c, d, nd])
deps = EmptyDependency(level=deps.level, rule_name=None)
deps.why = [dep0] + why
dab, dcd = ab_cd._d
a, b = dab._obj.points
c, d = dcd._obj.points
ang = int(num) * 180 / int(den)
add = []
if not self.is_equal(ab_cd, nd):
deps1 = deps.populate('aconst', [a, b, c, d, nd])
deps1.algebra = dab, dcd, ang % 180
self.make_equal(ab_cd, nd, deps=deps1)
self.cache_dep('aconst', [a, b, c, d, nd], deps1)
add += [deps1]
if not self.is_equal(cd_ab, dn):
deps2 = deps.populate('aconst', [c, d, a, b, dn])
deps2.algebra = dcd, dab, 180 - ang % 180
self.make_equal(cd_ab, dn, deps=deps2)
self.cache_dep('aconst', [c, d, a, b, dn], deps2)
add += [deps2]
return add
def add_s_angle(
self, points: list[Point], deps: EmptyDependency
) -> list[Dependency]:
"""Add that an angle abx is equal to constant y."""
a, b, x, y = points
n, d = ar.simplify(y % 180, 180)
nd, dn = self.get_or_create_const_ang(n, d)
if nd == self.halfpi:
return self.add_perp([a, b, b, x], deps)
ab, why1 = self.get_line_thru_pair_why(a, b)
bx, why2 = self.get_line_thru_pair_why(b, x)
self.connect_val(ab, deps=None)
self.connect_val(bx, deps=None)
add = []
if ab.val == bx.val:
return add
deps.why += why1 + why2
for p, q, pq in [(a, b, ab), (b, x, bx)]:
p_, q_ = pq.val._obj.points
if {p, q} == {p_, q_}:
continue
dep = Dependency('para', [p, q, p_, q_], None, deps.level)
deps.why += [dep.why_me_or_cache(self, None)]
xba, abx, why = self._get_or_create_angle(bx, ab, deps=None)
if why:
dep0 = deps.populate('aconst', [b, x, a, b, nd])
deps = EmptyDependency(level=deps.level, rule_name=None)
deps.why = [dep0] + why
dab, dbx = abx._d
a, b = dab._obj.points
c, x = dbx._obj.points
if not self.is_equal(xba, nd):
deps1 = deps.populate('aconst', [c, x, a, b, nd])
deps1.algebra = dbx, dab, y % 180
self.make_equal(xba, nd, deps=deps1)
self.cache_dep('aconst', [c, x, a, b, nd], deps1)
add += [deps1]
if not self.is_equal(abx, dn):
deps2 = deps.populate('aconst', [a, b, c, x, dn])
deps2.algebra = dab, dbx, 180 - (y % 180)
self.make_equal(abx, dn, deps=deps2)
self.cache_dep('s_angle', [a, b, c, x, dn], deps2)
add += [deps2]
return add
def check_aconst(self, points: list[Point], verbose: bool = False) -> bool:
"""Check if the angle is equal to a certain constant."""
a, b, c, d, nd = points
_ = verbose
if isinstance(nd, str):
name = nd
else:
name = nd.name
num, den = name.split('pi/')
ang, _ = self.get_or_create_const_ang(int(num), int(den))
ab = self._get_line(a, b)
cd = self._get_line(c, d)
if not ab or not cd:
return False
if not (ab.val and cd.val):
return False
for ang1, _, _ in gm.all_angles(ab._val, cd._val):
if self.is_equal(ang1, ang):
return True
return False
def check_acompute(self, points: list[Point]) -> bool:
"""Check if an angle has a constant value."""
a, b, c, d = points
ab = self._get_line(a, b)
cd = self._get_line(c, d)
if not ab or not cd:
return False
if not (ab.val and cd.val):
return False
for ang0 in self.aconst.values():
for ang in ang0.val.neighbors(Angle):
d1, d2 = ang.directions
if ab.val == d1 and cd.val == d2:
return True
return False
def check_eqangle(self, points: list[Point]) -> bool:
"""Check if two angles are equal."""
a, b, c, d, m, n, p, q = points
if {a, b} == {c, d} and {m, n} == {p, q}:
return True
if {a, b} == {m, n} and {c, d} == {p, q}:
return True
if (a == b) or (c == d) or (m == n) or (p == q):
return False
ab = self._get_line(a, b)
cd = self._get_line(c, d)
mn = self._get_line(m, n)
pq = self._get_line(p, q)
if {a, b} == {c, d} and mn and pq and self.is_equal(mn, pq):
return True
if {a, b} == {m, n} and cd and pq and self.is_equal(cd, pq):
return True
if {p, q} == {m, n} and ab and cd and self.is_equal(ab, cd):
return True
if {p, q} == {c, d} and ab and mn and self.is_equal(ab, mn):
return True
if not ab or not cd or not mn or not pq:
return False
if self.is_equal(ab, cd) and self.is_equal(mn, pq):
return True
if self.is_equal(ab, mn) and self.is_equal(cd, pq):
return True
if not (ab.val and cd.val and mn.val and pq.val):
return False
if (ab.val, cd.val) == (mn.val, pq.val) or (ab.val, mn.val) == (
cd.val,
pq.val,
):
return True
for ang1, _, _ in gm.all_angles(ab._val, cd._val):
for ang2, _, _ in gm.all_angles(mn._val, pq._val):
if self.is_equal(ang1, ang2):
return True
if self.check_perp([a, b, m, n]) and self.check_perp([c, d, p, q]):
return True
if self.check_perp([a, b, p, q]) and self.check_perp([c, d, m, n]):
return True
return False
def _get_ratio(self, l1: Length, l2: Length) -> tuple[Ratio, Ratio]:
for r in self.type2nodes[Ratio]:
if r.lengths == (l1, l2):
return r, r.opposite
return None, None
def _get_or_create_ratio(
self, s1: Segment, s2: Segment, deps: Dependency
) -> tuple[Ratio, Ratio, list[Dependency]]:
return self._get_or_create_ratio_l(s1._val, s2._val, deps)
def _get_or_create_ratio_l(
self, l1: Length, l2: Length, deps: Dependency
) -> tuple[Ratio, Ratio, list[Dependency]]:
"""Get or create a new Ratio from two Lenghts l1 and l2."""
for r in self.type2nodes[Ratio]:
if r.lengths == (l1.rep(), l2.rep()):
l1_, l2_ = r._l
why1 = l1.why_equal([l1_], None) + l1_.why_rep()
why2 = l2.why_equal([l2_], None) + l2_.why_rep()
return r, r.opposite, why1 + why2
l1, why1 = l1.rep_and_why()
l2, why2 = l2.rep_and_why()
r12 = self.new_node(Ratio, f'{l1.name}/{l2.name}')
r21 = self.new_node(Ratio, f'{l2.name}/{l1.name}')
self.connect(l1, r12, deps)
self.connect(l2, r21, deps)
self.connect(r12, r21, deps)
r12.set_lengths(l1, l2)
r21.set_lengths(l2, l1)
r12.opposite = r21
r21.opposite = r12
return r12, r21, why1 + why2
def add_cong2(
self, points: list[Point], deps: EmptyDependency
) -> list[Dependency]:
m, n, a, b = points
add = []
add += self.add_cong([m, a, n, a], deps)
add += self.add_cong([m, b, n, b], deps)
return add
def add_eqratio3(
self, points: list[Point], deps: EmptyDependency
) -> list[Dependency]:
"""Add three eqratios through a list of 6 points (due to parallel lines)."""
a, b, c, d, m, n = points
# a -- b
# m -- n
# c -- d
add = []
add += self.add_eqratio([m, a, m, c, n, b, n, d], deps)
add += self.add_eqratio([a, m, a, c, b, n, b, d], deps)
add += self.add_eqratio([c, m, c, a, d, n, d, b], deps)
if m == n:
add += self.add_eqratio([m, a, m, c, a, b, c, d], deps)
return add
def add_eqratio4(
self, points: list[Point], deps: EmptyDependency
) -> list[Dependency]:
o, a, b, c, d = points
# o
# a b
# c d
add = self.add_eqratio3([a, b, c, d, o, o], deps)
add += self.add_eqratio([o, a, o, c, a, b, c, d], deps)
return add
def _add_eqratio(
self,
a: Point,
b: Point,
c: Point,
d: Point,
m: Point,
n: Point,
p: Point,
q: Point,
ab: Segment,
cd: Segment,
mn: Segment,
pq: Segment,
deps: EmptyDependency,
) -> list[Dependency]:
"""Add a new eqratio from 8 points (core)."""
if deps:
deps = deps.copy()
args = [a, b, c, d, m, n, p, q]
i = 0
for x, y, xy in [(a, b, ab), (c, d, cd), (m, n, mn), (p, q, pq)]:
if {x, y} == set(xy.points):
continue
x_, y_ = list(xy.points)
if deps:
deps = deps.extend(self, 'eqratio', list(args), 'cong', [x, y, x_, y_])
args[2 * i - 2] = x_
args[2 * i - 1] = y_
add = []
ab_cd, cd_ab, why1 = self._get_or_create_ratio(ab, cd, deps=None)
mn_pq, pq_mn, why2 = self._get_or_create_ratio(mn, pq, deps=None)
why = why1 + why2
if why:
dep0 = deps.populate('eqratio', args)
deps = EmptyDependency(level=deps.level, rule_name=None)
deps.why = [dep0] + why
lab, lcd = ab_cd._l
lmn, lpq = mn_pq._l
a, b = lab._obj.points
c, d = lcd._obj.points
m, n = lmn._obj.points
p, q = lpq._obj.points
is_eq1 = self.is_equal(ab_cd, mn_pq)
deps1 = None
if deps:
deps1 = deps.populate('eqratio', [a, b, c, d, m, n, p, q])
deps1.algebra = [ab._val, cd._val, mn._val, pq._val]
if not is_eq1:
add += [deps1]
self.cache_dep('eqratio', [a, b, c, d, m, n, p, q], deps1)
self.make_equal(ab_cd, mn_pq, deps=deps1)
is_eq2 = self.is_equal(cd_ab, pq_mn)
deps2 = None
if deps:
deps2 = deps.populate('eqratio', [c, d, a, b, p, q, m, n])
deps2.algebra = [cd._val, ab._val, pq._val, mn._val]
if not is_eq2:
add += [deps2]
self.cache_dep('eqratio', [c, d, a, b, p, q, m, n], deps2)
self.make_equal(cd_ab, pq_mn, deps=deps2)
return add
def add_eqratio(
self, points: list[Point], deps: EmptyDependency
) -> list[Dependency]:
"""Add a new eqratio from 8 points."""
if deps:
deps = deps.copy()
a, b, c, d, m, n, p, q = points
ab = self._get_or_create_segment(a, b, deps=None)
cd = self._get_or_create_segment(c, d, deps=None)
mn = self._get_or_create_segment(m, n, deps=None)
pq = self._get_or_create_segment(p, q, deps=None)
add = self.maybe_make_equal_pairs(
a, b, c, d, m, n, p, q, ab, cd, mn, pq, deps
)
if add is not None:
return add
self.connect_val(ab, deps=None)
self.connect_val(cd, deps=None)
self.connect_val(mn, deps=None)
self.connect_val(pq, deps=None)
add = []
if (
ab.val != cd.val
and mn.val != pq.val
and (ab.val != mn.val or cd.val != pq.val)
):
add += self._add_eqratio(a, b, c, d, m, n, p, q, ab, cd, mn, pq, deps)
if (
ab.val != mn.val
and cd.val != pq.val
and (ab.val != cd.val or mn.val != pq.val)
):
add += self._add_eqratio( # pylint: disable=arguments-out-of-order
a,
b,
m,
n,
c,
d,
p,
q,
ab,
mn,
cd,
pq,
deps,
)
return add
def check_rconst(self, points: list[Point], verbose: bool = False) -> bool:
"""Check whether a ratio is equal to some given constant."""
_ = verbose
a, b, c, d, nd = points
if isinstance(nd, str):
name = nd
else:
name = nd.name
num, den = name.split('/')
rat, _ = self.get_or_create_const_rat(int(num), int(den))
ab = self._get_segment(a, b)
cd = self._get_segment(c, d)
if not ab or not cd:
return False
if not (ab.val and cd.val):
return False
for rat1, _, _ in gm.all_ratios(ab._val, cd._val):
if self.is_equal(rat1, rat):
return True
return False
def check_rcompute(self, points: list[Point]) -> bool:
"""Check whether a ratio is equal to some constant."""
a, b, c, d = points
ab = self._get_segment(a, b)
cd = self._get_segment(c, d)
if not ab or not cd:
return False
if not (ab.val and cd.val):
return False
for rat0 in self.rconst.values():
for rat in rat0.val.neighbors(Ratio):
l1, l2 = rat.lengths
if ab.val == l1 and cd.val == l2:
return True
return False
def check_eqratio(self, points: list[Point]) -> bool:
"""Check if 8 points make an eqratio predicate."""
a, b, c, d, m, n, p, q = points
if {a, b} == {c, d} and {m, n} == {p, q}:
return True
if {a, b} == {m, n} and {c, d} == {p, q}:
return True
ab = self._get_segment(a, b)
cd = self._get_segment(c, d)
mn = self._get_segment(m, n)
pq = self._get_segment(p, q)
if {a, b} == {c, d} and mn and pq and self.is_equal(mn, pq):
return True
if {a, b} == {m, n} and cd and pq and self.is_equal(cd, pq):
return True
if {p, q} == {m, n} and ab and cd and self.is_equal(ab, cd):
return True
if {p, q} == {c, d} and ab and mn and self.is_equal(ab, mn):
return True
if not ab or not cd or not mn or not pq:
return False
if self.is_equal(ab, cd) and self.is_equal(mn, pq):
return True
if self.is_equal(ab, mn) and self.is_equal(cd, pq):
return True
if not (ab.val and cd.val and mn.val and pq.val):
return False
if (ab.val, cd.val) == (mn.val, pq.val) or (ab.val, mn.val) == (
cd.val,
pq.val,
):
return True
for rat1, _, _ in gm.all_ratios(ab._val, cd._val):
for rat2, _, _ in gm.all_ratios(mn._val, pq._val):
if self.is_equal(rat1, rat2):
return True
return False
def add_simtri_check(
self, points: list[Point], deps: EmptyDependency
) -> list[Dependency]:
if nm.same_clock(*[p.num for p in points]):
return self.add_simtri(points, deps)
return self.add_simtri2(points, deps)
def add_contri_check(
self, points: list[Point], deps: EmptyDependency
) -> list[Dependency]:
if nm.same_clock(*[p.num for p in points]):
return self.add_contri(points, deps)
return self.add_contri2(points, deps)
def enum_sides(
self, points: list[Point]
) -> Generator[list[Point], None, None]:
a, b, c, x, y, z = points
yield [a, b, x, y]
yield [b, c, y, z]
yield [c, a, z, x]
def enum_triangle(
self, points: list[Point]
) -> Generator[list[Point], None, None]:
a, b, c, x, y, z = points
yield [a, b, a, c, x, y, x, z]
yield [b, a, b, c, y, x, y, z]
yield [c, a, c, b, z, x, z, y]
def enum_triangle2(
self, points: list[Point]
) -> Generator[list[Point], None, None]:
a, b, c, x, y, z = points
yield [a, b, a, c, x, z, x, y]
yield [b, a, b, c, y, z, y, x]
yield [c, a, c, b, z, y, z, x]
def add_simtri(
self, points: list[Point], deps: EmptyDependency
) -> list[Dependency]:
"""Add two similar triangles."""
add = []
hashs = [d.hashed() for d in deps.why]
for args in self.enum_triangle(points):
if problem.hashed('eqangle6', args) in hashs:
continue
add += self.add_eqangle(args, deps=deps)
for args in self.enum_triangle(points):
if problem.hashed('eqratio6', args) in hashs:
continue
add += self.add_eqratio(args, deps=deps)
return add
def check_simtri(self, points: list[Point]) -> bool:
a, b, c, x, y, z = points
return self.check_eqangle([a, b, a, c, x, y, x, z]) and self.check_eqangle(
[b, a, b, c, y, x, y, z]
)
def add_simtri2(
self, points: list[Point], deps: EmptyDependency
) -> list[Dependency]:
"""Add two similar reflected triangles."""
add = []
hashs = [d.hashed() for d in deps.why]
for args in self.enum_triangle2(points):
if problem.hashed('eqangle6', args) in hashs:
continue
add += self.add_eqangle(args, deps=deps)
for args in self.enum_triangle(points):
if problem.hashed('eqratio6', args) in hashs:
continue
add += self.add_eqratio(args, deps=deps)
return add
def add_contri(
self, points: list[Point], deps: EmptyDependency
) -> list[Dependency]:
"""Add two congruent triangles."""
add = []
hashs = [d.hashed() for d in deps.why]
for args in self.enum_triangle(points):
if problem.hashed('eqangle6', args) in hashs:
continue
add += self.add_eqangle(args, deps=deps)
for args in self.enum_sides(points):
if problem.hashed('cong', args) in hashs:
continue
add += self.add_cong(args, deps=deps)
return add
def check_contri(self, points: list[Point]) -> bool:
a, b, c, x, y, z = points
return (
self.check_cong([a, b, x, y])
and self.check_cong([b, c, y, z])
and self.check_cong([c, a, z, x])
)
def add_contri2(
self, points: list[Point], deps: EmptyDependency
) -> list[Dependency]:
"""Add two congruent reflected triangles."""
add = []
hashs = [d.hashed() for d in deps.why]
for args in self.enum_triangle2(points):
if problem.hashed('eqangle6', args) in hashs:
continue
add += self.add_eqangle(args, deps=deps)
for args in self.enum_sides(points):
if problem.hashed('cong', args) in hashs:
continue
add += self.add_cong(args, deps=deps)
return add
def in_cache(self, name: str, args: list[Point]) -> bool:
return problem.hashed(name, args) in self.cache
def cache_dep(
self, name: str, args: list[Point], premises: list[Dependency]
) -> None:
hashed = problem.hashed(name, args)
if hashed in self.cache:
return
self.cache[hashed] = premises
def all_same_line(
self, a: Point, b: Point
) -> Generator[tuple[Point, Point], None, None]:
ab = self._get_line(a, b)
if ab is None:
return
for p1, p2 in utils.comb2(ab.neighbors(Point)):
if {p1, p2} != {a, b}:
yield p1, p2
def all_same_angle(
self, a: Point, b: Point, c: Point, d: Point
) -> Generator[tuple[Point, Point, Point, Point], None, None]:
for x, y in self.all_same_line(a, b):
for m, n in self.all_same_line(c, d):
yield x, y, m, n
def additionally_draw(self, name: str, args: list[Point]) -> None:
"""Draw some extra line/circles for illustration purpose."""
if name in ['circle']:
center, point = args[:2]
circle = self.new_node(Circle, f'({center.name},{point.name})')
circle.num = nm.Circle(center.num, p1=point.num)
circle.points = center, point
if name in ['on_circle', 'tangent']:
center, point = args[-2:]
circle = self.new_node(Circle, f'({center.name},{point.name})')
circle.num = nm.Circle(center.num, p1=point.num)
circle.points = center, point
if name in ['incenter', 'excenter', 'incenter2', 'excenter2']:
d, a, b, c = [x for x in args[-4:]]
a, b, c = sorted([a, b, c], key=lambda x: x.name.lower())
circle = self.new_node(Circle, f'({d.name},h.{a.name}{b.name})')
p = d.num.foot(nm.Line(a.num, b.num))
circle.num = nm.Circle(d.num, p1=p)
circle.points = d, a, b, c
if name in ['cc_tangent']:
o, a, w, b = args[-4:]
c1 = self.new_node(Circle, f'({o.name},{a.name})')
c1.num = nm.Circle(o.num, p1=a.num)
c1.points = o, a
c2 = self.new_node(Circle, f'({w.name},{b.name})')
c2.num = nm.Circle(w.num, p1=b.num)
c2.points = w, b
if name in ['ninepoints']:
a, b, c = args[-3:]
a, b, c = sorted([a, b, c], key=lambda x: x.name.lower())
circle = self.new_node(Circle, f'(,m.{a.name}{b.name}{c.name})')
p1 = (b.num + c.num) * 0.5
p2 = (c.num + a.num) * 0.5
p3 = (a.num + b.num) * 0.5
circle.num = nm.Circle(p1=p1, p2=p2, p3=p3)
circle.points = (None, None, a, b, c)
if name in ['2l1c']:
a, b, c, o = args[:4]
a, b, c = sorted([a, b, c], key=lambda x: x.name.lower())
circle = self.new_node(Circle, f'({o.name},{a.name}{b.name}{c.name})')
circle.num = nm.Circle(p1=a.num, p2=b.num, p3=c.num)
circle.points = (a, b, c)
def add_clause(
self,
clause: problem.Clause,
plevel: int,
definitions: dict[str, problem.Definition],
verbose: int = False,
) -> tuple[list[Dependency], int]:
"""Add a new clause of construction, e.g. a new excenter."""
existing_points = self.all_points()
new_points = [Point(name) for name in clause.points]
new_points_dep_points = set()
new_points_dep = []
# Step 1: check for all deps.
for c in clause.constructions:
cdef = definitions[c.name]
if len(cdef.construction.args) != len(c.args):
if len(cdef.construction.args) - len(c.args) == len(clause.points):
c.args = clause.points + c.args
else:
correct_form = ' '.join(cdef.points + ['=', c.name] + cdef.args)
raise ValueError('Argument mismatch. ' + correct_form)
mapping = dict(zip(cdef.construction.args, c.args))
c_name = 'midp' if c.name == 'midpoint' else c.name
deps = EmptyDependency(level=0, rule_name=problem.CONSTRUCTION_RULE)
deps.construction = Dependency(c_name, c.args, rule_name=None, level=0)
for d in cdef.deps.constructions:
args = self.names2points([mapping[a] for a in d.args])
new_points_dep_points.update(args)
if not self.check(d.name, args):
raise DepCheckFailError(
d.name + ' ' + ' '.join([x.name for x in args])
)
deps.why += [
Dependency(
d.name, args, rule_name=problem.CONSTRUCTION_RULE, level=0
)
]
new_points_dep += [deps]
# Step 2: draw.
def range_fn() -> (
list[Union[nm.Point, nm.Line, nm.Circle, nm.HalfLine, nm.HoleCircle]]
):
to_be_intersected = []
for c in clause.constructions:
cdef = definitions[c.name]
mapping = dict(zip(cdef.construction.args, c.args))
for n in cdef.numerics:
args = [mapping[a] for a in n.args]
args = list(map(lambda x: self.get(x, lambda: int(x)), args))
to_be_intersected += nm.sketch(n.name, args)
return to_be_intersected
is_total_free = (
len(clause.constructions) == 1 and clause.constructions[0].name in FREE
)
is_semi_free = (
len(clause.constructions) == 1
and clause.constructions[0].name in INTERSECT
)
existing_points = [p.num for p in existing_points]
def draw_fn() -> list[nm.Point]:
to_be_intersected = range_fn()
return nm.reduce(to_be_intersected, existing_points)
rely_on = set()
for c in clause.constructions:
cdef = definitions[c.name]
mapping = dict(zip(cdef.construction.args, c.args))
for n in cdef.numerics:
args = [mapping[a] for a in n.args]
args = list(map(lambda x: self.get(x, lambda: int(x)), args))
rely_on.update([a for a in args if isinstance(a, Point)])
for p in rely_on:
p.change.update(new_points)
nums = draw_fn()
for p, num, num0 in zip(new_points, nums, clause.nums):
p.co_change = new_points
if isinstance(num0, nm.Point):
num = num0
elif isinstance(num0, (tuple, list)):
x, y = num0
num = nm.Point(x, y)
p.num = num
# check two things.
if nm.check_too_close(nums, existing_points):
raise PointTooCloseError()
if nm.check_too_far(nums, existing_points):
raise PointTooFarError()
# Commit: now that all conditions are passed.
# add these points to current graph.
for p in new_points:
self._name2point[p.name] = p
self._name2node[p.name] = p
self.type2nodes[Point].append(p)
for p in new_points:
p.why = sum([d.why for d in new_points_dep], []) # to generate txt logs.
p.group = new_points
p.dep_points = new_points_dep_points
p.dep_points.update(new_points)
p.plevel = plevel
# movement dependency:
rely_dict_0 = defaultdict(lambda: [])
for c in clause.constructions:
cdef = definitions[c.name]
mapping = dict(zip(cdef.construction.args, c.args))
for p, ps in cdef.rely.items():
p = mapping[p]
ps = [mapping[x] for x in ps]
rely_dict_0[p].append(ps)
rely_dict = {}
for p, pss in rely_dict_0.items():
ps = sum(pss, [])
if len(pss) > 1:
ps = [x for x in ps if x != p]
p = self._name2point[p]
ps = self.names2nodes(ps)
rely_dict[p] = ps
for p in new_points:
p.rely_on = set(rely_dict.get(p, []))
for x in p.rely_on:
if not hasattr(x, 'base_rely_on'):
x.base_rely_on = set()
p.base_rely_on = set.union(*[x.base_rely_on for x in p.rely_on] + [set()])
if is_total_free or is_semi_free:
p.rely_on.add(p)
p.base_rely_on.add(p)
plevel_done = set()
added = []
basics = []
# Step 3: build the basics.
for c, deps in zip(clause.constructions, new_points_dep):
cdef = definitions[c.name]
mapping = dict(zip(cdef.construction.args, c.args))
# not necessary for proofing, but for visualization.
c_args = list(map(lambda x: self.get(x, lambda: int(x)), c.args))
self.additionally_draw(c.name, c_args)
for points, bs in cdef.basics:
if points:
points = self.names2nodes([mapping[p] for p in points])
points = [p for p in points if p not in plevel_done]
for p in points:
p.plevel = plevel
plevel_done.update(points)
plevel += 1
else:
continue
for b in bs:
if b.name != 'rconst':
args = [mapping[a] for a in b.args]
else:
num, den = map(int, b.args[-2:])
rat, _ = self.get_or_create_const_rat(num, den)
args = [mapping[a] for a in b.args[:-2]] + [rat.name]
args = list(map(lambda x: self.get(x, lambda: int(x)), args))
adds = self.add_piece(name=b.name, args=args, deps=deps)
basics.append((b.name, args, deps))
if adds:
added += adds
for add in adds:
self.cache_dep(add.name, add.args, add)
assert len(plevel_done) == len(new_points)
for p in new_points:
p.basics = basics
return added, plevel
def all_eqangle_same_lines(self) -> Generator[tuple[Point, ...], None, None]:
for l1, l2 in utils.perm2(self.type2nodes[Line]):
for a, b, c, d, e, f, g, h in utils.all_8points(l1, l2, l1, l2):
if (a, b, c, d) != (e, f, g, h):
yield a, b, c, d, e, f, g, h
def all_eqangles_distinct_linepairss(
self,
) -> Generator[tuple[Line, ...], None, None]:
"""No eqangles betcause para-para, or para-corresponding, or same."""
for measure in self.type2nodes[Measure]:
angs = measure.neighbors(Angle)
line_pairss = []
for ang in angs:
d1, d2 = ang.directions
if d1 is None or d2 is None:
continue
l1s = d1.neighbors(Line)
l2s = d2.neighbors(Line)
# Any pair in this is para-para.
para_para = list(utils.cross(l1s, l2s))
line_pairss.append(para_para)
for pairs1, pairs2 in utils.comb2(line_pairss):
for pair1, pair2 in utils.cross(pairs1, pairs2):
(l1, l2), (l3, l4) = pair1, pair2
yield l1, l2, l3, l4
def all_eqangles_8points(self) -> Generator[tuple[Point, ...], None, None]:
"""List all sets of 8 points that make two equal angles."""
# Case 1: (l1-l2) = (l3-l4), including because l1//l3, l2//l4 (para-para)
angss = []
for measure in self.type2nodes[Measure]:
angs = measure.neighbors(Angle)
angss.append(angs)
# include the angs that do not have any measure.
angss.extend([[ang] for ang in self.type2nodes[Angle] if ang.val is None])
line_pairss = []
for angs in angss:
line_pairs = set()
for ang in angs:
d1, d2 = ang.directions
if d1 is None or d2 is None:
continue
l1s = d1.neighbors(Line)
l2s = d2.neighbors(Line)
line_pairs.update(set(utils.cross(l1s, l2s)))
line_pairss.append(line_pairs)
# include (d1, d2) in which d1 does not have any angles.
noang_ds = [d for d in self.type2nodes[Direction] if not d.neighbors(Angle)]
for d1 in noang_ds:
for d2 in self.type2nodes[Direction]:
if d1 == d2:
continue
l1s = d1.neighbors(Line)
l2s = d2.neighbors(Line)
if len(l1s) < 2 and len(l2s) < 2:
continue
line_pairss.append(set(utils.cross(l1s, l2s)))
line_pairss.append(set(utils.cross(l2s, l1s)))
# Case 2: d1 // d2 => (d1-d3) = (d2-d3)
# include lines that does not have any direction.
nodir_ls = [l for l in self.type2nodes[Line] if l.val is None]
for line in nodir_ls:
for d in self.type2nodes[Direction]:
l1s = d.neighbors(Line)
if len(l1s) < 2:
continue
l2s = [line]
line_pairss.append(set(utils.cross(l1s, l2s)))
line_pairss.append(set(utils.cross(l2s, l1s)))
record = set()
for line_pairs in line_pairss:
for pair1, pair2 in utils.perm2(list(line_pairs)):
(l1, l2), (l3, l4) = pair1, pair2
if l1 == l2 or l3 == l4:
continue
if (l1, l2) == (l3, l4):
continue
if (l1, l2, l3, l4) in record:
continue
record.add((l1, l2, l3, l4))
for a, b, c, d, e, f, g, h in utils.all_8points(l1, l2, l3, l4):
yield (a, b, c, d, e, f, g, h)
for a, b, c, d, e, f, g, h in self.all_eqangle_same_lines():
yield a, b, c, d, e, f, g, h
def all_eqangles_6points(self) -> Generator[tuple[Point, ...], None, None]:
"""List all sets of 6 points that make two equal angles."""
record = set()
for a, b, c, d, e, f, g, h in self.all_eqangles_8points():
if (
a not in (c, d)
and b not in (c, d)
or e not in (g, h)
and f not in (g, h)
):
continue
if b in (c, d):
a, b = b, a # now a in c, d
if f in (g, h):
e, f = f, e # now e in g, h
if a == d:
c, d = d, c # now a == c
if e == h:
g, h = h, g # now e == g
if (a, b, c, d, e, f, g, h) in record:
continue
record.add((a, b, c, d, e, f, g, h))
yield a, b, c, d, e, f, g, h # where a==c, e==g
def all_paras(self) -> Generator[tuple[Point, ...], None, None]:
for d in self.type2nodes[Direction]:
for l1, l2 in utils.perm2(d.neighbors(Line)):
for a, b, c, d in utils.all_4points(l1, l2):
yield a, b, c, d
def all_perps(self) -> Generator[tuple[Point, ...], None, None]:
for ang in self.vhalfpi.neighbors(Angle):
d1, d2 = ang.directions
if d1 is None or d2 is None:
continue
if d1 == d2:
continue
for l1, l2 in utils.cross(d1.neighbors(Line), d2.neighbors(Line)):
for a, b, c, d in utils.all_4points(l1, l2):
yield a, b, c, d
def all_congs(self) -> Generator[tuple[Point, ...], None, None]:
for l in self.type2nodes[Length]:
for s1, s2 in utils.perm2(l.neighbors(Segment)):
(a, b), (c, d) = s1.points, s2.points
for x, y in [(a, b), (b, a)]:
for m, n in [(c, d), (d, c)]:
yield x, y, m, n
def all_eqratios_8points(self) -> Generator[tuple[Point, ...], None, None]:
"""List all sets of 8 points that make two equal ratios."""
ratss = []
for value in self.type2nodes[Value]:
rats = value.neighbors(Ratio)
ratss.append(rats)
# include the rats that do not have any val.
ratss.extend([[rat] for rat in self.type2nodes[Ratio] if rat.val is None])
seg_pairss = []
for rats in ratss:
seg_pairs = set()
for rat in rats:
l1, l2 = rat.lengths
if l1 is None or l2 is None:
continue
s1s = l1.neighbors(Segment)
s2s = l2.neighbors(Segment)
seg_pairs.update(utils.cross(s1s, s2s))
seg_pairss.append(seg_pairs)
# include (l1, l2) in which l1 does not have any ratio.
norat_ls = [l for l in self.type2nodes[Length] if not l.neighbors(Ratio)]
for l1 in norat_ls:
for l2 in self.type2nodes[Length]:
if l1 == l2:
continue
s1s = l1.neighbors(Segment)
s2s = l2.neighbors(Segment)
if len(s1s) < 2 and len(s2s) < 2:
continue
seg_pairss.append(set(utils.cross(s1s, s2s)))
seg_pairss.append(set(utils.cross(s2s, s1s)))
# include Seg that does not have any Length.
nolen_ss = [s for s in self.type2nodes[Segment] if s.val is None]
for seg in nolen_ss:
for l in self.type2nodes[Length]:
s1s = l.neighbors(Segment)
if len(s1s) == 1:
continue
s2s = [seg]
seg_pairss.append(set(utils.cross(s1s, s2s)))
seg_pairss.append(set(utils.cross(s2s, s1s)))
record = set()
for seg_pairs in seg_pairss:
for pair1, pair2 in utils.perm2(list(seg_pairs)):
(s1, s2), (s3, s4) = pair1, pair2
if s1 == s2 or s3 == s4:
continue
if (s1, s2) == (s3, s4):
continue
if (s1, s2, s3, s4) in record:
continue
record.add((s1, s2, s3, s4))
a, b = s1.points
c, d = s2.points
e, f = s3.points
g, h = s4.points
for x, y in [(a, b), (b, a)]:
for z, t in [(c, d), (d, c)]:
for m, n in [(e, f), (f, e)]:
for p, q in [(g, h), (h, g)]:
yield (x, y, z, t, m, n, p, q)
segss = []
# finally the list of ratios that is equal to 1.0
for length in self.type2nodes[Length]:
segs = length.neighbors(Segment)
segss.append(segs)
segs_pair = list(utils.perm2(list(segss)))
segs_pair += list(zip(segss, segss))
for segs1, segs2 in segs_pair:
for s1, s2 in utils.perm2(list(segs1)):
for s3, s4 in utils.perm2(list(segs2)):
if (s1, s2) == (s3, s4) or (s1, s3) == (s2, s4):
continue
if (s1, s2, s3, s4) in record:
continue
record.add((s1, s2, s3, s4))
a, b = s1.points
c, d = s2.points
e, f = s3.points
g, h = s4.points
for x, y in [(a, b), (b, a)]:
for z, t in [(c, d), (d, c)]:
for m, n in [(e, f), (f, e)]:
for p, q in [(g, h), (h, g)]:
yield (x, y, z, t, m, n, p, q)
def all_eqratios_6points(self) -> Generator[tuple[Point, ...], None, None]:
"""List all sets of 6 points that make two equal angles."""
record = set()
for a, b, c, d, e, f, g, h in self.all_eqratios_8points():
if (
a not in (c, d)
and b not in (c, d)
or e not in (g, h)
and f not in (g, h)
):
continue
if b in (c, d):
a, b = b, a
if f in (g, h):
e, f = f, e
if a == d:
c, d = d, c
if e == h:
g, h = h, g
if (a, b, c, d, e, f, g, h) in record:
continue
record.add((a, b, c, d, e, f, g, h))
yield a, b, c, d, e, f, g, h # now a==c, e==g
def all_cyclics(self) -> Generator[tuple[Point, ...], None, None]:
for c in self.type2nodes[Circle]:
for x, y, z, t in utils.perm4(c.neighbors(Point)):
yield x, y, z, t
def all_colls(self) -> Generator[tuple[Point, ...], None, None]:
for l in self.type2nodes[Line]:
for x, y, z in utils.perm3(l.neighbors(Point)):
yield x, y, z
def all_midps(self) -> Generator[tuple[Point, ...], None, None]:
for l in self.type2nodes[Line]:
for a, b, c in utils.perm3(l.neighbors(Point)):
if self.check_cong([a, b, a, c]):
yield a, b, c
def all_circles(self) -> Generator[tuple[Point, ...], None, None]:
for l in self.type2nodes[Length]:
p2p = defaultdict(list)
for s in l.neighbors(Segment):
a, b = s.points
p2p[a].append(b)
p2p[b].append(a)
for p, ps in p2p.items():
if len(ps) >= 3:
for a, b, c in utils.perm3(ps):
yield p, a, b, c
def two_points_on_direction(self, d: Direction) -> tuple[Point, Point]:
l = d.neighbors(Line)[0]
p1, p2 = l.neighbors(Point)[:2]
return p1, p2
def two_points_of_length(self, l: Length) -> tuple[Point, Point]:
s = l.neighbors(Segment)[0]
p1, p2 = s.points
return p1, p2
def create_consts_str(g: Graph, s: str) -> Union[Ratio, Angle]:
if 'pi/' in s:
n, d = s.split('pi/')
n, d = int(n), int(d)
p0, _ = g.get_or_create_const_ang(n, d)
else:
n, d = s.split('/')
n, d = int(n), int(d)
p0, _ = g.get_or_create_const_rat(n, d)
return p0
def create_consts(g: Graph, p: gm.Node) -> Union[Ratio, Angle]:
if isinstance(p, Angle):
n, d = p.name.split('pi/')
n, d = int(n), int(d)
p0, _ = g.get_or_create_const_ang(n, d)
if isinstance(p, Ratio):
n, d = p.name.split('/')
n, d = int(n), int(d)
p0, _ = g.get_or_create_const_rat(n, d)
return p0 # pylint: disable=undefined-variable