# 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 objects to represent problems, theorems, proofs, traceback.""" from __future__ import annotations from collections import defaultdict # pylint: disable=g-importing-member from typing import Any import geometry as gm import pretty as pt # pylint: disable=protected-access # pylint: disable=unused-variable # pylint: disable=unused-argument # pylint: disable=unused-assignment def reshape(l: list[Any], n: int = 1) -> list[list[Any]]: assert len(l) % n == 0 columns = [[] for i in range(n)] for i, x in enumerate(l): columns[i % n].append(x) return zip(*columns) def isint(x: str) -> bool: try: int(x) return True except: # pylint: disable=bare-except return False class Construction: """One predicate.""" @classmethod def from_txt(cls, data: str) -> Construction: data = data.split(' ') return Construction(data[0], data[1:]) def __init__(self, name: str, args: list[str]): self.name = name self.args = args def translate(self, mapping: dict[str, str]) -> Construction: args = [a if isint(a) else mapping[a] for a in self.args] return Construction(self.name, args) def txt(self) -> str: return ' '.join([self.name] + list(self.args)) class Clause: """One construction (>= 1 predicate).""" @classmethod def from_txt(cls, data: str) -> Clause: if data == ' =': return Clause([], []) points, constructions = data.split(' = ') return Clause( points.split(' '), [Construction.from_txt(c) for c in constructions.split(', ')], ) def __init__(self, points: list[str], constructions: list[Construction]): self.points = [] self.nums = [] for p in points: num = None if isinstance(p, str) and '@' in p: p, num = p.split('@') x, y = num.split('_') num = float(x), float(y) self.points.append(p) self.nums.append(num) self.constructions = constructions def translate(self, mapping: dict[str, str]) -> Clause: points0 = [] for p in self.points: pcount = len(mapping) + 1 name = chr(96 + pcount) if name > 'z': # pcount = 26 -> name = 'z' name = chr(97 + (pcount - 1) % 26) + str((pcount - 1) // 26) p0 = mapping.get(p, name) mapping[p] = p0 points0.append(p0) return Clause(points0, [c.translate(mapping) for c in self.constructions]) def add(self, name: str, args: list[str]) -> None: self.constructions.append(Construction(name, args)) def txt(self) -> str: return ( ' '.join(self.points) + ' = ' + ', '.join(c.txt() for c in self.constructions) ) def _gcd(x: int, y: int) -> int: while y: x, y = y, x % y return x def simplify(n: int, d: int) -> tuple[int, int]: g = _gcd(n, d) return (n // g, d // g) def compare_fn(dep: Dependency) -> tuple[Dependency, str]: return (dep, pt.pretty(dep)) def sort_deps(deps: list[Dependency]) -> list[Dependency]: return sorted(deps, key=compare_fn) class Problem: """Describe one problem to solve.""" @classmethod def from_txt_file( cls, fname: str, to_dict: bool = False, translate: bool = True ): """Load a problem from a text file.""" with open(fname, 'r') as f: lines = f.read().split('\n') lines = [l for l in lines if l] data = [ cls.from_txt(url + '\n' + problem, translate) for (url, problem) in reshape(lines, 2) ] if to_dict: return cls.to_dict(data) return data @classmethod def from_txt(cls, data: str, translate: bool = True) -> Problem: """Load a problem from a str object.""" url = '' if '\n' in data: url, data = data.split('\n') if ' ? ' in data: clauses, goal = data.split(' ? ') goal = Construction.from_txt(goal) else: clauses, goal = data, None clauses = clauses.split('; ') problem = Problem( url=url, clauses=[Clause.from_txt(c) for c in clauses], goal=goal ) if translate: return problem.translate() return problem @classmethod def to_dict(cls, data: list[Problem]) -> dict[str, Problem]: return {p.url: p for p in data} def __init__(self, url: str, clauses: list[Clause], goal: Construction): self.url = url self.clauses = clauses self.goal = goal def copy(self) -> Problem: return Problem(self.url, list(self.clauses), self.goal) def translate(self) -> Problem: # to single-char point names """Translate point names into alphabetical.""" mapping = {} clauses = [] for clause in self.clauses: clauses.append(clause.translate(mapping)) if self.goal: goal = self.goal.translate(mapping) else: goal = self.goal p = Problem(self.url, clauses, goal) p.mapping = mapping return p def txt(self) -> str: return ( '; '.join([c.txt() for c in self.clauses]) + ' ? ' + self.goal.txt() if self.goal else '' ) def setup_str_from_problem(self, definitions: list[Definition]) -> str: """Construct the string from Problem object.""" ref = 0 string = [] for clause in self.clauses: group = {} p2deps = defaultdict(list) for c in clause.constructions: cdef = definitions[c.name] if len(c.args) != len(cdef.construction.args): assert len(c.args) + len(clause.points) == len(cdef.construction.args) c.args = clause.points + c.args mapping = dict(zip(cdef.construction.args, c.args)) for points, bs in cdef.basics: points = tuple([mapping[x] for x in points]) for p in points: group[p] = points for b in bs: args = [mapping[a] for a in b.args] name = b.name if b.name in ['s_angle', 'aconst']: x, y, z, v = args name = 'aconst' v = int(v) if v < 0: v = -v x, z = z, x m, n = simplify(int(v), 180) args = [y, z, y, x, f'{m}pi/{n}'] p2deps[points].append(hashed_txt(name, args)) for k, v in p2deps.items(): p2deps[k] = sort_deps(v) points = clause.points while points: p = points[0] gr = group[p] points = [x for x in points if x not in gr] deps_str = [] for dep in p2deps[gr]: ref_str = '{:02}'.format(ref) dep_str = pt.pretty(dep) if dep[0] == 'aconst': m, n = map(int, dep[-1].split('pi/')) mn = f'{m}. pi / {n}.' dep_str = ' '.join(dep_str.split()[:-1] + [mn]) deps_str.append(dep_str + ' ' + ref_str) ref += 1 string.append(' '.join(gr) + ' : ' + ' '.join(deps_str)) string = '{S} ' + ' ; '.join([s.strip() for s in string]) goal = self.goal string += ' ? ' + pt.pretty([goal.name] + goal.args) return string def parse_rely(s: str) -> dict[str, str]: result = {} if not s: return result s = [x.strip() for x in s.split(',')] for x in s: a, b = x.split(':') a, b = a.strip().split(), b.strip().split() result.update({m: b for m in a}) return result class Definition: """Definitions of construction statements.""" @classmethod def from_txt_file(cls, fname: str, to_dict: bool = False) -> Definition: with open(fname, 'r') as f: lines = f.read() return cls.from_string(lines, to_dict) @classmethod def from_string(cls, string: str, to_dict: bool = False) -> Definition: lines = string.split('\n') data = [cls.from_txt('\n'.join(group)) for group in reshape(lines, 6)] if to_dict: return cls.to_dict(data) return data @classmethod def to_dict(cls, data: list[Definition]) -> dict[str, Definition]: return {d.construction.name: d for d in data} @classmethod def from_txt(cls, data: str) -> Definition: """Load definitions from a str object.""" construction, rely, deps, basics, numerics, _ = data.split('\n') basics = [] if not basics else [b.strip() for b in basics.split(';')] levels = [] for bs in basics: if ':' in bs: points, bs = bs.split(':') points = points.strip().split() else: points = [] if bs.strip(): bs = [Construction.from_txt(b.strip()) for b in bs.strip().split(',')] else: bs = [] levels.append((points, bs)) numerics = [] if not numerics else numerics.split(', ') return Definition( construction=Construction.from_txt(construction), rely=parse_rely(rely), deps=Clause.from_txt(deps), basics=levels, numerics=[Construction.from_txt(c) for c in numerics], ) def __init__( self, construction: Construction, rely: dict[str, str], deps: Clause, basics: list[tuple[list[str], list[Construction]]], numerics: list[Construction], ): self.construction = construction self.rely = rely self.deps = deps self.basics = basics self.numerics = numerics args = set() for num in numerics: args.update(num.args) self.points = [] self.args = [] for p in self.construction.args: if p in args: self.args.append(p) else: self.points.append(p) class Theorem: """Deduction rule.""" @classmethod def from_txt_file(cls, fname: str, to_dict: bool = False) -> Theorem: with open(fname, 'r') as f: theorems = f.read() return cls.from_string(theorems, to_dict) @classmethod def from_string(cls, string: str, to_dict: bool = False) -> Theorem: """Load deduction rule from a str object.""" theorems = string.split('\n') theorems = [l for l in theorems if l and not l.startswith('#')] theorems = [cls.from_txt(l) for l in theorems] for i, th in enumerate(theorems): th.rule_name = 'r{:02}'.format(i) if to_dict: result = {} for t in theorems: if t.name in result: t.name += '_' result[t.rule_name] = t return result return theorems @classmethod def from_txt(cls, data: str) -> Theorem: premises, conclusion = data.split(' => ') premises = premises.split(', ') conclusion = conclusion.split(', ') return Theorem( premise=[Construction.from_txt(p) for p in premises], conclusion=[Construction.from_txt(c) for c in conclusion], ) def __init__( self, premise: list[Construction], conclusion: list[Construction] ): if len(conclusion) != 1: raise ValueError('Cannot have more than one conclusion') self.name = '_'.join([p.name for p in premise + conclusion]) self.premise = premise self.conclusion = conclusion self.is_arg_reduce = False assert len(self.conclusion) == 1 con = self.conclusion[0] if con.name in [ 'eqratio3', 'midp', 'contri', 'simtri', 'contri2', 'simtri2', 'simtri*', 'contri*', ]: return prem_args = set(sum([p.args for p in self.premise], [])) con_args = set(con.args) if len(prem_args) <= len(con_args): self.is_arg_reduce = True def txt(self) -> str: premise_txt = ', '.join([clause.txt() for clause in self.premise]) conclusion_txt = ', '.join([clause.txt() for clause in self.conclusion]) return f'{premise_txt} => {conclusion_txt}' def conclusion_name_args( self, mapping: dict[str, gm.Point] ) -> tuple[str, list[gm.Point]]: mapping = {arg: p for arg, p in mapping.items() if isinstance(arg, str)} c = self.conclusion[0] args = [mapping[a] for a in c.args] return c.name, args def why_eqratio( d1: gm.Direction, d2: gm.Direction, d3: gm.Direction, d4: gm.Direction, level: int, ) -> list[Dependency]: """Why two ratios are equal, returns a Dependency objects.""" all12 = list(gm.all_ratios(d1, d2, level)) all34 = list(gm.all_ratios(d3, d4, level)) min_why = None for ang12, d1s, d2s in all12: for ang34, d3s, d4s in all34: why0 = gm.why_equal(ang12, ang34, level) if why0 is None: continue d1_, d2_ = ang12._l d3_, d4_ = ang34._l why1 = gm.bfs_backtrack(d1, [d1_], d1s) why2 = gm.bfs_backtrack(d2, [d2_], d2s) why3 = gm.bfs_backtrack(d3, [d3_], d3s) why4 = gm.bfs_backtrack(d4, [d4_], d4s) why = why0 + why1 + why2 + why3 + why4 if min_why is None or len(why) < len(min_why[0]): min_why = why, ang12, ang34, why0, why1, why2, why3, why4 if min_why is None: return None _, ang12, ang34, why0, why1, why2, why3, why4 = min_why d1_, d2_ = ang12._l d3_, d4_ = ang34._l if d1 == d1_ and d2 == d2_ and d3 == d3_ and d4 == d4_: return why0 (a_, b_), (c_, d_) = d1_._obj.points, d2_._obj.points (e_, f_), (g_, h_) = d3_._obj.points, d4_._obj.points deps = [] if why0: dep = Dependency('eqratio', [a_, b_, c_, d_, e_, f_, g_, h_], '', level) dep.why = why0 deps.append(dep) (a, b), (c, d) = d1._obj.points, d2._obj.points (e, f), (g, h) = d3._obj.points, d4._obj.points for why, (x, y), (x_, y_) in zip( [why1, why2, why3, why4], [(a, b), (c, d), (e, f), (g, h)], [(a_, b_), (c_, d_), (e_, f_), (g_, h_)], ): if why: dep = Dependency('cong', [x, y, x_, y_], '', level) dep.why = why deps.append(dep) return deps def why_eqangle( d1: gm.Direction, d2: gm.Direction, d3: gm.Direction, d4: gm.Direction, level: int, verbose: bool = False, ) -> list[Dependency]: """Why two angles are equal, returns a Dependency objects.""" all12 = list(gm.all_angles(d1, d2, level)) all34 = list(gm.all_angles(d3, d4, level)) min_why = None for ang12, d1s, d2s in all12: for ang34, d3s, d4s in all34: why0 = gm.why_equal(ang12, ang34, level) if why0 is None: continue d1_, d2_ = ang12._d d3_, d4_ = ang34._d why1 = gm.bfs_backtrack(d1, [d1_], d1s) why2 = gm.bfs_backtrack(d2, [d2_], d2s) why3 = gm.bfs_backtrack(d3, [d3_], d3s) why4 = gm.bfs_backtrack(d4, [d4_], d4s) why = why0 + why1 + why2 + why3 + why4 if min_why is None or len(why) < len(min_why[0]): min_why = why, ang12, ang34, why0, why1, why2, why3, why4 if min_why is None: return None _, ang12, ang34, why0, why1, why2, why3, why4 = min_why why0 = gm.why_equal(ang12, ang34, level) d1_, d2_ = ang12._d d3_, d4_ = ang34._d if d1 == d1_ and d2 == d2_ and d3 == d3_ and d4 == d4_: return (d1_, d2_, d3_, d4_), why0 (a_, b_), (c_, d_) = d1_._obj.points, d2_._obj.points (e_, f_), (g_, h_) = d3_._obj.points, d4_._obj.points deps = [] if why0: dep = Dependency('eqangle', [a_, b_, c_, d_, e_, f_, g_, h_], '', None) dep.why = why0 deps.append(dep) (a, b), (c, d) = d1._obj.points, d2._obj.points (e, f), (g, h) = d3._obj.points, d4._obj.points for why, d_xy, (x, y), d_xy_, (x_, y_) in zip( [why1, why2, why3, why4], [d1, d2, d3, d4], [(a, b), (c, d), (e, f), (g, h)], [d1_, d2_, d3_, d4_], [(a_, b_), (c_, d_), (e_, f_), (g_, h_)], ): xy, xy_ = d_xy._obj, d_xy_._obj if why: if xy == xy_: name = 'collx' else: name = 'para' dep = Dependency(name, [x_, y_, x, y], '', None) dep.why = why deps.append(dep) return (d1_, d2_, d3_, d4_), deps CONSTRUCTION_RULE = 'c0' class EmptyDependency: """Empty dependency predicate ready to get filled up.""" def __init__(self, level: int, rule_name: str): self.level = level self.rule_name = rule_name or '' self.empty = True self.why = [] self.trace = None def populate(self, name: str, args: list[gm.Point]) -> Dependency: dep = Dependency(name, args, self.rule_name, self.level) dep.trace2 = self.trace dep.why = list(self.why) return dep def copy(self) -> EmptyDependency: other = EmptyDependency(self.level, self.rule_name) other.why = list(self.why) return other def extend( self, g: Any, name0: str, args0: list[gm.Point], name: str, args: list[gm.Point], ) -> EmptyDependency: """Extend the dependency list by (name, args).""" dep0 = self.populate(name0, args0) deps = EmptyDependency(level=self.level, rule_name=None) dep = Dependency(name, args, None, deps.level) deps.why = [dep0, dep.why_me_or_cache(g, None)] return deps def extend_many( self, g: Any, name0: str, args0: list[gm.Point], name_args: list[tuple[str, list[gm.Point]]], ) -> EmptyDependency: """Extend the dependency list by many name_args.""" if not name_args: return self dep0 = self.populate(name0, args0) deps = EmptyDependency(level=self.level, rule_name=None) deps.why = [dep0] for name, args in name_args: dep = Dependency(name, args, None, deps.level) deps.why += [dep.why_me_or_cache(g, None)] return deps def maybe_make_equal_pairs( a: gm.Point, b: gm.Point, c: gm.Point, d: gm.Point, m: gm.Point, n: gm.Point, p: gm.Point, q: gm.Point, ab: gm.Line, mn: gm.Line, g: Any, level: int, ) -> list[Dependency]: """Make a-b:c-d==m-n:p-q in case a-b==m-n or c-d==p-q.""" if ab != mn: return why = [] eqname = 'para' if isinstance(ab, gm.Line) else 'cong' colls = [a, b, m, n] if len(set(colls)) > 2 and eqname == 'para': dep = Dependency('collx', colls, None, level) dep.why_me(g, level) why += [dep] dep = Dependency(eqname, [c, d, p, q], None, level) dep.why_me(g, level) why += [dep] return why class Dependency(Construction): """Dependency is a predicate that other predicates depend on.""" def __init__( self, name: str, args: list[gm.Point], rule_name: str, level: int ): super().__init__(name, args) self.rule_name = rule_name or '' self.level = level self.why = [] self._stat = None self.trace = None def _find(self, dep_hashed: tuple[str, ...]) -> Dependency: for w in self.why: f = w._find(dep_hashed) if f: return f if w.hashed() == dep_hashed: return w def remove_loop(self) -> Dependency: f = self._find(self.hashed()) if f: return f return self def copy(self) -> Dependency: dep = Dependency(self.name, self.args, self.rule_name, self.level) dep.trace = self.trace dep.why = list(self.why) return dep def why_me_or_cache(self, g: Any, level: int) -> Dependency: if self.hashed() in g.cache: return g.cache[self.hashed()] self.why_me(g, level) return self def populate(self, name: str, args: list[gm.Point]) -> Dependency: assert self.rule_name == CONSTRUCTION_RULE, self.rule_name dep = Dependency(self.name, self.args, self.rule_name, self.level) dep.why = list(self.why) return dep def why_me(self, g: Any, level: int) -> None: """Figure out the dependencies predicates of self.""" name, args = self.name, self.args hashed_me = hashed(name, args) if hashed_me in g.cache: dep = g.cache[hashed_me] self.why = dep.why self.rule_name = dep.rule_name return if self.name == 'para': a, b, c, d = self.args if {a, b} == {c, d}: self.why = [] return ab = g._get_line(a, b) cd = g._get_line(c, d) if ab == cd: if {a, b} == {c, d}: self.why = [] self.rule_name = '' return dep = Dependency('coll', list({a, b, c, d}), 't??', None) self.why = [dep.why_me_or_cache(g, level)] return for (x, y), xy in zip([(a, b), (c, d)], [ab, cd]): x_, y_ = xy.points if {x, y} == {x_, y_}: continue d = Dependency('collx', [x, y, x_, y_], None, level) self.why += [d.why_me_or_cache(g, level)] whypara = g.why_equal(ab, cd, None) self.why += whypara elif self.name == 'midp': m, a, b = self.args ma = g._get_segment(m, a) mb = g._get_segment(m, b) dep = Dependency('coll', [m, a, b], None, None).why_me_or_cache(g, None) self.why = [dep] + g.why_equal(ma, mb, level) elif self.name == 'perp': a, b, c, d = self.args ab = g._get_line(a, b) cd = g._get_line(c, d) for (x, y), xy in zip([(a, b), (c, d)], [ab, cd]): x_, y_ = xy.points if {x, y} == {x_, y_}: continue d = Dependency('collx', [x, y, x_, y_], None, level) self.why += [d.why_me_or_cache(g, level)] _, why = why_eqangle(ab._val, cd._val, cd._val, ab._val, level) a, b = ab.points c, d = cd.points if hashed(self.name, [a, b, c, d]) != self.hashed(): d = Dependency(self.name, [a, b, c, d], None, level) d.why = why why = [d] self.why += why elif self.name == 'cong': a, b, c, d = self.args ab = g._get_segment(a, b) cd = g._get_segment(c, d) self.why = g.why_equal(ab, cd, level) elif self.name == 'coll': _, why = gm.line_of_and_why(self.args, level) self.why = why elif self.name == 'collx': if g.check_coll(self.args): args = list(set(self.args)) hashed_me = hashed('coll', args) if hashed_me in g.cache: dep = g.cache[hashed_me] self.why = [dep] self.rule_name = '' return _, self.why = gm.line_of_and_why(args, level) else: self.name = 'para' self.why_me(g, level) elif self.name == 'cyclic': _, why = gm.circle_of_and_why(self.args, level) self.why = why elif self.name == 'circle': o, a, b, c = self.args oa = g._get_segment(o, a) ob = g._get_segment(o, b) oc = g._get_segment(o, c) self.why = g.why_equal(oa, ob, level) + g.why_equal(oa, oc, level) elif self.name in ['eqangle', 'eqangle6']: a, b, c, d, m, n, p, q = self.args ab, why1 = g.get_line_thru_pair_why(a, b) cd, why2 = g.get_line_thru_pair_why(c, d) mn, why3 = g.get_line_thru_pair_why(m, n) pq, why4 = g.get_line_thru_pair_why(p, q) if ab is None or cd is None or mn is None or pq is None: if {a, b} == {m, n}: d = Dependency('para', [c, d, p, q], None, level) self.why = [d.why_me_or_cache(g, level)] if {a, b} == {c, d}: d = Dependency('para', [p, q, m, n], None, level) self.why = [d.why_me_or_cache(g, level)] if {c, d} == {p, q}: d = Dependency('para', [a, b, m, n], None, level) self.why = [d.why_me_or_cache(g, level)] if {p, q} == {m, n}: d = Dependency('para', [a, b, c, d], None, level) self.why = [d.why_me_or_cache(g, level)] return for (x, y), xy, whyxy in zip( [(a, b), (c, d), (m, n), (p, q)], [ab, cd, mn, pq], [why1, why2, why3, why4], ): x_, y_ = xy.points if {x, y} == {x_, y_}: continue d = Dependency('collx', [x, y, x_, y_], None, level) d.why = whyxy self.why += [d] a, b = ab.points c, d = cd.points m, n = mn.points p, q = pq.points diff = hashed(self.name, [a, b, c, d, m, n, p, q]) != self.hashed() whyeqangle = None if ab._val and cd._val and mn._val and pq._val: whyeqangle = why_eqangle(ab._val, cd._val, mn._val, pq._val, level) if whyeqangle: (dab, dcd, dmn, dpq), whyeqangle = whyeqangle if diff: d = Dependency('eqangle', [a, b, c, d, m, n, p, q], None, level) d.why = whyeqangle whyeqangle = [d] self.why += whyeqangle else: if (ab == cd and mn == pq) or (ab == mn and cd == pq): self.why += [] elif ab == mn: self.why += maybe_make_equal_pairs( a, b, c, d, m, n, p, q, ab, mn, g, level ) elif cd == pq: self.why += maybe_make_equal_pairs( c, d, a, b, p, q, m, n, cd, pq, g, level ) elif ab == cd: self.why += maybe_make_equal_pairs( a, b, m, n, c, d, p, q, ab, cd, g, level ) elif mn == pq: self.why += maybe_make_equal_pairs( m, n, a, b, p, q, c, d, mn, pq, g, level ) elif g.is_equal(ab, mn) or g.is_equal(cd, pq): dep1 = Dependency('para', [a, b, m, n], None, level) dep1.why_me(g, level) dep2 = Dependency('para', [c, d, p, q], None, level) dep2.why_me(g, level) self.why += [dep1, dep2] elif g.is_equal(ab, cd) or g.is_equal(mn, pq): dep1 = Dependency('para', [a, b, c, d], None, level) dep1.why_me(g, level) dep2 = Dependency('para', [m, n, p, q], None, level) dep2.why_me(g, level) self.why += [dep1, dep2] elif ab._val and cd._val and mn._val and pq._val: self.why = why_eqangle(ab._val, cd._val, mn._val, pq._val, level) elif self.name in ['eqratio', 'eqratio6']: a, b, c, d, m, n, p, q = self.args ab = g._get_segment(a, b) cd = g._get_segment(c, d) mn = g._get_segment(m, n) pq = g._get_segment(p, q) if ab is None or cd is None or mn is None or pq is None: if {a, b} == {m, n}: d = Dependency('cong', [c, d, p, q], None, level) self.why = [d.why_me_or_cache(g, level)] if {a, b} == {c, d}: d = Dependency('cong', [p, q, m, n], None, level) self.why = [d.why_me_or_cache(g, level)] if {c, d} == {p, q}: d = Dependency('cong', [a, b, m, n], None, level) self.why = [d.why_me_or_cache(g, level)] if {p, q} == {m, n}: d = Dependency('cong', [a, b, c, d], None, level) self.why = [d.why_me_or_cache(g, level)] return if ab._val and cd._val and mn._val and pq._val: self.why = why_eqratio(ab._val, cd._val, mn._val, pq._val, level) if self.why is None: self.why = [] if (ab == cd and mn == pq) or (ab == mn and cd == pq): self.why = [] elif ab == mn: self.why += maybe_make_equal_pairs( a, b, c, d, m, n, p, q, ab, mn, g, level ) elif cd == pq: self.why += maybe_make_equal_pairs( c, d, a, b, p, q, m, n, cd, pq, g, level ) elif ab == cd: self.why += maybe_make_equal_pairs( a, b, m, n, c, d, p, q, ab, cd, g, level ) elif mn == pq: self.why += maybe_make_equal_pairs( m, n, a, b, p, q, c, d, mn, pq, g, level ) elif g.is_equal(ab, mn) or g.is_equal(cd, pq): dep1 = Dependency('cong', [a, b, m, n], None, level) dep1.why_me(g, level) dep2 = Dependency('cong', [c, d, p, q], None, level) dep2.why_me(g, level) self.why += [dep1, dep2] elif g.is_equal(ab, cd) or g.is_equal(mn, pq): dep1 = Dependency('cong', [a, b, c, d], None, level) dep1.why_me(g, level) dep2 = Dependency('cong', [m, n, p, q], None, level) dep2.why_me(g, level) self.why += [dep1, dep2] elif ab._val and cd._val and mn._val and pq._val: self.why = why_eqangle(ab._val, cd._val, mn._val, pq._val, level) elif self.name in ['diff', 'npara', 'nperp', 'ncoll', 'sameside']: self.why = [] elif self.name == 'simtri': a, b, c, x, y, z = self.args dep1 = Dependency('eqangle', [a, b, a, c, x, y, x, z], '', level) dep1.why_me(g, level) dep2 = Dependency('eqangle', [b, a, b, c, y, x, y, z], '', level) dep2.why_me(g, level) self.rule_name = 'r34' self.why = [dep1, dep2] elif self.name == 'contri': a, b, c, x, y, z = self.args dep1 = Dependency('cong', [a, b, x, y], '', level) dep1.why_me(g, level) dep2 = Dependency('cong', [b, c, y, z], '', level) dep2.why_me(g, level) dep3 = Dependency('cong', [c, a, z, x], '', level) dep3.why_me(g, level) self.rule_name = 'r32' self.why = [dep1, dep2, dep3] elif self.name == 'ind': pass elif self.name == 'aconst': a, b, c, d, ang0 = self.args measure = ang0._val for ang in measure.neighbors(gm.Angle): if ang == ang0: continue d1, d2 = ang._d l1, l2 = d1._obj, d2._obj (a1, b1), (c1, d1) = l1.points, l2.points if not g.check_para_or_coll([a, b, a1, b1]) or not g.check_para_or_coll( [c, d, c1, d1] ): continue self.why = [] for args in [(a, b, a1, b1), (c, d, c1, d1)]: if g.check_coll(args): if len(set(args)) > 2: dep = Dependency('coll', args, None, None) self.why.append(dep.why_me_or_cache(g, level)) else: dep = Dependency('para', args, None, None) self.why.append(dep.why_me_or_cache(g, level)) self.why += gm.why_equal(ang, ang0) break elif self.name == 'rconst': a, b, c, d, rat0 = self.args val = rat0._val for rat in val.neighbors(gm.Ratio): if rat == rat0: continue l1, l2 = rat._l s1, s2 = l1._obj, l2._obj (a1, b1), (c1, d1) = list(s1.points), list(s2.points) if not g.check_cong([a, b, a1, b1]) or not g.check_cong([c, d, c1, d1]): continue self.why = [] for args in [(a, b, a1, b1), (c, d, c1, d1)]: if len(set(args)) > 2: dep = Dependency('cong', args, None, None) self.why.append(dep.why_me_or_cache(g, level)) self.why += gm.why_equal(rat, rat0) break else: raise ValueError('Not recognize', self.name) def hashed(self, rename: bool = False) -> tuple[str, ...]: return hashed(self.name, self.args, rename=rename) def hashed( name: str, args: list[gm.Point], rename: bool = False ) -> tuple[str, ...]: if name == 's_angle': args = [p.name if not rename else p.new_name for p in args[:-1]] + [ str(args[-1]) ] else: args = [p.name if not rename else p.new_name for p in args] return hashed_txt(name, args) def hashed_txt(name: str, args: list[str]) -> tuple[str, ...]: """Return a tuple unique to name and args upto arg permutation equivariant.""" if name in ['const', 'aconst', 'rconst']: a, b, c, d, y = args a, b = sorted([a, b]) c, d = sorted([c, d]) return name, a, b, c, d, y if name in ['npara', 'nperp', 'para', 'cong', 'perp', 'collx']: a, b, c, d = args a, b = sorted([a, b]) c, d = sorted([c, d]) (a, b), (c, d) = sorted([(a, b), (c, d)]) return (name, a, b, c, d) if name in ['midp', 'midpoint']: a, b, c = args b, c = sorted([b, c]) return (name, a, b, c) if name in ['coll', 'cyclic', 'ncoll', 'diff', 'triangle']: return (name,) + tuple(sorted(list(set(args)))) if name == 'circle': x, a, b, c = args return (name, x) + tuple(sorted([a, b, c])) if name in ['eqangle', 'eqratio', 'eqangle6', 'eqratio6']: a, b, c, d, e, f, g, h = args a, b = sorted([a, b]) c, d = sorted([c, d]) e, f = sorted([e, f]) g, h = sorted([g, h]) if tuple(sorted([a, b, e, f])) > tuple(sorted([c, d, g, h])): a, b, e, f, c, d, g, h = c, d, g, h, a, b, e, f if (a, b, c, d) > (e, f, g, h): a, b, c, d, e, f, g, h = e, f, g, h, a, b, c, d if name == 'eqangle6': name = 'eqangle' if name == 'eqratio6': name = 'eqratio' return (name,) + (a, b, c, d, e, f, g, h) if name in ['contri', 'simtri', 'simtri2', 'contri2', 'contri*', 'simtri*']: a, b, c, x, y, z = args (a, x), (b, y), (c, z) = sorted([(a, x), (b, y), (c, z)], key=sorted) (a, b, c), (x, y, z) = sorted([(a, b, c), (x, y, z)], key=sorted) return (name, a, b, c, x, y, z) if name in ['eqratio3']: a, b, c, d, o, o = args # pylint: disable=redeclared-assigned-name (a, c), (b, d) = sorted([(a, c), (b, d)], key=sorted) (a, b), (c, d) = sorted([(a, b), (c, d)], key=sorted) return (name, a, b, c, d, o, o) if name in ['sameside', 's_angle']: return (name,) + tuple(args) raise ValueError(f'Not recognize {name} to hash.')