diff --git "a/ag4masses/alphageometry/graph.py" "b/ag4masses/alphageometry/graph.py" --- "a/ag4masses/alphageometry/graph.py" +++ "b/ag4masses/alphageometry/graph.py" @@ -1,3057 +1,3151 @@ -# 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 +# 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, SemiCircle +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_semicircle' + '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: [], + SemiCircle: [], + } + 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 = {} + self._triplet2semicircle = {} + + 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, num, den + 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 == 'semicircle': + return self.add_semicircle(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 == 'semicircle': + return self.check_semicircle(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_semicircle_thru_triplet(self, p1: Point, p2: Point, p3: Point) -> SemiCircle: + p1, p2, p3 = sorted([p1, p2, p3], key=lambda x: x.name) + if (p1, p2, p3) in self._triplet2semicircle: + return self._triplet2semicircle[(p1, p2, p3)] + return self.get_new_semicircle_thru_triplet(p1, p2, p3) + + def get_new_semicircle_thru_triplet(self, p1: Point, p2: Point, p3: Point) -> SemiCircle: + """Get a new SemiCircle 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() + semicircle = self.new_node(SemiCircle, f'({name})') + semicircle.num = nm.SemiCircle(p1=p1.num, p2=p2.num, p3=p3.num) + semicircle.points = p1, p2, p3 + + self.connect(p1, semicircle, deps=None) + self.connect(p2, semicircle, deps=None) + self.connect(p3, semicircle, deps=None) + self._triplet2semicircle[(p1, p2, p3)] = semicircle + return semicircle + + 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 add_semicircle( + 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, b, 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 check_semicircle(self, points: list[Point]) -> bool: + + o, a, b = points + + # Check if points a and b lie on the semicircle boundary + return self.check_cong([o, a, o, b]) + + def cyclic_dep(self, points: list[Point], p: Point) -> list[Dependency]: + for p1, p2, p3 in utils.comb3(points): + if isinstance(self, SemiCircle): + # Additional check to ensure the points are within the semicircle + if self.is_within_boundary(p1) and self.is_within_boundary(p2) and self.is_within_boundary(p3) and self.is_within_boundary(p): + 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) + else: + 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 _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 isinstance(self, SemiCircle): + # Additional check to ensure the points are within the semicircle + if self.is_within_boundary(p1) and self.is_within_boundary(p2) and self.is_within_boundary(p3) and self.is_within_boundary(p): + 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) + else: + 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 ['on_semicircle', 'tangent']: + center, point = args[-2:] + circle = self.new_node(SemiCircle, f'({center.name},{point.name})') + circle.num = nm.SemiCircle(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) + + if name in ['semicircle']: + o, c, b, a = args[:4] + a, b, c = sorted([a, b, c], key=lambda x: x.name.lower()) + semicircle = self.new_node(SemiCircle, f'({o.name},{a.name}{b.name}{c.name})') + semicircle.num = nm.SemiCircle(center=o.num, p1=a.num, p2=b.num, p3=c.num) + semicircle.points = (a, b, c) + + if name in ['tangent_semicircle']: + center, point, tangent_point = args[:3] + semicircle = self.new_node(SemiCircle, f'({center.name},{point.name})') + semicircle.num = nm.SemiCircle(center.num, p1=point.num) + semicircle.points = center, point + + tangent = self.new_node(Line, f'tangent({tangent_point.name})') + tangent.num = nm.Line(tangent_point.num, p1=point.num) + tangent.points = tangent_point, point + + + 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, nm.SemiCircle]] + ): + 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 all_semicircles(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 \ No newline at end of file