HugoVoxx commited on
Commit
be3b34d
·
verified ·
1 Parent(s): ce9c114

Upload 96 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. ag4masses/alphageometry/__pycache__/alphageometry.cpython-310.pyc +0 -0
  2. ag4masses/alphageometry/__pycache__/ar.cpython-310.pyc +0 -0
  3. ag4masses/alphageometry/__pycache__/beam_search.cpython-310.pyc +0 -0
  4. ag4masses/alphageometry/__pycache__/dd.cpython-310.pyc +0 -0
  5. ag4masses/alphageometry/__pycache__/ddar.cpython-310.pyc +0 -0
  6. ag4masses/alphageometry/__pycache__/decoder_stack.cpython-310.pyc +0 -0
  7. ag4masses/alphageometry/__pycache__/geometry.cpython-310.pyc +0 -0
  8. ag4masses/alphageometry/__pycache__/graph.cpython-310.pyc +0 -0
  9. ag4masses/alphageometry/__pycache__/graph_utils.cpython-310.pyc +0 -0
  10. ag4masses/alphageometry/__pycache__/lm_inference.cpython-310.pyc +0 -0
  11. ag4masses/alphageometry/__pycache__/models.cpython-310.pyc +0 -0
  12. ag4masses/alphageometry/__pycache__/numericals.cpython-310.pyc +0 -0
  13. ag4masses/alphageometry/__pycache__/pretty.cpython-310.pyc +0 -0
  14. ag4masses/alphageometry/__pycache__/problem.cpython-310.pyc +0 -0
  15. ag4masses/alphageometry/__pycache__/trace_back.cpython-310.pyc +0 -0
  16. ag4masses/alphageometry/__pycache__/transformer_layer.cpython-310.pyc +0 -0
  17. ag4masses/alphageometry/alphageometry.py +755 -0
  18. ag4masses/alphageometry/alphageometry_test.py +103 -0
  19. ag4masses/alphageometry/ar.py +752 -0
  20. ag4masses/alphageometry/ar_test.py +204 -0
  21. ag4masses/alphageometry/beam_search.py +463 -0
  22. ag4masses/alphageometry/dd.py +1156 -0
  23. ag4masses/alphageometry/dd_test.py +79 -0
  24. ag4masses/alphageometry/ddar.py +159 -0
  25. ag4masses/alphageometry/ddar_test.py +65 -0
  26. ag4masses/alphageometry/decoder_stack.py +55 -0
  27. ag4masses/alphageometry/defs.txt +407 -0
  28. ag4masses/alphageometry/download.sh +17 -0
  29. ag4masses/alphageometry/examples.txt +8 -0
  30. ag4masses/alphageometry/fig1.svg +0 -0
  31. ag4masses/alphageometry/geometry.py +578 -0
  32. ag4masses/alphageometry/geometry_150M_generate.gin +47 -0
  33. ag4masses/alphageometry/geometry_test.py +80 -0
  34. ag4masses/alphageometry/graph.py +3057 -0
  35. ag4masses/alphageometry/graph_test.py +164 -0
  36. ag4masses/alphageometry/graph_utils.py +132 -0
  37. ag4masses/alphageometry/graph_utils_test.py +145 -0
  38. ag4masses/alphageometry/imo_ag_30.txt +60 -0
  39. ag4masses/alphageometry/jgex_ag_231.txt +462 -0
  40. ag4masses/alphageometry/lm_inference.py +189 -0
  41. ag4masses/alphageometry/lm_inference_test.py +89 -0
  42. ag4masses/alphageometry/models.py +178 -0
  43. ag4masses/alphageometry/numericals.py +1923 -0
  44. ag4masses/alphageometry/numericals_test.py +313 -0
  45. ag4masses/alphageometry/pretty.py +216 -0
  46. ag4masses/alphageometry/problem.py +1133 -0
  47. ag4masses/alphageometry/problem_test.py +61 -0
  48. ag4masses/alphageometry/rules.txt +43 -0
  49. ag4masses/alphageometry/trace_back.py +374 -0
  50. ag4masses/alphageometry/trace_back_test.py +61 -0
ag4masses/alphageometry/__pycache__/alphageometry.cpython-310.pyc ADDED
Binary file (17.8 kB). View file
 
ag4masses/alphageometry/__pycache__/ar.cpython-310.pyc ADDED
Binary file (22.2 kB). View file
 
ag4masses/alphageometry/__pycache__/beam_search.cpython-310.pyc ADDED
Binary file (9.45 kB). View file
 
ag4masses/alphageometry/__pycache__/dd.cpython-310.pyc ADDED
Binary file (28.1 kB). View file
 
ag4masses/alphageometry/__pycache__/ddar.cpython-310.pyc ADDED
Binary file (3.44 kB). View file
 
ag4masses/alphageometry/__pycache__/decoder_stack.cpython-310.pyc ADDED
Binary file (1.44 kB). View file
 
ag4masses/alphageometry/__pycache__/geometry.cpython-310.pyc ADDED
Binary file (15.7 kB). View file
 
ag4masses/alphageometry/__pycache__/graph.cpython-310.pyc ADDED
Binary file (77.3 kB). View file
 
ag4masses/alphageometry/__pycache__/graph_utils.cpython-310.pyc ADDED
Binary file (3.41 kB). View file
 
ag4masses/alphageometry/__pycache__/lm_inference.cpython-310.pyc ADDED
Binary file (5.45 kB). View file
 
ag4masses/alphageometry/__pycache__/models.cpython-310.pyc ADDED
Binary file (5.1 kB). View file
 
ag4masses/alphageometry/__pycache__/numericals.cpython-310.pyc ADDED
Binary file (54.4 kB). View file
 
ag4masses/alphageometry/__pycache__/pretty.cpython-310.pyc ADDED
Binary file (5.25 kB). View file
 
ag4masses/alphageometry/__pycache__/problem.cpython-310.pyc ADDED
Binary file (31.4 kB). View file
 
ag4masses/alphageometry/__pycache__/trace_back.cpython-310.pyc ADDED
Binary file (8.75 kB). View file
 
ag4masses/alphageometry/__pycache__/transformer_layer.cpython-310.pyc ADDED
Binary file (10.5 kB). View file
 
ag4masses/alphageometry/alphageometry.py ADDED
@@ -0,0 +1,755 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Run DD+AR or AlphaGeometry solver.
17
+
18
+ Please refer to README.md for detailed instructions.
19
+ """
20
+
21
+ import time
22
+ import traceback
23
+
24
+ from absl import app
25
+ from absl import flags
26
+ from absl import logging
27
+ import ddar
28
+ import graph as gh
29
+ import lm_inference as lm
30
+ import pretty as pt
31
+ import problem as pr
32
+
33
+ #=============
34
+ import sys, os, math, re
35
+ import multiprocessing
36
+ import warnings
37
+ warnings.filterwarnings("ignore")
38
+ model = None # global variable used in multi-processing workers
39
+
40
+ _GIN_SEARCH_PATHS = flags.DEFINE_list(
41
+ 'gin_search_paths',
42
+ ['third_party/py/meliad/transformer/configs'],
43
+ 'List of paths where the Gin config files are located.',
44
+ )
45
+ _GIN_FILE = flags.DEFINE_multi_string(
46
+ 'gin_file', ['base_htrans.gin'], 'List of Gin config files.'
47
+ )
48
+ _GIN_PARAM = flags.DEFINE_multi_string(
49
+ 'gin_param', None, 'Newline separated list of Gin parameter bindings.'
50
+ )
51
+
52
+ _PROBLEMS_FILE = flags.DEFINE_string(
53
+ 'problems_file',
54
+ 'imo_ag_30.txt',
55
+ 'text file contains the problem strings. See imo_ag_30.txt for example.',
56
+ )
57
+ _PROBLEM_NAME = flags.DEFINE_string(
58
+ 'problem_name',
59
+ 'imo_2000_p1',
60
+ 'name of the problem to solve, must be in the problem_file.',
61
+ )
62
+ _MODE = flags.DEFINE_string(
63
+ 'mode', 'ddar', 'either `ddar` (DD+AR) or `alphageometry`')
64
+ _DEFS_FILE = flags.DEFINE_string(
65
+ 'defs_file',
66
+ 'defs.txt',
67
+ 'definitions of available constructions to state a problem.',
68
+ )
69
+ _RULES_FILE = flags.DEFINE_string(
70
+ 'rules_file', 'rules.txt', 'list of deduction rules used by DD.'
71
+ )
72
+ _CKPT_PATH = flags.DEFINE_string('ckpt_path', '', 'checkpoint of the LM model.')
73
+ _VOCAB_PATH = flags.DEFINE_string(
74
+ 'vocab_path', '', 'path to the LM vocab file.'
75
+ )
76
+ _OUT_FILE = flags.DEFINE_string(
77
+ 'out_file', '', 'path to the solution output file.'
78
+ ) # pylint: disable=line-too-long
79
+ _BEAM_SIZE = flags.DEFINE_integer(
80
+ 'beam_size', 1, 'beam size of the proof search.'
81
+ ) # pylint: disable=line-too-long
82
+ _SEARCH_DEPTH = flags.DEFINE_integer(
83
+ 'search_depth', 1, 'search depth of the proof search.'
84
+ ) # pylint: disable=line-too-long
85
+
86
+ #===================================
87
+ _N_WORKSERS = flags.DEFINE_integer(
88
+ 'n_workers', 1, 'number of workers'
89
+ )# pylint: disable=line-too-long
90
+
91
+ DEFINITIONS = None # contains definitions of construction actions
92
+ RULES = None # contains rules of deductions
93
+
94
+
95
+ def natural_language_statement(logical_statement: pr.Dependency) -> str:
96
+ """Convert logical_statement to natural language.
97
+
98
+ Args:
99
+ logical_statement: pr.Dependency with .name and .args
100
+
101
+ Returns:
102
+ a string of (pseudo) natural language of the predicate for human reader.
103
+ """
104
+ names = [a.name.upper() for a in logical_statement.args]
105
+ names = [(n[0] + '_' + n[1:]) if len(n) > 1 else n for n in names]
106
+ return pt.pretty_nl(logical_statement.name, names)
107
+
108
+
109
+ def proof_step_string(
110
+ proof_step: pr.Dependency, refs: dict[tuple[str, ...], int], last_step: bool
111
+ ) -> str:
112
+ """Translate proof to natural language.
113
+
114
+ Args:
115
+ proof_step: pr.Dependency with .name and .args
116
+ refs: dict(hash: int) to keep track of derived predicates
117
+ last_step: boolean to keep track whether this is the last step.
118
+
119
+ Returns:
120
+ a string of (pseudo) natural language of the proof step for human reader.
121
+ """
122
+ premises, [conclusion] = proof_step
123
+
124
+ premises_nl = ' & '.join(
125
+ [
126
+ natural_language_statement(p) + ' [{:02}]'.format(refs[p.hashed()])
127
+ for p in premises
128
+ ]
129
+ )
130
+
131
+ if not premises:
132
+ premises_nl = 'similarly'
133
+
134
+ refs[conclusion.hashed()] = len(refs)
135
+
136
+ conclusion_nl = natural_language_statement(conclusion)
137
+ if not last_step:
138
+ conclusion_nl += ' [{:02}]'.format(refs[conclusion.hashed()])
139
+
140
+ return f'{premises_nl} \u21d2 {conclusion_nl}'
141
+
142
+
143
+ def write_solution(g: gh.Graph, p: pr.Problem, out_file: str) -> None:
144
+ """Output the solution to out_file.
145
+
146
+ Args:
147
+ g: gh.Graph object, containing the proof state.
148
+ p: pr.Problem object, containing the theorem.
149
+ out_file: file to write to, empty string to skip writing to file.
150
+ """
151
+ setup, aux, proof_steps, refs = ddar.get_proof_steps(
152
+ g, p.goal, merge_trivials=False
153
+ )
154
+
155
+ solution = ''
156
+ solution += 'Theo đề bài ta có:\n'
157
+ premises_nl = []
158
+ for premises, [points] in setup:
159
+ solution += ' '.join([p.name.upper() for p in points]) + ' '
160
+ if not premises:
161
+ continue
162
+ premises_nl += [
163
+ natural_language_statement(p) + ' [{:02}]'.format(refs[p.hashed()])
164
+ for p in premises
165
+ ]
166
+ solution += ': Points\n' + '\n'.join(premises_nl)
167
+
168
+ solution += '\n\nCác điểm cần dựng thêm:\n'
169
+ aux_premises_nl = []
170
+ if len(aux) == 0:
171
+ solution += 'Không cần dựng thêm điểm nào.'
172
+ else:
173
+ for premises, [points] in aux:
174
+ solution += ' '.join([p.name.upper() for p in points]) + ' '
175
+ aux_premises_nl += [
176
+ natural_language_statement(p) + ' [{:02}]'.format(refs[p.hashed()])
177
+ for p in premises
178
+ ]
179
+ solution += ': Points\n' + '\n'.join(aux_premises_nl)
180
+
181
+ # some special case where the deduction rule has a well known name.
182
+ r2name = {
183
+ 'r32': '(SSS)',
184
+ 'r33': '(SAS)',
185
+ 'r34': '(Similar Triangles)',
186
+ 'r35': '(Similar Triangles)',
187
+ 'r36': '(ASA)',
188
+ 'r37': '(ASA)',
189
+ 'r38': '(Similar Triangles)',
190
+ 'r39': '(Similar Triangles)',
191
+ 'r40': '(Congruent Triangles)',
192
+ 'a00': '(Distance chase)',
193
+ 'a01': '(Ratio chase)',
194
+ 'a02': '(Angle chase)',
195
+ }
196
+
197
+ solution += '\n\nCác bước chứng minh:\n'
198
+ for i, step in enumerate(proof_steps):
199
+ _, [con] = step
200
+ nl = proof_step_string(step, refs, last_step=i == len(proof_steps) - 1)
201
+ rule_name = r2name.get(con.rule_name, '')
202
+ nl = nl.replace('\u21d2', f'{rule_name}\u21d2 ')
203
+ solution += '{:03}. '.format(i + 1) + nl + '\n'
204
+ logging.info(solution)
205
+ if out_file:
206
+ with open(out_file, 'w') as f:
207
+ f.write(solution)
208
+ logging.info('Solution written to %s.', out_file)
209
+
210
+ def get_lm(ckpt_init: str, vocab_path: str) -> lm.LanguageModelInference:
211
+ lm.parse_gin_configuration(
212
+ _GIN_FILE.value, _GIN_PARAM.value, gin_paths=_GIN_SEARCH_PATHS.value
213
+ )
214
+
215
+ return lm.LanguageModelInference(vocab_path, ckpt_init, mode='beam_search')
216
+
217
+
218
+ def run_ddar(g: gh.Graph, p: pr.Problem, out_file: str) -> bool:
219
+ """Run DD+AR.
220
+
221
+ Args:
222
+ g: gh.Graph object, containing the proof state.
223
+ p: pr.Problem object, containing the problem statement.
224
+ out_file: path to output file if solution is found.
225
+
226
+ Returns:
227
+ Boolean, whether DD+AR finishes successfully.
228
+ """
229
+ ddar.solve(g, RULES, p, max_level=1000)
230
+
231
+ goal_args = g.names2nodes(p.goal.args)
232
+ if not g.check(p.goal.name, goal_args):
233
+ logging.info('DD+AR failed to solve the problem.')
234
+ return False
235
+
236
+ write_solution(g, p, out_file)
237
+
238
+ gh.nm.draw(
239
+ g.type2nodes[gh.Point],
240
+ g.type2nodes[gh.Line],
241
+ g.type2nodes[gh.Circle],
242
+ g.type2nodes[gh.Segment],
243
+ save_to="ag4mout/output.png",)
244
+ return True
245
+
246
+
247
+ def translate_constrained_to_constructive(
248
+ point: str, name: str, args: list[str]
249
+ ) -> tuple[str, list[str]]:
250
+ """Translate a predicate from constraint-based to construction-based.
251
+
252
+ Args:
253
+ point: str: name of the new point
254
+ name: str: name of the predicate, e.g., perp, para, etc.
255
+ args: list[str]: list of predicate args.
256
+
257
+ Returns:
258
+ (name, args): translated to constructive predicate.
259
+ """
260
+ if name in ['T', 'perp']:
261
+ a, b, c, d = args
262
+ if point in [c, d]:
263
+ a, b, c, d = c, d, a, b
264
+ if point == b:
265
+ a, b = b, a
266
+ if point == d:
267
+ c, d = d, c
268
+ if a == c and a == point:
269
+ return 'on_dia', [a, b, d]
270
+ return 'on_tline', [a, b, c, d]
271
+
272
+ elif name in ['P', 'para']:
273
+ a, b, c, d = args
274
+ if point in [c, d]:
275
+ a, b, c, d = c, d, a, b
276
+ if point == b:
277
+ a, b = b, a
278
+ return 'on_pline', [a, b, c, d]
279
+
280
+ elif name in ['D', 'cong']:
281
+ a, b, c, d = args
282
+ if point in [c, d]:
283
+ a, b, c, d = c, d, a, b
284
+ if point == b:
285
+ a, b = b, a
286
+ if point == d:
287
+ c, d = d, c
288
+ if a == c and a == point:
289
+ return 'on_bline', [a, b, d]
290
+ if b in [c, d]:
291
+ if b == d:
292
+ c, d = d, c # pylint: disable=unused-variable
293
+ return 'on_circle', [a, b, d]
294
+ return 'eqdistance', [a, b, c, d]
295
+
296
+ elif name in ['C', 'coll']:
297
+ a, b, c = args
298
+ if point == b:
299
+ a, b = b, a
300
+ if point == c:
301
+ a, b, c = c, a, b
302
+ return 'on_line', [a, b, c]
303
+
304
+ elif name in ['^', 'eqangle']:
305
+ a, b, c, d, e, f = args
306
+
307
+ if point in [d, e, f]:
308
+ a, b, c, d, e, f = d, e, f, a, b, c
309
+
310
+ x, b, y, c, d = b, c, e, d, f
311
+ if point == b:
312
+ a, b, c, d = b, a, d, c
313
+
314
+ if point == d and x == y: # x p x b = x c x p
315
+ return 'angle_bisector', [point, b, x, c]
316
+
317
+ if point == x:
318
+ return 'eqangle3', [x, a, b, y, c, d]
319
+
320
+ return 'on_aline', [a, x, b, c, y, d]
321
+
322
+ elif name in ['cyclic', 'O']:
323
+ a, b, c = [x for x in args if x != point]
324
+ return 'on_circum', [point, a, b, c]
325
+
326
+ return name, args
327
+
328
+
329
+ def check_valid_args(name: str, args: list[str]) -> bool:
330
+ """Check whether a predicate is grammarically correct.
331
+
332
+ Args:
333
+ name: str: name of the predicate
334
+ args: list[str]: args of the predicate
335
+
336
+ Returns:
337
+ bool: whether the predicate arg count is valid.
338
+ """
339
+ if name == 'perp':
340
+ if len(args) != 4:
341
+ return False
342
+ a, b, c, d = args
343
+ if len({a, b}) < 2:
344
+ return False
345
+ if len({c, d}) < 2:
346
+ return False
347
+ elif name == 'para':
348
+ if len(args) != 4:
349
+ return False
350
+ a, b, c, d = args
351
+ if len({a, b, c, d}) < 4:
352
+ return False
353
+ elif name == 'cong':
354
+ if len(args) != 4:
355
+ return False
356
+ a, b, c, d = args
357
+ if len({a, b}) < 2:
358
+ return False
359
+ if len({c, d}) < 2:
360
+ return False
361
+ elif name == 'coll':
362
+ if len(args) != 3:
363
+ return False
364
+ a, b, c = args
365
+ if len({a, b, c}) < 3:
366
+ return False
367
+ elif name == 'cyclic':
368
+ if len(args) != 4:
369
+ return False
370
+ a, b, c, d = args
371
+ if len({a, b, c, d}) < 4:
372
+ return False
373
+ elif name == 'eqangle':
374
+ if len(args) != 8:
375
+ return False
376
+ a, b, c, d, e, f, g, h = args
377
+ if len({a, b, c, d}) < 3:
378
+ return False
379
+ if len({e, f, g, h}) < 3:
380
+ return False
381
+ return True
382
+
383
+
384
+ def try_translate_constrained_to_construct(string: str, g: gh.Graph) -> str:
385
+ """Whether a string of aux construction can be constructed.
386
+
387
+ Args:
388
+ string: str: the string describing aux construction.
389
+ g: gh.Graph: the current proof state.
390
+
391
+ Returns:
392
+ str: whether this construction is valid. If not, starts with "ERROR:".
393
+ """
394
+ if string[-1] != ';':
395
+ return 'ERROR: must end with ;'
396
+
397
+ logging.info(f'PID={os.getpid()}: !! try_translate_constrained_to_construct: string=%s', string)
398
+
399
+ # sometimes the LM may return ill-formed result with multiple colons.
400
+ # example:
401
+ #
402
+ # napoleon2
403
+ # a1 a2 a3 = triangle; c3 = s_angle a1 a2 c3 30, s_angle a2 a1 c3 150; c1 = s_angle a2 a3 c1 30, s_angle a3 a2 c1 150; c2 = s_angle a3 a1 c2 30, s_angle a1 a3 c2 150 ? cong c1 c2 c1 c3
404
+ #
405
+ # in the process,
406
+ # I0210 17:58:01.513668 140016515833856 alphageometry.py:550] Decoding from {S} a : ; b : ; c : ; d : ^ a d a b 5. pi / 6. 00 ^ b d b a 1. pi / 6. 01 ; e : ^ b e b c 5. pi / 6. 02 ^ c e c b 1. pi / 6. 03 ; f : ^ a f a c 1. pi / 6. 04 ^ c f c a 5. pi / 6. 05 ? D e f e d {F1} x00 g : C a b g 06 D a g b g 07 ; x00 h : C c b h 08 D c h b h 09 ; x00
407
+ # I0210 18:01:38.182158 140016515833856 alphageometry.py:384] !! try_translate_constrained_to_construct: string=i : C a c i 10 D a i c i 11 ? V d f {F1} x00 j : D g j h j 12 D h j i j 13 ;
408
+
409
+ #XXX
410
+ # str_parts = string.split(' : ')
411
+ # if len(str_parts) != 2:
412
+ # return f'ERROR: string has multiple colons: |{string}|'
413
+ mch = re.match('(.*?)( \? | \. \{)', string)
414
+ if mch :
415
+ strFixed = mch.group(1) + ';'
416
+ logging.info(f'ID={os.getpid()}: Bad LM output: {string}. Changed to {strFixed}')
417
+ string = strFixed
418
+
419
+ # sometimes the constraint in string is empty:
420
+ # 0407 17:11:35.470240 126383800963072 alphageometry.py:394] !! try_translate_constrained_to_construct: string=j : ;
421
+ hdprem = string.split(' : ')
422
+ if len(hdprem) !=2 or hdprem[1].strip()==';' :
423
+ logging.info(f'ID={os.getpid()}: Bad LM output: {string}. ERROR')
424
+ return f'ERROR: Bad LM output: {string}'
425
+ head, prem_str = hdprem
426
+ point = head.strip()
427
+
428
+ if len(point) != 1 or point == ' ':
429
+ return f'ERROR: invalid point name {point}'
430
+
431
+ existing_points = [p.name for p in g.all_points()]
432
+ if point in existing_points:
433
+ return f'ERROR: point {point} already exists.'
434
+
435
+ prem_toks = prem_str.split()[:-1] # remove the EOS ' ;'
436
+ prems = [[]]
437
+
438
+ for i, tok in enumerate(prem_toks):
439
+ if tok.isdigit():
440
+ if i < len(prem_toks) - 1:
441
+ prems.append([])
442
+ else:
443
+ prems[-1].append(tok)
444
+
445
+ if len(prems) > 2:
446
+ return 'ERROR: there cannot be more than two predicates.'
447
+
448
+ clause_txt = point + ' = '
449
+ constructions = []
450
+
451
+ for prem in prems:
452
+ name, *args = prem
453
+
454
+ if point not in args:
455
+ return f'ERROR: {point} not found in predicate args.'
456
+
457
+ if not check_valid_args(pt.map_symbol(name), args):
458
+ return 'ERROR: Invalid predicate ' + name + ' ' + ' '.join(args)
459
+
460
+ for a in args:
461
+ if a != point and a not in existing_points:
462
+ return f'ERROR: point {a} does not exist.'
463
+
464
+ try:
465
+ name, args = translate_constrained_to_constructive(point, name, args)
466
+ except: # pylint: disable=bare-except
467
+ return 'ERROR: Invalid predicate ' + name + ' ' + ' '.join(args)
468
+
469
+ if name == 'on_aline':
470
+ if args.count(point) > 1:
471
+ return f'ERROR: on_aline involves twice {point}'
472
+
473
+ constructions += [name + ' ' + ' '.join(args)]
474
+
475
+ clause_txt += ', '.join(constructions)
476
+ clause = pr.Clause.from_txt(clause_txt)
477
+
478
+ try:
479
+ g.copy().add_clause(clause, 0, DEFINITIONS)
480
+ except: # pylint: disable=bare-except
481
+ return 'ERROR: ' + traceback.format_exc()
482
+
483
+ return clause_txt
484
+
485
+
486
+ def insert_aux_to_premise(pstring: str, auxstring: str) -> str:
487
+ """Insert auxiliary constructs from proof to premise.
488
+
489
+ Args:
490
+ pstring: str: describing the problem to solve.
491
+ auxstring: str: describing the auxiliar construction.
492
+
493
+ Returns:
494
+ str: new pstring with auxstring inserted before the conclusion.
495
+ """
496
+ setup, goal = pstring.split(' ? ')
497
+ return setup + '; ' + auxstring + ' ? ' + goal
498
+
499
+
500
+ class BeamQueue:
501
+ """Keep only the top k objects according to their values."""
502
+
503
+ def __init__(self, max_size: int = 512):
504
+ self.queue = []
505
+ self.max_size = max_size
506
+
507
+ def add(self, node: object, val: float) -> None:
508
+ """Add a new node to this queue."""
509
+
510
+ if len(self.queue) < self.max_size:
511
+ self.queue.append((val, node))
512
+ return
513
+
514
+ # Find the minimum node:
515
+ min_idx, (min_val, _) = min(enumerate(self.queue), key=lambda x: x[1])
516
+
517
+ # replace it if the new node has higher value.
518
+ if val > min_val:
519
+ self.queue[min_idx] = (val, node)
520
+
521
+ def __iter__(self):
522
+ for val, node in self.queue:
523
+ yield val, node
524
+
525
+ def __len__(self) -> int:
526
+ return len(self.queue)
527
+
528
+ #XXX
529
+ def bqsearch_init():
530
+ global model
531
+ logging.info('Worker initializing. PID=%d', os.getpid())
532
+ model = get_lm(_CKPT_PATH.value, _VOCAB_PATH.value)
533
+
534
+ def bqsearch(i_nd, srch_inputs, out_file) -> tuple[int, bool, list]: # ( iNode, solved, [ (node, score) ] )
535
+ pid = os.getpid()
536
+ logging.info(f'Worker PID={pid} called for beam search node {i_nd}')
537
+
538
+ prev_score, (g, string, pstring) = srch_inputs
539
+ logging.info(f'Worker PID={pid}: Decoding from {string}')
540
+ outputs = model.beam_decode(string, eos_tokens=[';'])
541
+
542
+ # translate lm output to the constructive language.
543
+ # so that we can update the graph representing proof states:
544
+ translations = [
545
+ try_translate_constrained_to_construct(o, g)
546
+ for o in outputs['seqs_str']
547
+ ]
548
+
549
+ # couple the lm outputs with its translations
550
+ candidates = zip(outputs['seqs_str'], translations, outputs['scores'])
551
+
552
+ # bring the highest scoring candidate first
553
+ candidates = reversed(list(candidates))
554
+
555
+ ret = []
556
+ for lm_out, translation, score in candidates:
557
+ logging.info(f'Worker PID={pid}: LM output (score={score}): "{lm_out}"')
558
+ logging.info(f'Worker PID={pid}: Translation: "{translation}"')
559
+
560
+ if translation.startswith('ERROR:'):
561
+ # the construction is invalid.
562
+ continue
563
+
564
+ # Update the constructive statement of the problem with the aux point:
565
+ candidate_pstring = insert_aux_to_premise(pstring, translation)
566
+
567
+ #XXX
568
+ logging.info(f'Worker PID={pid}: string=|{string}| lm_out=|{lm_out}|')
569
+ logging.info(f'Worker PID={pid}: Solving: "{candidate_pstring}"')
570
+ p_new = pr.Problem.from_txt(candidate_pstring)
571
+
572
+ # This is the new proof state graph representation:
573
+ g_new, _ = gh.Graph.build_problem(p_new, DEFINITIONS)
574
+
575
+ try:
576
+ if run_ddar(g_new, p_new, out_file):
577
+ logging.info('Worker PID={pid}: Solved.')
578
+ return (i_nd, True, None)
579
+ except Exception as e:
580
+ logging.info(f'Worker PID={pid}: Error in run_ddar: {e}')
581
+
582
+ # Add the candidate to the beam queue.
583
+ ret.append( [
584
+ # The string for the new node is old_string + lm output +
585
+ # the special token asking for a new auxiliary point ' x00':
586
+ # node
587
+ (g_new, string + ' ' + lm_out + ' x00', candidate_pstring),
588
+ # the score of each node is sum of score of all nodes
589
+ # on the path to itself. For beam search, there is no need to
590
+ # normalize according to path length because all nodes in beam
591
+ # is of the same path length.
592
+ # val
593
+ prev_score + score ]
594
+ )
595
+
596
+ logging.info(f'Worker PID={pid} beam search node {i_nd}: returning')
597
+ return (i_nd, False, ret)
598
+
599
+ def run_alphageometry(
600
+ #XX model: lm.LanguageModelInference,
601
+ p: pr.Problem,
602
+ search_depth: int,
603
+ beam_size: int,
604
+ out_file: str,
605
+ ) -> bool:
606
+ """Simplified code to run AlphaGeometry proof search.
607
+
608
+ We removed all optimizations that are infrastructure-dependent, e.g.
609
+ parallelized model inference on multi GPUs,
610
+ parallelized DD+AR on multiple CPUs,
611
+ parallel execution of LM and DD+AR,
612
+ shared pool of CPU workers across different problems, etc.
613
+
614
+ Many other speed optimizations and abstractions are also removed to
615
+ better present the core structure of the proof search.
616
+
617
+ Args:
618
+ model: Interface with inference-related endpoints to JAX's model.
619
+ p: pr.Problem object describing the problem to solve.
620
+ search_depth: max proof search depth.
621
+ beam_size: beam size of the proof search.
622
+ out_file: path to output file if solution is found.
623
+
624
+ Returns:
625
+ boolean of whether this is solved.
626
+ """
627
+ # translate the problem to a string of grammar that the LM is trained on.
628
+ string = p.setup_str_from_problem(DEFINITIONS)
629
+ # special tokens prompting the LM to generate auxiliary points.
630
+ string += ' {F1} x00'
631
+ # the graph to represent the proof state.
632
+ g, _ = gh.Graph.build_problem(p, DEFINITIONS)
633
+
634
+ # First we run the symbolic engine DD+AR:
635
+ if run_ddar(g, p, out_file):
636
+ return True
637
+
638
+ # ?? when pickling graph for some problems, the default recursion limit 1000 is not enough,
639
+ # got 'maximum recursion depth exceeded while pickling an object' error
640
+ sys.setrecursionlimit(10000)
641
+
642
+ # beam search for the proof
643
+ # each node in the search tree is a 3-tuple:
644
+ # (<graph representation of proof state>,
645
+ # <string for LM to decode from>,
646
+ # <original problem string>)
647
+ beam_queue = BeamQueue(max_size=beam_size)
648
+ # originally the beam search tree starts with a single node (a 3-tuple):
649
+ beam_queue.add(
650
+ node=(g, string, p.txt()), val=0.0 # value of the root node is simply 0.
651
+ )
652
+
653
+ pool = None
654
+ if _N_WORKSERS.value == 1:
655
+ bqsearch_init()
656
+ else:
657
+ pool = multiprocessing.Pool(_N_WORKSERS.value, bqsearch_init)
658
+
659
+ for depth in range(search_depth):
660
+ logging.info(
661
+ 'Depth %s. There are %i nodes to expand:', depth, len(beam_queue)
662
+ )
663
+ for _, (_, string, _) in beam_queue:
664
+ logging.info(string)
665
+
666
+ new_queue = BeamQueue(max_size=beam_size) # to replace beam_queue.
667
+ if _N_WORKSERS.value==1:
668
+ for i, srch_inputs in enumerate(beam_queue):
669
+ _, solved, res = bqsearch(i, srch_inputs, out_file)
670
+ if solved:
671
+ return True
672
+ for node, val in res:
673
+ # Add the candidate to the beam queue.
674
+ new_queue.add(node, val)
675
+ # Note that the queue only maintain at most beam_size nodes
676
+ # so this new node might possibly be dropped depending on its value.
677
+ else:
678
+ jobs = [pool.apply_async(bqsearch, (i, srch_inputs, out_file)) for i, srch_inputs in enumerate(beam_queue)]
679
+
680
+ n_done = 0
681
+ while n_done < len(beam_queue):
682
+ for i, jobres in enumerate(jobs):
683
+ if jobres and jobres.ready():
684
+ n_done += 1
685
+ jobs[i] = None
686
+ _, solved, res = jobres.get()
687
+ if solved:
688
+ # Clean up resources
689
+ pool.terminate()
690
+ pool.join()
691
+ return True
692
+ for node, val in res:
693
+ # Add the candidate to the beam queue.
694
+ new_queue.add(node, val)
695
+ # Note that the queue only maintain at most beam_size nodes
696
+ # so this new node might possibly be dropped depending on its value.
697
+ time.sleep(1) # Adjust wait time as needed
698
+
699
+ # replace the old queue with new queue before the new proof search depth.
700
+ beam_queue = new_queue
701
+
702
+ # Clean up resources
703
+ if pool:
704
+ pool.terminate()
705
+ pool.join()
706
+ return False
707
+
708
+ def main(_):
709
+ global DEFINITIONS
710
+ global RULES
711
+
712
+ # definitions of terms used in our domain-specific language.
713
+ DEFINITIONS = pr.Definition.from_txt_file(_DEFS_FILE.value, to_dict=True)
714
+ # load inference rules used in DD.
715
+ RULES = pr.Theorem.from_txt_file(_RULES_FILE.value, to_dict=True)
716
+
717
+ # when using the language model,
718
+ # point names will be renamed to alphabetical a, b, c, d, e, ...
719
+ # instead of staying with their original names,
720
+ # in order to match the synthetic training data generation.
721
+ need_rename = _MODE.value != 'ddar'
722
+
723
+ # load problems from the problems_file,
724
+ problems = pr.Problem.from_txt_file(
725
+ _PROBLEMS_FILE.value, to_dict=True, translate=need_rename
726
+ )
727
+
728
+ if _PROBLEM_NAME.value not in problems:
729
+ raise ValueError(
730
+ f'Problem name `{_PROBLEM_NAME.value}` '
731
+ + f'not found in `{_PROBLEMS_FILE.value}`'
732
+ )
733
+
734
+ this_problem = problems[_PROBLEM_NAME.value]
735
+
736
+ if _MODE.value == 'ddar':
737
+ g, _ = gh.Graph.build_problem(this_problem, DEFINITIONS)
738
+ run_ddar(g, this_problem, _OUT_FILE.value)
739
+
740
+ elif _MODE.value == 'alphageometry':
741
+ #XX model = get_lm(_CKPT_PATH.value, _VOCAB_PATH.value)
742
+ run_alphageometry(
743
+ #XX model,
744
+ this_problem,
745
+ _SEARCH_DEPTH.value,
746
+ _BEAM_SIZE.value,
747
+ _OUT_FILE.value,
748
+ )
749
+
750
+ else:
751
+ raise ValueError(f'Unknown FLAGS.mode: {_MODE.value}')
752
+
753
+
754
+ if __name__ == '__main__':
755
+ app.run(main)
ag4masses/alphageometry/alphageometry_test.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Unit tests for alphageometry.py."""
17
+
18
+ import unittest
19
+
20
+ from absl.testing import absltest
21
+ import alphageometry
22
+
23
+
24
+ class AlphaGeometryTest(unittest.TestCase):
25
+
26
+ def test_translate_constrained_to_constructive(self):
27
+ self.assertEqual(
28
+ alphageometry.translate_constrained_to_constructive(
29
+ 'd', 'T', list('addb')
30
+ ),
31
+ ('on_dia', ['d', 'b', 'a']),
32
+ )
33
+ self.assertEqual(
34
+ alphageometry.translate_constrained_to_constructive(
35
+ 'd', 'T', list('adbc')
36
+ ),
37
+ ('on_tline', ['d', 'a', 'b', 'c']),
38
+ )
39
+ self.assertEqual(
40
+ alphageometry.translate_constrained_to_constructive(
41
+ 'd', 'P', list('bcda')
42
+ ),
43
+ ('on_pline', ['d', 'a', 'b', 'c']),
44
+ )
45
+ self.assertEqual(
46
+ alphageometry.translate_constrained_to_constructive(
47
+ 'd', 'D', list('bdcd')
48
+ ),
49
+ ('on_bline', ['d', 'c', 'b']),
50
+ )
51
+ self.assertEqual(
52
+ alphageometry.translate_constrained_to_constructive(
53
+ 'd', 'D', list('bdcb')
54
+ ),
55
+ ('on_circle', ['d', 'b', 'c']),
56
+ )
57
+ self.assertEqual(
58
+ alphageometry.translate_constrained_to_constructive(
59
+ 'd', 'D', list('bacd')
60
+ ),
61
+ ('eqdistance', ['d', 'c', 'b', 'a']),
62
+ )
63
+ self.assertEqual(
64
+ alphageometry.translate_constrained_to_constructive(
65
+ 'd', 'C', list('bad')
66
+ ),
67
+ ('on_line', ['d', 'b', 'a']),
68
+ )
69
+ self.assertEqual(
70
+ alphageometry.translate_constrained_to_constructive(
71
+ 'd', 'C', list('bad')
72
+ ),
73
+ ('on_line', ['d', 'b', 'a']),
74
+ )
75
+ self.assertEqual(
76
+ alphageometry.translate_constrained_to_constructive(
77
+ 'd', 'O', list('abcd')
78
+ ),
79
+ ('on_circum', ['d', 'a', 'b', 'c']),
80
+ )
81
+
82
+ def test_insert_aux_to_premise(self):
83
+ pstring = 'a b c = triangle a b c; d = on_tline d b a c, on_tline d c a b ? perp a d b c' # pylint: disable=line-too-long
84
+ auxstring = 'e = on_line e a c, on_line e b d'
85
+
86
+ target = 'a b c = triangle a b c; d = on_tline d b a c, on_tline d c a b; e = on_line e a c, on_line e b d ? perp a d b c' # pylint: disable=line-too-long
87
+ self.assertEqual(
88
+ alphageometry.insert_aux_to_premise(pstring, auxstring), target
89
+ )
90
+
91
+ def test_beam_queue(self):
92
+ beam_queue = alphageometry.BeamQueue(max_size=2)
93
+
94
+ beam_queue.add('a', 1)
95
+ beam_queue.add('b', 2)
96
+ beam_queue.add('c', 3)
97
+
98
+ beam_queue = list(beam_queue)
99
+ self.assertEqual(beam_queue, [(3, 'c'), (2, 'b')])
100
+
101
+
102
+ if __name__ == '__main__':
103
+ absltest.main()
ag4masses/alphageometry/ar.py ADDED
@@ -0,0 +1,752 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Implementing Algebraic Reasoning (AR)."""
17
+
18
+ from collections import defaultdict # pylint: disable=g-importing-member
19
+ from fractions import Fraction as frac # pylint: disable=g-importing-member
20
+ from typing import Any, Generator
21
+
22
+ import geometry as gm
23
+ import numpy as np
24
+ import problem as pr
25
+ from scipy import optimize
26
+
27
+
28
+ class InfQuotientError(Exception):
29
+ pass
30
+
31
+
32
+ def _gcd(x: int, y: int) -> int:
33
+ while y:
34
+ x, y = y, x % y
35
+ return x
36
+
37
+
38
+ def simplify(n: int, d: int) -> tuple[int, int]:
39
+ g = _gcd(n, d)
40
+ return (n // g, d // g)
41
+
42
+
43
+ # maximum denominator for a fraction.
44
+ MAX_DENOMINATOR = 1000000
45
+
46
+ # tolerance for fraction approximation
47
+ TOL = 1e-15
48
+
49
+
50
+ def get_quotient(v: float) -> tuple[int, int]:
51
+ n = v
52
+ d = 1
53
+ while abs(n - round(n)) > TOL:
54
+ d += 1
55
+ n += v
56
+ if d > MAX_DENOMINATOR:
57
+ e = InfQuotientError(v)
58
+ raise e
59
+
60
+ n = int(round(n))
61
+ return simplify(n, d)
62
+
63
+
64
+ def fix_v(v: float) -> float:
65
+ n, d = get_quotient(v)
66
+ return n / d
67
+
68
+
69
+ def fix(e: dict[str, float]) -> dict[str, float]:
70
+ return {k: fix_v(v) for k, v in e.items()}
71
+
72
+
73
+ def frac_string(f: frac) -> str:
74
+ n, d = get_quotient(f)
75
+ return f'{n}/{d}'
76
+
77
+
78
+ def hashed(e: dict[str, float]) -> tuple[tuple[str, float], ...]:
79
+ return tuple(sorted(list(e.items())))
80
+
81
+
82
+ def is_zero(e: dict[str, float]) -> bool:
83
+ return len(strip(e)) == 0 # pylint: disable=g-explicit-length-test
84
+
85
+
86
+ def strip(e: dict[str, float]) -> dict[str, float]:
87
+ return {v: c for v, c in e.items() if c != 0}
88
+
89
+
90
+ def plus(e1: dict[str, float], e2: dict[str, float]) -> dict[str, float]:
91
+ e = dict(e1)
92
+ for v, c in e2.items():
93
+ if v in e:
94
+ e[v] += c
95
+ else:
96
+ e[v] = c
97
+ return strip(e)
98
+
99
+
100
+ def plus_all(*es: list[dict[str, float]]) -> dict[str, float]:
101
+ result = {}
102
+ for e in es:
103
+ result = plus(result, e)
104
+ return result
105
+
106
+
107
+ def mult(e: dict[str, float], m: float) -> dict[str, float]:
108
+ return {v: m * c for v, c in e.items()}
109
+
110
+
111
+ def minus(e1: dict[str, float], e2: dict[str, float]) -> dict[str, float]:
112
+ return plus(e1, mult(e2, -1))
113
+
114
+
115
+ def div(e1: dict[str, float], e2: dict[str, float]) -> float:
116
+ """Divide e1 by e2."""
117
+ e1 = strip(e1)
118
+ e2 = strip(e2)
119
+ if set(e1.keys()) != set(e2.keys()):
120
+ return None
121
+
122
+ n, d = None, None
123
+
124
+ for v, c1 in e1.items():
125
+ c2 = e2[v] # we want c1/c2 = n/d => c1*d=c2*n
126
+ if n is not None and c1 * d != c2 * n:
127
+ return None
128
+ n, d = c1, c2
129
+ return frac(n) / frac(d)
130
+
131
+
132
+ def recon(e: dict[str, float], const: str) -> tuple[str, dict[str, float]]:
133
+ """Reconcile one variable in the expression e=0, given const."""
134
+ e = strip(e)
135
+ if len(e) == 0: # pylint: disable=g-explicit-length-test
136
+ return None
137
+
138
+ v0 = None
139
+ for v in e:
140
+ if v != const:
141
+ v0 = v
142
+ break
143
+ if v0 is None:
144
+ return v0
145
+
146
+ c0 = e.pop(v0)
147
+ return v0, {v: -c / c0 for v, c in e.items()}
148
+
149
+
150
+ def replace(
151
+ e: dict[str, float], v0: str, e0: dict[str, float]
152
+ ) -> dict[str, float]:
153
+ if v0 not in e:
154
+ return e
155
+ e = dict(e)
156
+ m = e.pop(v0)
157
+ return plus(e, mult(e0, m))
158
+
159
+
160
+ def comb2(elems: list[Any]) -> Generator[tuple[Any, Any], None, None]:
161
+ if len(elems) < 1:
162
+ return
163
+ for i, e1 in enumerate(elems[:-1]):
164
+ for e2 in elems[i + 1 :]:
165
+ yield e1, e2
166
+
167
+
168
+ def perm2(elems: list[Any]) -> Generator[tuple[Any, Any], None, None]:
169
+ for e1, e2 in comb2(elems):
170
+ yield e1, e2
171
+ yield e2, e1
172
+
173
+
174
+ def chain2(elems: list[Any]) -> Generator[tuple[Any, Any], None, None]:
175
+ if len(elems) < 2:
176
+ return
177
+ for i, e1 in enumerate(elems[:-1]):
178
+ yield e1, elems[i + 1]
179
+
180
+
181
+ def update_groups(
182
+ groups1: list[Any], groups2: list[Any]
183
+ ) -> tuple[list[Any], list[tuple[Any, Any]], list[list[Any]]]:
184
+ """Update groups of equivalent elements.
185
+
186
+ Given groups1 = [set1, set2, set3, ..]
187
+ where all elems within each set_i is defined to be "equivalent" to each other.
188
+ (but not across the sets)
189
+
190
+ Incoming groups2 = [set1, set2, ...] similar to set1 - it is the
191
+ additional equivalent information on elements in groups1.
192
+
193
+ Return the new updated groups1 and the set of links
194
+ that make it that way.
195
+
196
+ Example:
197
+ groups1 = [{1, 2}, {3, 4, 5}, {6, 7}]
198
+ groups2 = [{2, 3, 8}, {9, 10, 11}]
199
+
200
+ => new groups1 and links:
201
+ groups1 = [{1, 2, 3, 4, 5, 8}, {6, 7}, {9, 10, 11}]
202
+ links = (2, 3), (3, 8), (9, 10), (10, 11)
203
+
204
+ Explain: since groups2 says 2 and 3 are equivalent (with {2, 3, 8}),
205
+ then {1, 2} and {3, 4, 5} in groups1 will be merged,
206
+ because 2 and 3 each belong to those 2 groups.
207
+ Additionally 8 also belong to this same group.
208
+ {3, 4, 5} is left alone, while {9, 10, 11} is a completely new set.
209
+
210
+ The links to make this all happens is:
211
+ (2, 3): to merge {1, 2} and {3, 4, 5}
212
+ (3, 8): to link 8 into the merged({1, 2, 3, 4, 5})
213
+ (9, 10) and (10, 11): to make the new group {9, 10, 11}
214
+
215
+ Args:
216
+ groups1: a list of sets.
217
+ groups2: a list of sets.
218
+
219
+ Returns:
220
+ groups1, links, history: result of the update.
221
+ """
222
+ history = []
223
+ links = []
224
+ for g2 in groups2:
225
+ joins = [None] * len(groups1) # mark which one in groups1 is merged
226
+ merged_g1 = set() # merge them into this.
227
+ old = None # any elem in g2 that belong to any set in groups1 (old)
228
+ new = [] # all elem in g2 that is new
229
+
230
+ for e in g2:
231
+ found = False
232
+ for i, g1 in enumerate(groups1):
233
+ if e not in g1:
234
+ continue
235
+
236
+ found = True
237
+ if joins[i]:
238
+ continue
239
+
240
+ joins[i] = True
241
+ merged_g1.update(g1)
242
+
243
+ if old is not None:
244
+ links.append((old, e)) # link to make merging happen.
245
+ old = e
246
+
247
+ if not found: # e is new!
248
+ new.append(e)
249
+
250
+ # now chain elems in new together.
251
+ if old is not None and new:
252
+ links.append((old, new[0]))
253
+ merged_g1.update(new)
254
+
255
+ links += chain2(new)
256
+
257
+ new_groups1 = []
258
+ if merged_g1: # put the merged_g1 in first
259
+ new_groups1.append(merged_g1)
260
+
261
+ # put the remaining (unjoined) groups in
262
+ new_groups1 += [g1 for j, g1 in zip(joins, groups1) if not j]
263
+
264
+ if old is None and new:
265
+ new_groups1 += [set(new)]
266
+
267
+ groups1 = new_groups1
268
+ history.append(groups1)
269
+
270
+ return groups1, links, history
271
+
272
+
273
+ class Table:
274
+ """The coefficient matrix."""
275
+
276
+ def __init__(self, const: str = '1'):
277
+ self.const = const
278
+ self.v2e = {}
279
+ self.add_free(const) # the table {var: expression}
280
+
281
+ # to cache what is already derived/inputted
282
+ self.eqs = set()
283
+ self.groups = [] # groups of equal pairs.
284
+
285
+ # for why (linprog)
286
+ self.c = []
287
+ self.v2i = {} # v -> index of row in A.
288
+ self.deps = [] # equal number of columns.
289
+ self.A = np.zeros([0, 0]) # pylint: disable=invalid-name
290
+ self.do_why = True
291
+
292
+ def add_free(self, v: str) -> None:
293
+ self.v2e[v] = {v: frac(1)}
294
+
295
+ def replace(self, v0: str, e0: dict[str, float]) -> None:
296
+ for v, e in list(self.v2e.items()):
297
+ self.v2e[v] = replace(e, v0, e0)
298
+
299
+ def add_expr(self, vc: list[tuple[str, float]]) -> bool:
300
+ """Add a new equality, represented by the list of tuples vc=[(v, c), ..]."""
301
+ result = {}
302
+ free = []
303
+
304
+ for v, c in vc:
305
+ c = frac(c)
306
+ if v in self.v2e:
307
+ result = plus(result, mult(self.v2e[v], c))
308
+ else:
309
+ free += [(v, c)]
310
+
311
+ if free == []: # pylint: disable=g-explicit-bool-comparison
312
+ if is_zero(self.modulo(result)):
313
+ return False
314
+ result = recon(result, self.const)
315
+ if result is None:
316
+ return False
317
+ v, e = result
318
+ self.replace(v, e)
319
+
320
+ elif len(free) == 1:
321
+ v, m = free[0]
322
+ self.v2e[v] = mult(result, frac(-1, m))
323
+
324
+ else:
325
+ dependent_v = None
326
+ for v, m in free:
327
+ if dependent_v is None and v != self.const:
328
+ dependent_v = (v, m)
329
+ continue
330
+
331
+ self.add_free(v)
332
+ result = plus(result, {v: m})
333
+
334
+ v, m = dependent_v
335
+ self.v2e[v] = mult(result, frac(-1, m))
336
+
337
+ return True
338
+
339
+ def register(self, vc: list[tuple[str, float]], dep: pr.Dependency) -> None:
340
+ """Register a new equality vc=[(v, c), ..] with traceback dependency dep."""
341
+ result = plus_all(*[{v: c} for v, c in vc])
342
+ if is_zero(result):
343
+ return
344
+
345
+ vs, _ = zip(*vc)
346
+ for v in vs:
347
+ if v not in self.v2i:
348
+ self.v2i[v] = len(self.v2i)
349
+
350
+ (m, n), l = self.A.shape, len(self.v2i)
351
+ if l > m:
352
+ self.A = np.concatenate([self.A, np.zeros([l - m, n])], 0)
353
+
354
+ new_column = np.zeros([len(self.v2i), 2]) # N, 2
355
+ for v, c in vc:
356
+ new_column[self.v2i[v], 0] += float(c)
357
+ new_column[self.v2i[v], 1] -= float(c)
358
+
359
+ self.A = np.concatenate([self.A, new_column], 1)
360
+ self.c += [1.0, -1.0]
361
+ self.deps += [dep]
362
+
363
+ def register2(
364
+ self, a: str, b: str, m: float, n: float, dep: pr.Dependency
365
+ ) -> None:
366
+ self.register([(a, m), (b, -n)], dep)
367
+
368
+ def register3(self, a: str, b: str, f: float, dep: pr.Dependency) -> None:
369
+ self.register([(a, 1), (b, -1), (self.const, -f)], dep)
370
+
371
+ def register4(
372
+ self, a: str, b: str, c: str, d: str, dep: pr.Dependency
373
+ ) -> None:
374
+ self.register([(a, 1), (b, -1), (c, -1), (d, 1)], dep)
375
+
376
+ def why(self, e: dict[str, float]) -> list[Any]:
377
+ """AR traceback == MILP."""
378
+ if not self.do_why:
379
+ return []
380
+ # why expr == 0?
381
+ # Solve min(c^Tx) s.t. A_eq * x = b_eq, x >= 0
382
+ e = strip(e)
383
+ if not e:
384
+ return []
385
+
386
+ b_eq = [0] * len(self.v2i)
387
+ for v, c in e.items():
388
+ b_eq[self.v2i[v]] += float(c)
389
+
390
+ try:
391
+ x = optimize.linprog(c=self.c, A_eq=self.A, b_eq=b_eq, method='highs')[
392
+ 'x'
393
+ ]
394
+ except: # pylint: disable=bare-except
395
+ x = optimize.linprog(
396
+ c=self.c,
397
+ A_eq=self.A,
398
+ b_eq=b_eq,
399
+ )['x']
400
+
401
+ deps = []
402
+ for i, dep in enumerate(self.deps):
403
+ if x[2 * i] > 1e-12 or x[2 * i + 1] > 1e-12:
404
+ if dep not in deps:
405
+ deps.append(dep)
406
+ return deps
407
+
408
+ def record_eq(self, v1: str, v2: str, v3: str, v4: str) -> None:
409
+ self.eqs.add((v1, v2, v3, v4))
410
+ self.eqs.add((v2, v1, v4, v3))
411
+ self.eqs.add((v3, v4, v1, v2))
412
+ self.eqs.add((v4, v3, v2, v1))
413
+
414
+ def check_record_eq(self, v1: str, v2: str, v3: str, v4: str) -> bool:
415
+ if (v1, v2, v3, v4) in self.eqs:
416
+ return True
417
+ if (v2, v1, v4, v3) in self.eqs:
418
+ return True
419
+ if (v3, v4, v1, v2) in self.eqs:
420
+ return True
421
+ if (v4, v3, v2, v1) in self.eqs:
422
+ return True
423
+ return False
424
+
425
+ def add_eq2(
426
+ self, a: str, b: str, m: float, n: float, dep: pr.Dependency
427
+ ) -> None:
428
+ # a/b = m/n
429
+ if not self.add_expr([(a, n), (b, -m)]):
430
+ return []
431
+ self.register2(a, b, m, n, dep)
432
+
433
+ def add_eq3(self, a: str, b: str, f: float, dep: pr.Dependency) -> None:
434
+ # a - b = f * constant
435
+ self.eqs.add((a, b, frac(f)))
436
+ self.eqs.add((b, a, frac(1 - f)))
437
+
438
+ if not self.add_expr([(a, 1), (b, -1), (self.const, -f)]):
439
+ return []
440
+
441
+ self.register3(a, b, f, dep)
442
+
443
+ def add_eq4(self, a: str, b: str, c: str, d: str, dep: pr.Dependency) -> None:
444
+ # a - b = c - d
445
+ self.record_eq(a, b, c, d)
446
+ self.record_eq(a, c, b, d)
447
+
448
+ expr = list(minus({a: 1, b: -1}, {c: 1, d: -1}).items())
449
+
450
+ if not self.add_expr(expr):
451
+ return []
452
+
453
+ self.register4(a, b, c, d, dep)
454
+ self.groups, _, _ = update_groups(
455
+ self.groups, [{(a, b), (c, d)}, {(b, a), (d, c)}]
456
+ )
457
+
458
+ def pairs(self) -> Generator[list[tuple[str, str]], None, None]:
459
+ for v1, v2 in perm2(list(self.v2e.keys())): # pylint: disable=g-builtin-op
460
+ if v1 == self.const or v2 == self.const:
461
+ continue
462
+ yield v1, v2
463
+
464
+ def modulo(self, e: dict[str, float]) -> dict[str, float]:
465
+ return strip(e)
466
+
467
+ def get_all_eqs(
468
+ self,
469
+ ) -> dict[tuple[tuple[str, float], ...], list[tuple[str, str]]]:
470
+ h2pairs = defaultdict(list)
471
+ for v1, v2 in self.pairs():
472
+ e1, e2 = self.v2e[v1], self.v2e[v2]
473
+ e12 = minus(e1, e2)
474
+ h12 = hashed(self.modulo(e12))
475
+ h2pairs[h12].append((v1, v2))
476
+ return h2pairs
477
+
478
+ def get_all_eqs_and_why(
479
+ self, return_quads: bool = True
480
+ ) -> Generator[Any, None, None]:
481
+ """Check all 4/3/2-permutations for new equalities."""
482
+ groups = []
483
+
484
+ for h, vv in self.get_all_eqs().items():
485
+ if h == (): # pylint: disable=g-explicit-bool-comparison
486
+ for v1, v2 in vv:
487
+ if (v1, v2) in self.eqs or (v2, v1) in self.eqs:
488
+ continue
489
+ self.eqs.add((v1, v2))
490
+ # why v1 - v2 = e12 ? (note modulo(e12) == 0)
491
+ why_dict = minus({v1: 1, v2: -1}, minus(self.v2e[v1], self.v2e[v2]))
492
+ yield v1, v2, self.why(why_dict)
493
+ continue
494
+
495
+ if len(h) == 1 and h[0][0] == self.const:
496
+ for v1, v2 in vv:
497
+ frac = h[0][1] # pylint: disable=redefined-outer-name
498
+ if (v1, v2, frac) in self.eqs:
499
+ continue
500
+ self.eqs.add((v1, v2, frac))
501
+ # why v1 - v2 = e12 ? (note modulo(e12) == 0)
502
+ why_dict = minus({v1: 1, v2: -1}, minus(self.v2e[v1], self.v2e[v2]))
503
+ value = simplify(frac.numerator, frac.denominator)
504
+ yield v1, v2, value, self.why(why_dict)
505
+ continue
506
+
507
+ groups.append(vv)
508
+
509
+ if not return_quads:
510
+ return
511
+
512
+ self.groups, links, _ = update_groups(self.groups, groups)
513
+ for (v1, v2), (v3, v4) in links:
514
+ if self.check_record_eq(v1, v2, v3, v4):
515
+ continue
516
+ e12 = minus(self.v2e[v1], self.v2e[v2])
517
+ e34 = minus(self.v2e[v3], self.v2e[v4])
518
+
519
+ why_dict = minus( # why (v1-v2)-(v3-v4)=e12-e34?
520
+ minus({v1: 1, v2: -1}, {v3: 1, v4: -1}), minus(e12, e34)
521
+ )
522
+ self.record_eq(v1, v2, v3, v4)
523
+ yield v1, v2, v3, v4, self.why(why_dict)
524
+
525
+
526
+ class GeometricTable(Table):
527
+ """Abstract class representing the coefficient matrix (table) A."""
528
+
529
+ def __init__(self, name: str = ''):
530
+ super().__init__(name)
531
+ self.v2obj = {}
532
+
533
+ def get_name(self, objs: list[Any]) -> list[str]:
534
+ self.v2obj.update({o.name: o for o in objs})
535
+ return [o.name for o in objs]
536
+
537
+ def map2obj(self, names: list[str]) -> list[Any]:
538
+ return [self.v2obj[n] for n in names]
539
+
540
+ def get_all_eqs_and_why(
541
+ self, return_quads: bool
542
+ ) -> Generator[Any, None, None]:
543
+ for out in super().get_all_eqs_and_why(return_quads):
544
+ if len(out) == 3:
545
+ x, y, why = out
546
+ x, y = self.map2obj([x, y])
547
+ yield x, y, why
548
+ if len(out) == 4:
549
+ x, y, f, why = out
550
+ x, y = self.map2obj([x, y])
551
+ yield x, y, f, why
552
+ if len(out) == 5:
553
+ a, b, x, y, why = out
554
+ a, b, x, y = self.map2obj([a, b, x, y])
555
+ yield a, b, x, y, why
556
+
557
+
558
+ class RatioTable(GeometricTable):
559
+ """Coefficient matrix A for log(distance)."""
560
+
561
+ def __init__(self, name: str = ''):
562
+ name = name or '1'
563
+ super().__init__(name)
564
+ self.one = self.const
565
+
566
+ def add_eq(self, l1: gm.Length, l2: gm.Length, dep: pr.Dependency) -> None:
567
+ l1, l2 = self.get_name([l1, l2])
568
+ return super().add_eq3(l1, l2, 0.0, dep)
569
+
570
+ def add_const_ratio(
571
+ self, l1: gm.Length, l2: gm.Length, m: float, n: float, dep: pr.Dependency
572
+ ) -> None:
573
+ l1, l2 = self.get_name([l1, l2])
574
+ return super().add_eq2(l1, l2, m, n, dep)
575
+
576
+ def add_eqratio(
577
+ self,
578
+ l1: gm.Length,
579
+ l2: gm.Length,
580
+ l3: gm.Length,
581
+ l4: gm.Length,
582
+ dep: pr.Dependency,
583
+ ) -> None:
584
+ l1, l2, l3, l4 = self.get_name([l1, l2, l3, l4])
585
+ return self.add_eq4(l1, l2, l3, l4, dep)
586
+
587
+ def get_all_eqs_and_why(self) -> Generator[Any, None, None]:
588
+ return super().get_all_eqs_and_why(True)
589
+
590
+
591
+ class AngleTable(GeometricTable):
592
+ """Coefficient matrix A for slope(direction)."""
593
+
594
+ def __init__(self, name: str = ''):
595
+ name = name or 'pi'
596
+ super().__init__(name)
597
+ self.pi = self.const
598
+
599
+ def modulo(self, e: dict[str, float]) -> dict[str, float]:
600
+ e = strip(e)
601
+ if self.pi not in e:
602
+ return super().modulo(e)
603
+
604
+ e[self.pi] = e[self.pi] % 1
605
+ return strip(e)
606
+
607
+ def add_para(
608
+ self, d1: gm.Direction, d2: gm.Direction, dep: pr.Dependency
609
+ ) -> None:
610
+ return self.add_const_angle(d1, d2, 0, dep)
611
+
612
+ def add_const_angle(
613
+ self, d1: gm.Direction, d2: gm.Direction, ang: float, dep: pr.Dependency
614
+ ) -> None:
615
+ if ang and d2._obj.num > d1._obj.num: # pylint: disable=protected-access
616
+ d1, d2 = d2, d1
617
+ ang = 180 - ang
618
+
619
+ d1, d2 = self.get_name([d1, d2])
620
+
621
+ num, den = simplify(ang, 180)
622
+ ang = frac(int(num), int(den))
623
+ return super().add_eq3(d1, d2, ang, dep)
624
+
625
+ def add_eqangle(
626
+ self,
627
+ d1: gm.Direction,
628
+ d2: gm.Direction,
629
+ d3: gm.Direction,
630
+ d4: gm.Direction,
631
+ dep: pr.Dependency,
632
+ ) -> None:
633
+ """Add the inequality d1-d2=d3-d4."""
634
+ # Use string as variables.
635
+ l1, l2, l3, l4 = [d._obj.num for d in [d1, d2, d3, d4]] # pylint: disable=protected-access
636
+ d1, d2, d3, d4 = self.get_name([d1, d2, d3, d4])
637
+ ang1 = {d1: 1, d2: -1}
638
+ ang2 = {d3: 1, d4: -1}
639
+
640
+ if l2 > l1:
641
+ ang1 = plus({self.pi: 1}, ang1)
642
+ if l4 > l3:
643
+ ang2 = plus({self.pi: 1}, ang2)
644
+
645
+ ang12 = minus(ang1, ang2)
646
+ self.record_eq(d1, d2, d3, d4)
647
+ self.record_eq(d1, d3, d2, d4)
648
+
649
+ expr = list(ang12.items())
650
+ if not self.add_expr(expr):
651
+ return []
652
+
653
+ self.register(expr, dep)
654
+
655
+ def get_all_eqs_and_why(self) -> Generator[Any, None, None]:
656
+ return super().get_all_eqs_and_why(True)
657
+
658
+
659
+ class DistanceTable(GeometricTable):
660
+ """Coefficient matrix A for position(point, line)."""
661
+
662
+ def __init__(self, name: str = ''):
663
+ name = name or '1:1'
664
+ self.merged = {}
665
+ self.ratios = set()
666
+ super().__init__(name)
667
+
668
+ def pairs(self) -> Generator[tuple[str, str], None, None]:
669
+ l2vs = defaultdict(list)
670
+ for v in list(self.v2e.keys()): # pylint: disable=g-builtin-op
671
+ if v == self.const:
672
+ continue
673
+ l, p = v.split(':')
674
+ l2vs[l].append(p)
675
+
676
+ for l, ps in l2vs.items():
677
+ for p1, p2 in perm2(ps):
678
+ yield l + ':' + p1, l + ':' + p2
679
+
680
+ def name(self, l: gm.Line, p: gm.Point) -> str:
681
+ v = l.name + ':' + p.name
682
+ self.v2obj[v] = (l, p)
683
+ return v
684
+
685
+ def map2obj(self, names: list[str]) -> list[gm.Point]:
686
+ return [self.v2obj[n][1] for n in names]
687
+
688
+ def add_cong(
689
+ self,
690
+ l12: gm.Line,
691
+ l34: gm.Line,
692
+ p1: gm.Point,
693
+ p2: gm.Point,
694
+ p3: gm.Point,
695
+ p4: gm.Point,
696
+ dep: pr.Dependency,
697
+ ) -> None:
698
+ """Add that distance between p1 and p2 (on l12) == p3 and p4 (on l34)."""
699
+ if p2.num > p1.num:
700
+ p1, p2 = p2, p1
701
+ if p4.num > p3.num:
702
+ p3, p4 = p4, p3
703
+
704
+ p1 = self.name(l12, p1)
705
+ p2 = self.name(l12, p2)
706
+ p3 = self.name(l34, p3)
707
+ p4 = self.name(l34, p4)
708
+ return super().add_eq4(p1, p2, p3, p4, dep)
709
+
710
+ def get_all_eqs_and_why(self) -> Generator[Any, None, None]:
711
+ for x in super().get_all_eqs_and_why(True):
712
+ yield x
713
+
714
+ # Now we figure out all the const ratios.
715
+ h2pairs = defaultdict(list)
716
+ for v1, v2 in self.pairs():
717
+ if (v1, v2) in self.merged:
718
+ continue
719
+ e1, e2 = self.v2e[v1], self.v2e[v2]
720
+ e12 = minus(e1, e2)
721
+ h12 = hashed(e12)
722
+ h2pairs[h12].append((v1, v2, e12))
723
+
724
+ for (_, vves1), (_, vves2) in perm2(list(h2pairs.items())):
725
+ v1, v2, e12 = vves1[0]
726
+ for v1_, v2_, _ in vves1[1:]:
727
+ self.merged[(v1_, v2_)] = (v1, v2)
728
+
729
+ v3, v4, e34 = vves2[0]
730
+ for v3_, v4_, _ in vves2[1:]:
731
+ self.merged[(v3_, v4_)] = (v3, v4)
732
+
733
+ if (v1, v2, v3, v4) in self.ratios:
734
+ continue
735
+
736
+ d12 = div(e12, e34)
737
+ if d12 is None or d12 > 1 or d12 < 0:
738
+ continue
739
+
740
+ self.ratios.add((v1, v2, v3, v4))
741
+ self.ratios.add((v2, v1, v4, v3))
742
+
743
+ n, d = d12.numerator, d12.denominator
744
+
745
+ # (v1 - v2) * d = (v3 - v4) * n
746
+ why_dict = minus(
747
+ minus({v1: d, v2: -d}, {v3: n, v4: -n}),
748
+ minus(mult(e12, d), mult(e34, n)), # there is no modulo, so this is 0
749
+ )
750
+
751
+ v1, v2, v3, v4 = self.map2obj([v1, v2, v3, v4])
752
+ yield v1, v2, v3, v4, abs(n), abs(d), self.why(why_dict)
ag4masses/alphageometry/ar_test.py ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Unit tests for ar.py."""
17
+ import unittest
18
+
19
+ from absl.testing import absltest
20
+ import ar
21
+ import graph as gh
22
+ import problem as pr
23
+
24
+
25
+ class ARTest(unittest.TestCase):
26
+
27
+ @classmethod
28
+ def setUpClass(cls):
29
+ super().setUpClass()
30
+ cls.defs = pr.Definition.from_txt_file('defs.txt', to_dict=True)
31
+ cls.rules = pr.Theorem.from_txt_file('rules.txt', to_dict=True)
32
+
33
+ def test_update_groups(self):
34
+ """Test for update_groups."""
35
+ groups1 = [{1, 2}, {3, 4, 5}, {6, 7}]
36
+ groups2 = [{2, 3, 8}, {9, 10, 11}]
37
+
38
+ _, links, history = ar.update_groups(groups1, groups2)
39
+ self.assertEqual(
40
+ history,
41
+ [
42
+ [{1, 2, 3, 4, 5, 8}, {6, 7}],
43
+ [{1, 2, 3, 4, 5, 8}, {6, 7}, {9, 10, 11}],
44
+ ],
45
+ )
46
+ self.assertEqual(links, [(2, 3), (3, 8), (9, 10), (10, 11)])
47
+
48
+ groups1 = [{1, 2}, {3, 4}, {5, 6}, {7, 8}]
49
+ groups2 = [{2, 3, 8, 9, 10}, {3, 6, 11}]
50
+
51
+ _, links, history = ar.update_groups(groups1, groups2)
52
+ self.assertEqual(
53
+ history,
54
+ [
55
+ [{1, 2, 3, 4, 7, 8, 9, 10}, {5, 6}],
56
+ [{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}],
57
+ ],
58
+ )
59
+ self.assertEqual(links, [(2, 3), (3, 8), (8, 9), (9, 10), (3, 6), (6, 11)])
60
+
61
+ groups1 = []
62
+ groups2 = [{1, 2}, {3, 4}, {5, 6}, {2, 3}]
63
+
64
+ _, links, history = ar.update_groups(groups1, groups2)
65
+ self.assertEqual(
66
+ history,
67
+ [
68
+ [{1, 2}],
69
+ [{1, 2}, {3, 4}],
70
+ [{1, 2}, {3, 4}, {5, 6}],
71
+ [{1, 2, 3, 4}, {5, 6}],
72
+ ],
73
+ )
74
+ self.assertEqual(links, [(1, 2), (3, 4), (5, 6), (2, 3)])
75
+
76
+ def test_generic_table_simple(self):
77
+ tb = ar.Table()
78
+
79
+ # If a-b = b-c & d-a = c-d
80
+ tb.add_eq4('a', 'b', 'b', 'c', 'fact1')
81
+ tb.add_eq4('d', 'a', 'c', 'd', 'fact2')
82
+ tb.add_eq4('x', 'y', 'z', 't', 'fact3') # distractor fact
83
+
84
+ # Then b=d, because {fact1, fact2} but not fact3.
85
+ result = list(tb.get_all_eqs_and_why())
86
+ self.assertIn(('b', 'd', ['fact1', 'fact2']), result)
87
+
88
+ def test_angle_table_inbisector_exbisector(self):
89
+ """Test that AR can figure out bisector & ex-bisector are perpendicular."""
90
+ # Load the scenario that we have cd is bisector of acb and
91
+ # ce is the ex-bisector of acb.
92
+ p = pr.Problem.from_txt(
93
+ 'a b c = triangle a b c; d = incenter d a b c; e = excenter e a b c ?'
94
+ ' perp d c c e'
95
+ )
96
+ g, _ = gh.Graph.build_problem(p, ARTest.defs)
97
+
98
+ # Create an external angle table:
99
+ tb = ar.AngleTable('pi')
100
+
101
+ # Add bisector & ex-bisector facts into the table:
102
+ ca, cd, cb, ce = g.names2nodes(['d(ac)', 'd(cd)', 'd(bc)', 'd(ce)'])
103
+ tb.add_eqangle(ca, cd, cd, cb, 'fact1')
104
+ tb.add_eqangle(ce, ca, cb, ce, 'fact2')
105
+
106
+ # Add a distractor fact to make sure traceback does not include this fact
107
+ ab = g.names2nodes(['d(ab)'])[0]
108
+ tb.add_eqangle(ab, cb, cb, ca, 'fact3')
109
+
110
+ # Check for all new equalities
111
+ result = list(tb.get_all_eqs_and_why())
112
+
113
+ # halfpi is represented as a tuple (1, 2)
114
+ halfpi = (1, 2)
115
+
116
+ # check that cd-ce == halfpi and this is because fact1 & fact2, not fact3
117
+ self.assertCountEqual(
118
+ result,
119
+ [
120
+ (cd, ce, halfpi, ['fact1', 'fact2']),
121
+ (ce, cd, halfpi, ['fact1', 'fact2']),
122
+ ],
123
+ )
124
+
125
+ def test_angle_table_equilateral_triangle(self):
126
+ """Test that AR can figure out triangles with 3 equal angles => each is pi/3."""
127
+ # Load an equaliteral scenario
128
+ p = pr.Problem.from_txt('a b c = ieq_triangle ? cong a b a c')
129
+ g, _ = gh.Graph.build_problem(p, ARTest.defs)
130
+
131
+ # Add two eqangles facts because ieq_triangle only add congruent sides
132
+ a, b, c = g.names2nodes('abc')
133
+ g.add_eqangle([a, b, b, c, b, c, c, a], pr.EmptyDependency(0, None))
134
+ g.add_eqangle([b, c, c, a, c, a, a, b], pr.EmptyDependency(0, None))
135
+
136
+ # Create an external angle table:
137
+ tb = ar.AngleTable('pi')
138
+
139
+ # Add the fact that there are three equal angles
140
+ ab, bc, ca = g.names2nodes(['d(ab)', 'd(bc)', 'd(ac)'])
141
+ tb.add_eqangle(ab, bc, bc, ca, 'fact1')
142
+ tb.add_eqangle(bc, ca, ca, ab, 'fact2')
143
+
144
+ # Now check for all new equalities
145
+ result = list(tb.get_all_eqs_and_why())
146
+ result = [(x.name, y.name, z, t) for x, y, z, t in result]
147
+
148
+ # 1/3 pi is represented as a tuple angle_60
149
+ angle_60 = (1, 3)
150
+ angle_120 = (2, 3)
151
+
152
+ # check that angles constants are created and figured out:
153
+ self.assertCountEqual(
154
+ result,
155
+ [
156
+ ('d(bc)', 'd(ac)', angle_120, ['fact1', 'fact2']),
157
+ ('d(ab)', 'd(bc)', angle_120, ['fact1', 'fact2']),
158
+ ('d(ac)', 'd(ab)', angle_120, ['fact1', 'fact2']),
159
+ ('d(ac)', 'd(bc)', angle_60, ['fact1', 'fact2']),
160
+ ('d(bc)', 'd(ab)', angle_60, ['fact1', 'fact2']),
161
+ ('d(ab)', 'd(ac)', angle_60, ['fact1', 'fact2']),
162
+ ],
163
+ )
164
+
165
+ def test_incenter_excenter_touchpoints(self):
166
+ """Test that AR can figure out incenter/excenter touchpoints are equidistant to midpoint."""
167
+
168
+ p = pr.Problem.from_txt(
169
+ 'a b c = triangle a b c; d1 d2 d3 d = incenter2 a b c; e1 e2 e3 e ='
170
+ ' excenter2 a b c ? perp d c c e',
171
+ translate=False,
172
+ )
173
+ g, _ = gh.Graph.build_problem(p, ARTest.defs)
174
+
175
+ a, b, c, ab, bc, ca, d1, d2, d3, e1, e2, e3 = g.names2nodes(
176
+ ['a', 'b', 'c', 'ab', 'bc', 'ac', 'd1', 'd2', 'd3', 'e1', 'e2', 'e3']
177
+ )
178
+
179
+ # Create an external distance table:
180
+ tb = ar.DistanceTable()
181
+
182
+ # DD can figure out the following facts,
183
+ # we manually add them to AR.
184
+ tb.add_cong(ab, ca, a, d3, a, d2, 'fact1')
185
+ tb.add_cong(ab, ca, a, e3, a, e2, 'fact2')
186
+ tb.add_cong(ca, bc, c, d2, c, d1, 'fact5')
187
+ tb.add_cong(ca, bc, c, e2, c, e1, 'fact6')
188
+ tb.add_cong(bc, ab, b, d1, b, d3, 'fact3')
189
+ tb.add_cong(bc, ab, b, e1, b, e3, 'fact4')
190
+
191
+ # Now we check whether tb has figured out that
192
+ # distance(b, d1) == distance(e1, c)
193
+
194
+ # linear comb exprssion of each variables:
195
+ b = tb.v2e['bc:b']
196
+ c = tb.v2e['bc:c']
197
+ d1 = tb.v2e['bc:d1']
198
+ e1 = tb.v2e['bc:e1']
199
+
200
+ self.assertEqual(ar.minus(d1, b), ar.minus(c, e1))
201
+
202
+
203
+ if __name__ == '__main__':
204
+ absltest.main()
ag4masses/alphageometry/beam_search.py ADDED
@@ -0,0 +1,463 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Fast decoding routines for inference from a trained model.
17
+
18
+ Modified https://github.com/google/flax/blob/main/examples/wmt/decode.py
19
+ to acommodate
20
+
21
+ (a) continued decoding from a previous beam cache.
22
+ (b) init with with a single beam and then expand into beam_size beams.
23
+ """
24
+
25
+ from typing import Any
26
+
27
+ import flax
28
+ import jax
29
+ from jax import lax
30
+ import jax.numpy as jnp
31
+ import numpy as np
32
+
33
+
34
+ # Constants
35
+ # "Effective negative infinity" constant for masking in beam search.
36
+ NEG_INF = np.array(-1.0e7)
37
+
38
+ # Beam search parameters
39
+ BEAM_SEARCH_DEFAULT_ALPHA = 0.6
40
+ MAX_DECODE_LEN = 32
41
+
42
+ # Brevity penalty parameters
43
+ BREVITY_LEN_BIAS_NUMERATOR = 5.0
44
+ BREVITY_LEN_BIAS_DENOMINATOR = 6.0
45
+
46
+
47
+ def brevity_penalty(alpha: float, length: int):
48
+ """Brevity penalty function for beam search penalizing short sequences.
49
+
50
+ Args:
51
+ alpha: float: brevity-penalty scaling parameter.
52
+ length: int: length of considered sequence.
53
+
54
+ Returns:
55
+ Brevity penalty score as jax scalar.
56
+ """
57
+ return jnp.power(
58
+ ((BREVITY_LEN_BIAS_NUMERATOR + length) / BREVITY_LEN_BIAS_DENOMINATOR),
59
+ alpha,
60
+ )
61
+
62
+
63
+ # Beam handling utility functions:
64
+
65
+
66
+ def add_beam_dim(x: jnp.ndarray, beam_size: int) -> jnp.ndarray:
67
+ """Creates new beam dimension in non-scalar array and tiles into it."""
68
+ if x.ndim == 0: # ignore scalars (e.g. cache index)
69
+ return x
70
+ x = jnp.expand_dims(x, axis=1)
71
+ tile_dims = [1] * x.ndim
72
+ tile_dims[1] = beam_size
73
+ return jnp.tile(x, tile_dims)
74
+
75
+
76
+ def add_beam_dim_cache(
77
+ cache: tuple[dict[str, jnp.ndarray], ...], beam_size: int
78
+ ) -> tuple[dict[str, jnp.ndarray], ...]:
79
+ """Creates new beam dimension in non-scalar array and tiles into it."""
80
+ new_cache = []
81
+
82
+ for layer in cache:
83
+ new_layer = {}
84
+ for key, x in layer.items():
85
+ if key in ['keys', 'vals']:
86
+ x = add_beam_dim(x, beam_size)
87
+ new_layer[key] = x
88
+ new_cache.append(new_layer)
89
+
90
+ return tuple(new_cache)
91
+
92
+
93
+ def flatten_beam_dim(x):
94
+ """Flattens the first two dimensions of a non-scalar array."""
95
+ if x.ndim < 2: # ignore scalars (e.g. cache index)
96
+ return x
97
+ return x.reshape((x.shape[0] * x.shape[1],) + x.shape[2:])
98
+
99
+
100
+ def unflatten_beam_dim(x, batch_size, beam_size):
101
+ """Unflattens the first, flat batch*beam dimension of a non-scalar array."""
102
+ if x.ndim == 0: # ignore scalars (e.g. cache index)
103
+ return x
104
+ assert batch_size * beam_size == x.shape[0]
105
+ return x.reshape((batch_size, beam_size) + x.shape[1:])
106
+
107
+
108
+ def flat_batch_beam_expand(x, beam_size):
109
+ """Expands the each batch item by beam_size in batch_dimension."""
110
+ return flatten_beam_dim(add_beam_dim(x, beam_size))
111
+
112
+
113
+ def gather_beams(nested, beam_indices, batch_size, new_beam_size):
114
+ """Gathers the beam slices indexed by beam_indices into new beam array.
115
+
116
+ Args:
117
+ nested: pytree of arrays or scalars (the latter ignored).
118
+ beam_indices: array of beam_indices
119
+ batch_size: int: size of batch.
120
+ new_beam_size: int: size of _new_ beam dimension.
121
+
122
+ Returns:
123
+ New pytree with new beam arrays.
124
+ [batch_size, old_beam_size, ...] --> [batch_size, new_beam_size, ...]
125
+ """
126
+ batch_indices = jnp.reshape(
127
+ jnp.arange(batch_size * new_beam_size) // new_beam_size,
128
+ (batch_size, new_beam_size),
129
+ )
130
+
131
+ def gather_fn(x):
132
+ if x.ndim == 0: # ignore scalars (e.g. cache index)
133
+ return x
134
+ else:
135
+ return x[batch_indices, beam_indices]
136
+
137
+ return jax.tree_util.tree_map(gather_fn, nested)
138
+
139
+
140
+ def gather_topk_beams(nested, score_or_log_prob, batch_size, new_beam_size):
141
+ """Gathers the top-k beam slices given by score_or_log_prob array.
142
+
143
+ Args:
144
+ nested: pytree of arrays or scalars (the latter ignored).
145
+ score_or_log_prob: [batch_size, old_beam_size] array of values to sort by
146
+ for top-k selection of beam slices.
147
+ batch_size: int: size of batch.
148
+ new_beam_size: int: size of _new_ top-k selected beam dimension
149
+
150
+ Returns:
151
+ New pytree with new beam arrays containing top k new_beam_size slices.
152
+ [batch_size, old_beam_size, ...] --> [batch_size, new_beam_size, ...]
153
+ """
154
+ _, topk_indices = lax.top_k(score_or_log_prob, k=new_beam_size)
155
+ topk_indices = jnp.flip(topk_indices, axis=1)
156
+ return gather_beams(nested, topk_indices, batch_size, new_beam_size)
157
+
158
+
159
+ def apply_on_cache(fn, cache, *args, **kwargs):
160
+ """Apply fn(val) only when key is 'keys' or 'val'."""
161
+ new_cache = []
162
+ for layer in cache:
163
+ new_layer = {}
164
+ for key, val in layer.items():
165
+ if key in ['keys', 'values', 'current_index', 'relative_position_bias']:
166
+ val = fn(val, *args, **kwargs)
167
+ new_layer[key] = val
168
+ new_cache.append(new_layer)
169
+ return tuple(new_cache)
170
+
171
+
172
+ # Beam search state:
173
+
174
+
175
+ @flax.struct.dataclass
176
+ class BeamState:
177
+ """Holds beam search state data."""
178
+
179
+ # The position of the decoding loop in the length dimension.
180
+ cur_index: jax.Array # scalar int32: current decoded length index
181
+ # The active sequence log probabilities and finished sequence scores.
182
+ live_logprobs: jax.Array # float32: [batch_size, beam_size]
183
+ finished_scores: jax.Array # float32: [batch_size, beam_size]
184
+ # The current active-beam-searching and finished sequences.
185
+ live_seqs: jax.Array # int32: [batch_size, beam_size, max_decode_len]
186
+ finished_seqs: jax.Array # int32: [batch_size, beam_size,
187
+ # max_decode_len]
188
+ # Records which of the 'finished_seqs' is occupied and not a filler slot.
189
+ finished_flags: jax.Array # bool: [batch_size, beam_size]
190
+ # The current state of the autoregressive decoding caches.
191
+ cache: Any # Any pytree of arrays, e.g. flax attention Cache object
192
+
193
+
194
+ def beam_init(seed_token, batch_size, beam_size, max_decode_len, cache):
195
+ """Initializes the beam search state data structure."""
196
+ cur_index0 = jnp.array(0)
197
+ live_logprobs0 = jnp.tile(
198
+ jnp.array([0.0] + [NEG_INF] * (beam_size - 1)), [batch_size, 1]
199
+ )
200
+ finished_scores0 = jnp.ones((batch_size, beam_size)) * NEG_INF
201
+
202
+ live_seqs0 = jnp.concatenate(
203
+ [
204
+ jnp.reshape(seed_token, (batch_size, beam_size, 1)),
205
+ jnp.zeros((batch_size, beam_size, max_decode_len - 1), jnp.int32),
206
+ ],
207
+ axis=-1,
208
+ ) # (batch, beam, max_decode_len)
209
+
210
+ finished_seqs0 = jnp.zeros((batch_size, beam_size, max_decode_len), jnp.int32)
211
+ finished_flags0 = jnp.zeros((batch_size, beam_size), jnp.bool_)
212
+ beam_cache0 = apply_on_cache(lambda x: jnp.expand_dims(x, axis=0), cache)
213
+ return BeamState(
214
+ cur_index=cur_index0,
215
+ live_logprobs=live_logprobs0,
216
+ finished_scores=finished_scores0,
217
+ live_seqs=live_seqs0,
218
+ finished_seqs=finished_seqs0,
219
+ finished_flags=finished_flags0,
220
+ cache=beam_cache0,
221
+ )
222
+
223
+
224
+ # Beam search routine:
225
+
226
+
227
+ def beam_search_flat(
228
+ seed_token,
229
+ cache,
230
+ tokens_to_logits,
231
+ alpha=BEAM_SEARCH_DEFAULT_ALPHA,
232
+ eos=None,
233
+ max_decode_len=MAX_DECODE_LEN,
234
+ mask=None,
235
+ ):
236
+ """Beam search for LM.
237
+
238
+ inputs and cache is already flat! i.e. first dimention == batch*beam.
239
+
240
+ Args:
241
+ seed_token: array: [beam_size, 1] int32 sequence of tokens.
242
+ cache: flax attention cache.
243
+ tokens_to_logits: fast autoregressive decoder function taking single token
244
+ slices and cache and returning next-token logits and updated cache.
245
+ alpha: float: scaling factor for brevity penalty.
246
+ eos: array: [vocab] 1 for end-of-sentence tokens, 0 for not.
247
+ max_decode_len: int: maximum length of decoded translations.
248
+ mask: array: [vocab] binary mask for vocab. 1 to keep the prob, 0 to set the
249
+ prob := 0.
250
+
251
+ Returns:
252
+ Tuple of:
253
+ [beam_size, max_decode_len] top-scoring sequences
254
+ [beam_size] beam-search scores.
255
+ """
256
+ # We liberally annotate shape information for clarity below.
257
+ batch_size, beam_size = 1, seed_token.shape[0]
258
+ mask = mask.reshape((1, 1, -1))
259
+ eos = eos.reshape((1, 1, -1))
260
+ mask_bias = (1 - mask) * NEG_INF
261
+
262
+ # initialize beam search state
263
+ beam_search_init_state = beam_init(
264
+ seed_token, batch_size, beam_size, max_decode_len, cache
265
+ )
266
+
267
+ def beam_search_loop_cond_fn(state):
268
+ """Beam search loop termination condition."""
269
+ # Have we reached max decoding length?
270
+ not_at_end = state.cur_index < max_decode_len - 1
271
+
272
+ # Is no further progress in the beam search possible?
273
+ # Get the best possible scores from alive sequences.
274
+ min_brevity_penalty = brevity_penalty(alpha, max_decode_len)
275
+ best_live_scores = state.live_logprobs[:, -1:] / min_brevity_penalty
276
+ # Get the worst scores from finished sequences.
277
+ worst_finished_scores = jnp.min(
278
+ state.finished_scores, axis=1, keepdims=True
279
+ )
280
+ # Mask out scores from slots without any actual finished sequences.
281
+ worst_finished_scores = jnp.where(
282
+ state.finished_flags, worst_finished_scores, NEG_INF
283
+ )
284
+ # If no best possible live score is better than current worst finished
285
+ # scores, the search cannot improve the finished set further.
286
+ search_terminated = jnp.all(worst_finished_scores > best_live_scores)
287
+
288
+ # If we're not at the max decode length, and the search hasn't terminated,
289
+ # continue looping.
290
+ return not_at_end & (~search_terminated)
291
+
292
+ def beam_search_loop_body_fn(state):
293
+ """Beam search loop state update function."""
294
+ # Collect the current position slice along length to feed the fast
295
+ # autoregressive decoder model. Flatten the beam dimension into batch
296
+ # dimension for feeding into the model.
297
+ # --> [batch * beam, 1]
298
+ flat_ids = flatten_beam_dim(
299
+ lax.dynamic_slice(
300
+ state.live_seqs, (0, 0, state.cur_index), (batch_size, beam_size, 1)
301
+ )
302
+ )
303
+ # Flatten beam dimension into batch to be compatible with model.
304
+ # {[batch, beam, ...], ...} --> {[batch * beam, ...], ...}
305
+ flat_cache = apply_on_cache(flatten_beam_dim, state.cache)
306
+
307
+ # Call fast-decoder model on current tokens to get next-position logits.
308
+ # --> [batch * beam, vocab]
309
+ flat_logits, new_flat_cache = tokens_to_logits(flat_ids, flat_cache)
310
+
311
+ # unflatten beam dimension
312
+ # [batch * beam, vocab] --> [batch, beam, vocab]
313
+ logits = unflatten_beam_dim(flat_logits, batch_size, beam_size)
314
+
315
+ # Unflatten beam dimension in attention cache arrays
316
+ # {[batch * beam, ...], ...} --> {[batch, beam, ...], ...}
317
+ new_cache = apply_on_cache(
318
+ unflatten_beam_dim, new_flat_cache, batch_size, beam_size
319
+ )
320
+
321
+ # Gather log probabilities from logits
322
+ candidate_log_probs = jax.nn.log_softmax(logits)
323
+ # Add new logprobs to existing prefix logprobs.
324
+ # --> [batch, beam, vocab]
325
+ log_probs = candidate_log_probs + jnp.expand_dims(
326
+ state.live_logprobs, axis=2
327
+ )
328
+
329
+ # We'll need the vocab size, gather it from the log probability dimension.
330
+ vocab_size = log_probs.shape[2]
331
+
332
+ # mask away some tokens.
333
+ log_probs += mask_bias # [batch,beam,vocab]+[1,1,vocab]
334
+
335
+ # Each item in batch has beam_size * vocab_size candidate sequences.
336
+ # For each item, get the top 2*k candidates with the highest log-
337
+ # probabilities. We gather the top 2*K beams here so that even if the best
338
+ # K sequences reach EOS simultaneously, we have another K sequences
339
+ # remaining to continue the live beam search.
340
+ beams_to_keep = 2 * beam_size
341
+ # Flatten beam and vocab dimensions.
342
+ flat_log_probs = log_probs.reshape((batch_size, beam_size * vocab_size))
343
+ # Gather the top 2*K scores from _all_ beams.
344
+ # --> [batch, 2*beams], [batch, 2*beams]
345
+ topk_log_probs, topk_indices = lax.top_k(flat_log_probs, k=beams_to_keep)
346
+ # Recover the beam index by floor division.
347
+ topk_beam_indices = topk_indices // vocab_size
348
+ # Gather 2*k top beams.
349
+ # --> [batch, 2*beams, length]
350
+ topk_seq = gather_beams(
351
+ state.live_seqs, topk_beam_indices, batch_size, beams_to_keep
352
+ )
353
+
354
+ # Append the most probable 2*K token IDs to the top 2*K sequences
355
+ # Recover token id by modulo division and expand Id array for broadcasting.
356
+ # --> [batch, 2*beams, 1]
357
+ topk_ids = jnp.expand_dims(topk_indices % vocab_size, axis=2)
358
+ # Update sequences for the 2*K top-k new sequences.
359
+ # --> [batch, 2*beams, length]
360
+ topk_seq = lax.dynamic_update_slice(
361
+ topk_seq, topk_ids, (0, 0, state.cur_index + 1)
362
+ )
363
+
364
+ # Update LIVE (in-progress) sequences:
365
+ # Did any of these sequences reach an end marker?
366
+ # --> [batch, 2*beams]
367
+ last_token = topk_seq[:, :, state.cur_index + 1]
368
+ last_token = jax.nn.one_hot(last_token, vocab_size, dtype=jnp.bfloat16)
369
+
370
+ # any([batch, 2b, vocab] * [1, 1, vocab], axis=-1) == [batch, 2b]
371
+ newly_finished = jnp.any(last_token * eos, axis=-1)
372
+
373
+ # To prevent these newly finished sequences from being added to the LIVE
374
+ # set of active beam search sequences, set their log probs to a very large
375
+ # negative value.
376
+ new_log_probs = topk_log_probs + newly_finished * NEG_INF
377
+ # Determine the top k beam indices (from top 2*k beams) from log probs.
378
+ # --> [batch, beams]
379
+ _, new_topk_indices = lax.top_k(new_log_probs, k=beam_size)
380
+ new_topk_indices = jnp.flip(new_topk_indices, axis=1)
381
+ # Gather the top k beams (from top 2*k beams).
382
+ # --> [batch, beams, length], [batch, beams]
383
+ top_alive_seq, top_alive_log_probs = gather_beams(
384
+ [topk_seq, new_log_probs], new_topk_indices, batch_size, beam_size
385
+ )
386
+
387
+ # Determine the top k beam indices from the original set of all beams.
388
+ # --> [batch, beams]
389
+ top_alive_indices = gather_beams(
390
+ topk_beam_indices, new_topk_indices, batch_size, beam_size
391
+ )
392
+ # With these, gather the top k beam-associated caches.
393
+ # --> {[batch, beams, ...], ...}
394
+ top_alive_cache = apply_on_cache(
395
+ gather_beams, new_cache, top_alive_indices, batch_size, beam_size
396
+ )
397
+
398
+ # Update FINISHED (reached end of sentence) sequences:
399
+ # Calculate new seq scores from log probabilities.
400
+ new_scores = topk_log_probs / brevity_penalty(alpha, state.cur_index + 1)
401
+ # Mask out the still unfinished sequences by adding large negative value.
402
+ # --> [batch, 2*beams]
403
+ new_scores += (~newly_finished) * NEG_INF
404
+
405
+ # Combine sequences, scores, and flags along the beam dimension and compare
406
+ # new finished sequence scores to existing finished scores and select the
407
+ # best from the new set of beams.
408
+ finished_seqs = jnp.concatenate( # --> [batch, 3*beams, length]
409
+ [state.finished_seqs, topk_seq], axis=1
410
+ )
411
+ finished_scores = jnp.concatenate( # --> [batch, 3*beams]
412
+ [state.finished_scores, new_scores], axis=1
413
+ )
414
+ finished_flags = jnp.concatenate( # --> [batch, 3*beams]
415
+ [state.finished_flags, newly_finished], axis=1
416
+ )
417
+ # --> [batch, beams, length], [batch, beams], [batch, beams]
418
+ top_finished_seq, top_finished_scores, top_finished_flags = (
419
+ gather_topk_beams(
420
+ [finished_seqs, finished_scores, finished_flags],
421
+ finished_scores,
422
+ batch_size,
423
+ beam_size,
424
+ )
425
+ )
426
+
427
+ return BeamState(
428
+ cur_index=state.cur_index + 1,
429
+ live_logprobs=top_alive_log_probs,
430
+ finished_scores=top_finished_scores,
431
+ live_seqs=top_alive_seq,
432
+ finished_seqs=top_finished_seq,
433
+ finished_flags=top_finished_flags,
434
+ cache=top_alive_cache,
435
+ )
436
+
437
+ # Run while loop and get final beam search state.
438
+ final_state = lax.while_loop(
439
+ beam_search_loop_cond_fn, beam_search_loop_body_fn, beam_search_init_state
440
+ )
441
+
442
+ # Account for the edge-case where there are no finished sequences for a
443
+ # particular batch item. If so, return live sequences for that batch item.
444
+ # --> [batch]
445
+ none_finished = jnp.any(final_state.finished_flags, axis=1)
446
+ # --> [batch, beams, length]
447
+ finished_seqs = jnp.where(
448
+ none_finished[:, None, None],
449
+ final_state.finished_seqs,
450
+ final_state.live_seqs,
451
+ )
452
+ # --> [batch, beams]
453
+ finished_scores = jnp.where(
454
+ none_finished[:, None],
455
+ final_state.finished_scores,
456
+ final_state.live_logprobs,
457
+ )
458
+
459
+ finished_seqs = jnp.reshape(finished_seqs, (beam_size, max_decode_len))
460
+ finished_scores = jnp.reshape(finished_scores, (beam_size,))
461
+
462
+ final_cache = apply_on_cache(flatten_beam_dim, final_state.cache)
463
+ return finished_seqs, finished_scores, final_cache
ag4masses/alphageometry/dd.py ADDED
@@ -0,0 +1,1156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Implements Deductive Database (DD)."""
17
+
18
+ # pylint: disable=g-multiple-import,g-importing-member
19
+ from collections import defaultdict
20
+ import time
21
+ from typing import Any, Callable, Generator
22
+
23
+ import geometry as gm
24
+ import graph as gh
25
+ import graph_utils as utils
26
+ import numericals as nm
27
+ import problem as pr
28
+ from problem import Dependency, EmptyDependency
29
+
30
+
31
+ def intersect1(set1: set[Any], set2: set[Any]) -> Any:
32
+ for x in set1:
33
+ if x in set2:
34
+ return x
35
+ return None
36
+
37
+
38
+ def diff_point(l: gm.Line, a: gm.Point) -> gm.Point:
39
+ for x in l.neighbors(gm.Point):
40
+ if x != a:
41
+ return x
42
+ return None
43
+
44
+
45
+ # pylint: disable=protected-access
46
+ # pylint: disable=unused-argument
47
+
48
+
49
+ def match_eqratio_eqratio_eqratio(
50
+ g: gh.Graph,
51
+ g_matcher: Callable[str, list[tuple[gm.Point, ...]]],
52
+ theorem: pr.Theorem,
53
+ ) -> Generator[dict[str, gm.Point], None, None]:
54
+ """Match eqratio a b c d m n p q, eqratio c d e f p q r u => eqratio a b e f m n r u."""
55
+ for m1 in g.type2nodes[gm.Value]:
56
+ for m2 in g.type2nodes[gm.Value]:
57
+ rats1 = []
58
+ for rat in m1.neighbors(gm.Ratio):
59
+ l1, l2 = rat.lengths
60
+ if l1 is None or l2 is None:
61
+ continue
62
+ rats1.append((l1, l2))
63
+
64
+ rats2 = []
65
+ for rat in m2.neighbors(gm.Ratio):
66
+ l1, l2 = rat.lengths
67
+ if l1 is None or l2 is None:
68
+ continue
69
+ rats2.append((l1, l2))
70
+
71
+ pairs = []
72
+ for (l1, l2), (l3, l4) in utils.cross(rats1, rats2):
73
+ if l2 == l3:
74
+ pairs.append((l1, l2, l4))
75
+
76
+ for (l1, l12, l2), (l3, l34, l4) in utils.comb2(pairs):
77
+ if (l1, l12, l2) == (l3, l34, l4):
78
+ continue
79
+ if l1 == l2 or l3 == l4:
80
+ continue
81
+ if l1 == l12 or l12 == l2 or l3 == l34 or l4 == l34:
82
+ continue
83
+ # d12 - d1 = d34 - d3 = m1
84
+ # d2 - d12 = d4 - d34 = m2
85
+ # => d2 - d1 = d4 - d3 (= m1+m2)
86
+ a, b = g.two_points_of_length(l1)
87
+ c, d = g.two_points_of_length(l12)
88
+ m, n = g.two_points_of_length(l3)
89
+ p, q = g.two_points_of_length(l34)
90
+ # eqangle a b c d m n p q
91
+ e, f = g.two_points_of_length(l2)
92
+ r, u = g.two_points_of_length(l4)
93
+ yield dict(zip('abcdefmnpqru', [a, b, c, d, e, f, m, n, p, q, r, u]))
94
+
95
+
96
+ def match_eqangle_eqangle_eqangle(
97
+ g: gh.Graph,
98
+ g_matcher: Callable[str, list[tuple[gm.Point, ...]]],
99
+ theorem: pr.Theorem,
100
+ ) -> Generator[dict[str, gm.Point], None, None]:
101
+ """Match eqangle a b c d m n p q, eqangle c d e f p q r u => eqangle a b e f m n r u."""
102
+ for m1 in g.type2nodes[gm.Measure]:
103
+ for m2 in g.type2nodes[gm.Measure]:
104
+ angs1 = []
105
+ for ang in m1.neighbors(gm.Angle):
106
+ d1, d2 = ang.directions
107
+ if d1 is None or d2 is None:
108
+ continue
109
+ angs1.append((d1, d2))
110
+
111
+ angs2 = []
112
+ for ang in m2.neighbors(gm.Angle):
113
+ d1, d2 = ang.directions
114
+ if d1 is None or d2 is None:
115
+ continue
116
+ angs2.append((d1, d2))
117
+
118
+ pairs = []
119
+ for (d1, d2), (d3, d4) in utils.cross(angs1, angs2):
120
+ if d2 == d3:
121
+ pairs.append((d1, d2, d4))
122
+
123
+ for (d1, d12, d2), (d3, d34, d4) in utils.comb2(pairs):
124
+ if (d1, d12, d2) == (d3, d34, d4):
125
+ continue
126
+ if d1 == d2 or d3 == d4:
127
+ continue
128
+ if d1 == d12 or d12 == d2 or d3 == d34 or d4 == d34:
129
+ continue
130
+ # d12 - d1 = d34 - d3 = m1
131
+ # d2 - d12 = d4 - d34 = m2
132
+ # => d2 - d1 = d4 - d3
133
+ a, b = g.two_points_on_direction(d1)
134
+ c, d = g.two_points_on_direction(d12)
135
+ m, n = g.two_points_on_direction(d3)
136
+ p, q = g.two_points_on_direction(d34)
137
+ # eqangle a b c d m n p q
138
+ e, f = g.two_points_on_direction(d2)
139
+ r, u = g.two_points_on_direction(d4)
140
+ yield dict(zip('abcdefmnpqru', [a, b, c, d, e, f, m, n, p, q, r, u]))
141
+
142
+
143
+ def match_perp_perp_npara_eqangle(
144
+ g: gh.Graph,
145
+ g_matcher: Callable[str, list[tuple[gm.Point, ...]]],
146
+ theorem: pr.Theorem,
147
+ ) -> Generator[dict[str, gm.Point], None, None]:
148
+ """Match perp A B C D, perp E F G H, npara A B E F => eqangle A B E F C D G H."""
149
+ dpairs = []
150
+ for ang in g.vhalfpi.neighbors(gm.Angle):
151
+ d1, d2 = ang.directions
152
+ if d1 is None or d2 is None:
153
+ continue
154
+ dpairs.append((d1, d2))
155
+
156
+ for (d1, d2), (d3, d4) in utils.comb2(dpairs):
157
+ a, b = g.two_points_on_direction(d1)
158
+ c, d = g.two_points_on_direction(d2)
159
+ m, n = g.two_points_on_direction(d3)
160
+ p, q = g.two_points_on_direction(d4)
161
+ if g.check_npara([a, b, m, n]):
162
+ if ({a, b}, {c, d}) == ({m, n}, {p, q}):
163
+ continue
164
+ if ({a, b}, {c, d}) == ({p, q}, {m, n}):
165
+ continue
166
+
167
+ yield dict(zip('ABCDEFGH', [a, b, c, d, m, n, p, q]))
168
+
169
+
170
+ def match_circle_coll_eqangle_midp(
171
+ g: gh.Graph,
172
+ g_matcher: Callable[str, list[tuple[gm.Point, ...]]],
173
+ theorem: pr.Theorem,
174
+ ) -> Generator[dict[str, gm.Point], None, None]:
175
+ """Match circle O A B C, coll M B C, eqangle A B A C O B O M => midp M B C."""
176
+ for p, a, b, c in g.all_circles():
177
+ ab = g._get_line(a, b)
178
+ if ab is None:
179
+ continue
180
+ if ab.val is None:
181
+ continue
182
+ ac = g._get_line(a, c)
183
+ if ac is None:
184
+ continue
185
+ if ac.val is None:
186
+ continue
187
+ pb = g._get_line(p, b)
188
+ if pb is None:
189
+ continue
190
+ if pb.val is None:
191
+ continue
192
+
193
+ bc = g._get_line(b, c)
194
+ if bc is None:
195
+ continue
196
+ bc_points = bc.neighbors(gm.Point, return_set=True)
197
+
198
+ anga, _ = g._get_angle(ab.val, ac.val)
199
+
200
+ for angp in pb.val.neighbors(gm.Angle):
201
+ if not g.is_equal(anga, angp):
202
+ continue
203
+
204
+ _, d = angp.directions
205
+ for l in d.neighbors(gm.Line):
206
+ l_points = l.neighbors(gm.Point, return_set=True)
207
+ m = intersect1(bc_points, l_points)
208
+ if m is not None:
209
+ yield dict(zip('ABCMO', [a, b, c, m, p]))
210
+
211
+
212
+ def match_midp_perp_cong(
213
+ g: gh.Graph,
214
+ g_matcher: Callable[str, list[tuple[gm.Point, ...]]],
215
+ theorem: pr.Theorem,
216
+ ) -> Generator[dict[str, gm.Point], None, None]:
217
+ """Match midp M A B, perp O M A B => cong O A O B."""
218
+ for m, a, b in g.all_midps():
219
+ ab = g._get_line(a, b)
220
+ for l in m.neighbors(gm.Line):
221
+ if g.check_perpl(l, ab):
222
+ for o in l.neighbors(gm.Point):
223
+ if o != m:
224
+ yield dict(zip('ABMO', [a, b, m, o]))
225
+
226
+
227
+ def match_cyclic_eqangle_cong(
228
+ g: gh.Graph,
229
+ g_matcher: Callable[str, list[tuple[gm.Point, ...]]],
230
+ theorem: pr.Theorem,
231
+ ) -> Generator[dict[str, gm.Point], None, None]:
232
+ """Match cyclic A B C P Q R, eqangle C A C B R P R Q => cong A B P Q."""
233
+ for c in g.type2nodes[gm.Circle]:
234
+ ps = c.neighbors(gm.Point)
235
+ for (a, b, c), (x, y, z) in utils.comb2(list(utils.perm3(ps))):
236
+ if {a, b, c} == {x, y, z}:
237
+ continue
238
+ if g.check_eqangle([c, a, c, b, z, x, z, y]):
239
+ yield dict(zip('ABCPQR', [a, b, c, x, y, z]))
240
+
241
+
242
+ def match_circle_eqangle_perp(
243
+ g: gh.Graph,
244
+ g_matcher: Callable[str, list[tuple[gm.Point, ...]]],
245
+ theorem: pr.Theorem,
246
+ ) -> Generator[dict[str, gm.Point], None, None]:
247
+ """Match circle O A B C, eqangle A X A B C A C B => perp O A A X."""
248
+ for p, a, b, c in g.all_circles():
249
+ ca = g._get_line(c, a)
250
+ if ca is None:
251
+ continue
252
+ cb = g._get_line(c, b)
253
+ if cb is None:
254
+ continue
255
+ ab = g._get_line(a, b)
256
+ if ab is None:
257
+ continue
258
+
259
+ if ca.val is None:
260
+ continue
261
+ if cb.val is None:
262
+ continue
263
+ if ab.val is None:
264
+ continue
265
+
266
+ c_ang, _ = g._get_angle(cb.val, ca.val)
267
+ if c_ang is None:
268
+ continue
269
+
270
+ for ang in ab.val.neighbors(gm.Angle):
271
+ if g.is_equal(ang, c_ang):
272
+ _, d = ang.directions
273
+ for l in d.neighbors(gm.Line):
274
+ if a not in l.neighbors(gm.Point):
275
+ continue
276
+ x = diff_point(l, a)
277
+ if x is None:
278
+ continue
279
+ yield dict(zip('OABCX', [p, a, b, c, x]))
280
+ break
281
+
282
+
283
+ def match_circle_perp_eqangle(
284
+ g: gh.Graph,
285
+ g_matcher: Callable[str, list[tuple[gm.Point, ...]]],
286
+ theorem: pr.Theorem,
287
+ ) -> Generator[dict[str, gm.Point], None, None]:
288
+ """Match circle O A B C, perp O A A X => eqangle A X A B C A C B."""
289
+ for p, a, b, c in g.all_circles():
290
+ pa = g._get_line(p, a)
291
+ if pa is None:
292
+ continue
293
+ if pa.val is None:
294
+ continue
295
+ for l in a.neighbors(gm.Line):
296
+ if g.check_perpl(pa, l):
297
+ x = diff_point(l, a)
298
+ if x is not None:
299
+ yield dict(zip('OABCX', [p, a, b, c, x]))
300
+
301
+
302
+ def match_perp_perp_ncoll_para(
303
+ g: gh.Graph,
304
+ g_matcher: Callable[str, list[tuple[gm.Point, ...]]],
305
+ theorem: pr.Theorem,
306
+ ) -> Generator[dict[str, gm.Point], None, None]:
307
+ """Match perp A B C D, perp C D E F, ncoll A B E => para A B E F."""
308
+ d2d = defaultdict(list)
309
+ for ang in g.vhalfpi.neighbors(gm.Angle):
310
+ d1, d2 = ang.directions
311
+ if d1 is None or d2 is None:
312
+ continue
313
+ d2d[d1] += [d2]
314
+ d2d[d2] += [d1]
315
+
316
+ for x, ys in d2d.items():
317
+ if len(ys) < 2:
318
+ continue
319
+ c, d = g.two_points_on_direction(x)
320
+ for y1, y2 in utils.comb2(ys):
321
+ a, b = g.two_points_on_direction(y1)
322
+ e, f = g.two_points_on_direction(y2)
323
+ if nm.check_ncoll([a.num, b.num, e.num]):
324
+ yield dict(zip('ABCDEF', [a, b, c, d, e, f]))
325
+
326
+
327
+ def match_eqangle6_ncoll_cong(
328
+ g: gh.Graph,
329
+ g_matcher: Callable[str, list[tuple[gm.Point, ...]]],
330
+ theorem: pr.Theorem,
331
+ ) -> Generator[dict[str, gm.Point], None, None]:
332
+ """Match eqangle6 A O A B B A B O, ncoll O A B => cong O A O B."""
333
+ for a in g.type2nodes[gm.Point]:
334
+ for b, c in utils.comb2(g.type2nodes[gm.Point]):
335
+ if a == b or a == c:
336
+ continue
337
+ if g.check_eqangle([b, a, b, c, c, b, c, a]):
338
+ if g.check_ncoll([a, b, c]):
339
+ yield dict(zip('OAB', [a, b, c]))
340
+
341
+
342
+ def match_eqangle_perp_perp(
343
+ g: gh.Graph,
344
+ g_matcher: Callable[str, list[tuple[gm.Point, ...]]],
345
+ theorem: pr.Theorem,
346
+ ) -> Generator[dict[str, gm.Point], None, None]:
347
+ """Match eqangle A B P Q C D U V, perp P Q U V => perp A B C D."""
348
+ for ang in g.vhalfpi.neighbors(gm.Angle):
349
+ # d1 perp d2
350
+ d1, d2 = ang.directions
351
+ if d1 is None or d2 is None:
352
+ continue
353
+ for d3, d4 in utils.comb2(g.type2nodes[gm.Direction]):
354
+ if d1 == d3 or d2 == d4:
355
+ continue
356
+ # if d1 - d3 = d2 - d4 => d3 perp d4
357
+ a13, a31 = g._get_angle(d1, d3)
358
+ a24, a42 = g._get_angle(d2, d4)
359
+ if a13 is None or a31 is None or a24 is None or a42 is None:
360
+ continue
361
+ if g.is_equal(a13, a24) and g.is_equal(a31, a42):
362
+ a, b = g.two_points_on_direction(d1)
363
+ c, d = g.two_points_on_direction(d2)
364
+ m, n = g.two_points_on_direction(d3)
365
+ p, q = g.two_points_on_direction(d4)
366
+ yield dict(zip('ABCDPQUV', [m, n, p, q, a, b, c, d]))
367
+
368
+
369
+ def match_eqangle_ncoll_cyclic(
370
+ g: gh.Graph,
371
+ g_matcher: Callable[str, list[tuple[gm.Point, ...]]],
372
+ theorem: pr.Theorem,
373
+ ) -> Generator[dict[str, gm.Point], None, None]:
374
+ """Match eqangle6 P A P B Q A Q B, ncoll P Q A B => cyclic A B P Q."""
375
+ for l1, l2, l3, l4 in g.all_eqangles_distinct_linepairss():
376
+ if len(set([l1, l2, l3, l4])) < 4:
377
+ continue # they all must be distinct.
378
+
379
+ p1s = l1.neighbors(gm.Point, return_set=True)
380
+ p2s = l2.neighbors(gm.Point, return_set=True)
381
+ p3s = l3.neighbors(gm.Point, return_set=True)
382
+ p4s = l4.neighbors(gm.Point, return_set=True)
383
+
384
+ p = intersect1(p1s, p2s)
385
+ if not p:
386
+ continue
387
+ q = intersect1(p3s, p4s)
388
+ if not q:
389
+ continue
390
+ a = intersect1(p1s, p3s)
391
+ if not a:
392
+ continue
393
+ b = intersect1(p2s, p4s)
394
+ if not b:
395
+ continue
396
+ if len(set([a, b, p, q])) < 4:
397
+ continue
398
+
399
+ if not g.check_ncoll([a, b, p, q]):
400
+ continue
401
+
402
+ yield dict(zip('ABPQ', [a, b, p, q]))
403
+
404
+
405
+ def match_eqangle_para(
406
+ g: gh.Graph,
407
+ g_matcher: Callable[str, list[tuple[gm.Point, ...]]],
408
+ theorem: pr.Theorem,
409
+ ) -> Generator[dict[str, gm.Point], None, None]:
410
+ """Match eqangle A B P Q C D P Q => para A B C D."""
411
+ for measure in g.type2nodes[gm.Measure]:
412
+ angs = measure.neighbors(gm.Angle)
413
+ d12, d21 = defaultdict(list), defaultdict(list)
414
+ for ang in angs:
415
+ d1, d2 = ang.directions
416
+ if d1 is None or d2 is None:
417
+ continue
418
+ d12[d1].append(d2)
419
+ d21[d2].append(d1)
420
+
421
+ for d1, d2s in d12.items():
422
+ a, b = g.two_points_on_direction(d1)
423
+ for d2, d3 in utils.comb2(d2s):
424
+ c, d = g.two_points_on_direction(d2)
425
+ e, f = g.two_points_on_direction(d3)
426
+ yield dict(zip('ABCDPQ', [c, d, e, f, a, b]))
427
+
428
+
429
+ def match_cyclic_eqangle(
430
+ g: gh.Graph,
431
+ g_matcher: Callable[str, list[tuple[gm.Point, ...]]],
432
+ theorem: pr.Theorem,
433
+ ) -> Generator[dict[str, gm.Point], None, None]:
434
+ """Match cyclic A B P Q => eqangle P A P B Q A Q B."""
435
+ record = set()
436
+ for a, b, c, d in g_matcher('cyclic'):
437
+ if (a, b, c, d) in record:
438
+ continue
439
+ record.add((a, b, c, d))
440
+ record.add((a, b, d, c))
441
+ record.add((b, a, c, d))
442
+ record.add((b, a, d, c))
443
+ yield dict(zip('ABPQ', [a, b, c, d]))
444
+
445
+
446
+ def rotate_simtri(
447
+ a: gm.Point, b: gm.Point, c: gm.Point, x: gm.Point, y: gm.Point, z: gm.Point
448
+ ) -> Generator[tuple[gm.Point, ...], None, None]:
449
+ """Rotate points around for similar triangle predicates."""
450
+ yield (z, y, x, c, b, a)
451
+ for p in [
452
+ (b, c, a, y, z, x),
453
+ (c, a, b, z, x, y),
454
+ (x, y, z, a, b, c),
455
+ (y, z, x, b, c, a),
456
+ (z, x, y, c, a, b),
457
+ ]:
458
+ yield p
459
+ yield p[::-1]
460
+
461
+
462
+ def match_cong_cong_cong_cyclic(
463
+ g: gh.Graph,
464
+ g_matcher: Callable[str, list[tuple[gm.Point, ...]]],
465
+ theorem: pr.Theorem,
466
+ ) -> Generator[dict[str, gm.Point], None, None]:
467
+ """Match cong O A O B, cong O B O C, cong O C O D => cyclic A B C D."""
468
+ for l in g.type2nodes[gm.Length]:
469
+ p2p = defaultdict(list)
470
+ for s in l.neighbors(gm.Segment):
471
+ a, b = s.points
472
+ p2p[a].append(b)
473
+ p2p[b].append(a)
474
+
475
+ for p, ps in p2p.items():
476
+ if len(ps) >= 4:
477
+ for a, b, c, d in utils.comb4(ps):
478
+ yield dict(zip('OABCD', [p, a, b, c, d]))
479
+
480
+
481
+ def match_cong_cong_cong_ncoll_contri(
482
+ g: gh.Graph,
483
+ g_matcher: Callable[str, list[tuple[gm.Point, ...]]],
484
+ theorem: pr.Theorem,
485
+ ) -> Generator[dict[str, gm.Point], None, None]:
486
+ """Match cong A B P Q, cong B C Q R, cong C A R P, ncoll A B C => contri* A B C P Q R."""
487
+ record = set()
488
+ for a, b, p, q in g_matcher('cong'):
489
+ for c in g.type2nodes[gm.Point]:
490
+ for r in g.type2nodes[gm.Point]:
491
+ if any([x in record for x in rotate_simtri(a, b, c, p, q, r)]):
492
+ continue
493
+ if not g.check_ncoll([a, b, c]):
494
+ continue
495
+ if g.check_cong([b, c, q, r]) and g.check_cong([c, a, r, p]):
496
+ record.add((a, b, c, p, q, r))
497
+ yield dict(zip('ABCPQR', [a, b, c, p, q, r]))
498
+
499
+
500
+ def match_cong_cong_eqangle6_ncoll_contri(
501
+ g: gh.Graph,
502
+ g_matcher: Callable[str, list[tuple[gm.Point, ...]]],
503
+ theorem: pr.Theorem,
504
+ ) -> Generator[dict[str, gm.Point], None, None]:
505
+ """Match cong A B P Q, cong B C Q R, eqangle6 B A B C Q P Q R, ncoll A B C => contri* A B C P Q R."""
506
+ record = set()
507
+ for a, b, p, q in g_matcher('cong'):
508
+ for c in g.type2nodes[gm.Point]:
509
+ if c in (a, b):
510
+ continue
511
+ for r in g.type2nodes[gm.Point]:
512
+ if r in (p, q):
513
+ continue
514
+
515
+ in_record = False
516
+ for x in [
517
+ (c, b, a, r, q, p),
518
+ (p, q, r, a, b, c),
519
+ (r, q, p, c, b, a),
520
+ ]:
521
+ if x in record:
522
+ in_record = True
523
+ break
524
+
525
+ if in_record:
526
+ continue
527
+
528
+ if not g.check_cong([b, c, q, r]):
529
+ continue
530
+ if not g.check_ncoll([a, b, c]):
531
+ continue
532
+
533
+ if nm.same_clock(a.num, b.num, c.num, p.num, q.num, r.num):
534
+ if g.check_eqangle([b, a, b, c, q, p, q, r]):
535
+ record.add((a, b, c, p, q, r))
536
+ yield dict(zip('ABCPQR', [a, b, c, p, q, r]))
537
+ else:
538
+ if g.check_eqangle([b, a, b, c, q, r, q, p]):
539
+ record.add((a, b, c, p, q, r))
540
+ yield dict(zip('ABCPQR', [a, b, c, p, q, r]))
541
+
542
+
543
+ def match_eqratio6_eqangle6_ncoll_simtri(
544
+ g: gh.Graph,
545
+ g_matcher: Callable[str, list[tuple[gm.Point, ...]]],
546
+ theorem: pr.Theorem,
547
+ ) -> Generator[dict[str, gm.Point], None, None]:
548
+ """Match eqratio6 B A B C Q P Q R, eqratio6 C A C B R P R Q, ncoll A B C => simtri* A B C P Q R."""
549
+ enums = g_matcher('eqratio6')
550
+
551
+ record = set()
552
+ for b, a, b, c, q, p, q, r in enums: # pylint: disable=redeclared-assigned-name,unused-variable
553
+ if (a, b, c) == (p, q, r):
554
+ continue
555
+ if any([x in record for x in rotate_simtri(a, b, c, p, q, r)]):
556
+ continue
557
+ if not g.check_ncoll([a, b, c]):
558
+ continue
559
+
560
+ if nm.same_clock(a.num, b.num, c.num, p.num, q.num, r.num):
561
+ if g.check_eqangle([b, a, b, c, q, p, q, r]):
562
+ record.add((a, b, c, p, q, r))
563
+ yield dict(zip('ABCPQR', [a, b, c, p, q, r]))
564
+ elif g.check_eqangle([b, a, b, c, q, r, q, p]):
565
+ record.add((a, b, c, p, q, r))
566
+ yield dict(zip('ABCPQR', [a, b, c, p, q, r]))
567
+
568
+
569
+ def match_eqangle6_eqangle6_ncoll_simtri(
570
+ g: gh.Graph,
571
+ g_matcher: Callable[str, list[tuple[gm.Point, ...]]],
572
+ theorem: pr.Theorem,
573
+ ) -> Generator[dict[str, gm.Point], None, None]:
574
+ """Match eqangle6 B A B C Q P Q R, eqangle6 C A C B R P R Q, ncoll A B C => simtri A B C P Q R."""
575
+ enums = g_matcher('eqangle6')
576
+
577
+ record = set()
578
+ for b, a, b, c, q, p, q, r in enums: # pylint: disable=redeclared-assigned-name,unused-variable
579
+ if (a, b, c) == (p, q, r):
580
+ continue
581
+ if any([x in record for x in rotate_simtri(a, b, c, p, q, r)]):
582
+ continue
583
+ if not g.check_eqangle([c, a, c, b, r, p, r, q]):
584
+ continue
585
+ if not g.check_ncoll([a, b, c]):
586
+ continue
587
+
588
+ mapping = dict(zip('ABCPQR', [a, b, c, p, q, r]))
589
+ record.add((a, b, c, p, q, r))
590
+ yield mapping
591
+
592
+
593
+ def match_eqratio6_eqratio6_ncoll_simtri(
594
+ g: gh.Graph,
595
+ g_matcher: Callable[str, list[tuple[gm.Point, ...]]],
596
+ theorem: pr.Theorem,
597
+ ) -> Generator[dict[str, gm.Point], None, None]:
598
+ """Match eqratio6 B A B C Q P Q R, eqratio6 C A C B R P R Q, ncoll A B C => simtri* A B C P Q R."""
599
+ enums = g_matcher('eqratio6')
600
+
601
+ record = set()
602
+ for b, a, b, c, q, p, q, r in enums: # pylint: disable=redeclared-assigned-name,unused-variable
603
+ if (a, b, c) == (p, q, r):
604
+ continue
605
+ if any([x in record for x in rotate_simtri(a, b, c, p, q, r)]):
606
+ continue
607
+ if not g.check_eqratio([c, a, c, b, r, p, r, q]):
608
+ continue
609
+ if not g.check_ncoll([a, b, c]):
610
+ continue
611
+
612
+ mapping = dict(zip('ABCPQR', [a, b, c, p, q, r]))
613
+ record.add((a, b, c, p, q, r))
614
+ yield mapping
615
+
616
+
617
+ def match_eqangle6_eqangle6_ncoll_simtri2(
618
+ g: gh.Graph,
619
+ g_matcher: Callable[str, list[tuple[gm.Point, ...]]],
620
+ theorem: pr.Theorem,
621
+ ) -> Generator[dict[str, gm.Point], None, None]:
622
+ """Match eqangle6 B A B C Q R Q P, eqangle6 C A C B R Q R P, ncoll A B C => simtri2 A B C P Q R."""
623
+ enums = g_matcher('eqangle6')
624
+
625
+ record = set()
626
+ for b, a, b, c, q, r, q, p in enums: # pylint: disable=redeclared-assigned-name,unused-variable
627
+ if (a, b, c) == (p, q, r):
628
+ continue
629
+ if any([x in record for x in rotate_simtri(a, b, c, p, q, r)]):
630
+ continue
631
+ if not g.check_eqangle([c, a, c, b, r, q, r, p]):
632
+ continue
633
+ if not g.check_ncoll([a, b, c]):
634
+ continue
635
+
636
+ mapping = dict(zip('ABCPQR', [a, b, c, p, q, r]))
637
+ record.add((a, b, c, p, q, r))
638
+ yield mapping
639
+
640
+
641
+ def rotate_contri(
642
+ a: gm.Point, b: gm.Point, c: gm.Point, x: gm.Point, y: gm.Point, z: gm.Point
643
+ ) -> Generator[tuple[gm.Point, ...], None, None]:
644
+ for p in [(b, a, c, y, x, z), (x, y, z, a, b, c), (y, x, z, b, a, c)]:
645
+ yield p
646
+
647
+
648
+ def match_eqangle6_eqangle6_ncoll_cong_contri(
649
+ g: gh.Graph,
650
+ g_matcher: Callable[str, list[tuple[gm.Point, ...]]],
651
+ theorem: pr.Theorem,
652
+ ) -> Generator[dict[str, gm.Point], None, None]:
653
+ """Match eqangle6 B A B C Q P Q R, eqangle6 C A C B R P R Q, ncoll A B C, cong A B P Q => contri A B C P Q R."""
654
+ enums = g_matcher('eqangle6')
655
+
656
+ record = set()
657
+ for b, a, b, c, q, p, q, r in enums: # pylint: disable=redeclared-assigned-name,unused-variable
658
+ if not g.check_cong([a, b, p, q]):
659
+ continue
660
+ if (a, b, c) == (p, q, r):
661
+ continue
662
+ if any([x in record for x in rotate_contri(a, b, c, p, q, r)]):
663
+ continue
664
+ if not g.check_eqangle([c, a, c, b, r, p, r, q]):
665
+ continue
666
+
667
+ if not g.check_ncoll([a, b, c]):
668
+ continue
669
+
670
+ mapping = dict(zip('ABCPQR', [a, b, c, p, q, r]))
671
+ record.add((a, b, c, p, q, r))
672
+ yield mapping
673
+
674
+
675
+ def match_eqratio6_eqratio6_ncoll_cong_contri(
676
+ g: gh.Graph,
677
+ g_matcher: Callable[str, list[tuple[gm.Point, ...]]],
678
+ theorem: pr.Theorem,
679
+ ) -> Generator[dict[str, gm.Point], None, None]:
680
+ """Match eqratio6 B A B C Q P Q R, eqratio6 C A C B R P R Q, ncoll A B C, cong A B P Q => contri* A B C P Q R."""
681
+ enums = g_matcher('eqratio6')
682
+
683
+ record = set()
684
+ for b, a, b, c, q, p, q, r in enums: # pylint: disable=redeclared-assigned-name,unused-variable
685
+ if not g.check_cong([a, b, p, q]):
686
+ continue
687
+ if (a, b, c) == (p, q, r):
688
+ continue
689
+ if any([x in record for x in rotate_contri(a, b, c, p, q, r)]):
690
+ continue
691
+ if not g.check_eqratio([c, a, c, b, r, p, r, q]):
692
+ continue
693
+
694
+ if not g.check_ncoll([a, b, c]):
695
+ continue
696
+
697
+ mapping = dict(zip('ABCPQR', [a, b, c, p, q, r]))
698
+ record.add((a, b, c, p, q, r))
699
+ yield mapping
700
+
701
+
702
+ def match_eqangle6_eqangle6_ncoll_cong_contri2(
703
+ g: gh.Graph,
704
+ g_matcher: Callable[str, list[tuple[gm.Point, ...]]],
705
+ theorem: pr.Theorem,
706
+ ) -> Generator[dict[str, gm.Point], None, None]:
707
+ """Match eqangle6 B A B C Q R Q P, eqangle6 C A C B R Q R P, ncoll A B C, cong A B P Q => contri2 A B C P Q R."""
708
+ enums = g_matcher('eqangle6')
709
+
710
+ record = set()
711
+ for b, a, b, c, q, r, q, p in enums: # pylint: disable=redeclared-assigned-name,unused-variable
712
+ if not g.check_cong([a, b, p, q]):
713
+ continue
714
+ if (a, b, c) == (p, q, r):
715
+ continue
716
+ if any([x in record for x in rotate_contri(a, b, c, p, q, r)]):
717
+ continue
718
+ if not g.check_eqangle([c, a, c, b, r, q, r, p]):
719
+ continue
720
+ if not g.check_ncoll([a, b, c]):
721
+ continue
722
+
723
+ mapping = dict(zip('ABCPQR', [a, b, c, p, q, r]))
724
+ record.add((a, b, c, p, q, r))
725
+ yield mapping
726
+
727
+
728
+ def match_eqratio6_coll_ncoll_eqangle6(
729
+ g: gh.Graph,
730
+ g_matcher: Callable[str, list[tuple[gm.Point, ...]]],
731
+ theorem: pr.Theorem,
732
+ ) -> Generator[dict[str, gm.Point], None, None]:
733
+ """Match eqratio6 d b d c a b a c, coll d b c, ncoll a b c => eqangle6 a b a d a d a c."""
734
+ records = set()
735
+ for b, d, c in g_matcher('coll'):
736
+ for a in g.all_points():
737
+ if g.check_coll([a, b, c]):
738
+ continue
739
+ if (a, b, d, c) in records or (a, c, d, b) in records:
740
+ continue
741
+ records.add((a, b, d, c))
742
+
743
+ if g.check_eqratio([d, b, d, c, a, b, a, c]):
744
+ yield dict(zip('abcd', [a, b, c, d]))
745
+
746
+
747
+ def match_eqangle6_coll_ncoll_eqratio6(
748
+ g: gh.Graph,
749
+ g_matcher: Callable[str, list[tuple[gm.Point, ...]]],
750
+ theorem: pr.Theorem,
751
+ ) -> Generator[dict[str, gm.Point], None, None]:
752
+ """Match eqangle6 a b a d a d a c, coll d b c, ncoll a b c => eqratio6 d b d c a b a c."""
753
+ records = set()
754
+ for b, d, c in g_matcher('coll'):
755
+ for a in g.all_points():
756
+ if g.check_coll([a, b, c]):
757
+ continue
758
+ if (a, b, d, c) in records or (a, c, d, b) in records:
759
+ continue
760
+ records.add((a, b, d, c))
761
+
762
+ if g.check_eqangle([a, b, a, d, a, d, a, c]):
763
+ yield dict(zip('abcd', [a, b, c, d]))
764
+
765
+
766
+ def match_eqangle6_ncoll_cyclic(
767
+ g: gh.Graph,
768
+ g_matcher: Callable[str, list[tuple[gm.Point, ...]]],
769
+ theorem: pr.Theorem,
770
+ ) -> Generator[dict[str, gm.Point], None, None]:
771
+ """Match eqangle6 P A P B Q A Q B, ncoll P Q A B => cyclic A B P Q."""
772
+ for a, b, a, c, x, y, x, z in g_matcher('eqangle6'): # pylint: disable=redeclared-assigned-name,unused-variable
773
+ if (b, c) != (y, z) or a == x:
774
+ continue
775
+ if nm.check_ncoll([x.num for x in [a, b, c, x]]):
776
+ yield dict(zip('ABPQ', [b, c, a, x]))
777
+
778
+
779
+ def match_all(
780
+ name: str, g: gh.Graph
781
+ ) -> Generator[tuple[gm.Point, ...], None, None]:
782
+ """Match all instances of a certain relation."""
783
+ if name in ['ncoll', 'npara', 'nperp']:
784
+ return []
785
+ if name == 'coll':
786
+ return g.all_colls()
787
+ if name == 'para':
788
+ return g.all_paras()
789
+ if name == 'perp':
790
+ return g.all_perps()
791
+ if name == 'cong':
792
+ return g.all_congs()
793
+ if name == 'eqangle':
794
+ return g.all_eqangles_8points()
795
+ if name == 'eqangle6':
796
+ return g.all_eqangles_6points()
797
+ if name == 'eqratio':
798
+ return g.all_eqratios_8points()
799
+ if name == 'eqratio6':
800
+ return g.all_eqratios_6points()
801
+ if name == 'cyclic':
802
+ return g.all_cyclics()
803
+ if name == 'midp':
804
+ return g.all_midps()
805
+ if name == 'circle':
806
+ return g.all_circles()
807
+ raise ValueError(f'Unrecognize {name}')
808
+
809
+
810
+ def cache_match(
811
+ graph: gh.Graph,
812
+ ) -> Callable[str, list[tuple[gm.Point, ...]]]:
813
+ """Cache throughout one single BFS level."""
814
+ cache = {}
815
+
816
+ def match_fn(name: str) -> list[tuple[gm.Point, ...]]:
817
+ if name in cache:
818
+ return cache[name]
819
+
820
+ result = list(match_all(name, graph))
821
+ cache[name] = result
822
+ return result
823
+
824
+ return match_fn
825
+
826
+
827
+ def try_to_map(
828
+ clause_enum: list[tuple[pr.Clause, list[tuple[gm.Point, ...]]]],
829
+ mapping: dict[str, gm.Point],
830
+ ) -> Generator[dict[str, gm.Point], None, None]:
831
+ """Recursively try to match the remaining points given current mapping."""
832
+ if not clause_enum:
833
+ yield mapping
834
+ return
835
+
836
+ clause, enum = clause_enum[0]
837
+ for points in enum:
838
+ mpcpy = dict(mapping)
839
+
840
+ fail = False
841
+ for p, a in zip(points, clause.args):
842
+ if a in mpcpy and mpcpy[a] != p or p in mpcpy and mpcpy[p] != a:
843
+ fail = True
844
+ break
845
+ mpcpy[a] = p
846
+ mpcpy[p] = a
847
+
848
+ if fail:
849
+ continue
850
+
851
+ for m in try_to_map(clause_enum[1:], mpcpy):
852
+ yield m
853
+
854
+
855
+ def match_generic(
856
+ g: gh.Graph,
857
+ cache: Callable[str, list[tuple[gm.Point, ...]]],
858
+ theorem: pr.Theorem
859
+ ) -> Generator[dict[str, gm.Point], None, None]:
860
+ """Match any generic rule that is not one of the above match_*() rules."""
861
+ clause2enum = {}
862
+
863
+ clauses = []
864
+ numerical_checks = []
865
+ for clause in theorem.premise:
866
+ if clause.name in ['ncoll', 'npara', 'nperp', 'sameside']:
867
+ numerical_checks.append(clause)
868
+ continue
869
+
870
+ enum = cache(clause.name)
871
+ if len(enum) == 0: # pylint: disable=g-explicit-length-test
872
+ return 0
873
+
874
+ clause2enum[clause] = enum
875
+ clauses.append((len(set(clause.args)), clause))
876
+
877
+ clauses = sorted(clauses, key=lambda x: x[0], reverse=True)
878
+ _, clauses = zip(*clauses)
879
+
880
+ for mapping in try_to_map([(c, clause2enum[c]) for c in clauses], {}):
881
+ if not mapping:
882
+ continue
883
+
884
+ checks_ok = True
885
+ for check in numerical_checks:
886
+ args = [mapping[a] for a in check.args]
887
+ if check.name == 'ncoll':
888
+ checks_ok = g.check_ncoll(args)
889
+ elif check.name == 'npara':
890
+ checks_ok = g.check_npara(args)
891
+ elif check.name == 'nperp':
892
+ checks_ok = g.check_nperp(args)
893
+ elif check.name == 'sameside':
894
+ checks_ok = g.check_sameside(args)
895
+ if not checks_ok:
896
+ break
897
+ if not checks_ok:
898
+ continue
899
+
900
+ yield mapping
901
+
902
+
903
+ BUILT_IN_FNS = {
904
+ 'cong_cong_cong_cyclic': match_cong_cong_cong_cyclic,
905
+ 'cong_cong_cong_ncoll_contri*': match_cong_cong_cong_ncoll_contri,
906
+ 'cong_cong_eqangle6_ncoll_contri*': match_cong_cong_eqangle6_ncoll_contri,
907
+ 'eqangle6_eqangle6_ncoll_simtri': match_eqangle6_eqangle6_ncoll_simtri,
908
+ 'eqangle6_eqangle6_ncoll_cong_contri': (
909
+ match_eqangle6_eqangle6_ncoll_cong_contri
910
+ ), # pylint: disable=line-too-long
911
+ 'eqangle6_eqangle6_ncoll_simtri2': match_eqangle6_eqangle6_ncoll_simtri2,
912
+ 'eqangle6_eqangle6_ncoll_cong_contri2': (
913
+ match_eqangle6_eqangle6_ncoll_cong_contri2
914
+ ), # pylint: disable=line-too-long
915
+ 'eqratio6_eqratio6_ncoll_simtri*': match_eqratio6_eqratio6_ncoll_simtri,
916
+ 'eqratio6_eqratio6_ncoll_cong_contri*': (
917
+ match_eqratio6_eqratio6_ncoll_cong_contri
918
+ ), # pylint: disable=line-too-long
919
+ 'eqangle_para': match_eqangle_para,
920
+ 'eqangle_ncoll_cyclic': match_eqangle_ncoll_cyclic,
921
+ 'eqratio6_eqangle6_ncoll_simtri*': match_eqratio6_eqangle6_ncoll_simtri,
922
+ 'eqangle_perp_perp': match_eqangle_perp_perp,
923
+ 'eqangle6_ncoll_cong': match_eqangle6_ncoll_cong,
924
+ 'perp_perp_ncoll_para': match_perp_perp_ncoll_para,
925
+ 'circle_perp_eqangle': match_circle_perp_eqangle,
926
+ 'circle_eqangle_perp': match_circle_eqangle_perp,
927
+ 'cyclic_eqangle_cong': match_cyclic_eqangle_cong,
928
+ 'midp_perp_cong': match_midp_perp_cong,
929
+ 'perp_perp_npara_eqangle': match_perp_perp_npara_eqangle,
930
+ 'cyclic_eqangle': match_cyclic_eqangle,
931
+ 'eqangle_eqangle_eqangle': match_eqangle_eqangle_eqangle,
932
+ 'eqratio_eqratio_eqratio': match_eqratio_eqratio_eqratio,
933
+ 'eqratio6_coll_ncoll_eqangle6': match_eqratio6_coll_ncoll_eqangle6,
934
+ 'eqangle6_coll_ncoll_eqratio6': match_eqangle6_coll_ncoll_eqratio6,
935
+ 'eqangle6_ncoll_cyclic': match_eqangle6_ncoll_cyclic,
936
+ }
937
+
938
+
939
+ SKIP_THEOREMS = set()
940
+
941
+
942
+ def set_skip_theorems(theorems: set[str]) -> None:
943
+ SKIP_THEOREMS.update(theorems)
944
+
945
+
946
+ MAX_BRANCH = 50_000
947
+
948
+
949
+ def match_one_theorem(
950
+ g: gh.Graph,
951
+ cache: Callable[str, list[tuple[gm.Point, ...]]],
952
+ theorem: pr.Theorem
953
+ ) -> Generator[dict[str, gm.Point], None, None]:
954
+ """Match all instances of a single theorem (rule)."""
955
+ if cache is None:
956
+ cache = cache_match(g)
957
+
958
+ if theorem.name in SKIP_THEOREMS:
959
+ return []
960
+
961
+ if theorem.name.split('_')[-1] in SKIP_THEOREMS:
962
+ return []
963
+
964
+ if theorem.name in BUILT_IN_FNS:
965
+ mps = BUILT_IN_FNS[theorem.name](g, cache, theorem)
966
+ else:
967
+ mps = match_generic(g, cache, theorem)
968
+
969
+ mappings = []
970
+ for mp in mps:
971
+ mappings.append(mp)
972
+ if len(mappings) > MAX_BRANCH: # cap branching at this number.
973
+ break
974
+
975
+ return mappings
976
+
977
+
978
+ def match_all_theorems(
979
+ g: gh.Graph, theorems: list[pr.Theorem], goal: pr.Clause
980
+ ) -> dict[pr.Theorem, dict[pr.Theorem, dict[str, gm.Point]]]:
981
+ """Match all instances of all theorems (rules)."""
982
+ cache = cache_match(g)
983
+ # for BFS, collect all potential matches
984
+ # and then do it at the same time
985
+ theorem2mappings = {}
986
+
987
+ # Step 1: list all matches
988
+ for _, theorem in theorems.items():
989
+ name = theorem.name
990
+ if name.split('_')[-1] in [
991
+ 'acompute',
992
+ 'rcompute',
993
+ 'fixl',
994
+ 'fixc',
995
+ 'fixb',
996
+ 'fixt',
997
+ 'fixp',
998
+ ]:
999
+ if goal and goal.name != name:
1000
+ continue
1001
+
1002
+ mappings = match_one_theorem(g, cache, theorem)
1003
+ if len(mappings): # pylint: disable=g-explicit-length-test
1004
+ theorem2mappings[theorem] = list(mappings)
1005
+ return theorem2mappings
1006
+
1007
+
1008
+ def bfs_one_level(
1009
+ g: gh.Graph,
1010
+ theorems: list[pr.Theorem],
1011
+ level: int,
1012
+ controller: pr.Problem,
1013
+ verbose: bool = False,
1014
+ nm_check: bool = False,
1015
+ timeout: int = 600,
1016
+ ) -> tuple[
1017
+ list[pr.Dependency],
1018
+ dict[str, list[tuple[gm.Point, ...]]],
1019
+ dict[str, list[tuple[gm.Point, ...]]],
1020
+ int,
1021
+ ]:
1022
+ """Forward deduce one breadth-first level."""
1023
+
1024
+ # Step 1: match all theorems:
1025
+ theorem2mappings = match_all_theorems(g, theorems, controller.goal)
1026
+
1027
+ # Step 2: traceback for each deduce:
1028
+ theorem2deps = {}
1029
+ t0 = time.time()
1030
+ for theorem, mappings in theorem2mappings.items():
1031
+ if time.time() - t0 > timeout:
1032
+ break
1033
+ mp_deps = []
1034
+ for mp in mappings:
1035
+ deps = EmptyDependency(level=level, rule_name=theorem.rule_name)
1036
+ fail = False # finding why deps might fail.
1037
+
1038
+ for p in theorem.premise:
1039
+ p_args = [mp[a] for a in p.args]
1040
+ # Trivial deps.
1041
+ if p.name == 'cong':
1042
+ a, b, c, d = p_args
1043
+ if {a, b} == {c, d}:
1044
+ continue
1045
+ if p.name == 'para':
1046
+ a, b, c, d = p_args
1047
+ if {a, b} == {c, d}:
1048
+ continue
1049
+
1050
+ if theorem.name in [
1051
+ 'cong_cong_eqangle6_ncoll_contri*',
1052
+ 'eqratio6_eqangle6_ncoll_simtri*',
1053
+ ]:
1054
+ if p.name in ['eqangle', 'eqangle6']: # SAS or RAR
1055
+ b, a, b, c, y, x, y, z = ( # pylint: disable=redeclared-assigned-name,unused-variable
1056
+ p_args
1057
+ )
1058
+ if not nm.same_clock(a.num, b.num, c.num, x.num, y.num, z.num):
1059
+ p_args = b, a, b, c, y, z, y, x
1060
+
1061
+ dep = Dependency(p.name, p_args, rule_name='', level=level)
1062
+ try:
1063
+ dep = dep.why_me_or_cache(g, level)
1064
+ except: # pylint: disable=bare-except
1065
+ fail = True
1066
+ break
1067
+
1068
+ if dep.why is None:
1069
+ fail = True
1070
+ break
1071
+ g.cache_dep(p.name, p_args, dep)
1072
+ deps.why.append(dep)
1073
+
1074
+ if fail:
1075
+ continue
1076
+
1077
+ mp_deps.append((mp, deps))
1078
+ theorem2deps[theorem] = mp_deps
1079
+
1080
+ theorem2deps = list(theorem2deps.items())
1081
+
1082
+ # Step 3: add conclusions to graph.
1083
+ # Note that we do NOT mix step 2 and 3, strictly going for BFS.
1084
+ added = []
1085
+ for theorem, mp_deps in theorem2deps:
1086
+ for mp, deps in mp_deps:
1087
+ if time.time() - t0 > timeout:
1088
+ break
1089
+ name, args = theorem.conclusion_name_args(mp)
1090
+ hash_conclusion = pr.hashed(name, args)
1091
+ if hash_conclusion in g.cache:
1092
+ continue
1093
+
1094
+ add = g.add_piece(name, args, deps=deps)
1095
+ added += add
1096
+
1097
+ branching = len(added)
1098
+
1099
+ # Check if goal is found
1100
+ if controller.goal:
1101
+ args = []
1102
+
1103
+ for a in controller.goal.args:
1104
+ if a in g._name2node:
1105
+ a = g._name2node[a]
1106
+ elif '/' in a:
1107
+ a = create_consts_str(g, a)
1108
+ elif a.isdigit():
1109
+ a = int(a)
1110
+ args.append(a)
1111
+
1112
+ if g.check(controller.goal.name, args):
1113
+ return added, {}, {}, branching
1114
+
1115
+ # Run AR, but do NOT apply to the proof state (yet).
1116
+ for dep in added:
1117
+ g.add_algebra(dep, level)
1118
+ derives, eq4s = g.derive_algebra(level, verbose=False)
1119
+
1120
+ branching += sum([len(x) for x in derives.values()])
1121
+ branching += sum([len(x) for x in eq4s.values()])
1122
+
1123
+ return added, derives, eq4s, branching
1124
+
1125
+
1126
+ def create_consts_str(g: gh.Graph, s: str) -> gm.Angle | gm.Ratio:
1127
+ if 'pi/' in s:
1128
+ n, d = s.split('pi/')
1129
+ n, d = int(n), int(d)
1130
+ p0, _ = g.get_or_create_const_ang(n, d)
1131
+ else:
1132
+ n, d = s.split('/')
1133
+ n, d = int(n), int(d)
1134
+ p0, _ = g.get_or_create_const_rat(n, d)
1135
+ return p0
1136
+
1137
+
1138
+ def do_algebra(
1139
+ g: gh.Graph, added: list[pr.Dependency], verbose: bool = False
1140
+ ) -> None:
1141
+ for add in added:
1142
+ g.add_algebra(add, None)
1143
+ derives, eq4s = g.derive_algebra(level=None, verbose=verbose)
1144
+ apply_derivations(g, derives)
1145
+ apply_derivations(g, eq4s)
1146
+
1147
+
1148
+ def apply_derivations(
1149
+ g: gh.Graph, derives: dict[str, list[tuple[gm.Point, ...]]]
1150
+ ) -> list[pr.Dependency]:
1151
+ applied = []
1152
+ all_derives = list(derives.items())
1153
+ for name, args in all_derives:
1154
+ for arg in args:
1155
+ applied += g.do_algebra(name, arg)
1156
+ return applied
ag4masses/alphageometry/dd_test.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Unit tests for dd."""
17
+ import unittest
18
+
19
+ from absl.testing import absltest
20
+ import dd
21
+ import graph as gh
22
+ import problem as pr
23
+
24
+
25
+ MAX_LEVEL = 1000
26
+
27
+
28
+ class DDTest(unittest.TestCase):
29
+
30
+ @classmethod
31
+ def setUpClass(cls):
32
+ super().setUpClass()
33
+ cls.defs = pr.Definition.from_txt_file('defs.txt', to_dict=True)
34
+ cls.rules = pr.Theorem.from_txt_file('rules.txt', to_dict=True)
35
+
36
+ def test_imo_2022_p4_should_succeed(self):
37
+ p = pr.Problem.from_txt(
38
+ 'a b = segment a b; g1 = on_tline g1 a a b; g2 = on_tline g2 b b a; m ='
39
+ ' on_circle m g1 a, on_circle m g2 b; n = on_circle n g1 a, on_circle n'
40
+ ' g2 b; c = on_pline c m a b, on_circle c g1 a; d = on_pline d m a b,'
41
+ ' on_circle d g2 b; e = on_line e a c, on_line e b d; p = on_line p a'
42
+ ' n, on_line p c d; q = on_line q b n, on_line q c d ? cong e p e q'
43
+ )
44
+ g, _ = gh.Graph.build_problem(p, DDTest.defs)
45
+ goal_args = g.names2nodes(p.goal.args)
46
+
47
+ success = False
48
+ for level in range(MAX_LEVEL):
49
+ added, _, _, _ = dd.bfs_one_level(g, DDTest.rules, level, p)
50
+ if g.check(p.goal.name, goal_args):
51
+ success = True
52
+ break
53
+ if not added: # saturated
54
+ break
55
+
56
+ self.assertTrue(success)
57
+
58
+ def test_incenter_excenter_should_fail(self):
59
+ p = pr.Problem.from_txt(
60
+ 'a b c = triangle a b c; d = incenter d a b c; e = excenter e a b c ?'
61
+ ' perp d c c e'
62
+ )
63
+ g, _ = gh.Graph.build_problem(p, DDTest.defs)
64
+ goal_args = g.names2nodes(p.goal.args)
65
+
66
+ success = False
67
+ for level in range(MAX_LEVEL):
68
+ added, _, _, _ = dd.bfs_one_level(g, DDTest.rules, level, p)
69
+ if g.check(p.goal.name, goal_args):
70
+ success = True
71
+ break
72
+ if not added: # saturated
73
+ break
74
+
75
+ self.assertFalse(success)
76
+
77
+
78
+ if __name__ == '__main__':
79
+ absltest.main()
ag4masses/alphageometry/ddar.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Implements the combination DD+AR."""
17
+ import time
18
+
19
+ from absl import logging
20
+ import dd
21
+ import graph as gh
22
+ import problem as pr
23
+ from problem import Dependency # pylint: disable=g-importing-member
24
+ import trace_back
25
+
26
+
27
+ def saturate_or_goal(
28
+ g: gh.Graph,
29
+ theorems: list[pr.Theorem],
30
+ level_times: list[float],
31
+ p: pr.Problem,
32
+ max_level: int = 100,
33
+ timeout: int = 600,
34
+ ) -> tuple[
35
+ list[dict[str, list[tuple[gh.Point, ...]]]],
36
+ list[dict[str, list[tuple[gh.Point, ...]]]],
37
+ list[int],
38
+ list[pr.Dependency],
39
+ ]:
40
+ """Run DD until saturation or goal found."""
41
+ derives = []
42
+ eq4s = []
43
+ branching = []
44
+ all_added = []
45
+
46
+ while len(level_times) < max_level:
47
+ level = len(level_times) + 1
48
+
49
+ t = time.time()
50
+ added, derv, eq4, n_branching = dd.bfs_one_level(
51
+ g, theorems, level, p, verbose=False, nm_check=True, timeout=timeout
52
+ )
53
+ all_added += added
54
+ branching.append(n_branching)
55
+
56
+ derives.append(derv)
57
+ eq4s.append(eq4)
58
+ level_time = time.time() - t
59
+
60
+ logging.info(f'Depth {level}/{max_level} time = {level_time}') # pylint: disable=logging-fstring-interpolation
61
+ level_times.append(level_time)
62
+
63
+ if p.goal is not None:
64
+ goal_args = list(map(lambda x: g.get(x, lambda: int(x)), p.goal.args))
65
+ if g.check(p.goal.name, goal_args): # found goal
66
+ break
67
+
68
+ if not added: # saturated
69
+ break
70
+
71
+ if level_time > timeout:
72
+ break
73
+
74
+ return derives, eq4s, branching, all_added
75
+
76
+
77
+ def solve(
78
+ g: gh.Graph,
79
+ theorems: list[pr.Problem],
80
+ controller: pr.Problem,
81
+ max_level: int = 1000,
82
+ timeout: int = 600,
83
+ ) -> tuple[gh.Graph, list[float], str, list[int], list[pr.Dependency]]:
84
+ """Alternate between DD and AR until goal is found."""
85
+ status = 'saturated'
86
+ level_times = []
87
+
88
+ dervs, eq4 = g.derive_algebra(level=0, verbose=False)
89
+ derives = [dervs]
90
+ eq4s = [eq4]
91
+ branches = []
92
+ all_added = []
93
+
94
+ while len(level_times) < max_level:
95
+ dervs, eq4, next_branches, added = saturate_or_goal(
96
+ g, theorems, level_times, controller, max_level, timeout=timeout
97
+ )
98
+ all_added += added
99
+
100
+ derives += dervs
101
+ eq4s += eq4
102
+ branches += next_branches
103
+
104
+ # Now, it is either goal or saturated
105
+ if controller.goal is not None:
106
+ goal_args = g.names2points(controller.goal.args)
107
+ if g.check(controller.goal.name, goal_args): # found goal
108
+ status = 'solved'
109
+ break
110
+
111
+ if not derives: # officially saturated.
112
+ logging.info("derives empty, breaking")
113
+ break
114
+
115
+ # Now we resort to algebra derivations.
116
+ added = []
117
+ while derives and not added:
118
+ added += dd.apply_derivations(g, derives.pop(0))
119
+
120
+ if added:
121
+ continue
122
+
123
+ # Final help from AR.
124
+ while eq4s and not added:
125
+ added += dd.apply_derivations(g, eq4s.pop(0))
126
+
127
+ all_added += added
128
+
129
+ if not added: # Nothing left. saturated.
130
+ logging.info("Nothing added, breaking")
131
+ break
132
+
133
+ return g, level_times, status, branches, all_added
134
+
135
+
136
+ def get_proof_steps(
137
+ g: gh.Graph, goal: pr.Clause, merge_trivials: bool = False
138
+ ) -> tuple[
139
+ list[pr.Dependency],
140
+ list[pr.Dependency],
141
+ list[tuple[list[pr.Dependency], list[pr.Dependency]]],
142
+ dict[tuple[str, ...], int],
143
+ ]:
144
+ """Extract proof steps from the built DAG."""
145
+ goal_args = g.names2nodes(goal.args)
146
+ query = Dependency(goal.name, goal_args, None, None)
147
+
148
+ setup, aux, log, setup_points = trace_back.get_logs(
149
+ query, g, merge_trivials=merge_trivials
150
+ )
151
+
152
+ refs = {}
153
+ setup = trace_back.point_log(setup, refs, set())
154
+ aux = trace_back.point_log(aux, refs, setup_points)
155
+
156
+ setup = [(prems, [tuple(p)]) for p, prems in setup]
157
+ aux = [(prems, [tuple(p)]) for p, prems in aux]
158
+
159
+ return setup, aux, log, refs
ag4masses/alphageometry/ddar_test.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Unit tests for ddar.py."""
17
+ import unittest
18
+
19
+ from absl.testing import absltest
20
+ import ddar
21
+ import graph as gh
22
+ import problem as pr
23
+
24
+
25
+ class DDARTest(unittest.TestCase):
26
+
27
+ @classmethod
28
+ def setUpClass(cls):
29
+ super().setUpClass()
30
+ cls.defs = pr.Definition.from_txt_file('defs.txt', to_dict=True)
31
+ cls.rules = pr.Theorem.from_txt_file('rules.txt', to_dict=True)
32
+
33
+ def test_orthocenter_should_fail(self):
34
+ txt = 'a b c = triangle a b c; d = on_tline d b a c, on_tline d c a b ? perp a d b c' # pylint: disable=line-too-long
35
+ p = pr.Problem.from_txt(txt)
36
+ g, _ = gh.Graph.build_problem(p, DDARTest.defs)
37
+
38
+ ddar.solve(g, DDARTest.rules, p, max_level=1000)
39
+ goal_args = g.names2nodes(p.goal.args)
40
+ self.assertFalse(g.check(p.goal.name, goal_args))
41
+
42
+ def test_orthocenter_aux_should_succeed(self):
43
+ txt = 'a b c = triangle a b c; d = on_tline d b a c, on_tline d c a b; e = on_line e a c, on_line e b d ? perp a d b c' # pylint: disable=line-too-long
44
+ p = pr.Problem.from_txt(txt)
45
+ g, _ = gh.Graph.build_problem(p, DDARTest.defs)
46
+
47
+ ddar.solve(g, DDARTest.rules, p, max_level=1000)
48
+ goal_args = g.names2nodes(p.goal.args)
49
+ self.assertTrue(g.check(p.goal.name, goal_args))
50
+
51
+ def test_incenter_excenter_should_succeed(self):
52
+ # Note that this same problem should fail in dd_test.py
53
+ p = pr.Problem.from_txt(
54
+ 'a b c = triangle a b c; d = incenter d a b c; e = excenter e a b c ?'
55
+ ' perp d c c e'
56
+ ) # pylint: disable=line-too-long
57
+ g, _ = gh.Graph.build_problem(p, DDARTest.defs)
58
+
59
+ ddar.solve(g, DDARTest.rules, p, max_level=1000)
60
+ goal_args = g.names2nodes(p.goal.args)
61
+ self.assertTrue(g.check(p.goal.name, goal_args))
62
+
63
+
64
+ if __name__ == '__main__':
65
+ absltest.main()
ag4masses/alphageometry/decoder_stack.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """The decoder stack in inference mode."""
17
+
18
+ from typing import Any, Tuple
19
+
20
+ import gin
21
+ from transformer import decoder_stack
22
+ import transformer_layer as tl
23
+
24
+
25
+ struct = decoder_stack.struct
26
+ nn_components = decoder_stack.nn_components
27
+ position = decoder_stack.position
28
+ jnp = decoder_stack.jnp
29
+ attention = decoder_stack.attention
30
+
31
+ DStackWindowState = decoder_stack.DStackWindowState
32
+
33
+ Array = Any
34
+
35
+ TransformerTaskConfig = decoder_stack.TransformerTaskConfig
36
+
37
+ DStackDecoderState = Tuple[tl.DecoderState, ...]
38
+
39
+
40
+ @gin.configurable
41
+ class DecoderStackGenerate(decoder_stack.DecoderStack):
42
+ """Stack of transformer decoder layers."""
43
+
44
+ layer_factory = tl.TransformerLayerGenerate
45
+
46
+ def init_decoder_state_vanilla(
47
+ self, sequence_length: int, start_of_sequence: Array
48
+ ) -> DStackDecoderState:
49
+ """Return initial state for autoregressive generation."""
50
+ return tuple(
51
+ [
52
+ layer.init_decoder_state_vanilla(sequence_length, start_of_sequence)
53
+ for layer in self.transformer_layers
54
+ ]
55
+ )
ag4masses/alphageometry/defs.txt ADDED
@@ -0,0 +1,407 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ angle_bisector x a b c
2
+ x : a b c x
3
+ a b c = ncoll a b c
4
+ x : eqangle b a b x b x b c
5
+ bisect a b c
6
+
7
+ angle_mirror x a b c
8
+ x : a b c x
9
+ a b c = ncoll a b c
10
+ x : eqangle b a b c b c b x
11
+ amirror a b c
12
+
13
+ circle x a b c
14
+ x : a b c
15
+ a b c = ncoll a b c
16
+ x : cong x a x b, cong x b x c
17
+ bline a b, bline a c
18
+
19
+ circumcenter x a b c
20
+ x : a b c
21
+ a b c = ncoll a b c
22
+ x : cong x a x b, cong x b x c
23
+ bline a b, bline a c
24
+
25
+ eq_quadrangle a b c d
26
+ d : a b c d
27
+ =
28
+ a : ; b : ; c : ; d : cong d a b c
29
+ eq_quadrangle
30
+
31
+ eq_trapezoid a b c d
32
+ d : a b c
33
+ =
34
+ a : ; b : ; c : ; d : para d c a b, cong d a b c
35
+ eq_trapezoid
36
+
37
+ eq_triangle x b c
38
+ x : b c
39
+ b c = diff b c
40
+ x : cong x b b c, cong b c c x; eqangle b x b c c b c x, eqangle x c x b b x b c
41
+ circle b b c, circle c b c
42
+
43
+ eqangle2 x a b c
44
+ x : a b c x
45
+ a b c = ncoll a b c
46
+ x : eqangle a b a x c x c b
47
+ eqangle2 a b c
48
+
49
+ eqdia_quadrangle a b c d
50
+ d : a b c d
51
+ =
52
+ a : ; b : ; c : ; d : cong d b a c
53
+ eqdia_quadrangle
54
+
55
+ eqdistance x a b c
56
+ x : a b c x
57
+ a b c = diff b c
58
+ x : cong x a b c
59
+ circle a b c
60
+
61
+ foot x a b c
62
+ x : a b c
63
+ a b c = ncoll a b c
64
+ x : perp x a b c, coll x b c
65
+ tline a b c, line b c
66
+
67
+ free a
68
+ a : a
69
+ =
70
+ a :
71
+ free
72
+
73
+ incenter x a b c
74
+ x : a b c
75
+ a b c = ncoll a b c
76
+ x : eqangle a b a x a x a c, eqangle c a c x c x c b; eqangle b c b x b x b a
77
+ bisect a b c, bisect b c a
78
+
79
+ incenter2 x y z i a b c
80
+ i : a b c, x : i b c, y : i c a, z : i a b
81
+ a b c = ncoll a b c
82
+ i : eqangle a b a i a i a c, eqangle c a c i c i c b; eqangle b c b i b i b a; x : coll x b c, perp i x b c; y : coll y c a, perp i y c a; z : coll z a b, perp i z a b; cong i x i y, cong i y i z
83
+ incenter2 a b c
84
+
85
+ excenter x a b c
86
+ x : a b c
87
+ a b c = ncoll a b c
88
+ x : eqangle a b a x a x a c, eqangle c a c x c x c b; eqangle b c b x b x b a
89
+ bisect b a c, exbisect b c a
90
+
91
+ excenter2 x y z i a b c
92
+ i : a b c, x : i b c, y : i c a, z : i a b
93
+ a b c = ncoll a b c
94
+ i : eqangle a b a i a i a c, eqangle c a c i c i c b; eqangle b c b i b i b a; x : coll x b c, perp i x b c; y : coll y c a, perp i y c a; z : coll z a b, perp i z a b; cong i x i y, cong i y i z
95
+ excenter2 a b c
96
+
97
+ centroid x y z i a b c
98
+ x : b c, y : c a, z : a b, i : a x b y
99
+ a b c = ncoll a b c
100
+ x : coll x b c, cong x b x c; y : coll y c a, cong y c y a; z : coll z a b, cong z a z b; i : coll a x i, coll b y i; coll c z i
101
+ centroid a b c
102
+
103
+ ninepoints x y z i a b c
104
+ x : b c, y : c a, z : a b, i : x y z
105
+ a b c = ncoll a b c
106
+ x : coll x b c, cong x b x c; y : coll y c a, cong y c y a; z : coll z a b, cong z a z b; i : cong i x i y, cong i y i z
107
+ ninepoints a b c
108
+
109
+ intersection_cc x o w a
110
+ x : o w a
111
+ o w a = ncoll o w a
112
+ x : cong o a o x, cong w a w x
113
+ circle o o a, circle w w a
114
+
115
+ intersection_lc x a o b
116
+ x : a o b
117
+ a o b = diff a b, diff o b, nperp b o b a
118
+ x : coll x a b, cong o b o x
119
+ line b a, circle o o b
120
+
121
+ intersection_ll x a b c d
122
+ x : a b c d
123
+ a b c d = npara a b c d, ncoll a b c d
124
+ x : coll x a b, coll x c d
125
+ line a b, line c d
126
+
127
+ intersection_lp x a b c m n
128
+ x : a b c m n
129
+ a b c m n = npara m n a b, ncoll a b c, ncoll c m n
130
+ x : coll x a b, para c x m n
131
+ line a b, pline c m n
132
+
133
+ intersection_lt x a b c d e
134
+ x : a b c d e
135
+ a b c d e = ncoll a b c, nperp a b d e
136
+ x : coll x a b, perp x c d e
137
+ line a b, tline c d e
138
+
139
+ intersection_pp x a b c d e f
140
+ x : a b c d e f
141
+ a b c d e f = diff a d, npara b c e f
142
+ x : para x a b c, para x d e f
143
+ pline a b c, pline d e f
144
+
145
+ intersection_tt x a b c d e f
146
+ x : a b c d e f
147
+ a b c d e f = diff a d, npara b c e f
148
+ x : perp x a b c, perp x d e f
149
+ tline a b c, tline d e f
150
+
151
+ iso_triangle a b c
152
+ c : a b c
153
+ =
154
+ a : ; b : ; c : eqangle b a b c c b c a, cong a b a c
155
+ isos
156
+
157
+ lc_tangent x a o
158
+ x : x a o
159
+ a o = diff a o
160
+ x : perp a x a o
161
+ tline a a o
162
+
163
+ midpoint x a b
164
+ x : a b
165
+ a b = diff a b
166
+ x : coll x a b, cong x a x b
167
+ midp a b
168
+
169
+ mirror x a b
170
+ x : a b
171
+ a b = diff a b
172
+ x : coll x a b, cong b a b x
173
+ pmirror a b
174
+
175
+ nsquare x a b
176
+ x : a b
177
+ a b = diff a b
178
+ x : cong x a a b, perp x a a b
179
+ rotaten90 a b
180
+
181
+ on_aline x a b c d e
182
+ x : x a b c d e
183
+ a b c d e = ncoll c d e
184
+ x : eqangle a x a b d c d e
185
+ aline e d c b a
186
+
187
+ on_aline2 x a b c d e
188
+ x : x a b c d e
189
+ a b c d e = ncoll c d e
190
+ x : eqangle x a x b d c d e
191
+ aline2 e d c b a
192
+
193
+ on_bline x a b
194
+ x : x a b
195
+ a b = diff a b
196
+ x : cong x a x b, eqangle a x a b b a b x
197
+ bline a b
198
+
199
+ on_circle x o a
200
+ x : x o a
201
+ o a = diff o a
202
+ x : cong o x o a
203
+ circle o o a
204
+
205
+ on_line x a b
206
+ x : x a b
207
+ a b = diff a b
208
+ x : coll x a b
209
+ line a b
210
+
211
+ on_pline x a b c
212
+ x : x a b c
213
+ a b c = diff b c, ncoll a b c
214
+ x : para x a b c
215
+ pline a b c
216
+
217
+ on_tline x a b c
218
+ x : x a b c
219
+ a b c = diff b c
220
+ x : perp x a b c
221
+ tline a b c
222
+
223
+ orthocenter x a b c
224
+ x : a b c
225
+ a b c = ncoll a b c
226
+ x : perp x a b c, perp x b c a; perp x c a b
227
+ tline a b c, tline b c a
228
+
229
+ parallelogram a b c x
230
+ x : a b c
231
+ a b c = ncoll a b c
232
+ x : para a b c x, para a x b c; cong a b c x, cong a x b c
233
+ pline a b c, pline c a b
234
+
235
+ pentagon a b c d e
236
+
237
+ =
238
+ a : ; b : ; c : ; d : ; e :
239
+ pentagon
240
+
241
+ psquare x a b
242
+ x : a b
243
+ a b = diff a b
244
+ x : cong x a a b, perp x a a b
245
+ rotatep90 a b
246
+
247
+ quadrangle a b c d
248
+
249
+ =
250
+ a : ; b : ; c : ; d :
251
+ quadrangle
252
+
253
+ r_trapezoid a b c d
254
+ d : a b c
255
+ =
256
+ a : ; b : ; c : ; d : para a b c d, perp a b a d
257
+ r_trapezoid
258
+
259
+ r_triangle a b c
260
+ c : a b c
261
+ =
262
+ a : ; b : ; c : perp a b a c
263
+ r_triangle
264
+
265
+ rectangle a b c d
266
+ c : a b c , d : a b c
267
+ =
268
+ a : ; b : ; c : perp a b b c ; d : para a b c d, para a d b c; perp a b a d, cong a b c d, cong a d b c, cong a c b d
269
+ rectangle
270
+
271
+ reflect x a b c
272
+ x : a b c
273
+ a b c = diff b c, ncoll a b c
274
+ x : cong b a b x, cong c a c x; perp b c a x
275
+ reflect a b c
276
+
277
+ risos a b c
278
+ c : a b
279
+ =
280
+ a : ; b : ; c : perp a b a c, cong a b a c; eqangle b a b c c b c a
281
+ risos
282
+
283
+ s_angle a b x y
284
+ x : a b x
285
+ a b = diff a b
286
+ x : s_angle a b x y
287
+ s_angle a b y
288
+
289
+ segment a b
290
+
291
+ =
292
+ a : ; b :
293
+ segment
294
+
295
+ shift x b c d
296
+ x : b c d
297
+ b c d = diff d b
298
+ x : cong x b c d, cong x c b d
299
+ shift d c b
300
+
301
+ square a b x y
302
+ x : a b, y : a b x
303
+ a b = diff a b
304
+ x : perp a b b x, cong a b b x; y : para a b x y, para a y b x; perp a y y x, cong b x x y, cong x y y a, perp a x b y, cong a x b y
305
+ square a b
306
+
307
+ isquare a b c d
308
+ c : a b , d : a b c
309
+ =
310
+ a : ; b : ; c : perp a b b c, cong a b b c; d : para a b c d, para a d b c; perp a d d c, cong b c c d, cong c d d a, perp a c b d, cong a c b d
311
+ isquare
312
+
313
+ trapezoid a b c d
314
+ d : a b c d
315
+ =
316
+ a : ; b : ; c : ; d : para a b c d
317
+ trapezoid
318
+
319
+ triangle a b c
320
+
321
+ =
322
+ a : ; b : ; c :
323
+ triangle
324
+
325
+ triangle12 a b c
326
+ c : a b c
327
+ =
328
+ a : ; b : ; c : rconst a b a c 1 2
329
+ triangle12
330
+
331
+ 2l1c x y z i a b c o
332
+ x : a b c o y z i, y : a b c o x z i, z : a b c o x y i, i : a b c o x y z
333
+ a b c o = cong o a o b, ncoll a b c
334
+ x y z i : coll x a c, coll y b c, cong o a o z, coll i o z, cong i x i y, cong i y i z, perp i x a c, perp i y b c
335
+ 2l1c a b c o
336
+
337
+ e5128 x y a b c d
338
+ x : a b c d y, y : a b c d x
339
+ a b c d = cong c b c d, perp b c b a
340
+ x y : cong c b c x, coll y a b, coll x y d, eqangle a b a d x a x y
341
+ e5128 a b c d
342
+
343
+ 3peq x y z a b c
344
+ z : b c z , x : a b c z y, y : a b c z x
345
+ a b c = ncoll a b c
346
+ z : coll z b c ; x y : coll x a b, coll y a c, coll x y z, cong z x z y
347
+ 3peq a b c
348
+
349
+ trisect x y a b c
350
+ x : a b c y, y : a b c x
351
+ a b c = ncoll a b c
352
+ x y : coll x a c, coll y a c, eqangle b a b x b x b y, eqangle b x b y b y b c
353
+ trisect a b c
354
+
355
+ trisegment x y a b
356
+ x : a b y, y : a b x
357
+ a b = diff a b
358
+ x y : coll x a b, coll y a b, cong x a x y, cong y x y b
359
+ trisegment a b
360
+
361
+ on_dia x a b
362
+ x : x a b
363
+ a b = diff a b
364
+ x : perp x a x b
365
+ dia a b
366
+
367
+ ieq_triangle a b c
368
+ c : a b
369
+ =
370
+ a : ; b : ; c : cong a b b c, cong b c c a; eqangle a b a c c a c b, eqangle c a c b b c b a
371
+ ieq_triangle
372
+
373
+ on_opline x a b
374
+ x : x a b
375
+ a b = diff a b
376
+ x : coll x a b
377
+ on_opline a b
378
+
379
+ cc_tangent0 x y o a w b
380
+ x : o a w b y, y : o a w b x
381
+ o a w b = diff o a, diff w b, diff o w
382
+ x y : cong o x o a, cong w y w b, perp x o x y, perp y w y x
383
+ cc_tangent0 o a w b
384
+
385
+ cc_tangent x y z i o a w b
386
+ x : o a w b y, y : o a w b x, z : o a w b i, i : o a w b z
387
+ o a w b = diff o a, diff w b, diff o w
388
+ x y : cong o x o a, cong w y w b, perp x o x y, perp y w y x; z i : cong o z o a, cong w i w b, perp z o z i, perp i w i z
389
+ cc_tangent o a w b
390
+
391
+ eqangle3 x a b d e f
392
+ x : x a b d e f
393
+ a b d e f = ncoll d e f, diff a b, diff d e, diff e f
394
+ x : eqangle x a x b d e d f
395
+ eqangle3 a b d e f
396
+
397
+ tangent x y a o b
398
+ x y : o a b
399
+ a o b = diff o a, diff o b, diff a b
400
+ x : cong o x o b, perp a x o x; y : cong o y o b, perp a y o y
401
+ tangent a o b
402
+
403
+ on_circum x a b c
404
+ x : a b c
405
+ a b c = ncoll a b c
406
+ x : cyclic a b c x
407
+ cyclic a b c
ag4masses/alphageometry/download.sh ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ gdown --folder https://bit.ly/alphageometry
17
+ export DATA=ag_ckpt_vocab
ag4masses/alphageometry/examples.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ orthocenter
2
+ a b c = triangle; h = on_tline b a c, on_tline c a b ? perp a h b c
3
+ orthocenter_aux
4
+ a b c = triangle; d = on_tline d b a c, on_tline d c a b; e = on_line e a c, on_line e b d ? perp a d b c
5
+ incenter_excenter
6
+ a b c = triangle a b c; d1 d2 d3 d = incenter2 a b c; e1 e2 e3 e = excenter2 a b c ? perp d c c e
7
+ euler
8
+ a b c = triangle a b c; h = orthocenter a b c; h1 = foot a b c; h2 = foot b c a; h3 = foot c a b; g1 g2 g3 g = centroid g1 g2 g3 g a b c; o = circle a b c ? coll h g o
ag4masses/alphageometry/fig1.svg ADDED
ag4masses/alphageometry/geometry.py ADDED
@@ -0,0 +1,578 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Implements geometric objects used in the graph representation."""
17
+ from __future__ import annotations
18
+ from collections import defaultdict # pylint: disable=g-importing-member
19
+ from typing import Any, Type
20
+
21
+ # pylint: disable=protected-access
22
+
23
+
24
+ class Node:
25
+ r"""Node in the proof state graph.
26
+
27
+ Can be Point, Line, Circle, etc.
28
+
29
+ Each node maintains a merge history to
30
+ other nodes if they are (found out to be) equivalent
31
+
32
+ a -> b -
33
+ \
34
+ c -> d -> e -> f -> g
35
+
36
+ d.merged_to = e
37
+ d.rep = g
38
+ d.merged_from = {a, b, c, d}
39
+ d.equivs = {a, b, c, d, e, f, g}
40
+ """
41
+
42
+ def __init__(self, name: str = '', graph: Any = None):
43
+ self.name = name or str(self)
44
+ self.graph = graph
45
+
46
+ self.edge_graph = {}
47
+ # Edge graph: what other nodes is connected to this node.
48
+ # edge graph = {
49
+ # other1: {self1: deps, self2: deps},
50
+ # other2: {self2: deps, self3: deps}
51
+ # }
52
+
53
+ self.merge_graph = {}
54
+ # Merge graph: history of merges with other nodes.
55
+ # merge_graph = {self1: {self2: deps1, self3: deps2}}
56
+
57
+ self.rep_by = None # represented by.
58
+ self.members = {self}
59
+
60
+ self._val = None
61
+ self._obj = None
62
+
63
+ self.deps = []
64
+
65
+ # numerical representation.
66
+ self.num = None
67
+ self.change = set() # what other nodes' num rely on this node?
68
+
69
+ def set_rep(self, node: Node) -> None:
70
+ if node == self:
71
+ return
72
+ self.rep_by = node
73
+ node.merge_edge_graph(self.edge_graph)
74
+ node.members.update(self.members)
75
+
76
+ def rep(self) -> Node:
77
+ x = self
78
+ while x.rep_by:
79
+ x = x.rep_by
80
+ return x
81
+
82
+ def why_rep(self) -> list[Any]:
83
+ return self.why_equal([self.rep()], None)
84
+
85
+ def rep_and_why(self) -> tuple[Node, list[Any]]:
86
+ rep = self.rep()
87
+ return rep, self.why_equal([rep], None)
88
+
89
+ def neighbors(
90
+ self, oftype: Type[Node], return_set: bool = False, do_rep: bool = True
91
+ ) -> list[Node]:
92
+ """Neighbors of this node in the proof state graph."""
93
+ if do_rep:
94
+ rep = self.rep()
95
+ else:
96
+ rep = self
97
+ result = set()
98
+
99
+ for n in rep.edge_graph:
100
+ if oftype is None or oftype and isinstance(n, oftype):
101
+ if do_rep:
102
+ result.add(n.rep())
103
+ else:
104
+ result.add(n)
105
+
106
+ if return_set:
107
+ return result
108
+ return list(result)
109
+
110
+ def merge_edge_graph(
111
+ self, new_edge_graph: dict[Node, dict[Node, list[Node]]]
112
+ ) -> None:
113
+ for x, xdict in new_edge_graph.items():
114
+ if x in self.edge_graph:
115
+ self.edge_graph[x].update(dict(xdict))
116
+ else:
117
+ self.edge_graph[x] = dict(xdict)
118
+
119
+ def merge(self, nodes: list[Node], deps: list[Any]) -> None:
120
+ for node in nodes:
121
+ self.merge_one(node, deps)
122
+
123
+ def merge_one(self, node: Node, deps: list[Any]) -> None:
124
+ node.rep().set_rep(self.rep())
125
+
126
+ if node in self.merge_graph:
127
+ return
128
+
129
+ self.merge_graph[node] = deps
130
+ node.merge_graph[self] = deps
131
+
132
+ def is_val(self, node: Node) -> bool:
133
+ return (
134
+ isinstance(self, Line)
135
+ and isinstance(node, Direction)
136
+ or isinstance(self, Segment)
137
+ and isinstance(node, Length)
138
+ or isinstance(self, Angle)
139
+ and isinstance(node, Measure)
140
+ or isinstance(self, Ratio)
141
+ and isinstance(node, Value)
142
+ )
143
+
144
+ def set_val(self, node: Node) -> None:
145
+ self._val = node
146
+
147
+ def set_obj(self, node: Node) -> None:
148
+ self._obj = node
149
+
150
+ @property
151
+ def val(self) -> Node:
152
+ if self._val is None:
153
+ return None
154
+ return self._val.rep()
155
+
156
+ @property
157
+ def obj(self) -> Node:
158
+ if self._obj is None:
159
+ return None
160
+ return self._obj.rep()
161
+
162
+ def equivs(self) -> set[Node]:
163
+ return self.rep().members
164
+
165
+ def connect_to(self, node: Node, deps: list[Any] = None) -> None:
166
+ rep = self.rep()
167
+
168
+ if node in rep.edge_graph:
169
+ rep.edge_graph[node].update({self: deps})
170
+ else:
171
+ rep.edge_graph[node] = {self: deps}
172
+
173
+ if self.is_val(node):
174
+ self.set_val(node)
175
+ node.set_obj(self)
176
+
177
+ def equivs_upto(self, level: int) -> dict[Node, Node]:
178
+ """What are the equivalent nodes up to a certain level."""
179
+ parent = {self: None}
180
+ visited = set()
181
+ queue = [self]
182
+ i = 0
183
+
184
+ while i < len(queue):
185
+ current = queue[i]
186
+ i += 1
187
+ visited.add(current)
188
+
189
+ for neighbor in current.merge_graph:
190
+ if (
191
+ level is not None
192
+ and current.merge_graph[neighbor].level is not None
193
+ and current.merge_graph[neighbor].level >= level
194
+ ):
195
+ continue
196
+ if neighbor not in visited:
197
+ queue.append(neighbor)
198
+ parent[neighbor] = current
199
+
200
+ return parent
201
+
202
+ def why_equal(self, others: list[Node], level: int) -> list[Any]:
203
+ """BFS why this node is equal to other nodes."""
204
+ others = set(others)
205
+ found = 0
206
+
207
+ parent = {}
208
+ queue = [self]
209
+ i = 0
210
+
211
+ while i < len(queue):
212
+ current = queue[i]
213
+ if current in others:
214
+ found += 1
215
+ if found == len(others):
216
+ break
217
+
218
+ i += 1
219
+
220
+ for neighbor in current.merge_graph:
221
+ if (
222
+ level is not None
223
+ and current.merge_graph[neighbor].level is not None
224
+ and current.merge_graph[neighbor].level >= level
225
+ ):
226
+ continue
227
+ if neighbor not in parent:
228
+ queue.append(neighbor)
229
+ parent[neighbor] = current
230
+
231
+ return bfs_backtrack(self, others, parent)
232
+
233
+ def why_equal_groups(
234
+ self, groups: list[list[Node]], level: int
235
+ ) -> tuple[list[Any], list[Node]]:
236
+ """BFS for why self is equal to at least one member of each group."""
237
+ others = [None for _ in groups]
238
+ found = 0
239
+
240
+ parent = {}
241
+ queue = [self]
242
+ i = 0
243
+
244
+ while i < len(queue):
245
+ current = queue[i]
246
+
247
+ for j, grp in enumerate(groups):
248
+ if others[j] is None and current in grp:
249
+ others[j] = current
250
+ found += 1
251
+
252
+ if found == len(others):
253
+ break
254
+
255
+ i += 1
256
+
257
+ for neighbor in current.merge_graph:
258
+ if (
259
+ level is not None
260
+ and current.merge_graph[neighbor].level is not None
261
+ and current.merge_graph[neighbor].level >= level
262
+ ):
263
+ continue
264
+ if neighbor not in parent:
265
+ queue.append(neighbor)
266
+ parent[neighbor] = current
267
+
268
+ return bfs_backtrack(self, others, parent), others
269
+
270
+ def why_val(self, level: int) -> list[Any]:
271
+ return self._val.why_equal([self.val], level)
272
+
273
+ def why_connect(self, node: Node, level: int = None) -> list[Any]:
274
+ rep = self.rep()
275
+ equivs = list(rep.edge_graph[node].keys())
276
+ if not equivs:
277
+ return None
278
+ equiv = equivs[0]
279
+ dep = rep.edge_graph[node][equiv]
280
+ return [dep] + self.why_equal(equiv, level)
281
+
282
+
283
+ def why_connect(*pairs: list[tuple[Node, Node]]) -> list[Any]:
284
+ result = []
285
+ for node1, node2 in pairs:
286
+ result += node1.why_connect(node2)
287
+ return result
288
+
289
+
290
+ def is_equiv(x: Node, y: Node, level: int = None) -> bool:
291
+ level = level or float('inf')
292
+ return x.why_equal([y], level) is not None
293
+
294
+
295
+ def is_equal(x: Node, y: Node, level: int = None) -> bool:
296
+ if x == y:
297
+ return True
298
+ if x._val is None or y._val is None:
299
+ return False
300
+ if x.val != y.val:
301
+ return False
302
+ return is_equiv(x._val, y._val, level)
303
+
304
+
305
+ def bfs_backtrack(
306
+ root: Node, leafs: list[Node], parent: dict[Node, Node]
307
+ ) -> list[Any]:
308
+ """Return the path given BFS trace of parent nodes."""
309
+ backtracked = {root} # no need to backtrack further when touching this set.
310
+ deps = []
311
+ for node in leafs:
312
+ if node is None:
313
+ return None
314
+ if node in backtracked:
315
+ continue
316
+ if node not in parent:
317
+ return None
318
+ while node not in backtracked:
319
+ backtracked.add(node)
320
+ deps.append(node.merge_graph[parent[node]])
321
+ node = parent[node]
322
+
323
+ return deps
324
+
325
+
326
+ class Point(Node):
327
+ pass
328
+
329
+
330
+ class Line(Node):
331
+ """Node of type Line."""
332
+
333
+ def new_val(self) -> Direction:
334
+ return Direction()
335
+
336
+ def why_coll(self, points: list[Point], level: int = None) -> list[Any]:
337
+ """Why points are connected to self."""
338
+ level = level or float('inf')
339
+
340
+ groups = []
341
+ for p in points:
342
+ group = [
343
+ l
344
+ for l, d in self.edge_graph[p].items()
345
+ if d is None or d.level < level
346
+ ]
347
+ if not group:
348
+ return None
349
+ groups.append(group)
350
+
351
+ min_deps = None
352
+ for line in groups[0]:
353
+ deps, others = line.why_equal_groups(groups[1:], level)
354
+ if deps is None:
355
+ continue
356
+ for p, o in zip(points, [line] + others):
357
+ deps.append(self.edge_graph[p][o])
358
+ if min_deps is None or len(deps) < len(min_deps):
359
+ min_deps = deps
360
+
361
+ if min_deps is None:
362
+ return None
363
+ return [d for d in min_deps if d is not None]
364
+
365
+
366
+ class Segment(Node):
367
+
368
+ def new_val(self) -> Length:
369
+ return Length()
370
+
371
+
372
+ class Circle(Node):
373
+ """Node of type Circle."""
374
+
375
+ def why_cyclic(self, points: list[Point], level: int = None) -> list[Any]:
376
+ """Why points are connected to self."""
377
+ level = level or float('inf')
378
+
379
+ groups = []
380
+ for p in points:
381
+ group = [
382
+ c
383
+ for c, d in self.edge_graph[p].items()
384
+ if d is None or d.level < level
385
+ ]
386
+ if not group:
387
+ return None
388
+ groups.append(group)
389
+
390
+ min_deps = None
391
+ for circle in groups[0]:
392
+ deps, others = circle.why_equal_groups(groups[1:], level)
393
+ if deps is None:
394
+ continue
395
+ for p, o in zip(points, [circle] + others):
396
+ deps.append(self.edge_graph[p][o])
397
+
398
+ if min_deps is None or len(deps) < len(min_deps):
399
+ min_deps = deps
400
+
401
+ if min_deps is None:
402
+ return None
403
+ return [d for d in min_deps if d is not None]
404
+
405
+
406
+ def why_equal(x: Node, y: Node, level: int = None) -> list[Any]:
407
+ if x == y:
408
+ return []
409
+ if not x._val or not y._val:
410
+ return None
411
+ if x._val == y._val:
412
+ return []
413
+ return x._val.why_equal([y._val], level)
414
+
415
+
416
+ class Direction(Node):
417
+ pass
418
+
419
+
420
+ def get_lines_thru_all(*points: list[Point]) -> list[Line]:
421
+ line2count = defaultdict(lambda: 0)
422
+ points = set(points)
423
+ for p in points:
424
+ for l in p.neighbors(Line):
425
+ line2count[l] += 1
426
+ return [l for l, count in line2count.items() if count == len(points)]
427
+
428
+
429
+ def line_of_and_why(
430
+ points: list[Point], level: int = None
431
+ ) -> tuple[Line, list[Any]]:
432
+ """Why points are collinear."""
433
+ for l0 in get_lines_thru_all(*points):
434
+ for l in l0.equivs():
435
+ if all([p in l.edge_graph for p in points]):
436
+ x, y = l.points
437
+ colls = list({x, y} | set(points))
438
+ # if len(colls) < 3:
439
+ # return l, []
440
+ why = l.why_coll(colls, level)
441
+ if why is not None:
442
+ return l, why
443
+
444
+ return None, None
445
+
446
+
447
+ def get_circles_thru_all(*points: list[Point]) -> list[Circle]:
448
+ circle2count = defaultdict(lambda: 0)
449
+ points = set(points)
450
+ for p in points:
451
+ for c in p.neighbors(Circle):
452
+ circle2count[c] += 1
453
+ return [c for c, count in circle2count.items() if count == len(points)]
454
+
455
+
456
+ def circle_of_and_why(
457
+ points: list[Point], level: int = None
458
+ ) -> tuple[Circle, list[Any]]:
459
+ """Why points are concyclic."""
460
+ for c0 in get_circles_thru_all(*points):
461
+ for c in c0.equivs():
462
+ if all([p in c.edge_graph for p in points]):
463
+ cycls = list(set(points))
464
+ why = c.why_cyclic(cycls, level)
465
+ if why is not None:
466
+ return c, why
467
+
468
+ return None, None
469
+
470
+
471
+ def name_map(struct: Any) -> Any:
472
+ if isinstance(struct, list):
473
+ return [name_map(x) for x in struct]
474
+ elif isinstance(struct, tuple):
475
+ return tuple([name_map(x) for x in struct])
476
+ elif isinstance(struct, set):
477
+ return set([name_map(x) for x in struct])
478
+ elif isinstance(struct, dict):
479
+ return {name_map(x): name_map(y) for x, y in struct.items()}
480
+ else:
481
+ return getattr(struct, 'name', '')
482
+
483
+
484
+ class Angle(Node):
485
+ """Node of type Angle."""
486
+
487
+ def new_val(self) -> Measure:
488
+ return Measure()
489
+
490
+ def set_directions(self, d1: Direction, d2: Direction) -> None:
491
+ self._d = d1, d2
492
+
493
+ @property
494
+ def directions(self) -> tuple[Direction, Direction]:
495
+ d1, d2 = self._d
496
+ if d1 is None or d2 is None:
497
+ return d1, d2
498
+ return d1.rep(), d2.rep()
499
+
500
+
501
+ class Measure(Node):
502
+ pass
503
+
504
+
505
+ class Length(Node):
506
+ pass
507
+
508
+
509
+ class Ratio(Node):
510
+ """Node of type Ratio."""
511
+
512
+ def new_val(self) -> Value:
513
+ return Value()
514
+
515
+ def set_lengths(self, l1: Length, l2: Length) -> None:
516
+ self._l = l1, l2
517
+
518
+ @property
519
+ def lengths(self) -> tuple[Length, Length]:
520
+ l1, l2 = self._l
521
+ if l1 is None or l2 is None:
522
+ return l1, l2
523
+ return l1.rep(), l2.rep()
524
+
525
+
526
+ class Value(Node):
527
+ pass
528
+
529
+
530
+ def all_angles(
531
+ d1: Direction, d2: Direction, level: int = None
532
+ ) -> tuple[Angle, list[Direction], list[Direction]]:
533
+ level = level or float('inf')
534
+ d1s = d1.equivs_upto(level)
535
+ d2s = d2.equivs_upto(level)
536
+
537
+ for ang in d1.rep().neighbors(Angle):
538
+ d1_, d2_ = ang._d
539
+ if d1_ in d1s and d2_ in d2s:
540
+ yield ang, d1s, d2s
541
+
542
+
543
+ def all_ratios(
544
+ d1, d2, level=None
545
+ ) -> tuple[Angle, list[Direction], list[Direction]]:
546
+ level = level or float('inf')
547
+ d1s = d1.equivs_upto(level)
548
+ d2s = d2.equivs_upto(level)
549
+
550
+ for ang in d1.rep().neighbors(Ratio):
551
+ d1_, d2_ = ang._l
552
+ if d1_ in d1s and d2_ in d2s:
553
+ yield ang, d1s, d2s
554
+
555
+
556
+ RANKING = {
557
+ Point: 0,
558
+ Line: 1,
559
+ Segment: 2,
560
+ Circle: 3,
561
+ Direction: 4,
562
+ Length: 5,
563
+ Angle: 6,
564
+ Ratio: 7,
565
+ Measure: 8,
566
+ Value: 9,
567
+ }
568
+
569
+
570
+ def val_type(x: Node) -> Type[Node]:
571
+ if isinstance(x, Line):
572
+ return Direction
573
+ if isinstance(x, Segment):
574
+ return Length
575
+ if isinstance(x, Angle):
576
+ return Measure
577
+ if isinstance(x, Ratio):
578
+ return Value
ag4masses/alphageometry/geometry_150M_generate.gin ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ NUM_EMBEDDINGS = 1024
2
+
3
+ # Number of parameters = 152M
4
+ NUM_LAYERS = 12
5
+ EMBED_DIM = 1024
6
+ NUM_HEADS = 8
7
+ HEAD_DIM = 128
8
+ MLP_DIM = 4096
9
+
10
+
11
+ transformer_layer.TransformerLayerGenerate:
12
+ num_heads = %NUM_HEADS
13
+ head_size = %HEAD_DIM
14
+ window_length = 1024
15
+ use_long_xl_architecture = False
16
+ max_unrolled_windows = -1 # Always unroll.
17
+ relative_position_type = "t5" # Can be "fourier", "t5", or None.
18
+ use_causal_mask = True
19
+ attn_dropout_rate = %ATTN_DROPOUT_RATE # Attention matrix dropout.
20
+ memory_num_neighbors = 0
21
+ dtype = %DTYPE
22
+
23
+ decoder_stack.DecoderStackGenerate:
24
+ num_layers = %NUM_LAYERS
25
+ embedding_size = %EMBED_DIM
26
+ embedding_stddev = 1.0
27
+ layer_factory = @transformer_layer.TransformerLayerGenerate
28
+ dstack_window_length = 0
29
+ use_absolute_positions = False
30
+ use_final_layernorm = True # Final layernorm before token lookup.
31
+ final_dropout_rate = %DROPOUT_RATE # Dropout before token lookup.
32
+ final_mlp_factory = None # Final MLP to predict target tokens.
33
+ recurrent_layer_indices = ()
34
+ memory_factory = None # e.g. @memory_factory.memory_on_tpu_factory
35
+ memory_layer_indices = ()
36
+ dtype = %DTYPE
37
+
38
+
39
+ models.DecoderOnlyLanguageModelGenerate:
40
+ num_heads = %NUM_HEADS
41
+ head_size = %HEAD_DIM
42
+ task_config = @decoder_stack.TransformerTaskConfig()
43
+ decoder_factory = @decoder_stack.DecoderStackGenerate
44
+
45
+
46
+ training_loop.Trainer:
47
+ model_definition = @models.DecoderOnlyLanguageModelGenerate
ag4masses/alphageometry/geometry_test.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Unit tests for geometry.py."""
17
+ import unittest
18
+
19
+ from absl.testing import absltest
20
+ import geometry as gm
21
+
22
+
23
+ class GeometryTest(unittest.TestCase):
24
+
25
+ def _setup_equality_example(self):
26
+ # Create 4 nodes a, b, c, d
27
+ # and their lengths
28
+ a = gm.Segment('a')
29
+ la = gm.Length('l(a)')
30
+ a.connect_to(la)
31
+ la.connect_to(a)
32
+
33
+ b = gm.Segment('b')
34
+ lb = gm.Length('l(b)')
35
+ b.connect_to(lb)
36
+ lb.connect_to(b)
37
+
38
+ c = gm.Segment('c')
39
+ lc = gm.Length('l(c)')
40
+ c.connect_to(lc)
41
+ lc.connect_to(c)
42
+
43
+ d = gm.Segment('d')
44
+ ld = gm.Length('l(d)')
45
+ d.connect_to(ld)
46
+ ld.connect_to(d)
47
+
48
+ # Now let a=b, b=c, a=c, c=d
49
+ la.merge([lb], 'fact1')
50
+ lb.merge([lc], 'fact2')
51
+ la.merge([lc], 'fact3')
52
+ lc.merge([ld], 'fact4')
53
+ return a, b, c, d, la, lb, lc, ld
54
+
55
+ def test_merged_node_representative(self):
56
+ _, _, _, _, la, lb, lc, ld = self._setup_equality_example()
57
+
58
+ # all nodes are now represented by la.
59
+ self.assertEqual(la.rep(), la)
60
+ self.assertEqual(lb.rep(), la)
61
+ self.assertEqual(lc.rep(), la)
62
+ self.assertEqual(ld.rep(), la)
63
+
64
+ def test_merged_node_equivalence(self):
65
+ _, _, _, _, la, lb, lc, ld = self._setup_equality_example()
66
+ # all la, lb, lc, ld are equivalent
67
+ self.assertCountEqual(la.equivs(), [la, lb, lc, ld])
68
+ self.assertCountEqual(lb.equivs(), [la, lb, lc, ld])
69
+ self.assertCountEqual(lc.equivs(), [la, lb, lc, ld])
70
+ self.assertCountEqual(ld.equivs(), [la, lb, lc, ld])
71
+
72
+ def test_bfs_for_equality_transitivity(self):
73
+ a, _, _, d, _, _, _, _ = self._setup_equality_example()
74
+
75
+ # check that a==d because fact3 & fact4, not fact1 & fact2
76
+ self.assertCountEqual(gm.why_equal(a, d), ['fact3', 'fact4'])
77
+
78
+
79
+ if __name__ == '__main__':
80
+ absltest.main()
ag4masses/alphageometry/graph.py ADDED
@@ -0,0 +1,3057 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Implements the graph representation of the proof state."""
17
+
18
+ # pylint: disable=g-multiple-import
19
+ from __future__ import annotations
20
+
21
+ from collections import defaultdict # pylint: disable=g-importing-member
22
+ from typing import Callable, Generator, Optional, Type, Union
23
+
24
+ from absl import logging
25
+ import ar
26
+ import geometry as gm
27
+ from geometry import Angle, Direction, Length, Ratio
28
+ from geometry import Circle, Line, Point, Segment
29
+ from geometry import Measure, Value
30
+ import graph_utils as utils
31
+ import numericals as nm
32
+ import problem
33
+ from problem import Dependency, EmptyDependency
34
+
35
+
36
+ np = nm.np
37
+
38
+
39
+ FREE = [
40
+ 'free',
41
+ 'segment',
42
+ 'r_triangle',
43
+ 'risos',
44
+ 'triangle',
45
+ 'triangle12',
46
+ 'ieq_triangle',
47
+ 'eq_quadrangle',
48
+ 'eq_trapezoid',
49
+ 'eqdia_quadrangle',
50
+ 'quadrangle',
51
+ 'r_trapezoid',
52
+ 'rectangle',
53
+ 'isquare',
54
+ 'trapezoid',
55
+ 'pentagon',
56
+ 'iso_triangle',
57
+ ]
58
+
59
+ INTERSECT = [
60
+ 'angle_bisector',
61
+ 'angle_mirror',
62
+ 'eqdistance',
63
+ 'lc_tangent',
64
+ 'on_aline',
65
+ 'on_bline',
66
+ 'on_circle',
67
+ 'on_line',
68
+ 'on_pline',
69
+ 'on_tline',
70
+ 'on_dia',
71
+ 's_angle',
72
+ 'on_opline',
73
+ 'eqangle3',
74
+ ]
75
+
76
+
77
+ # pylint: disable=protected-access
78
+ # pylint: disable=unused-argument
79
+
80
+
81
+ class DepCheckFailError(Exception):
82
+ pass
83
+
84
+
85
+ class PointTooCloseError(Exception):
86
+ pass
87
+
88
+
89
+ class PointTooFarError(Exception):
90
+ pass
91
+
92
+
93
+ class Graph:
94
+ """Graph data structure representing proof state."""
95
+
96
+ def __init__(self):
97
+ self.type2nodes = {
98
+ Point: [],
99
+ Line: [],
100
+ Segment: [],
101
+ Circle: [],
102
+ Direction: [],
103
+ Length: [],
104
+ Angle: [],
105
+ Ratio: [],
106
+ Measure: [],
107
+ Value: [],
108
+ }
109
+ self._name2point = {}
110
+ self._name2node = {}
111
+
112
+ self.rconst = {} # contains all constant ratios
113
+ self.aconst = {} # contains all constant angles.
114
+
115
+ self.halfpi, _ = self.get_or_create_const_ang(1, 2)
116
+ self.vhalfpi = self.halfpi.val
117
+
118
+ self.atable = ar.AngleTable()
119
+ self.dtable = ar.DistanceTable()
120
+ self.rtable = ar.RatioTable()
121
+
122
+ # to quick access deps.
123
+ self.cache = {}
124
+
125
+ self._pair2line = {}
126
+ self._triplet2circle = {}
127
+
128
+ def copy(self) -> Graph:
129
+ """Make a copy of self."""
130
+ p, definitions = self.build_def
131
+
132
+ p = p.copy()
133
+ for clause in p.clauses:
134
+ clause.nums = []
135
+ for pname in clause.points:
136
+ clause.nums.append(self._name2node[pname].num)
137
+
138
+ g, _ = Graph.build_problem(p, definitions, verbose=False, init_copy=False)
139
+
140
+ g.build_clauses = list(getattr(self, 'build_clauses', []))
141
+ return g
142
+
143
+ def _create_const_ang(self, n: int, d: int) -> None:
144
+ n, d = ar.simplify(n, d)
145
+ ang = self.aconst[(n, d)] = self.new_node(Angle, f'{n}pi/{d}')
146
+ ang.set_directions(None, None)
147
+ self.connect_val(ang, deps=None)
148
+
149
+ def _create_const_rat(self, n: int, d: int) -> None:
150
+ n, d = ar.simplify(n, d)
151
+ rat = self.rconst[(n, d)] = self.new_node(Ratio, f'{n}/{d}')
152
+ rat.set_lengths(None, None)
153
+ self.connect_val(rat, deps=None)
154
+
155
+ def get_or_create_const_ang(self, n: int, d: int) -> None:
156
+ n, d = ar.simplify(n, d)
157
+ if (n, d) not in self.aconst:
158
+ self._create_const_ang(n, d)
159
+ ang1 = self.aconst[(n, d)]
160
+
161
+ n, d = ar.simplify(d - n, d)
162
+ if (n, d) not in self.aconst:
163
+ self._create_const_ang(n, d)
164
+ ang2 = self.aconst[(n, d)]
165
+ return ang1, ang2
166
+
167
+ def get_or_create_const_rat(self, n: int, d: int) -> None:
168
+ n, d = ar.simplify(n, d)
169
+ if (n, d) not in self.rconst:
170
+ self._create_const_rat(n, d)
171
+ rat1 = self.rconst[(n, d)]
172
+
173
+ if (d, n) not in self.rconst:
174
+ self._create_const_rat(d, n) # pylint: disable=arguments-out-of-order
175
+ rat2 = self.rconst[(d, n)]
176
+ return rat1, rat2
177
+
178
+ def add_algebra(self, dep: Dependency, level: int) -> None:
179
+ """Add new algebraic predicates."""
180
+ _ = level
181
+ if dep.name not in [
182
+ 'para',
183
+ 'perp',
184
+ 'eqangle',
185
+ 'eqratio',
186
+ 'aconst',
187
+ 'rconst',
188
+ 'cong',
189
+ ]:
190
+ return
191
+
192
+ name, args = dep.name, dep.args
193
+
194
+ if name == 'para':
195
+ ab, cd = dep.algebra
196
+ self.atable.add_para(ab, cd, dep)
197
+
198
+ if name == 'perp':
199
+ ab, cd = dep.algebra
200
+ self.atable.add_const_angle(ab, cd, 90, dep)
201
+
202
+ if name == 'eqangle':
203
+ ab, cd, mn, pq = dep.algebra
204
+ if (ab, cd) == (pq, mn):
205
+ self.atable.add_const_angle(ab, cd, 90, dep)
206
+ else:
207
+ self.atable.add_eqangle(ab, cd, mn, pq, dep)
208
+
209
+ if name == 'eqratio':
210
+ ab, cd, mn, pq = dep.algebra
211
+ if (ab, cd) == (pq, mn):
212
+ self.rtable.add_eq(ab, cd, dep)
213
+ else:
214
+ self.rtable.add_eqratio(ab, cd, mn, pq, dep)
215
+
216
+ if name == 'aconst':
217
+ bx, ab, y = dep.algebra
218
+ self.atable.add_const_angle(bx, ab, y, dep)
219
+
220
+ if name == 'rconst':
221
+ l1, l2, m, n = dep.algebra
222
+ self.rtable.add_const_ratio(l1, l2, m, n, dep)
223
+
224
+ if name == 'cong':
225
+ a, b, c, d = args
226
+ ab, _ = self.get_line_thru_pair_why(a, b)
227
+ cd, _ = self.get_line_thru_pair_why(c, d)
228
+ self.dtable.add_cong(ab, cd, a, b, c, d, dep)
229
+
230
+ ab, cd = dep.algebra
231
+ self.rtable.add_eq(ab, cd, dep)
232
+
233
+ def add_eqrat_const(
234
+ self, args: list[Point], deps: EmptyDependency
235
+ ) -> list[Dependency]:
236
+ """Add new algebraic predicates of type eqratio-constant."""
237
+ a, b, c, d, num, den = args
238
+ nd, dn = self.get_or_create_const_rat(num, den)
239
+
240
+ if num == den:
241
+ return self.add_cong([a, b, c, d], deps)
242
+
243
+ ab = self._get_or_create_segment(a, b, deps=None)
244
+ cd = self._get_or_create_segment(c, d, deps=None)
245
+
246
+ self.connect_val(ab, deps=None)
247
+ self.connect_val(cd, deps=None)
248
+
249
+ if ab.val == cd.val:
250
+ raise ValueError(f'{ab.name} and {cd.name} cannot be equal')
251
+
252
+ args = [a, b, c, d, nd]
253
+ i = 0
254
+ for x, y, xy in [(a, b, ab), (c, d, cd)]:
255
+ i += 1
256
+ x_, y_ = list(xy._val._obj.points)
257
+ if {x, y} == {x_, y_}:
258
+ continue
259
+ if deps:
260
+ deps = deps.extend(self, 'rconst', list(args), 'cong', [x, y, x_, y_])
261
+ args[2 * i - 2] = x_
262
+ args[2 * i - 1] = y_
263
+
264
+ ab_cd, cd_ab, why = self._get_or_create_ratio(ab, cd, deps=None)
265
+ if why:
266
+ dep0 = deps.populate('rconst', [a, b, c, d, nd])
267
+ deps = EmptyDependency(level=deps.level, rule_name=None)
268
+ deps.why = [dep0] + why
269
+
270
+ lab, lcd = ab_cd._l
271
+ a, b = list(lab._obj.points)
272
+ c, d = list(lcd._obj.points)
273
+
274
+ add = []
275
+ if not self.is_equal(ab_cd, nd):
276
+ args = [a, b, c, d, nd]
277
+ dep1 = deps.populate('rconst', args)
278
+ dep1.algebra = ab._val, cd._val, num, den
279
+ self.make_equal(nd, ab_cd, deps=dep1)
280
+ self.cache_dep('rconst', [a, b, c, d, nd], dep1)
281
+ add += [dep1]
282
+
283
+ if not self.is_equal(cd_ab, dn):
284
+ args = [c, d, a, b, dn]
285
+ dep2 = deps.populate('rconst', args)
286
+ dep2.algebra = cd._val, ab._val, den, num
287
+ self.make_equal(dn, cd_ab, deps=dep2)
288
+ self.cache_dep('rconst', [c, d, a, b, dn], dep2)
289
+ add += [dep2]
290
+
291
+ return add
292
+
293
+ def do_algebra(self, name: str, args: list[Point]) -> list[Dependency]:
294
+ """Derive (but not add) new algebraic predicates."""
295
+ if name == 'para':
296
+ a, b, dep = args
297
+ if gm.is_equiv(a, b):
298
+ return []
299
+ (x, y), (m, n) = a._obj.points, b._obj.points
300
+ return self.add_para([x, y, m, n], dep)
301
+
302
+ if name == 'aconst':
303
+ a, b, n, d, dep = args
304
+ ab, ba, why = self.get_or_create_angle_d(a, b, deps=None)
305
+ nd, dn = self.get_or_create_const_ang(n, d)
306
+
307
+ (x, y), (m, n) = a._obj.points, b._obj.points
308
+
309
+ if why:
310
+ dep0 = dep.populate('aconst', [x, y, m, n, nd])
311
+ dep = EmptyDependency(level=dep.level, rule_name=None)
312
+ dep.why = [dep0] + why
313
+
314
+ a, b = ab._d
315
+ (x, y), (m, n) = a._obj.points, b._obj.points
316
+
317
+ added = []
318
+ if not self.is_equal(ab, nd):
319
+ if nd == self.halfpi:
320
+ added += self.add_perp([x, y, m, n], dep)
321
+ # else:
322
+ name = 'aconst'
323
+ args = [x, y, m, n, nd]
324
+ dep1 = dep.populate(name, args)
325
+ self.cache_dep(name, args, dep1)
326
+ self.make_equal(nd, ab, deps=dep1)
327
+ added += [dep1]
328
+
329
+ if not self.is_equal(ba, dn):
330
+ if dn == self.halfpi:
331
+ added += self.add_perp([m, n, x, y], dep)
332
+ name = 'aconst'
333
+ args = [m, n, x, y, dn]
334
+ dep2 = dep.populate(name, args)
335
+ self.cache_dep(name, args, dep2)
336
+ self.make_equal(dn, ba, deps=dep2)
337
+ added += [dep2]
338
+ return added
339
+
340
+ if name == 'rconst':
341
+ a, b, c, d, num, den, dep = args
342
+ return self.add_eqrat_const([a, b, c, d, num, den], dep)
343
+
344
+ if name == 'eqangle':
345
+ d1, d2, d3, d4, dep = args
346
+ a, b = d1._obj.points
347
+ c, d = d2._obj.points
348
+ e, f = d3._obj.points
349
+ g, h = d4._obj.points
350
+
351
+ return self.add_eqangle([a, b, c, d, e, f, g, h], dep)
352
+
353
+ if name == 'eqratio':
354
+ d1, d2, d3, d4, dep = args
355
+ a, b = d1._obj.points
356
+ c, d = d2._obj.points
357
+ e, f = d3._obj.points
358
+ g, h = d4._obj.points
359
+
360
+ return self.add_eqratio([a, b, c, d, e, f, g, h], dep)
361
+
362
+ if name in ['cong', 'cong2']:
363
+ a, b, c, d, dep = args
364
+ if not (a != b and c != d and (a != c or b != d)):
365
+ return []
366
+ return self.add_cong([a, b, c, d], dep)
367
+
368
+ return []
369
+
370
+ def derive_algebra(
371
+ self, level: int, verbose: bool = False
372
+ ) -> tuple[
373
+ dict[str, list[tuple[Point, ...]]], dict[str, [tuple[Point, ...]]]
374
+ ]:
375
+ """Derive new algebraic predicates."""
376
+ derives = {}
377
+ ang_derives = self.derive_angle_algebra(level, verbose=verbose)
378
+ dist_derives = self.derive_distance_algebra(level, verbose=verbose)
379
+ rat_derives = self.derive_ratio_algebra(level, verbose=verbose)
380
+
381
+ derives.update(ang_derives)
382
+ derives.update(dist_derives)
383
+ derives.update(rat_derives)
384
+
385
+ # Separate eqangle and eqratio derivations
386
+ # As they are too numerous => slow down DD+AR.
387
+ # & reserve them only for last effort.
388
+ eqs = {'eqangle': derives.pop('eqangle'), 'eqratio': derives.pop('eqratio')}
389
+ return derives, eqs
390
+
391
+ def derive_ratio_algebra(
392
+ self, level: int, verbose: bool = False
393
+ ) -> dict[str, list[tuple[Point, ...]]]:
394
+ """Derive new eqratio predicates."""
395
+ added = {'cong2': [], 'eqratio': []}
396
+
397
+ for x in self.rtable.get_all_eqs_and_why():
398
+ x, why = x[:-1], x[-1]
399
+ dep = EmptyDependency(level=level, rule_name='a01')
400
+ dep.why = why
401
+
402
+ if len(x) == 2:
403
+ a, b = x
404
+ if gm.is_equiv(a, b):
405
+ continue
406
+
407
+ (m, n), (p, q) = a._obj.points, b._obj.points
408
+ added['cong2'].append((m, n, p, q, dep))
409
+
410
+ if len(x) == 4:
411
+ a, b, c, d = x
412
+ added['eqratio'].append((a, b, c, d, dep))
413
+
414
+ return added
415
+
416
+ def derive_angle_algebra(
417
+ self, level: int, verbose: bool = False
418
+ ) -> dict[str, list[tuple[Point, ...]]]:
419
+ """Derive new eqangles predicates."""
420
+ added = {'eqangle': [], 'aconst': [], 'para': []}
421
+
422
+ for x in self.atable.get_all_eqs_and_why():
423
+ x, why = x[:-1], x[-1]
424
+ dep = EmptyDependency(level=level, rule_name='a02')
425
+ dep.why = why
426
+
427
+ if len(x) == 2:
428
+ a, b = x
429
+ if gm.is_equiv(a, b):
430
+ continue
431
+
432
+ (e, f), (p, q) = a._obj.points, b._obj.points
433
+ if not nm.check('para', [e, f, p, q]):
434
+ continue
435
+
436
+ added['para'].append((a, b, dep))
437
+
438
+ if len(x) == 3:
439
+ a, b, (n, d) = x
440
+
441
+ (e, f), (p, q) = a._obj.points, b._obj.points
442
+ if not nm.check('aconst', [e, f, p, q, n, d]):
443
+ continue
444
+
445
+ added['aconst'].append((a, b, n, d, dep))
446
+
447
+ if len(x) == 4:
448
+ a, b, c, d = x
449
+ added['eqangle'].append((a, b, c, d, dep))
450
+
451
+ return added
452
+
453
+ def derive_distance_algebra(
454
+ self, level: int, verbose: bool = False
455
+ ) -> dict[str, list[tuple[Point, ...]]]:
456
+ """Derive new cong predicates."""
457
+ added = {'inci': [], 'cong': [], 'rconst': []}
458
+ for x in self.dtable.get_all_eqs_and_why():
459
+ x, why = x[:-1], x[-1]
460
+ dep = EmptyDependency(level=level, rule_name='a00')
461
+ dep.why = why
462
+
463
+ if len(x) == 2:
464
+ a, b = x
465
+ if a == b:
466
+ continue
467
+
468
+ dep.name = f'inci {a.name} {b.name}'
469
+ added['inci'].append((x, dep))
470
+
471
+ if len(x) == 4:
472
+ a, b, c, d = x
473
+ if not (a != b and c != d and (a != c or b != d)):
474
+ continue
475
+ added['cong'].append((a, b, c, d, dep))
476
+
477
+ if len(x) == 6:
478
+ a, b, c, d, num, den = x
479
+ if not (a != b and c != d and (a != c or b != d)):
480
+ continue
481
+ added['rconst'].append((a, b, c, d, num, den, dep))
482
+
483
+ return added
484
+
485
+ @classmethod
486
+ def build_problem(
487
+ cls,
488
+ pr: problem.Problem,
489
+ definitions: dict[str, problem.Definition],
490
+ verbose: bool = True,
491
+ init_copy: bool = True,
492
+ ) -> tuple[Graph, list[Dependency]]:
493
+ """Build a problem into a gr.Graph object."""
494
+ check = False
495
+ g = None
496
+ added = None
497
+ if verbose:
498
+ logging.info(pr.url)
499
+ logging.info(pr.txt())
500
+ while not check:
501
+ try:
502
+ g = Graph()
503
+ added = []
504
+ plevel = 0
505
+ for clause in pr.clauses:
506
+ adds, plevel = g.add_clause(
507
+ clause, plevel, definitions, verbose=verbose
508
+ )
509
+ added += adds
510
+ g.plevel = plevel
511
+
512
+ except (nm.InvalidLineIntersectError, nm.InvalidQuadSolveError):
513
+ continue
514
+ except DepCheckFailError:
515
+ continue
516
+ except (PointTooCloseError, PointTooFarError):
517
+ continue
518
+
519
+ if not pr.goal:
520
+ break
521
+
522
+ args = list(map(lambda x: g.get(x, lambda: int(x)), pr.goal.args))
523
+ check = nm.check(pr.goal.name, args)
524
+
525
+ g.url = pr.url
526
+ g.build_def = (pr, definitions)
527
+ for add in added:
528
+ g.add_algebra(add, level=0)
529
+
530
+ return g, added
531
+
532
+ def all_points(self) -> list[Point]:
533
+ """Return all nodes of type Point."""
534
+ return list(self.type2nodes[Point])
535
+
536
+ def all_nodes(self) -> list[gm.Node]:
537
+ """Return all nodes."""
538
+ return list(self._name2node.values())
539
+
540
+ def add_points(self, pnames: list[str]) -> list[Point]:
541
+ """Add new points with given names in list pnames."""
542
+ result = [self.new_node(Point, name) for name in pnames]
543
+ self._name2point.update(zip(pnames, result))
544
+ return result
545
+
546
+ def names2nodes(self, pnames: list[str]) -> list[gm.Node]:
547
+ return [self._name2node[name] for name in pnames]
548
+
549
+ def names2points(
550
+ self, pnames: list[str], create_new_point: bool = False
551
+ ) -> list[Point]:
552
+ """Return Point objects given names."""
553
+ result = []
554
+ for name in pnames:
555
+ if name not in self._name2node and not create_new_point:
556
+ raise ValueError(f'Cannot find point {name} in graph')
557
+ elif name in self._name2node:
558
+ obj = self._name2node[name]
559
+ else:
560
+ obj = self.new_node(Point, name)
561
+ result.append(obj)
562
+
563
+ return result
564
+
565
+ def names2points_or_int(self, pnames: list[str]) -> list[Point]:
566
+ """Return Point objects given names."""
567
+ result = []
568
+ for name in pnames:
569
+ if name.isdigit():
570
+ result += [int(name)]
571
+ elif 'pi/' in name:
572
+ n, d = name.split('pi/')
573
+ ang, _ = self.get_or_create_const_ang(int(n), int(d))
574
+ result += [ang]
575
+ elif '/' in name:
576
+ n, d = name.split('/')
577
+ rat, _ = self.get_or_create_const_rat(int(n), int(d))
578
+ result += [rat]
579
+ else:
580
+ result += [self._name2point[name]]
581
+
582
+ return result
583
+
584
+ def get(self, pointname: str, default_fn: Callable[str, Point]) -> Point:
585
+ if pointname in self._name2point:
586
+ return self._name2point[pointname]
587
+ if pointname in self._name2node:
588
+ return self._name2node[pointname]
589
+ return default_fn()
590
+
591
+ def new_node(self, oftype: Type[gm.Node], name: str = '') -> gm.Node:
592
+ node = oftype(name, self)
593
+
594
+ self.type2nodes[oftype].append(node)
595
+ self._name2node[name] = node
596
+
597
+ if isinstance(node, Point):
598
+ self._name2point[name] = node
599
+
600
+ return node
601
+
602
+ def merge(self, nodes: list[gm.Node], deps: Dependency) -> gm.Node:
603
+ """Merge all nodes."""
604
+ if len(nodes) < 2:
605
+ return
606
+
607
+ node0, *nodes1 = nodes
608
+ all_nodes = self.type2nodes[type(node0)]
609
+
610
+ # find node0 that exists in all_nodes to be the rep
611
+ # and merge all other nodes into node0
612
+ for node in nodes:
613
+ if node in all_nodes:
614
+ node0 = node
615
+ nodes1 = [n for n in nodes if n != node0]
616
+ break
617
+ return self.merge_into(node0, nodes1, deps)
618
+
619
+ def merge_into(
620
+ self, node0: gm.Node, nodes1: list[gm.Node], deps: Dependency
621
+ ) -> gm.Node:
622
+ """Merge nodes1 into a single node0."""
623
+ node0.merge(nodes1, deps)
624
+ for n in nodes1:
625
+ if n.rep() != n:
626
+ self.remove([n])
627
+
628
+ nodes = [node0] + nodes1
629
+ if any([node._val for node in nodes]):
630
+ for node in nodes:
631
+ self.connect_val(node, deps=None)
632
+
633
+ vals1 = [n._val for n in nodes1]
634
+ node0._val.merge(vals1, deps)
635
+
636
+ for v in vals1:
637
+ if v.rep() != v:
638
+ self.remove([v])
639
+
640
+ return node0
641
+
642
+ def remove(self, nodes: list[gm.Node]) -> None:
643
+ """Remove nodes out of self because they are merged."""
644
+ if not nodes:
645
+ return
646
+
647
+ for node in nodes:
648
+ all_nodes = self.type2nodes[type(nodes[0])]
649
+
650
+ if node in all_nodes:
651
+ all_nodes.remove(node)
652
+
653
+ if node.name in self._name2node.values():
654
+ self._name2node.pop(node.name)
655
+
656
+ def connect(self, a: gm.Node, b: gm.Node, deps: Dependency) -> None:
657
+ a.connect_to(b, deps)
658
+ b.connect_to(a, deps)
659
+
660
+ def connect_val(self, node: gm.Node, deps: Dependency) -> gm.Node:
661
+ """Connect a node into its value (equality) node."""
662
+ if node._val:
663
+ return node._val
664
+ name = None
665
+ if isinstance(node, Line):
666
+ name = 'd(' + node.name + ')'
667
+ if isinstance(node, Angle):
668
+ name = 'm(' + node.name + ')'
669
+ if isinstance(node, Segment):
670
+ name = 'l(' + node.name + ')'
671
+ if isinstance(node, Ratio):
672
+ name = 'r(' + node.name + ')'
673
+ v = self.new_node(gm.val_type(node), name)
674
+ self.connect(node, v, deps=deps)
675
+ return v
676
+
677
+ def is_equal(self, x: gm.Node, y: gm.Node, level: int = None) -> bool:
678
+ return gm.is_equal(x, y, level)
679
+
680
+ def add_piece(
681
+ self, name: str, args: list[Point], deps: EmptyDependency
682
+ ) -> list[Dependency]:
683
+ """Add a new predicate."""
684
+ if name in ['coll', 'collx']:
685
+ return self.add_coll(args, deps)
686
+ elif name == 'para':
687
+ return self.add_para(args, deps)
688
+ elif name == 'perp':
689
+ return self.add_perp(args, deps)
690
+ elif name == 'midp':
691
+ return self.add_midp(args, deps)
692
+ elif name == 'cong':
693
+ return self.add_cong(args, deps)
694
+ elif name == 'circle':
695
+ return self.add_circle(args, deps)
696
+ elif name == 'cyclic':
697
+ return self.add_cyclic(args, deps)
698
+ elif name in ['eqangle', 'eqangle6']:
699
+ return self.add_eqangle(args, deps)
700
+ elif name in ['eqratio', 'eqratio6']:
701
+ return self.add_eqratio(args, deps)
702
+ # numerical!
703
+ elif name == 's_angle':
704
+ return self.add_s_angle(args, deps)
705
+ elif name == 'aconst':
706
+ a, b, c, d, ang = args
707
+
708
+ if isinstance(ang, str):
709
+ name = ang
710
+ else:
711
+ name = ang.name
712
+
713
+ num, den = name.split('pi/')
714
+ num, den = int(num), int(den)
715
+ return self.add_aconst([a, b, c, d, num, den], deps)
716
+ elif name == 's_angle':
717
+ b, x, a, b, ang = ( # pylint: disable=redeclared-assigned-name,unused-variable
718
+ args
719
+ )
720
+
721
+ if isinstance(ang, str):
722
+ name = ang
723
+ else:
724
+ name = ang.name
725
+
726
+ n, d = name.split('pi/')
727
+ ang = int(n) * 180 / int(d)
728
+ return self.add_s_angle([a, b, x, ang], deps)
729
+ elif name == 'rconst':
730
+ a, b, c, d, rat = args
731
+
732
+ if isinstance(rat, str):
733
+ name = rat
734
+ else:
735
+ name = rat.name
736
+
737
+ num, den = name.split('/')
738
+ num, den = int(num), int(den)
739
+ return self.add_eqrat_const([a, b, c, d, num, den], deps)
740
+
741
+ # composite pieces:
742
+ elif name == 'cong2':
743
+ return self.add_cong2(args, deps)
744
+ elif name == 'eqratio3':
745
+ return self.add_eqratio3(args, deps)
746
+ elif name == 'eqratio4':
747
+ return self.add_eqratio4(args, deps)
748
+ elif name == 'simtri':
749
+ return self.add_simtri(args, deps)
750
+ elif name == 'contri':
751
+ return self.add_contri(args, deps)
752
+ elif name == 'simtri2':
753
+ return self.add_simtri2(args, deps)
754
+ elif name == 'contri2':
755
+ return self.add_contri2(args, deps)
756
+ elif name == 'simtri*':
757
+ return self.add_simtri_check(args, deps)
758
+ elif name == 'contri*':
759
+ return self.add_contri_check(args, deps)
760
+ elif name in ['acompute', 'rcompute']:
761
+ dep = deps.populate(name, args)
762
+ self.cache_dep(name, args, dep)
763
+ return [dep]
764
+ elif name in ['fixl', 'fixc', 'fixb', 'fixt', 'fixp']:
765
+ dep = deps.populate(name, args)
766
+ self.cache_dep(name, args, dep)
767
+ return [dep]
768
+ elif name in ['ind']:
769
+ return []
770
+ raise ValueError(f'Not recognize {name}')
771
+
772
+ def check(self, name: str, args: list[Point]) -> bool:
773
+ """Symbolically check if a predicate is True."""
774
+ if name == 'ncoll':
775
+ return self.check_ncoll(args)
776
+ if name == 'npara':
777
+ return self.check_npara(args)
778
+ if name == 'nperp':
779
+ return self.check_nperp(args)
780
+ if name == 'midp':
781
+ return self.check_midp(args)
782
+ if name == 'cong':
783
+ return self.check_cong(args)
784
+ if name == 'perp':
785
+ return self.check_perp(args)
786
+ if name == 'para':
787
+ return self.check_para(args)
788
+ if name == 'coll':
789
+ return self.check_coll(args)
790
+ if name == 'cyclic':
791
+ return self.check_cyclic(args)
792
+ if name == 'circle':
793
+ return self.check_circle(args)
794
+ if name == 'aconst':
795
+ return self.check_aconst(args)
796
+ if name == 'rconst':
797
+ return self.check_rconst(args)
798
+ if name == 'acompute':
799
+ return self.check_acompute(args)
800
+ if name == 'rcompute':
801
+ return self.check_rcompute(args)
802
+ if name in ['eqangle', 'eqangle6']:
803
+ if len(args) == 5:
804
+ return self.check_aconst(args)
805
+ return self.check_eqangle(args)
806
+ if name in ['eqratio', 'eqratio6']:
807
+ if len(args) == 5:
808
+ return self.check_rconst(args)
809
+ return self.check_eqratio(args)
810
+ if name in ['simtri', 'simtri2', 'simtri*']:
811
+ return self.check_simtri(args)
812
+ if name in ['contri', 'contri2', 'contri*']:
813
+ return self.check_contri(args)
814
+ if name == 'sameside':
815
+ return self.check_sameside(args)
816
+ if name in 'diff':
817
+ a, b = args
818
+ return not a.num.close(b.num)
819
+ if name in ['fixl', 'fixc', 'fixb', 'fixt', 'fixp']:
820
+ return self.in_cache(name, args)
821
+ if name in ['ind']:
822
+ return True
823
+ raise ValueError(f'Not recognize {name}')
824
+
825
+ def get_lines_thru_all(self, *points: list[gm.Point]) -> list[Line]:
826
+ line2count = defaultdict(lambda: 0)
827
+ points = set(points)
828
+ for p in points:
829
+ for l in p.neighbors(Line):
830
+ line2count[l] += 1
831
+ return [l for l, count in line2count.items() if count == len(points)]
832
+
833
+ def _get_line(self, a: Point, b: Point) -> Optional[Line]:
834
+ linesa = a.neighbors(Line)
835
+ for l in b.neighbors(Line):
836
+ if l in linesa:
837
+ return l
838
+ return None
839
+
840
+ def _get_line_all(self, a: Point, b: Point) -> Generator[Line, None, None]:
841
+ linesa = a.neighbors(Line, do_rep=False)
842
+ linesb = b.neighbors(Line, do_rep=False)
843
+ for l in linesb:
844
+ if l in linesa:
845
+ yield l
846
+
847
+ def _get_lines(self, *points: list[Point]) -> list[Line]:
848
+ """Return all lines that connect to >= 2 points."""
849
+ line2count = defaultdict(lambda: 0)
850
+ for p in points:
851
+ for l in p.neighbors(Line):
852
+ line2count[l] += 1
853
+ return [l for l, count in line2count.items() if count >= 2]
854
+
855
+ def get_circle_thru_triplet(self, p1: Point, p2: Point, p3: Point) -> Circle:
856
+ p1, p2, p3 = sorted([p1, p2, p3], key=lambda x: x.name)
857
+ if (p1, p2, p3) in self._triplet2circle:
858
+ return self._triplet2circle[(p1, p2, p3)]
859
+ return self.get_new_circle_thru_triplet(p1, p2, p3)
860
+
861
+ def get_new_circle_thru_triplet(
862
+ self, p1: Point, p2: Point, p3: Point
863
+ ) -> Circle:
864
+ """Get a new Circle that goes thru three given Points."""
865
+ p1, p2, p3 = sorted([p1, p2, p3], key=lambda x: x.name)
866
+ name = p1.name.lower() + p2.name.lower() + p3.name.lower()
867
+ circle = self.new_node(Circle, f'({name})')
868
+ circle.num = nm.Circle(p1=p1.num, p2=p2.num, p3=p3.num)
869
+ circle.points = p1, p2, p3
870
+
871
+ self.connect(p1, circle, deps=None)
872
+ self.connect(p2, circle, deps=None)
873
+ self.connect(p3, circle, deps=None)
874
+ self._triplet2circle[(p1, p2, p3)] = circle
875
+ return circle
876
+
877
+ def get_line_thru_pair(self, p1: Point, p2: Point) -> Line:
878
+ if (p1, p2) in self._pair2line:
879
+ return self._pair2line[(p1, p2)]
880
+ if (p2, p1) in self._pair2line:
881
+ return self._pair2line[(p2, p1)]
882
+ return self.get_new_line_thru_pair(p1, p2)
883
+
884
+ def get_new_line_thru_pair(self, p1: Point, p2: Point) -> Line:
885
+ if p1.name.lower() > p2.name.lower():
886
+ p1, p2 = p2, p1
887
+ name = p1.name.lower() + p2.name.lower()
888
+ line = self.new_node(Line, name)
889
+ line.num = nm.Line(p1.num, p2.num)
890
+ line.points = p1, p2
891
+
892
+ self.connect(p1, line, deps=None)
893
+ self.connect(p2, line, deps=None)
894
+ self._pair2line[(p1, p2)] = line
895
+ return line
896
+
897
+ def get_line_thru_pair_why(
898
+ self, p1: Point, p2: Point
899
+ ) -> tuple[Line, list[Dependency]]:
900
+ """Get one line thru two given points and the corresponding dependency list."""
901
+ if p1.name.lower() > p2.name.lower():
902
+ p1, p2 = p2, p1
903
+ if (p1, p2) in self._pair2line:
904
+ return self._pair2line[(p1, p2)].rep_and_why()
905
+
906
+ l, why = gm.line_of_and_why([p1, p2])
907
+ if l is None:
908
+ l = self.get_new_line_thru_pair(p1, p2)
909
+ why = []
910
+ return l, why
911
+
912
+ def coll_dep(self, points: list[Point], p: Point) -> list[Dependency]:
913
+ """Return the dep(.why) explaining why p is coll with points."""
914
+ for p1, p2 in utils.comb2(points):
915
+ if self.check_coll([p1, p2, p]):
916
+ dep = Dependency('coll', [p1, p2, p], None, None)
917
+ return dep.why_me_or_cache(self, None)
918
+
919
+ def add_coll(
920
+ self, points: list[Point], deps: EmptyDependency
921
+ ) -> list[Dependency]:
922
+ """Add a predicate that `points` are collinear."""
923
+ points = list(set(points))
924
+ og_points = list(points)
925
+
926
+ all_lines = []
927
+ for p1, p2 in utils.comb2(points):
928
+ all_lines.append(self.get_line_thru_pair(p1, p2))
929
+ points = sum([l.neighbors(Point) for l in all_lines], [])
930
+ points = list(set(points))
931
+
932
+ existed = set()
933
+ new = set()
934
+ for p1, p2 in utils.comb2(points):
935
+ if p1.name > p2.name:
936
+ p1, p2 = p2, p1
937
+ if (p1, p2) in self._pair2line:
938
+ line = self._pair2line[(p1, p2)]
939
+ existed.add(line)
940
+ else:
941
+ line = self.get_new_line_thru_pair(p1, p2)
942
+ new.add(line)
943
+
944
+ existed = sorted(existed, key=lambda l: l.name)
945
+ new = sorted(new, key=lambda l: l.name)
946
+
947
+ existed, new = list(existed), list(new)
948
+ if not existed:
949
+ line0, *lines = new
950
+ else:
951
+ line0, lines = existed[0], existed[1:] + new
952
+
953
+ add = []
954
+ line0, why0 = line0.rep_and_why()
955
+ a, b = line0.points
956
+ for line in lines:
957
+ c, d = line.points
958
+ args = list({a, b, c, d})
959
+ if len(args) < 3:
960
+ continue
961
+
962
+ whys = []
963
+ for x in args:
964
+ if x not in og_points:
965
+ whys.append(self.coll_dep(og_points, x))
966
+
967
+ abcd_deps = deps
968
+ if whys + why0:
969
+ dep0 = deps.populate('coll', og_points)
970
+ abcd_deps = EmptyDependency(level=deps.level, rule_name=None)
971
+ abcd_deps.why = [dep0] + whys
972
+
973
+ is_coll = self.check_coll(args)
974
+ dep = abcd_deps.populate('coll', args)
975
+ self.cache_dep('coll', args, dep)
976
+ self.merge_into(line0, [line], dep)
977
+
978
+ if not is_coll:
979
+ add += [dep]
980
+
981
+ return add
982
+
983
+ def check_coll(self, points: list[Point]) -> bool:
984
+ points = list(set(points))
985
+ if len(points) < 3:
986
+ return True
987
+ line2count = defaultdict(lambda: 0)
988
+ for p in points:
989
+ for l in p.neighbors(Line):
990
+ line2count[l] += 1
991
+ return any([count == len(points) for _, count in line2count.items()])
992
+
993
+ def why_coll(self, args: tuple[Line, list[Point]]) -> list[Dependency]:
994
+ line, points = args
995
+ return line.why_coll(points)
996
+
997
+ def check_ncoll(self, points: list[Point]) -> bool:
998
+ if self.check_coll(points):
999
+ return False
1000
+ return not nm.check_coll([p.num for p in points])
1001
+
1002
+ def check_sameside(self, points: list[Point]) -> bool:
1003
+ return nm.check_sameside([p.num for p in points])
1004
+
1005
+ def make_equal(self, x: gm.Node, y: gm.Node, deps: Dependency) -> None:
1006
+ """Make that two nodes x and y are equal, i.e. merge their value node."""
1007
+ if x.val is None:
1008
+ x, y = y, x
1009
+
1010
+ self.connect_val(x, deps=None)
1011
+ self.connect_val(y, deps=None)
1012
+ vx = x._val
1013
+ vy = y._val
1014
+
1015
+ if vx == vy:
1016
+ return
1017
+
1018
+ merges = [vx, vy]
1019
+
1020
+ if (
1021
+ isinstance(x, Angle)
1022
+ and x not in self.aconst.values()
1023
+ and y not in self.aconst.values()
1024
+ and x.directions == y.directions[::-1]
1025
+ and x.directions[0] != x.directions[1]
1026
+ ):
1027
+ merges = [self.vhalfpi, vx, vy]
1028
+
1029
+ self.merge(merges, deps)
1030
+
1031
+ def merge_vals(self, vx: gm.Node, vy: gm.Node, deps: Dependency) -> None:
1032
+ if vx == vy:
1033
+ return
1034
+ merges = [vx, vy]
1035
+ self.merge(merges, deps)
1036
+
1037
+ def why_equal(self, x: gm.Node, y: gm.Node, level: int) -> list[Dependency]:
1038
+ return gm.why_equal(x, y, level)
1039
+
1040
+ def _why_coll4(
1041
+ self,
1042
+ a: Point,
1043
+ b: Point,
1044
+ ab: Line,
1045
+ c: Point,
1046
+ d: Point,
1047
+ cd: Line,
1048
+ level: int,
1049
+ ) -> list[Dependency]:
1050
+ return self._why_coll2(a, b, ab, level) + self._why_coll2(c, d, cd, level)
1051
+
1052
+ def _why_coll8(
1053
+ self,
1054
+ a: Point,
1055
+ b: Point,
1056
+ ab: Line,
1057
+ c: Point,
1058
+ d: Point,
1059
+ cd: Line,
1060
+ m: Point,
1061
+ n: Point,
1062
+ mn: Line,
1063
+ p: Point,
1064
+ q: Point,
1065
+ pq: Line,
1066
+ level: int,
1067
+ ) -> list[Dependency]:
1068
+ """Dependency list of why 8 points are collinear."""
1069
+ why8 = self._why_coll4(a, b, ab, c, d, cd, level)
1070
+ why8 += self._why_coll4(m, n, mn, p, q, pq, level)
1071
+ return why8
1072
+
1073
+ def add_para(
1074
+ self, points: list[Point], deps: EmptyDependency
1075
+ ) -> list[Dependency]:
1076
+ """Add a new predicate that 4 points (2 lines) are parallel."""
1077
+ a, b, c, d = points
1078
+ ab, why1 = self.get_line_thru_pair_why(a, b)
1079
+ cd, why2 = self.get_line_thru_pair_why(c, d)
1080
+
1081
+ is_equal = self.is_equal(ab, cd)
1082
+
1083
+ (a, b), (c, d) = ab.points, cd.points
1084
+
1085
+ dep0 = deps.populate('para', points)
1086
+ deps = EmptyDependency(level=deps.level, rule_name=None)
1087
+
1088
+ deps = deps.populate('para', [a, b, c, d])
1089
+ deps.why = [dep0] + why1 + why2
1090
+
1091
+ self.make_equal(ab, cd, deps)
1092
+ deps.algebra = ab._val, cd._val
1093
+
1094
+ self.cache_dep('para', [a, b, c, d], deps)
1095
+ if not is_equal:
1096
+ return [deps]
1097
+ return []
1098
+
1099
+ def why_para(self, args: list[Point]) -> list[Dependency]:
1100
+ ab, cd, lvl = args
1101
+ return self.why_equal(ab, cd, lvl)
1102
+
1103
+ def check_para_or_coll(self, points: list[Point]) -> bool:
1104
+ return self.check_para(points) or self.check_coll(points)
1105
+
1106
+ def check_para(self, points: list[Point]) -> bool:
1107
+ a, b, c, d = points
1108
+ if (a == b) or (c == d):
1109
+ return False
1110
+ ab = self._get_line(a, b)
1111
+ cd = self._get_line(c, d)
1112
+ if not ab or not cd:
1113
+ return False
1114
+
1115
+ return self.is_equal(ab, cd)
1116
+
1117
+ def check_npara(self, points: list[Point]) -> bool:
1118
+ if self.check_para(points):
1119
+ return False
1120
+ return not nm.check_para([p.num for p in points])
1121
+
1122
+ def _get_angle(
1123
+ self, d1: Direction, d2: Direction
1124
+ ) -> tuple[Angle, Optional[Angle]]:
1125
+ for a in self.type2nodes[Angle]:
1126
+ if a.directions == (d1, d2):
1127
+ return a, a.opposite
1128
+ return None, None
1129
+
1130
+ def get_first_angle(
1131
+ self, l1: Line, l2: Line
1132
+ ) -> tuple[Angle, list[Dependency]]:
1133
+ """Get a first angle between line l1 and line l2."""
1134
+ d1, d2 = l1._val, l2._val
1135
+
1136
+ d1s = d1.all_reps()
1137
+ d2s = d2.all_reps()
1138
+
1139
+ found = d1.first_angle(d2s)
1140
+ if found is None:
1141
+ found = d2.first_angle(d1s)
1142
+ if found is None:
1143
+ return None, []
1144
+ ang, x2, x1 = found
1145
+ found = ang.opposite, x1, x2
1146
+
1147
+ ang, x1, x2 = found
1148
+ return ang, d1.deps_upto(x1) + d2.deps_upto(x2)
1149
+
1150
+ def _get_or_create_angle(
1151
+ self, l1: Line, l2: Line, deps: Dependency
1152
+ ) -> tuple[Angle, Angle, list[Dependency]]:
1153
+ return self.get_or_create_angle_d(l1._val, l2._val, deps)
1154
+
1155
+ def get_or_create_angle_d(
1156
+ self, d1: Direction, d2: Direction, deps: Dependency
1157
+ ) -> tuple[Angle, Angle, list[Dependency]]:
1158
+ """Get or create an angle between two Direction d1 and d2."""
1159
+ for a in self.type2nodes[Angle]:
1160
+ if a.directions == (d1.rep(), d2.rep()): # directions = _d.rep()
1161
+ d1_, d2_ = a._d
1162
+ why1 = d1.why_equal([d1_], None) + d1_.why_rep()
1163
+ why2 = d2.why_equal([d2_], None) + d2_.why_rep()
1164
+ return a, a.opposite, why1 + why2
1165
+
1166
+ d1, why1 = d1.rep_and_why()
1167
+ d2, why2 = d2.rep_and_why()
1168
+ a12 = self.new_node(Angle, f'{d1.name}-{d2.name}')
1169
+ a21 = self.new_node(Angle, f'{d2.name}-{d1.name}')
1170
+ self.connect(d1, a12, deps)
1171
+ self.connect(d2, a21, deps)
1172
+ self.connect(a12, a21, deps)
1173
+ a12.set_directions(d1, d2)
1174
+ a21.set_directions(d2, d1)
1175
+ a12.opposite = a21
1176
+ a21.opposite = a12
1177
+ return a12, a21, why1 + why2
1178
+
1179
+ def _add_para_or_coll(
1180
+ self,
1181
+ a: Point,
1182
+ b: Point,
1183
+ c: Point,
1184
+ d: Point,
1185
+ x: Point,
1186
+ y: Point,
1187
+ m: Point,
1188
+ n: Point,
1189
+ deps: EmptyDependency,
1190
+ ) -> list[Dependency]:
1191
+ """Add a new parallel or collinear predicate."""
1192
+ extends = [('perp', [x, y, m, n])]
1193
+ if {a, b} == {x, y}:
1194
+ pass
1195
+ elif self.check_para([a, b, x, y]):
1196
+ extends.append(('para', [a, b, x, y]))
1197
+ elif self.check_coll([a, b, x, y]):
1198
+ extends.append(('coll', set(list([a, b, x, y]))))
1199
+ else:
1200
+ return None
1201
+
1202
+ if m in [c, d] or n in [c, d] or c in [m, n] or d in [m, n]:
1203
+ pass
1204
+ elif self.check_coll([c, d, m]):
1205
+ extends.append(('coll', [c, d, m]))
1206
+ elif self.check_coll([c, d, n]):
1207
+ extends.append(('coll', [c, d, n]))
1208
+ elif self.check_coll([c, m, n]):
1209
+ extends.append(('coll', [c, m, n]))
1210
+ elif self.check_coll([d, m, n]):
1211
+ extends.append(('coll', [d, m, n]))
1212
+ else:
1213
+ deps = deps.extend_many(self, 'perp', [a, b, c, d], extends)
1214
+ return self.add_para([c, d, m, n], deps)
1215
+
1216
+ deps = deps.extend_many(self, 'perp', [a, b, c, d], extends)
1217
+ return self.add_coll(list(set([c, d, m, n])), deps)
1218
+
1219
+ def maybe_make_para_from_perp(
1220
+ self, points: list[Point], deps: EmptyDependency
1221
+ ) -> Optional[list[Dependency]]:
1222
+ """Maybe add a new parallel predicate from perp predicate."""
1223
+ a, b, c, d = points
1224
+ halfpi = self.aconst[(1, 2)]
1225
+ for ang in halfpi.val.neighbors(Angle):
1226
+ if ang == halfpi:
1227
+ continue
1228
+ d1, d2 = ang.directions
1229
+ x, y = d1._obj.points
1230
+ m, n = d2._obj.points
1231
+
1232
+ for args in [
1233
+ (a, b, c, d, x, y, m, n),
1234
+ (a, b, c, d, m, n, x, y),
1235
+ (c, d, a, b, x, y, m, n),
1236
+ (c, d, a, b, m, n, x, y),
1237
+ ]:
1238
+ args = args + (deps,)
1239
+ add = self._add_para_or_coll(*args)
1240
+ if add:
1241
+ return add
1242
+
1243
+ return None
1244
+
1245
+ def add_perp(
1246
+ self, points: list[Point], deps: EmptyDependency
1247
+ ) -> list[Dependency]:
1248
+ """Add a new perpendicular predicate from 4 points (2 lines)."""
1249
+ add = self.maybe_make_para_from_perp(points, deps)
1250
+ if add is not None:
1251
+ return add
1252
+
1253
+ a, b, c, d = points
1254
+ ab, why1 = self.get_line_thru_pair_why(a, b)
1255
+ cd, why2 = self.get_line_thru_pair_why(c, d)
1256
+
1257
+ (a, b), (c, d) = ab.points, cd.points
1258
+
1259
+ if why1 + why2:
1260
+ dep0 = deps.populate('perp', points)
1261
+ deps = EmptyDependency(level=deps.level, rule_name=None)
1262
+ deps.why = [dep0] + why1 + why2
1263
+
1264
+ self.connect_val(ab, deps=None)
1265
+ self.connect_val(cd, deps=None)
1266
+
1267
+ if ab.val == cd.val:
1268
+ raise ValueError(f'{ab.name} and {cd.name} Cannot be perp.')
1269
+
1270
+ args = [a, b, c, d]
1271
+ i = 0
1272
+ for x, y, xy in [(a, b, ab), (c, d, cd)]:
1273
+ i += 1
1274
+ x_, y_ = xy._val._obj.points
1275
+ if {x, y} == {x_, y_}:
1276
+ continue
1277
+ if deps:
1278
+ deps = deps.extend(self, 'perp', list(args), 'para', [x, y, x_, y_])
1279
+ args[2 * i - 2] = x_
1280
+ args[2 * i - 1] = y_
1281
+
1282
+ a12, a21, why = self._get_or_create_angle(ab, cd, deps=None)
1283
+
1284
+ if why:
1285
+ dep0 = deps.populate('perp', [a, b, c, d])
1286
+ deps = EmptyDependency(level=deps.level, rule_name=None)
1287
+ deps.why = [dep0] + why
1288
+
1289
+ dab, dcd = a12._d
1290
+ a, b = dab._obj.points
1291
+ c, d = dcd._obj.points
1292
+
1293
+ is_equal = self.is_equal(a12, a21)
1294
+ deps = deps.populate('perp', [a, b, c, d])
1295
+ deps.algebra = [dab, dcd]
1296
+ self.make_equal(a12, a21, deps=deps)
1297
+
1298
+ self.cache_dep('perp', [a, b, c, d], deps)
1299
+ self.cache_dep('eqangle', [a, b, c, d, c, d, a, b], deps)
1300
+
1301
+ if not is_equal:
1302
+ return [deps]
1303
+ return []
1304
+
1305
+ def why_perp(
1306
+ self, args: list[Union[Point, list[Dependency]]]
1307
+ ) -> list[Dependency]:
1308
+ a, b, deps = args
1309
+ return deps + self.why_equal(a, b, None)
1310
+
1311
+ def check_perpl(self, ab: Line, cd: Line) -> bool:
1312
+ if ab.val is None or cd.val is None:
1313
+ return False
1314
+ if ab.val == cd.val:
1315
+ return False
1316
+ a12, a21 = self._get_angle(ab.val, cd.val)
1317
+ if a12 is None or a21 is None:
1318
+ return False
1319
+ return self.is_equal(a12, a21)
1320
+
1321
+ def check_perp(self, points: list[Point]) -> bool:
1322
+ a, b, c, d = points
1323
+ ab = self._get_line(a, b)
1324
+ cd = self._get_line(c, d)
1325
+ if not ab or not cd:
1326
+ return False
1327
+ return self.check_perpl(ab, cd)
1328
+
1329
+ def check_nperp(self, points: list[Point]) -> bool:
1330
+ if self.check_perp(points):
1331
+ return False
1332
+ return not nm.check_perp([p.num for p in points])
1333
+
1334
+ def _get_segment(self, p1: Point, p2: Point) -> Optional[Segment]:
1335
+ for s in self.type2nodes[Segment]:
1336
+ if s.points == {p1, p2}:
1337
+ return s
1338
+ return None
1339
+
1340
+ def _get_or_create_segment(
1341
+ self, p1: Point, p2: Point, deps: Dependency
1342
+ ) -> Segment:
1343
+ """Get or create a Segment object between two Points p1 and p2."""
1344
+ if p1 == p2:
1345
+ raise ValueError(f'Creating same 0-length segment {p1.name}')
1346
+
1347
+ for s in self.type2nodes[Segment]:
1348
+ if s.points == {p1, p2}:
1349
+ return s
1350
+
1351
+ if p1.name > p2.name:
1352
+ p1, p2 = p2, p1
1353
+ s = self.new_node(Segment, name=f'{p1.name.upper()}{p2.name.upper()}')
1354
+ self.connect(p1, s, deps=deps)
1355
+ self.connect(p2, s, deps=deps)
1356
+ s.points = {p1, p2}
1357
+ return s
1358
+
1359
+ def add_cong(
1360
+ self, points: list[Point], deps: EmptyDependency
1361
+ ) -> list[Dependency]:
1362
+ """Add that two segments (4 points) are congruent."""
1363
+ a, b, c, d = points
1364
+ ab = self._get_or_create_segment(a, b, deps=None)
1365
+ cd = self._get_or_create_segment(c, d, deps=None)
1366
+
1367
+ is_equal = self.is_equal(ab, cd)
1368
+
1369
+ dep = deps.populate('cong', [a, b, c, d])
1370
+ self.make_equal(ab, cd, deps=dep)
1371
+ dep.algebra = ab._val, cd._val
1372
+
1373
+ self.cache_dep('cong', [a, b, c, d], dep)
1374
+
1375
+ result = []
1376
+
1377
+ if not is_equal:
1378
+ result += [dep]
1379
+
1380
+ if a not in [c, d] and b not in [c, d]:
1381
+ return result
1382
+
1383
+ if b in [c, d]:
1384
+ a, b = b, a
1385
+ if a == d:
1386
+ c, d = d, c # pylint: disable=unused-variable
1387
+
1388
+ result += self._maybe_add_cyclic_from_cong(a, b, d, dep)
1389
+ return result
1390
+
1391
+ def _maybe_add_cyclic_from_cong(
1392
+ self, a: Point, b: Point, c: Point, cong_ab_ac: Dependency
1393
+ ) -> list[Dependency]:
1394
+ """Maybe add a new cyclic predicate from given congruent segments."""
1395
+ ab = self._get_or_create_segment(a, b, deps=None)
1396
+
1397
+ # all eq segs with one end being a.
1398
+ segs = [s for s in ab.val.neighbors(Segment) if a in s.points]
1399
+
1400
+ # all points on circle (a, b)
1401
+ points = []
1402
+ for s in segs:
1403
+ x, y = list(s.points)
1404
+ points.append(x if y == a else y)
1405
+
1406
+ # for sure both b and c are in points
1407
+ points = [p for p in points if p not in [b, c]]
1408
+
1409
+ if len(points) < 2:
1410
+ return []
1411
+
1412
+ x, y = points[:2]
1413
+
1414
+ if self.check_cyclic([b, c, x, y]):
1415
+ return []
1416
+
1417
+ ax = self._get_or_create_segment(a, x, deps=None)
1418
+ ay = self._get_or_create_segment(a, y, deps=None)
1419
+ why = ab._val.why_equal([ax._val, ay._val], level=None)
1420
+ why += [cong_ab_ac]
1421
+
1422
+ deps = EmptyDependency(cong_ab_ac.level, '')
1423
+ deps.why = why
1424
+
1425
+ return self.add_cyclic([b, c, x, y], deps)
1426
+
1427
+ def check_cong(self, points: list[Point]) -> bool:
1428
+ a, b, c, d = points
1429
+ if {a, b} == {c, d}:
1430
+ return True
1431
+
1432
+ ab = self._get_segment(a, b)
1433
+ cd = self._get_segment(c, d)
1434
+ if ab is None or cd is None:
1435
+ return False
1436
+ return self.is_equal(ab, cd)
1437
+
1438
+ def why_cong(self, args: tuple[Segment, Segment]) -> list[Dependency]:
1439
+ ab, cd = args
1440
+ return self.why_equal(ab, cd, None)
1441
+
1442
+ def add_midp(
1443
+ self, points: list[Point], deps: EmptyDependency
1444
+ ) -> list[Dependency]:
1445
+ m, a, b = points
1446
+ add = self.add_coll(points, deps=deps)
1447
+ add += self.add_cong([m, a, m, b], deps)
1448
+ return add
1449
+
1450
+ def why_midp(
1451
+ self, args: tuple[Line, list[Point], Segment, Segment]
1452
+ ) -> list[Dependency]:
1453
+ line, points, ma, mb = args
1454
+ return self.why_coll([line, points]) + self.why_cong([ma, mb])
1455
+
1456
+ def check_midp(self, points: list[Point]) -> bool:
1457
+ if not self.check_coll(points):
1458
+ return False
1459
+ m, a, b = points
1460
+ return self.check_cong([m, a, m, b])
1461
+
1462
+ def add_circle(
1463
+ self, points: list[Point], deps: EmptyDependency
1464
+ ) -> list[Dependency]:
1465
+ o, a, b, c = points
1466
+ add = self.add_cong([o, a, o, b], deps=deps)
1467
+ add += self.add_cong([o, a, o, c], deps=deps)
1468
+ return add
1469
+
1470
+ def why_circle(
1471
+ self, args: tuple[Segment, Segment, Segment]
1472
+ ) -> list[Dependency]:
1473
+ oa, ob, oc = args
1474
+ return self.why_equal(oa, ob, None) and self.why_equal(oa, oc, None)
1475
+
1476
+ def check_circle(self, points: list[Point]) -> bool:
1477
+ o, a, b, c = points
1478
+ return self.check_cong([o, a, o, b]) and self.check_cong([o, a, o, c])
1479
+
1480
+ def get_circles_thru_all(self, *points: list[Point]) -> list[Circle]:
1481
+ circle2count = defaultdict(lambda: 0)
1482
+ points = set(points)
1483
+ for p in points:
1484
+ for c in p.neighbors(Circle):
1485
+ circle2count[c] += 1
1486
+ return [c for c, count in circle2count.items() if count == len(points)]
1487
+
1488
+ def _get_circles(self, *points: list[Point]) -> list[Circle]:
1489
+ circle2count = defaultdict(lambda: 0)
1490
+ for p in points:
1491
+ for c in p.neighbors(Circle):
1492
+ circle2count[c] += 1
1493
+ return [c for c, count in circle2count.items() if count >= 3]
1494
+
1495
+ def cyclic_dep(self, points: list[Point], p: Point) -> list[Dependency]:
1496
+ for p1, p2, p3 in utils.comb3(points):
1497
+ if self.check_cyclic([p1, p2, p3, p]):
1498
+ dep = Dependency('cyclic', [p1, p2, p3, p], None, None)
1499
+ return dep.why_me_or_cache(self, None)
1500
+
1501
+ def add_cyclic(
1502
+ self, points: list[Point], deps: EmptyDependency
1503
+ ) -> list[Dependency]:
1504
+ """Add a new cyclic predicate that 4 points are concyclic."""
1505
+ points = list(set(points))
1506
+ og_points = list(points)
1507
+
1508
+ all_circles = []
1509
+ for p1, p2, p3 in utils.comb3(points):
1510
+ all_circles.append(self.get_circle_thru_triplet(p1, p2, p3))
1511
+ points = sum([c.neighbors(Point) for c in all_circles], [])
1512
+ points = list(set(points))
1513
+
1514
+ existed = set()
1515
+ new = set()
1516
+ for p1, p2, p3 in utils.comb3(points):
1517
+ p1, p2, p3 = sorted([p1, p2, p3], key=lambda x: x.name)
1518
+
1519
+ if (p1, p2, p3) in self._triplet2circle:
1520
+ circle = self._triplet2circle[(p1, p2, p3)]
1521
+ existed.add(circle)
1522
+ else:
1523
+ circle = self.get_new_circle_thru_triplet(p1, p2, p3)
1524
+ new.add(circle)
1525
+
1526
+ existed = sorted(existed, key=lambda l: l.name)
1527
+ new = sorted(new, key=lambda l: l.name)
1528
+
1529
+ existed, new = list(existed), list(new)
1530
+ if not existed:
1531
+ circle0, *circles = new
1532
+ else:
1533
+ circle0, circles = existed[0], existed[1:] + new
1534
+
1535
+ add = []
1536
+ circle0, why0 = circle0.rep_and_why()
1537
+ a, b, c = circle0.points
1538
+ for circle in circles:
1539
+ d, e, f = circle.points
1540
+ args = list({a, b, c, d, e, f})
1541
+ if len(args) < 4:
1542
+ continue
1543
+ whys = []
1544
+ for x in [a, b, c, d, e, f]:
1545
+ if x not in og_points:
1546
+ whys.append(self.cyclic_dep(og_points, x))
1547
+ abcdef_deps = deps
1548
+ if whys + why0:
1549
+ dep0 = deps.populate('cyclic', og_points)
1550
+ abcdef_deps = EmptyDependency(level=deps.level, rule_name=None)
1551
+ abcdef_deps.why = [dep0] + whys
1552
+
1553
+ is_cyclic = self.check_cyclic(args)
1554
+
1555
+ dep = abcdef_deps.populate('cyclic', args)
1556
+ self.cache_dep('cyclic', args, dep)
1557
+ self.merge_into(circle0, [circle], dep)
1558
+ if not is_cyclic:
1559
+ add += [dep]
1560
+
1561
+ return add
1562
+
1563
+ def check_cyclic(self, points: list[Point]) -> bool:
1564
+ points = list(set(points))
1565
+ if len(points) < 4:
1566
+ return True
1567
+ circle2count = defaultdict(lambda: 0)
1568
+ for p in points:
1569
+ for c in p.neighbors(Circle):
1570
+ circle2count[c] += 1
1571
+ return any([count == len(points) for _, count in circle2count.items()])
1572
+
1573
+ def make_equal_pairs(
1574
+ self,
1575
+ a: Point,
1576
+ b: Point,
1577
+ c: Point,
1578
+ d: Point,
1579
+ m: Point,
1580
+ n: Point,
1581
+ p: Point,
1582
+ q: Point,
1583
+ ab: Line,
1584
+ cd: Line,
1585
+ mn: Line,
1586
+ pq: Line,
1587
+ deps: EmptyDependency,
1588
+ ) -> list[Dependency]:
1589
+ """Add ab/cd = mn/pq in case either two of (ab,cd,mn,pq) are equal."""
1590
+ depname = 'eqratio' if isinstance(ab, Segment) else 'eqangle'
1591
+ eqname = 'cong' if isinstance(ab, Segment) else 'para'
1592
+
1593
+ is_equal = self.is_equal(mn, pq)
1594
+
1595
+ if ab != cd:
1596
+ dep0 = deps.populate(depname, [a, b, c, d, m, n, p, q])
1597
+ deps = EmptyDependency(level=deps.level, rule_name=None)
1598
+
1599
+ dep = Dependency(eqname, [a, b, c, d], None, deps.level)
1600
+ deps.why = [dep0, dep.why_me_or_cache(self, None)]
1601
+
1602
+ elif eqname == 'para': # ab == cd.
1603
+ colls = [a, b, c, d]
1604
+ if len(set(colls)) > 2:
1605
+ dep0 = deps.populate(depname, [a, b, c, d, m, n, p, q])
1606
+ deps = EmptyDependency(level=deps.level, rule_name=None)
1607
+
1608
+ dep = Dependency('collx', colls, None, deps.level)
1609
+ deps.why = [dep0, dep.why_me_or_cache(self, None)]
1610
+
1611
+ deps = deps.populate(eqname, [m, n, p, q])
1612
+ self.make_equal(mn, pq, deps=deps)
1613
+
1614
+ deps.algebra = mn._val, pq._val
1615
+ self.cache_dep(eqname, [m, n, p, q], deps)
1616
+
1617
+ if is_equal:
1618
+ return []
1619
+ return [deps]
1620
+
1621
+ def maybe_make_equal_pairs(
1622
+ self,
1623
+ a: Point,
1624
+ b: Point,
1625
+ c: Point,
1626
+ d: Point,
1627
+ m: Point,
1628
+ n: Point,
1629
+ p: Point,
1630
+ q: Point,
1631
+ ab: Line,
1632
+ cd: Line,
1633
+ mn: Line,
1634
+ pq: Line,
1635
+ deps: EmptyDependency,
1636
+ ) -> Optional[list[Dependency]]:
1637
+ """Add ab/cd = mn/pq in case maybe either two of (ab,cd,mn,pq) are equal."""
1638
+ level = deps.level
1639
+ if self.is_equal(ab, cd, level):
1640
+ return self.make_equal_pairs(a, b, c, d, m, n, p, q, ab, cd, mn, pq, deps)
1641
+ elif self.is_equal(mn, pq, level):
1642
+ return self.make_equal_pairs( # pylint: disable=arguments-out-of-order
1643
+ m,
1644
+ n,
1645
+ p,
1646
+ q,
1647
+ a,
1648
+ b,
1649
+ c,
1650
+ d,
1651
+ mn,
1652
+ pq,
1653
+ ab,
1654
+ cd,
1655
+ deps,
1656
+ )
1657
+ elif self.is_equal(ab, mn, level):
1658
+ return self.make_equal_pairs( # pylint: disable=arguments-out-of-order
1659
+ a,
1660
+ b,
1661
+ m,
1662
+ n,
1663
+ c,
1664
+ d,
1665
+ p,
1666
+ q,
1667
+ ab,
1668
+ mn,
1669
+ cd,
1670
+ pq,
1671
+ deps,
1672
+ )
1673
+ elif self.is_equal(cd, pq, level):
1674
+ return self.make_equal_pairs( # pylint: disable=arguments-out-of-order
1675
+ c,
1676
+ d,
1677
+ p,
1678
+ q,
1679
+ a,
1680
+ b,
1681
+ m,
1682
+ n,
1683
+ cd,
1684
+ pq,
1685
+ ab,
1686
+ mn,
1687
+ deps,
1688
+ )
1689
+ else:
1690
+ return None
1691
+
1692
+ def _add_eqangle(
1693
+ self,
1694
+ a: Point,
1695
+ b: Point,
1696
+ c: Point,
1697
+ d: Point,
1698
+ m: Point,
1699
+ n: Point,
1700
+ p: Point,
1701
+ q: Point,
1702
+ ab: Line,
1703
+ cd: Line,
1704
+ mn: Line,
1705
+ pq: Line,
1706
+ deps: EmptyDependency,
1707
+ ) -> list[Dependency]:
1708
+ """Add eqangle core."""
1709
+ if deps:
1710
+ deps = deps.copy()
1711
+
1712
+ args = [a, b, c, d, m, n, p, q]
1713
+ i = 0
1714
+ for x, y, xy in [(a, b, ab), (c, d, cd), (m, n, mn), (p, q, pq)]:
1715
+ i += 1
1716
+ x_, y_ = xy._val._obj.points
1717
+ if {x, y} == {x_, y_}:
1718
+ continue
1719
+ if deps:
1720
+ deps = deps.extend(self, 'eqangle', list(args), 'para', [x, y, x_, y_])
1721
+
1722
+ args[2 * i - 2] = x_
1723
+ args[2 * i - 1] = y_
1724
+
1725
+ add = []
1726
+ ab_cd, cd_ab, why1 = self._get_or_create_angle(ab, cd, deps=None)
1727
+ mn_pq, pq_mn, why2 = self._get_or_create_angle(mn, pq, deps=None)
1728
+
1729
+ why = why1 + why2
1730
+ if why:
1731
+ dep0 = deps.populate('eqangle', args)
1732
+ deps = EmptyDependency(level=deps.level, rule_name=None)
1733
+ deps.why = [dep0] + why
1734
+
1735
+ dab, dcd = ab_cd._d
1736
+ dmn, dpq = mn_pq._d
1737
+
1738
+ a, b = dab._obj.points
1739
+ c, d = dcd._obj.points
1740
+ m, n = dmn._obj.points
1741
+ p, q = dpq._obj.points
1742
+
1743
+ is_eq1 = self.is_equal(ab_cd, mn_pq)
1744
+ deps1 = None
1745
+ if deps:
1746
+ deps1 = deps.populate('eqangle', [a, b, c, d, m, n, p, q])
1747
+ deps1.algebra = [dab, dcd, dmn, dpq]
1748
+ if not is_eq1:
1749
+ add += [deps1]
1750
+ self.cache_dep('eqangle', [a, b, c, d, m, n, p, q], deps1)
1751
+ self.make_equal(ab_cd, mn_pq, deps=deps1)
1752
+
1753
+ is_eq2 = self.is_equal(cd_ab, pq_mn)
1754
+ deps2 = None
1755
+ if deps:
1756
+ deps2 = deps.populate('eqangle', [c, d, a, b, p, q, m, n])
1757
+ deps2.algebra = [dcd, dab, dpq, dmn]
1758
+ if not is_eq2:
1759
+ add += [deps2]
1760
+ self.cache_dep('eqangle', [c, d, a, b, p, q, m, n], deps2)
1761
+ self.make_equal(cd_ab, pq_mn, deps=deps2)
1762
+
1763
+ return add
1764
+
1765
+ def add_eqangle(
1766
+ self, points: list[Point], deps: EmptyDependency
1767
+ ) -> list[Dependency]:
1768
+ """Add eqangle made by 8 points in `points`."""
1769
+ if deps:
1770
+ deps = deps.copy()
1771
+ a, b, c, d, m, n, p, q = points
1772
+ ab, why1 = self.get_line_thru_pair_why(a, b)
1773
+ cd, why2 = self.get_line_thru_pair_why(c, d)
1774
+ mn, why3 = self.get_line_thru_pair_why(m, n)
1775
+ pq, why4 = self.get_line_thru_pair_why(p, q)
1776
+
1777
+ a, b = ab.points
1778
+ c, d = cd.points
1779
+ m, n = mn.points
1780
+ p, q = pq.points
1781
+
1782
+ if deps and why1 + why2 + why3 + why4:
1783
+ dep0 = deps.populate('eqangle', points)
1784
+ deps = EmptyDependency(level=deps.level, rule_name=None)
1785
+ deps.why = [dep0] + why1 + why2 + why3 + why4
1786
+
1787
+ add = self.maybe_make_equal_pairs(
1788
+ a, b, c, d, m, n, p, q, ab, cd, mn, pq, deps
1789
+ )
1790
+
1791
+ if add is not None:
1792
+ return add
1793
+
1794
+ self.connect_val(ab, deps=None)
1795
+ self.connect_val(cd, deps=None)
1796
+ self.connect_val(mn, deps=None)
1797
+ self.connect_val(pq, deps=None)
1798
+
1799
+ add = []
1800
+ if (
1801
+ ab.val != cd.val
1802
+ and mn.val != pq.val
1803
+ and (ab.val != mn.val or cd.val != pq.val)
1804
+ ):
1805
+ add += self._add_eqangle(a, b, c, d, m, n, p, q, ab, cd, mn, pq, deps)
1806
+
1807
+ if (
1808
+ ab.val != mn.val
1809
+ and cd.val != pq.val
1810
+ and (ab.val != cd.val or mn.val != pq.val)
1811
+ ):
1812
+ add += self._add_eqangle( # pylint: disable=arguments-out-of-order
1813
+ a,
1814
+ b,
1815
+ m,
1816
+ n,
1817
+ c,
1818
+ d,
1819
+ p,
1820
+ q,
1821
+ ab,
1822
+ mn,
1823
+ cd,
1824
+ pq,
1825
+ deps,
1826
+ )
1827
+
1828
+ return add
1829
+
1830
+ def add_aconst(
1831
+ self, points: list[Point], deps: EmptyDependency
1832
+ ) -> list[Dependency]:
1833
+ """Add that an angle is equal to some constant."""
1834
+ a, b, c, d, num, den = points
1835
+ nd, dn = self.get_or_create_const_ang(num, den)
1836
+
1837
+ if nd == self.halfpi:
1838
+ return self.add_perp([a, b, c, d], deps)
1839
+
1840
+ ab, why1 = self.get_line_thru_pair_why(a, b)
1841
+ cd, why2 = self.get_line_thru_pair_why(c, d)
1842
+
1843
+ (a, b), (c, d) = ab.points, cd.points
1844
+ if why1 + why2:
1845
+ args = points[:-2] + [nd]
1846
+ dep0 = deps.populate('aconst', args)
1847
+ deps = EmptyDependency(level=deps.level, rule_name=None)
1848
+ deps.why = [dep0] + why1 + why2
1849
+
1850
+ self.connect_val(ab, deps=None)
1851
+ self.connect_val(cd, deps=None)
1852
+
1853
+ if ab.val == cd.val:
1854
+ raise ValueError(f'{ab.name} - {cd.name} cannot be {nd.name}')
1855
+
1856
+ args = [a, b, c, d, nd]
1857
+ i = 0
1858
+ for x, y, xy in [(a, b, ab), (c, d, cd)]:
1859
+ i += 1
1860
+ x_, y_ = xy._val._obj.points
1861
+ if {x, y} == {x_, y_}:
1862
+ continue
1863
+ if deps:
1864
+ deps = deps.extend(self, 'aconst', list(args), 'para', [x, y, x_, y_])
1865
+ args[2 * i - 2] = x_
1866
+ args[2 * i - 1] = y_
1867
+
1868
+ ab_cd, cd_ab, why = self._get_or_create_angle(ab, cd, deps=None)
1869
+ if why:
1870
+ dep0 = deps.populate('aconst', [a, b, c, d, nd])
1871
+ deps = EmptyDependency(level=deps.level, rule_name=None)
1872
+ deps.why = [dep0] + why
1873
+
1874
+ dab, dcd = ab_cd._d
1875
+ a, b = dab._obj.points
1876
+ c, d = dcd._obj.points
1877
+
1878
+ ang = int(num) * 180 / int(den)
1879
+ add = []
1880
+ if not self.is_equal(ab_cd, nd):
1881
+ deps1 = deps.populate('aconst', [a, b, c, d, nd])
1882
+ deps1.algebra = dab, dcd, ang % 180
1883
+ self.make_equal(ab_cd, nd, deps=deps1)
1884
+ self.cache_dep('aconst', [a, b, c, d, nd], deps1)
1885
+ add += [deps1]
1886
+
1887
+ if not self.is_equal(cd_ab, dn):
1888
+ deps2 = deps.populate('aconst', [c, d, a, b, dn])
1889
+ deps2.algebra = dcd, dab, 180 - ang % 180
1890
+ self.make_equal(cd_ab, dn, deps=deps2)
1891
+ self.cache_dep('aconst', [c, d, a, b, dn], deps2)
1892
+ add += [deps2]
1893
+ return add
1894
+
1895
+ def add_s_angle(
1896
+ self, points: list[Point], deps: EmptyDependency
1897
+ ) -> list[Dependency]:
1898
+ """Add that an angle abx is equal to constant y."""
1899
+ a, b, x, y = points
1900
+
1901
+ n, d = ar.simplify(y % 180, 180)
1902
+ nd, dn = self.get_or_create_const_ang(n, d)
1903
+
1904
+ if nd == self.halfpi:
1905
+ return self.add_perp([a, b, b, x], deps)
1906
+
1907
+ ab, why1 = self.get_line_thru_pair_why(a, b)
1908
+ bx, why2 = self.get_line_thru_pair_why(b, x)
1909
+
1910
+ self.connect_val(ab, deps=None)
1911
+ self.connect_val(bx, deps=None)
1912
+ add = []
1913
+
1914
+ if ab.val == bx.val:
1915
+ return add
1916
+
1917
+ deps.why += why1 + why2
1918
+
1919
+ for p, q, pq in [(a, b, ab), (b, x, bx)]:
1920
+ p_, q_ = pq.val._obj.points
1921
+ if {p, q} == {p_, q_}:
1922
+ continue
1923
+ dep = Dependency('para', [p, q, p_, q_], None, deps.level)
1924
+ deps.why += [dep.why_me_or_cache(self, None)]
1925
+
1926
+ xba, abx, why = self._get_or_create_angle(bx, ab, deps=None)
1927
+ if why:
1928
+ dep0 = deps.populate('aconst', [b, x, a, b, nd])
1929
+ deps = EmptyDependency(level=deps.level, rule_name=None)
1930
+ deps.why = [dep0] + why
1931
+
1932
+ dab, dbx = abx._d
1933
+ a, b = dab._obj.points
1934
+ c, x = dbx._obj.points
1935
+
1936
+ if not self.is_equal(xba, nd):
1937
+ deps1 = deps.populate('aconst', [c, x, a, b, nd])
1938
+ deps1.algebra = dbx, dab, y % 180
1939
+
1940
+ self.make_equal(xba, nd, deps=deps1)
1941
+ self.cache_dep('aconst', [c, x, a, b, nd], deps1)
1942
+ add += [deps1]
1943
+
1944
+ if not self.is_equal(abx, dn):
1945
+ deps2 = deps.populate('aconst', [a, b, c, x, dn])
1946
+ deps2.algebra = dab, dbx, 180 - (y % 180)
1947
+
1948
+ self.make_equal(abx, dn, deps=deps2)
1949
+ self.cache_dep('s_angle', [a, b, c, x, dn], deps2)
1950
+ add += [deps2]
1951
+ return add
1952
+
1953
+ def check_aconst(self, points: list[Point], verbose: bool = False) -> bool:
1954
+ """Check if the angle is equal to a certain constant."""
1955
+ a, b, c, d, nd = points
1956
+ _ = verbose
1957
+ if isinstance(nd, str):
1958
+ name = nd
1959
+ else:
1960
+ name = nd.name
1961
+ num, den = name.split('pi/')
1962
+ ang, _ = self.get_or_create_const_ang(int(num), int(den))
1963
+
1964
+ ab = self._get_line(a, b)
1965
+ cd = self._get_line(c, d)
1966
+ if not ab or not cd:
1967
+ return False
1968
+
1969
+ if not (ab.val and cd.val):
1970
+ return False
1971
+
1972
+ for ang1, _, _ in gm.all_angles(ab._val, cd._val):
1973
+ if self.is_equal(ang1, ang):
1974
+ return True
1975
+ return False
1976
+
1977
+ def check_acompute(self, points: list[Point]) -> bool:
1978
+ """Check if an angle has a constant value."""
1979
+ a, b, c, d = points
1980
+ ab = self._get_line(a, b)
1981
+ cd = self._get_line(c, d)
1982
+ if not ab or not cd:
1983
+ return False
1984
+
1985
+ if not (ab.val and cd.val):
1986
+ return False
1987
+
1988
+ for ang0 in self.aconst.values():
1989
+ for ang in ang0.val.neighbors(Angle):
1990
+ d1, d2 = ang.directions
1991
+ if ab.val == d1 and cd.val == d2:
1992
+ return True
1993
+ return False
1994
+
1995
+ def check_eqangle(self, points: list[Point]) -> bool:
1996
+ """Check if two angles are equal."""
1997
+ a, b, c, d, m, n, p, q = points
1998
+
1999
+ if {a, b} == {c, d} and {m, n} == {p, q}:
2000
+ return True
2001
+ if {a, b} == {m, n} and {c, d} == {p, q}:
2002
+ return True
2003
+
2004
+ if (a == b) or (c == d) or (m == n) or (p == q):
2005
+ return False
2006
+ ab = self._get_line(a, b)
2007
+ cd = self._get_line(c, d)
2008
+ mn = self._get_line(m, n)
2009
+ pq = self._get_line(p, q)
2010
+
2011
+ if {a, b} == {c, d} and mn and pq and self.is_equal(mn, pq):
2012
+ return True
2013
+ if {a, b} == {m, n} and cd and pq and self.is_equal(cd, pq):
2014
+ return True
2015
+ if {p, q} == {m, n} and ab and cd and self.is_equal(ab, cd):
2016
+ return True
2017
+ if {p, q} == {c, d} and ab and mn and self.is_equal(ab, mn):
2018
+ return True
2019
+
2020
+ if not ab or not cd or not mn or not pq:
2021
+ return False
2022
+
2023
+ if self.is_equal(ab, cd) and self.is_equal(mn, pq):
2024
+ return True
2025
+ if self.is_equal(ab, mn) and self.is_equal(cd, pq):
2026
+ return True
2027
+
2028
+ if not (ab.val and cd.val and mn.val and pq.val):
2029
+ return False
2030
+
2031
+ if (ab.val, cd.val) == (mn.val, pq.val) or (ab.val, mn.val) == (
2032
+ cd.val,
2033
+ pq.val,
2034
+ ):
2035
+ return True
2036
+
2037
+ for ang1, _, _ in gm.all_angles(ab._val, cd._val):
2038
+ for ang2, _, _ in gm.all_angles(mn._val, pq._val):
2039
+ if self.is_equal(ang1, ang2):
2040
+ return True
2041
+
2042
+ if self.check_perp([a, b, m, n]) and self.check_perp([c, d, p, q]):
2043
+ return True
2044
+ if self.check_perp([a, b, p, q]) and self.check_perp([c, d, m, n]):
2045
+ return True
2046
+
2047
+ return False
2048
+
2049
+ def _get_ratio(self, l1: Length, l2: Length) -> tuple[Ratio, Ratio]:
2050
+ for r in self.type2nodes[Ratio]:
2051
+ if r.lengths == (l1, l2):
2052
+ return r, r.opposite
2053
+ return None, None
2054
+
2055
+ def _get_or_create_ratio(
2056
+ self, s1: Segment, s2: Segment, deps: Dependency
2057
+ ) -> tuple[Ratio, Ratio, list[Dependency]]:
2058
+ return self._get_or_create_ratio_l(s1._val, s2._val, deps)
2059
+
2060
+ def _get_or_create_ratio_l(
2061
+ self, l1: Length, l2: Length, deps: Dependency
2062
+ ) -> tuple[Ratio, Ratio, list[Dependency]]:
2063
+ """Get or create a new Ratio from two Lenghts l1 and l2."""
2064
+ for r in self.type2nodes[Ratio]:
2065
+ if r.lengths == (l1.rep(), l2.rep()):
2066
+ l1_, l2_ = r._l
2067
+ why1 = l1.why_equal([l1_], None) + l1_.why_rep()
2068
+ why2 = l2.why_equal([l2_], None) + l2_.why_rep()
2069
+ return r, r.opposite, why1 + why2
2070
+
2071
+ l1, why1 = l1.rep_and_why()
2072
+ l2, why2 = l2.rep_and_why()
2073
+ r12 = self.new_node(Ratio, f'{l1.name}/{l2.name}')
2074
+ r21 = self.new_node(Ratio, f'{l2.name}/{l1.name}')
2075
+ self.connect(l1, r12, deps)
2076
+ self.connect(l2, r21, deps)
2077
+ self.connect(r12, r21, deps)
2078
+ r12.set_lengths(l1, l2)
2079
+ r21.set_lengths(l2, l1)
2080
+ r12.opposite = r21
2081
+ r21.opposite = r12
2082
+ return r12, r21, why1 + why2
2083
+
2084
+ def add_cong2(
2085
+ self, points: list[Point], deps: EmptyDependency
2086
+ ) -> list[Dependency]:
2087
+ m, n, a, b = points
2088
+ add = []
2089
+ add += self.add_cong([m, a, n, a], deps)
2090
+ add += self.add_cong([m, b, n, b], deps)
2091
+ return add
2092
+
2093
+ def add_eqratio3(
2094
+ self, points: list[Point], deps: EmptyDependency
2095
+ ) -> list[Dependency]:
2096
+ """Add three eqratios through a list of 6 points (due to parallel lines)."""
2097
+ a, b, c, d, m, n = points
2098
+ # a -- b
2099
+ # m -- n
2100
+ # c -- d
2101
+ add = []
2102
+ add += self.add_eqratio([m, a, m, c, n, b, n, d], deps)
2103
+ add += self.add_eqratio([a, m, a, c, b, n, b, d], deps)
2104
+ add += self.add_eqratio([c, m, c, a, d, n, d, b], deps)
2105
+ if m == n:
2106
+ add += self.add_eqratio([m, a, m, c, a, b, c, d], deps)
2107
+ return add
2108
+
2109
+ def add_eqratio4(
2110
+ self, points: list[Point], deps: EmptyDependency
2111
+ ) -> list[Dependency]:
2112
+ o, a, b, c, d = points
2113
+ # o
2114
+ # a b
2115
+ # c d
2116
+ add = self.add_eqratio3([a, b, c, d, o, o], deps)
2117
+ add += self.add_eqratio([o, a, o, c, a, b, c, d], deps)
2118
+ return add
2119
+
2120
+ def _add_eqratio(
2121
+ self,
2122
+ a: Point,
2123
+ b: Point,
2124
+ c: Point,
2125
+ d: Point,
2126
+ m: Point,
2127
+ n: Point,
2128
+ p: Point,
2129
+ q: Point,
2130
+ ab: Segment,
2131
+ cd: Segment,
2132
+ mn: Segment,
2133
+ pq: Segment,
2134
+ deps: EmptyDependency,
2135
+ ) -> list[Dependency]:
2136
+ """Add a new eqratio from 8 points (core)."""
2137
+ if deps:
2138
+ deps = deps.copy()
2139
+
2140
+ args = [a, b, c, d, m, n, p, q]
2141
+ i = 0
2142
+ for x, y, xy in [(a, b, ab), (c, d, cd), (m, n, mn), (p, q, pq)]:
2143
+ if {x, y} == set(xy.points):
2144
+ continue
2145
+ x_, y_ = list(xy.points)
2146
+ if deps:
2147
+ deps = deps.extend(self, 'eqratio', list(args), 'cong', [x, y, x_, y_])
2148
+ args[2 * i - 2] = x_
2149
+ args[2 * i - 1] = y_
2150
+
2151
+ add = []
2152
+ ab_cd, cd_ab, why1 = self._get_or_create_ratio(ab, cd, deps=None)
2153
+ mn_pq, pq_mn, why2 = self._get_or_create_ratio(mn, pq, deps=None)
2154
+
2155
+ why = why1 + why2
2156
+ if why:
2157
+ dep0 = deps.populate('eqratio', args)
2158
+ deps = EmptyDependency(level=deps.level, rule_name=None)
2159
+ deps.why = [dep0] + why
2160
+
2161
+ lab, lcd = ab_cd._l
2162
+ lmn, lpq = mn_pq._l
2163
+
2164
+ a, b = lab._obj.points
2165
+ c, d = lcd._obj.points
2166
+ m, n = lmn._obj.points
2167
+ p, q = lpq._obj.points
2168
+
2169
+ is_eq1 = self.is_equal(ab_cd, mn_pq)
2170
+ deps1 = None
2171
+ if deps:
2172
+ deps1 = deps.populate('eqratio', [a, b, c, d, m, n, p, q])
2173
+ deps1.algebra = [ab._val, cd._val, mn._val, pq._val]
2174
+ if not is_eq1:
2175
+ add += [deps1]
2176
+ self.cache_dep('eqratio', [a, b, c, d, m, n, p, q], deps1)
2177
+ self.make_equal(ab_cd, mn_pq, deps=deps1)
2178
+
2179
+ is_eq2 = self.is_equal(cd_ab, pq_mn)
2180
+ deps2 = None
2181
+ if deps:
2182
+ deps2 = deps.populate('eqratio', [c, d, a, b, p, q, m, n])
2183
+ deps2.algebra = [cd._val, ab._val, pq._val, mn._val]
2184
+ if not is_eq2:
2185
+ add += [deps2]
2186
+ self.cache_dep('eqratio', [c, d, a, b, p, q, m, n], deps2)
2187
+ self.make_equal(cd_ab, pq_mn, deps=deps2)
2188
+ return add
2189
+
2190
+ def add_eqratio(
2191
+ self, points: list[Point], deps: EmptyDependency
2192
+ ) -> list[Dependency]:
2193
+ """Add a new eqratio from 8 points."""
2194
+ if deps:
2195
+ deps = deps.copy()
2196
+ a, b, c, d, m, n, p, q = points
2197
+ ab = self._get_or_create_segment(a, b, deps=None)
2198
+ cd = self._get_or_create_segment(c, d, deps=None)
2199
+ mn = self._get_or_create_segment(m, n, deps=None)
2200
+ pq = self._get_or_create_segment(p, q, deps=None)
2201
+
2202
+ add = self.maybe_make_equal_pairs(
2203
+ a, b, c, d, m, n, p, q, ab, cd, mn, pq, deps
2204
+ )
2205
+
2206
+ if add is not None:
2207
+ return add
2208
+
2209
+ self.connect_val(ab, deps=None)
2210
+ self.connect_val(cd, deps=None)
2211
+ self.connect_val(mn, deps=None)
2212
+ self.connect_val(pq, deps=None)
2213
+
2214
+ add = []
2215
+ if (
2216
+ ab.val != cd.val
2217
+ and mn.val != pq.val
2218
+ and (ab.val != mn.val or cd.val != pq.val)
2219
+ ):
2220
+ add += self._add_eqratio(a, b, c, d, m, n, p, q, ab, cd, mn, pq, deps)
2221
+
2222
+ if (
2223
+ ab.val != mn.val
2224
+ and cd.val != pq.val
2225
+ and (ab.val != cd.val or mn.val != pq.val)
2226
+ ):
2227
+ add += self._add_eqratio( # pylint: disable=arguments-out-of-order
2228
+ a,
2229
+ b,
2230
+ m,
2231
+ n,
2232
+ c,
2233
+ d,
2234
+ p,
2235
+ q,
2236
+ ab,
2237
+ mn,
2238
+ cd,
2239
+ pq,
2240
+ deps,
2241
+ )
2242
+ return add
2243
+
2244
+ def check_rconst(self, points: list[Point], verbose: bool = False) -> bool:
2245
+ """Check whether a ratio is equal to some given constant."""
2246
+ _ = verbose
2247
+ a, b, c, d, nd = points
2248
+ if isinstance(nd, str):
2249
+ name = nd
2250
+ else:
2251
+ name = nd.name
2252
+ num, den = name.split('/')
2253
+ rat, _ = self.get_or_create_const_rat(int(num), int(den))
2254
+
2255
+ ab = self._get_segment(a, b)
2256
+ cd = self._get_segment(c, d)
2257
+
2258
+ if not ab or not cd:
2259
+ return False
2260
+
2261
+ if not (ab.val and cd.val):
2262
+ return False
2263
+
2264
+ for rat1, _, _ in gm.all_ratios(ab._val, cd._val):
2265
+ if self.is_equal(rat1, rat):
2266
+ return True
2267
+ return False
2268
+
2269
+ def check_rcompute(self, points: list[Point]) -> bool:
2270
+ """Check whether a ratio is equal to some constant."""
2271
+ a, b, c, d = points
2272
+ ab = self._get_segment(a, b)
2273
+ cd = self._get_segment(c, d)
2274
+
2275
+ if not ab or not cd:
2276
+ return False
2277
+
2278
+ if not (ab.val and cd.val):
2279
+ return False
2280
+
2281
+ for rat0 in self.rconst.values():
2282
+ for rat in rat0.val.neighbors(Ratio):
2283
+ l1, l2 = rat.lengths
2284
+ if ab.val == l1 and cd.val == l2:
2285
+ return True
2286
+ return False
2287
+
2288
+ def check_eqratio(self, points: list[Point]) -> bool:
2289
+ """Check if 8 points make an eqratio predicate."""
2290
+ a, b, c, d, m, n, p, q = points
2291
+
2292
+ if {a, b} == {c, d} and {m, n} == {p, q}:
2293
+ return True
2294
+ if {a, b} == {m, n} and {c, d} == {p, q}:
2295
+ return True
2296
+
2297
+ ab = self._get_segment(a, b)
2298
+ cd = self._get_segment(c, d)
2299
+ mn = self._get_segment(m, n)
2300
+ pq = self._get_segment(p, q)
2301
+
2302
+ if {a, b} == {c, d} and mn and pq and self.is_equal(mn, pq):
2303
+ return True
2304
+ if {a, b} == {m, n} and cd and pq and self.is_equal(cd, pq):
2305
+ return True
2306
+ if {p, q} == {m, n} and ab and cd and self.is_equal(ab, cd):
2307
+ return True
2308
+ if {p, q} == {c, d} and ab and mn and self.is_equal(ab, mn):
2309
+ return True
2310
+
2311
+ if not ab or not cd or not mn or not pq:
2312
+ return False
2313
+
2314
+ if self.is_equal(ab, cd) and self.is_equal(mn, pq):
2315
+ return True
2316
+ if self.is_equal(ab, mn) and self.is_equal(cd, pq):
2317
+ return True
2318
+
2319
+ if not (ab.val and cd.val and mn.val and pq.val):
2320
+ return False
2321
+
2322
+ if (ab.val, cd.val) == (mn.val, pq.val) or (ab.val, mn.val) == (
2323
+ cd.val,
2324
+ pq.val,
2325
+ ):
2326
+ return True
2327
+
2328
+ for rat1, _, _ in gm.all_ratios(ab._val, cd._val):
2329
+ for rat2, _, _ in gm.all_ratios(mn._val, pq._val):
2330
+ if self.is_equal(rat1, rat2):
2331
+ return True
2332
+ return False
2333
+
2334
+ def add_simtri_check(
2335
+ self, points: list[Point], deps: EmptyDependency
2336
+ ) -> list[Dependency]:
2337
+ if nm.same_clock(*[p.num for p in points]):
2338
+ return self.add_simtri(points, deps)
2339
+ return self.add_simtri2(points, deps)
2340
+
2341
+ def add_contri_check(
2342
+ self, points: list[Point], deps: EmptyDependency
2343
+ ) -> list[Dependency]:
2344
+ if nm.same_clock(*[p.num for p in points]):
2345
+ return self.add_contri(points, deps)
2346
+ return self.add_contri2(points, deps)
2347
+
2348
+ def enum_sides(
2349
+ self, points: list[Point]
2350
+ ) -> Generator[list[Point], None, None]:
2351
+ a, b, c, x, y, z = points
2352
+ yield [a, b, x, y]
2353
+ yield [b, c, y, z]
2354
+ yield [c, a, z, x]
2355
+
2356
+ def enum_triangle(
2357
+ self, points: list[Point]
2358
+ ) -> Generator[list[Point], None, None]:
2359
+ a, b, c, x, y, z = points
2360
+ yield [a, b, a, c, x, y, x, z]
2361
+ yield [b, a, b, c, y, x, y, z]
2362
+ yield [c, a, c, b, z, x, z, y]
2363
+
2364
+ def enum_triangle2(
2365
+ self, points: list[Point]
2366
+ ) -> Generator[list[Point], None, None]:
2367
+ a, b, c, x, y, z = points
2368
+ yield [a, b, a, c, x, z, x, y]
2369
+ yield [b, a, b, c, y, z, y, x]
2370
+ yield [c, a, c, b, z, y, z, x]
2371
+
2372
+ def add_simtri(
2373
+ self, points: list[Point], deps: EmptyDependency
2374
+ ) -> list[Dependency]:
2375
+ """Add two similar triangles."""
2376
+ add = []
2377
+ hashs = [d.hashed() for d in deps.why]
2378
+
2379
+ for args in self.enum_triangle(points):
2380
+ if problem.hashed('eqangle6', args) in hashs:
2381
+ continue
2382
+ add += self.add_eqangle(args, deps=deps)
2383
+
2384
+ for args in self.enum_triangle(points):
2385
+ if problem.hashed('eqratio6', args) in hashs:
2386
+ continue
2387
+ add += self.add_eqratio(args, deps=deps)
2388
+
2389
+ return add
2390
+
2391
+ def check_simtri(self, points: list[Point]) -> bool:
2392
+ a, b, c, x, y, z = points
2393
+ return self.check_eqangle([a, b, a, c, x, y, x, z]) and self.check_eqangle(
2394
+ [b, a, b, c, y, x, y, z]
2395
+ )
2396
+
2397
+ def add_simtri2(
2398
+ self, points: list[Point], deps: EmptyDependency
2399
+ ) -> list[Dependency]:
2400
+ """Add two similar reflected triangles."""
2401
+ add = []
2402
+ hashs = [d.hashed() for d in deps.why]
2403
+ for args in self.enum_triangle2(points):
2404
+ if problem.hashed('eqangle6', args) in hashs:
2405
+ continue
2406
+ add += self.add_eqangle(args, deps=deps)
2407
+
2408
+ for args in self.enum_triangle(points):
2409
+ if problem.hashed('eqratio6', args) in hashs:
2410
+ continue
2411
+ add += self.add_eqratio(args, deps=deps)
2412
+
2413
+ return add
2414
+
2415
+ def add_contri(
2416
+ self, points: list[Point], deps: EmptyDependency
2417
+ ) -> list[Dependency]:
2418
+ """Add two congruent triangles."""
2419
+ add = []
2420
+ hashs = [d.hashed() for d in deps.why]
2421
+ for args in self.enum_triangle(points):
2422
+ if problem.hashed('eqangle6', args) in hashs:
2423
+ continue
2424
+ add += self.add_eqangle(args, deps=deps)
2425
+
2426
+ for args in self.enum_sides(points):
2427
+ if problem.hashed('cong', args) in hashs:
2428
+ continue
2429
+ add += self.add_cong(args, deps=deps)
2430
+ return add
2431
+
2432
+ def check_contri(self, points: list[Point]) -> bool:
2433
+ a, b, c, x, y, z = points
2434
+ return (
2435
+ self.check_cong([a, b, x, y])
2436
+ and self.check_cong([b, c, y, z])
2437
+ and self.check_cong([c, a, z, x])
2438
+ )
2439
+
2440
+ def add_contri2(
2441
+ self, points: list[Point], deps: EmptyDependency
2442
+ ) -> list[Dependency]:
2443
+ """Add two congruent reflected triangles."""
2444
+ add = []
2445
+ hashs = [d.hashed() for d in deps.why]
2446
+ for args in self.enum_triangle2(points):
2447
+ if problem.hashed('eqangle6', args) in hashs:
2448
+ continue
2449
+ add += self.add_eqangle(args, deps=deps)
2450
+
2451
+ for args in self.enum_sides(points):
2452
+ if problem.hashed('cong', args) in hashs:
2453
+ continue
2454
+ add += self.add_cong(args, deps=deps)
2455
+
2456
+ return add
2457
+
2458
+ def in_cache(self, name: str, args: list[Point]) -> bool:
2459
+ return problem.hashed(name, args) in self.cache
2460
+
2461
+ def cache_dep(
2462
+ self, name: str, args: list[Point], premises: list[Dependency]
2463
+ ) -> None:
2464
+ hashed = problem.hashed(name, args)
2465
+ if hashed in self.cache:
2466
+ return
2467
+ self.cache[hashed] = premises
2468
+
2469
+ def all_same_line(
2470
+ self, a: Point, b: Point
2471
+ ) -> Generator[tuple[Point, Point], None, None]:
2472
+ ab = self._get_line(a, b)
2473
+ if ab is None:
2474
+ return
2475
+ for p1, p2 in utils.comb2(ab.neighbors(Point)):
2476
+ if {p1, p2} != {a, b}:
2477
+ yield p1, p2
2478
+
2479
+ def all_same_angle(
2480
+ self, a: Point, b: Point, c: Point, d: Point
2481
+ ) -> Generator[tuple[Point, Point, Point, Point], None, None]:
2482
+ for x, y in self.all_same_line(a, b):
2483
+ for m, n in self.all_same_line(c, d):
2484
+ yield x, y, m, n
2485
+
2486
+ def additionally_draw(self, name: str, args: list[Point]) -> None:
2487
+ """Draw some extra line/circles for illustration purpose."""
2488
+
2489
+ if name in ['circle']:
2490
+ center, point = args[:2]
2491
+ circle = self.new_node(Circle, f'({center.name},{point.name})')
2492
+ circle.num = nm.Circle(center.num, p1=point.num)
2493
+ circle.points = center, point
2494
+
2495
+ if name in ['on_circle', 'tangent']:
2496
+ center, point = args[-2:]
2497
+ circle = self.new_node(Circle, f'({center.name},{point.name})')
2498
+ circle.num = nm.Circle(center.num, p1=point.num)
2499
+ circle.points = center, point
2500
+
2501
+ if name in ['incenter', 'excenter', 'incenter2', 'excenter2']:
2502
+ d, a, b, c = [x for x in args[-4:]]
2503
+ a, b, c = sorted([a, b, c], key=lambda x: x.name.lower())
2504
+ circle = self.new_node(Circle, f'({d.name},h.{a.name}{b.name})')
2505
+ p = d.num.foot(nm.Line(a.num, b.num))
2506
+ circle.num = nm.Circle(d.num, p1=p)
2507
+ circle.points = d, a, b, c
2508
+
2509
+ if name in ['cc_tangent']:
2510
+ o, a, w, b = args[-4:]
2511
+ c1 = self.new_node(Circle, f'({o.name},{a.name})')
2512
+ c1.num = nm.Circle(o.num, p1=a.num)
2513
+ c1.points = o, a
2514
+
2515
+ c2 = self.new_node(Circle, f'({w.name},{b.name})')
2516
+ c2.num = nm.Circle(w.num, p1=b.num)
2517
+ c2.points = w, b
2518
+
2519
+ if name in ['ninepoints']:
2520
+ a, b, c = args[-3:]
2521
+ a, b, c = sorted([a, b, c], key=lambda x: x.name.lower())
2522
+ circle = self.new_node(Circle, f'(,m.{a.name}{b.name}{c.name})')
2523
+ p1 = (b.num + c.num) * 0.5
2524
+ p2 = (c.num + a.num) * 0.5
2525
+ p3 = (a.num + b.num) * 0.5
2526
+ circle.num = nm.Circle(p1=p1, p2=p2, p3=p3)
2527
+ circle.points = (None, None, a, b, c)
2528
+
2529
+ if name in ['2l1c']:
2530
+ a, b, c, o = args[:4]
2531
+ a, b, c = sorted([a, b, c], key=lambda x: x.name.lower())
2532
+ circle = self.new_node(Circle, f'({o.name},{a.name}{b.name}{c.name})')
2533
+ circle.num = nm.Circle(p1=a.num, p2=b.num, p3=c.num)
2534
+ circle.points = (a, b, c)
2535
+
2536
+ def add_clause(
2537
+ self,
2538
+ clause: problem.Clause,
2539
+ plevel: int,
2540
+ definitions: dict[str, problem.Definition],
2541
+ verbose: int = False,
2542
+ ) -> tuple[list[Dependency], int]:
2543
+ """Add a new clause of construction, e.g. a new excenter."""
2544
+ existing_points = self.all_points()
2545
+ new_points = [Point(name) for name in clause.points]
2546
+
2547
+ new_points_dep_points = set()
2548
+ new_points_dep = []
2549
+
2550
+ # Step 1: check for all deps.
2551
+ for c in clause.constructions:
2552
+ cdef = definitions[c.name]
2553
+
2554
+ if len(cdef.construction.args) != len(c.args):
2555
+ if len(cdef.construction.args) - len(c.args) == len(clause.points):
2556
+ c.args = clause.points + c.args
2557
+ else:
2558
+ correct_form = ' '.join(cdef.points + ['=', c.name] + cdef.args)
2559
+ raise ValueError('Argument mismatch. ' + correct_form)
2560
+
2561
+ mapping = dict(zip(cdef.construction.args, c.args))
2562
+ c_name = 'midp' if c.name == 'midpoint' else c.name
2563
+ deps = EmptyDependency(level=0, rule_name=problem.CONSTRUCTION_RULE)
2564
+ deps.construction = Dependency(c_name, c.args, rule_name=None, level=0)
2565
+
2566
+ for d in cdef.deps.constructions:
2567
+ args = self.names2points([mapping[a] for a in d.args])
2568
+ new_points_dep_points.update(args)
2569
+ if not self.check(d.name, args):
2570
+ raise DepCheckFailError(
2571
+ d.name + ' ' + ' '.join([x.name for x in args])
2572
+ )
2573
+ deps.why += [
2574
+ Dependency(
2575
+ d.name, args, rule_name=problem.CONSTRUCTION_RULE, level=0
2576
+ )
2577
+ ]
2578
+
2579
+ new_points_dep += [deps]
2580
+
2581
+ # Step 2: draw.
2582
+ def range_fn() -> (
2583
+ list[Union[nm.Point, nm.Line, nm.Circle, nm.HalfLine, nm.HoleCircle]]
2584
+ ):
2585
+ to_be_intersected = []
2586
+ for c in clause.constructions:
2587
+ cdef = definitions[c.name]
2588
+ mapping = dict(zip(cdef.construction.args, c.args))
2589
+ for n in cdef.numerics:
2590
+ args = [mapping[a] for a in n.args]
2591
+ args = list(map(lambda x: self.get(x, lambda: int(x)), args))
2592
+ to_be_intersected += nm.sketch(n.name, args)
2593
+
2594
+ return to_be_intersected
2595
+
2596
+ is_total_free = (
2597
+ len(clause.constructions) == 1 and clause.constructions[0].name in FREE
2598
+ )
2599
+ is_semi_free = (
2600
+ len(clause.constructions) == 1
2601
+ and clause.constructions[0].name in INTERSECT
2602
+ )
2603
+
2604
+ existing_points = [p.num for p in existing_points]
2605
+
2606
+ def draw_fn() -> list[nm.Point]:
2607
+ to_be_intersected = range_fn()
2608
+ return nm.reduce(to_be_intersected, existing_points)
2609
+
2610
+ rely_on = set()
2611
+ for c in clause.constructions:
2612
+ cdef = definitions[c.name]
2613
+ mapping = dict(zip(cdef.construction.args, c.args))
2614
+ for n in cdef.numerics:
2615
+ args = [mapping[a] for a in n.args]
2616
+ args = list(map(lambda x: self.get(x, lambda: int(x)), args))
2617
+ rely_on.update([a for a in args if isinstance(a, Point)])
2618
+
2619
+ for p in rely_on:
2620
+ p.change.update(new_points)
2621
+
2622
+ nums = draw_fn()
2623
+ for p, num, num0 in zip(new_points, nums, clause.nums):
2624
+ p.co_change = new_points
2625
+ if isinstance(num0, nm.Point):
2626
+ num = num0
2627
+ elif isinstance(num0, (tuple, list)):
2628
+ x, y = num0
2629
+ num = nm.Point(x, y)
2630
+
2631
+ p.num = num
2632
+
2633
+ # check two things.
2634
+ if nm.check_too_close(nums, existing_points):
2635
+ raise PointTooCloseError()
2636
+ if nm.check_too_far(nums, existing_points):
2637
+ raise PointTooFarError()
2638
+
2639
+ # Commit: now that all conditions are passed.
2640
+ # add these points to current graph.
2641
+ for p in new_points:
2642
+ self._name2point[p.name] = p
2643
+ self._name2node[p.name] = p
2644
+ self.type2nodes[Point].append(p)
2645
+
2646
+ for p in new_points:
2647
+ p.why = sum([d.why for d in new_points_dep], []) # to generate txt logs.
2648
+ p.group = new_points
2649
+ p.dep_points = new_points_dep_points
2650
+ p.dep_points.update(new_points)
2651
+ p.plevel = plevel
2652
+
2653
+ # movement dependency:
2654
+ rely_dict_0 = defaultdict(lambda: [])
2655
+
2656
+ for c in clause.constructions:
2657
+ cdef = definitions[c.name]
2658
+ mapping = dict(zip(cdef.construction.args, c.args))
2659
+ for p, ps in cdef.rely.items():
2660
+ p = mapping[p]
2661
+ ps = [mapping[x] for x in ps]
2662
+ rely_dict_0[p].append(ps)
2663
+
2664
+ rely_dict = {}
2665
+ for p, pss in rely_dict_0.items():
2666
+ ps = sum(pss, [])
2667
+ if len(pss) > 1:
2668
+ ps = [x for x in ps if x != p]
2669
+
2670
+ p = self._name2point[p]
2671
+ ps = self.names2nodes(ps)
2672
+ rely_dict[p] = ps
2673
+
2674
+ for p in new_points:
2675
+ p.rely_on = set(rely_dict.get(p, []))
2676
+ for x in p.rely_on:
2677
+ if not hasattr(x, 'base_rely_on'):
2678
+ x.base_rely_on = set()
2679
+ p.base_rely_on = set.union(*[x.base_rely_on for x in p.rely_on] + [set()])
2680
+ if is_total_free or is_semi_free:
2681
+ p.rely_on.add(p)
2682
+ p.base_rely_on.add(p)
2683
+
2684
+ plevel_done = set()
2685
+ added = []
2686
+ basics = []
2687
+ # Step 3: build the basics.
2688
+ for c, deps in zip(clause.constructions, new_points_dep):
2689
+ cdef = definitions[c.name]
2690
+ mapping = dict(zip(cdef.construction.args, c.args))
2691
+
2692
+ # not necessary for proofing, but for visualization.
2693
+ c_args = list(map(lambda x: self.get(x, lambda: int(x)), c.args))
2694
+ self.additionally_draw(c.name, c_args)
2695
+
2696
+ for points, bs in cdef.basics:
2697
+ if points:
2698
+ points = self.names2nodes([mapping[p] for p in points])
2699
+ points = [p for p in points if p not in plevel_done]
2700
+ for p in points:
2701
+ p.plevel = plevel
2702
+ plevel_done.update(points)
2703
+ plevel += 1
2704
+ else:
2705
+ continue
2706
+
2707
+ for b in bs:
2708
+ if b.name != 'rconst':
2709
+ args = [mapping[a] for a in b.args]
2710
+ else:
2711
+ num, den = map(int, b.args[-2:])
2712
+ rat, _ = self.get_or_create_const_rat(num, den)
2713
+ args = [mapping[a] for a in b.args[:-2]] + [rat.name]
2714
+
2715
+ args = list(map(lambda x: self.get(x, lambda: int(x)), args))
2716
+
2717
+ adds = self.add_piece(name=b.name, args=args, deps=deps)
2718
+ basics.append((b.name, args, deps))
2719
+ if adds:
2720
+ added += adds
2721
+ for add in adds:
2722
+ self.cache_dep(add.name, add.args, add)
2723
+
2724
+ assert len(plevel_done) == len(new_points)
2725
+ for p in new_points:
2726
+ p.basics = basics
2727
+
2728
+ return added, plevel
2729
+
2730
+ def all_eqangle_same_lines(self) -> Generator[tuple[Point, ...], None, None]:
2731
+ for l1, l2 in utils.perm2(self.type2nodes[Line]):
2732
+ for a, b, c, d, e, f, g, h in utils.all_8points(l1, l2, l1, l2):
2733
+ if (a, b, c, d) != (e, f, g, h):
2734
+ yield a, b, c, d, e, f, g, h
2735
+
2736
+ def all_eqangles_distinct_linepairss(
2737
+ self,
2738
+ ) -> Generator[tuple[Line, ...], None, None]:
2739
+ """No eqangles betcause para-para, or para-corresponding, or same."""
2740
+
2741
+ for measure in self.type2nodes[Measure]:
2742
+ angs = measure.neighbors(Angle)
2743
+ line_pairss = []
2744
+ for ang in angs:
2745
+ d1, d2 = ang.directions
2746
+ if d1 is None or d2 is None:
2747
+ continue
2748
+ l1s = d1.neighbors(Line)
2749
+ l2s = d2.neighbors(Line)
2750
+ # Any pair in this is para-para.
2751
+ para_para = list(utils.cross(l1s, l2s))
2752
+ line_pairss.append(para_para)
2753
+
2754
+ for pairs1, pairs2 in utils.comb2(line_pairss):
2755
+ for pair1, pair2 in utils.cross(pairs1, pairs2):
2756
+ (l1, l2), (l3, l4) = pair1, pair2
2757
+ yield l1, l2, l3, l4
2758
+
2759
+ def all_eqangles_8points(self) -> Generator[tuple[Point, ...], None, None]:
2760
+ """List all sets of 8 points that make two equal angles."""
2761
+ # Case 1: (l1-l2) = (l3-l4), including because l1//l3, l2//l4 (para-para)
2762
+ angss = []
2763
+ for measure in self.type2nodes[Measure]:
2764
+ angs = measure.neighbors(Angle)
2765
+ angss.append(angs)
2766
+
2767
+ # include the angs that do not have any measure.
2768
+ angss.extend([[ang] for ang in self.type2nodes[Angle] if ang.val is None])
2769
+
2770
+ line_pairss = []
2771
+ for angs in angss:
2772
+ line_pairs = set()
2773
+ for ang in angs:
2774
+ d1, d2 = ang.directions
2775
+ if d1 is None or d2 is None:
2776
+ continue
2777
+ l1s = d1.neighbors(Line)
2778
+ l2s = d2.neighbors(Line)
2779
+ line_pairs.update(set(utils.cross(l1s, l2s)))
2780
+ line_pairss.append(line_pairs)
2781
+
2782
+ # include (d1, d2) in which d1 does not have any angles.
2783
+ noang_ds = [d for d in self.type2nodes[Direction] if not d.neighbors(Angle)]
2784
+
2785
+ for d1 in noang_ds:
2786
+ for d2 in self.type2nodes[Direction]:
2787
+ if d1 == d2:
2788
+ continue
2789
+ l1s = d1.neighbors(Line)
2790
+ l2s = d2.neighbors(Line)
2791
+ if len(l1s) < 2 and len(l2s) < 2:
2792
+ continue
2793
+ line_pairss.append(set(utils.cross(l1s, l2s)))
2794
+ line_pairss.append(set(utils.cross(l2s, l1s)))
2795
+
2796
+ # Case 2: d1 // d2 => (d1-d3) = (d2-d3)
2797
+ # include lines that does not have any direction.
2798
+ nodir_ls = [l for l in self.type2nodes[Line] if l.val is None]
2799
+
2800
+ for line in nodir_ls:
2801
+ for d in self.type2nodes[Direction]:
2802
+ l1s = d.neighbors(Line)
2803
+ if len(l1s) < 2:
2804
+ continue
2805
+ l2s = [line]
2806
+ line_pairss.append(set(utils.cross(l1s, l2s)))
2807
+ line_pairss.append(set(utils.cross(l2s, l1s)))
2808
+
2809
+ record = set()
2810
+ for line_pairs in line_pairss:
2811
+ for pair1, pair2 in utils.perm2(list(line_pairs)):
2812
+ (l1, l2), (l3, l4) = pair1, pair2
2813
+ if l1 == l2 or l3 == l4:
2814
+ continue
2815
+ if (l1, l2) == (l3, l4):
2816
+ continue
2817
+ if (l1, l2, l3, l4) in record:
2818
+ continue
2819
+ record.add((l1, l2, l3, l4))
2820
+ for a, b, c, d, e, f, g, h in utils.all_8points(l1, l2, l3, l4):
2821
+ yield (a, b, c, d, e, f, g, h)
2822
+
2823
+ for a, b, c, d, e, f, g, h in self.all_eqangle_same_lines():
2824
+ yield a, b, c, d, e, f, g, h
2825
+
2826
+ def all_eqangles_6points(self) -> Generator[tuple[Point, ...], None, None]:
2827
+ """List all sets of 6 points that make two equal angles."""
2828
+ record = set()
2829
+ for a, b, c, d, e, f, g, h in self.all_eqangles_8points():
2830
+ if (
2831
+ a not in (c, d)
2832
+ and b not in (c, d)
2833
+ or e not in (g, h)
2834
+ and f not in (g, h)
2835
+ ):
2836
+ continue
2837
+
2838
+ if b in (c, d):
2839
+ a, b = b, a # now a in c, d
2840
+ if f in (g, h):
2841
+ e, f = f, e # now e in g, h
2842
+ if a == d:
2843
+ c, d = d, c # now a == c
2844
+ if e == h:
2845
+ g, h = h, g # now e == g
2846
+ if (a, b, c, d, e, f, g, h) in record:
2847
+ continue
2848
+ record.add((a, b, c, d, e, f, g, h))
2849
+ yield a, b, c, d, e, f, g, h # where a==c, e==g
2850
+
2851
+ def all_paras(self) -> Generator[tuple[Point, ...], None, None]:
2852
+ for d in self.type2nodes[Direction]:
2853
+ for l1, l2 in utils.perm2(d.neighbors(Line)):
2854
+ for a, b, c, d in utils.all_4points(l1, l2):
2855
+ yield a, b, c, d
2856
+
2857
+ def all_perps(self) -> Generator[tuple[Point, ...], None, None]:
2858
+ for ang in self.vhalfpi.neighbors(Angle):
2859
+ d1, d2 = ang.directions
2860
+ if d1 is None or d2 is None:
2861
+ continue
2862
+ if d1 == d2:
2863
+ continue
2864
+ for l1, l2 in utils.cross(d1.neighbors(Line), d2.neighbors(Line)):
2865
+ for a, b, c, d in utils.all_4points(l1, l2):
2866
+ yield a, b, c, d
2867
+
2868
+ def all_congs(self) -> Generator[tuple[Point, ...], None, None]:
2869
+ for l in self.type2nodes[Length]:
2870
+ for s1, s2 in utils.perm2(l.neighbors(Segment)):
2871
+ (a, b), (c, d) = s1.points, s2.points
2872
+ for x, y in [(a, b), (b, a)]:
2873
+ for m, n in [(c, d), (d, c)]:
2874
+ yield x, y, m, n
2875
+
2876
+ def all_eqratios_8points(self) -> Generator[tuple[Point, ...], None, None]:
2877
+ """List all sets of 8 points that make two equal ratios."""
2878
+ ratss = []
2879
+ for value in self.type2nodes[Value]:
2880
+ rats = value.neighbors(Ratio)
2881
+ ratss.append(rats)
2882
+
2883
+ # include the rats that do not have any val.
2884
+ ratss.extend([[rat] for rat in self.type2nodes[Ratio] if rat.val is None])
2885
+
2886
+ seg_pairss = []
2887
+ for rats in ratss:
2888
+ seg_pairs = set()
2889
+ for rat in rats:
2890
+ l1, l2 = rat.lengths
2891
+ if l1 is None or l2 is None:
2892
+ continue
2893
+ s1s = l1.neighbors(Segment)
2894
+ s2s = l2.neighbors(Segment)
2895
+ seg_pairs.update(utils.cross(s1s, s2s))
2896
+ seg_pairss.append(seg_pairs)
2897
+
2898
+ # include (l1, l2) in which l1 does not have any ratio.
2899
+ norat_ls = [l for l in self.type2nodes[Length] if not l.neighbors(Ratio)]
2900
+
2901
+ for l1 in norat_ls:
2902
+ for l2 in self.type2nodes[Length]:
2903
+ if l1 == l2:
2904
+ continue
2905
+ s1s = l1.neighbors(Segment)
2906
+ s2s = l2.neighbors(Segment)
2907
+ if len(s1s) < 2 and len(s2s) < 2:
2908
+ continue
2909
+ seg_pairss.append(set(utils.cross(s1s, s2s)))
2910
+ seg_pairss.append(set(utils.cross(s2s, s1s)))
2911
+
2912
+ # include Seg that does not have any Length.
2913
+ nolen_ss = [s for s in self.type2nodes[Segment] if s.val is None]
2914
+
2915
+ for seg in nolen_ss:
2916
+ for l in self.type2nodes[Length]:
2917
+ s1s = l.neighbors(Segment)
2918
+ if len(s1s) == 1:
2919
+ continue
2920
+ s2s = [seg]
2921
+ seg_pairss.append(set(utils.cross(s1s, s2s)))
2922
+ seg_pairss.append(set(utils.cross(s2s, s1s)))
2923
+
2924
+ record = set()
2925
+ for seg_pairs in seg_pairss:
2926
+ for pair1, pair2 in utils.perm2(list(seg_pairs)):
2927
+ (s1, s2), (s3, s4) = pair1, pair2
2928
+ if s1 == s2 or s3 == s4:
2929
+ continue
2930
+ if (s1, s2) == (s3, s4):
2931
+ continue
2932
+ if (s1, s2, s3, s4) in record:
2933
+ continue
2934
+ record.add((s1, s2, s3, s4))
2935
+ a, b = s1.points
2936
+ c, d = s2.points
2937
+ e, f = s3.points
2938
+ g, h = s4.points
2939
+
2940
+ for x, y in [(a, b), (b, a)]:
2941
+ for z, t in [(c, d), (d, c)]:
2942
+ for m, n in [(e, f), (f, e)]:
2943
+ for p, q in [(g, h), (h, g)]:
2944
+ yield (x, y, z, t, m, n, p, q)
2945
+
2946
+ segss = []
2947
+ # finally the list of ratios that is equal to 1.0
2948
+ for length in self.type2nodes[Length]:
2949
+ segs = length.neighbors(Segment)
2950
+ segss.append(segs)
2951
+
2952
+ segs_pair = list(utils.perm2(list(segss)))
2953
+ segs_pair += list(zip(segss, segss))
2954
+ for segs1, segs2 in segs_pair:
2955
+ for s1, s2 in utils.perm2(list(segs1)):
2956
+ for s3, s4 in utils.perm2(list(segs2)):
2957
+ if (s1, s2) == (s3, s4) or (s1, s3) == (s2, s4):
2958
+ continue
2959
+ if (s1, s2, s3, s4) in record:
2960
+ continue
2961
+ record.add((s1, s2, s3, s4))
2962
+ a, b = s1.points
2963
+ c, d = s2.points
2964
+ e, f = s3.points
2965
+ g, h = s4.points
2966
+
2967
+ for x, y in [(a, b), (b, a)]:
2968
+ for z, t in [(c, d), (d, c)]:
2969
+ for m, n in [(e, f), (f, e)]:
2970
+ for p, q in [(g, h), (h, g)]:
2971
+ yield (x, y, z, t, m, n, p, q)
2972
+
2973
+ def all_eqratios_6points(self) -> Generator[tuple[Point, ...], None, None]:
2974
+ """List all sets of 6 points that make two equal angles."""
2975
+ record = set()
2976
+ for a, b, c, d, e, f, g, h in self.all_eqratios_8points():
2977
+ if (
2978
+ a not in (c, d)
2979
+ and b not in (c, d)
2980
+ or e not in (g, h)
2981
+ and f not in (g, h)
2982
+ ):
2983
+ continue
2984
+ if b in (c, d):
2985
+ a, b = b, a
2986
+ if f in (g, h):
2987
+ e, f = f, e
2988
+ if a == d:
2989
+ c, d = d, c
2990
+ if e == h:
2991
+ g, h = h, g
2992
+ if (a, b, c, d, e, f, g, h) in record:
2993
+ continue
2994
+ record.add((a, b, c, d, e, f, g, h))
2995
+ yield a, b, c, d, e, f, g, h # now a==c, e==g
2996
+
2997
+ def all_cyclics(self) -> Generator[tuple[Point, ...], None, None]:
2998
+ for c in self.type2nodes[Circle]:
2999
+ for x, y, z, t in utils.perm4(c.neighbors(Point)):
3000
+ yield x, y, z, t
3001
+
3002
+ def all_colls(self) -> Generator[tuple[Point, ...], None, None]:
3003
+ for l in self.type2nodes[Line]:
3004
+ for x, y, z in utils.perm3(l.neighbors(Point)):
3005
+ yield x, y, z
3006
+
3007
+ def all_midps(self) -> Generator[tuple[Point, ...], None, None]:
3008
+ for l in self.type2nodes[Line]:
3009
+ for a, b, c in utils.perm3(l.neighbors(Point)):
3010
+ if self.check_cong([a, b, a, c]):
3011
+ yield a, b, c
3012
+
3013
+ def all_circles(self) -> Generator[tuple[Point, ...], None, None]:
3014
+ for l in self.type2nodes[Length]:
3015
+ p2p = defaultdict(list)
3016
+ for s in l.neighbors(Segment):
3017
+ a, b = s.points
3018
+ p2p[a].append(b)
3019
+ p2p[b].append(a)
3020
+ for p, ps in p2p.items():
3021
+ if len(ps) >= 3:
3022
+ for a, b, c in utils.perm3(ps):
3023
+ yield p, a, b, c
3024
+
3025
+ def two_points_on_direction(self, d: Direction) -> tuple[Point, Point]:
3026
+ l = d.neighbors(Line)[0]
3027
+ p1, p2 = l.neighbors(Point)[:2]
3028
+ return p1, p2
3029
+
3030
+ def two_points_of_length(self, l: Length) -> tuple[Point, Point]:
3031
+ s = l.neighbors(Segment)[0]
3032
+ p1, p2 = s.points
3033
+ return p1, p2
3034
+
3035
+
3036
+ def create_consts_str(g: Graph, s: str) -> Union[Ratio, Angle]:
3037
+ if 'pi/' in s:
3038
+ n, d = s.split('pi/')
3039
+ n, d = int(n), int(d)
3040
+ p0, _ = g.get_or_create_const_ang(n, d)
3041
+ else:
3042
+ n, d = s.split('/')
3043
+ n, d = int(n), int(d)
3044
+ p0, _ = g.get_or_create_const_rat(n, d)
3045
+ return p0
3046
+
3047
+
3048
+ def create_consts(g: Graph, p: gm.Node) -> Union[Ratio, Angle]:
3049
+ if isinstance(p, Angle):
3050
+ n, d = p.name.split('pi/')
3051
+ n, d = int(n), int(d)
3052
+ p0, _ = g.get_or_create_const_ang(n, d)
3053
+ if isinstance(p, Ratio):
3054
+ n, d = p.name.split('/')
3055
+ n, d = int(n), int(d)
3056
+ p0, _ = g.get_or_create_const_rat(n, d)
3057
+ return p0 # pylint: disable=undefined-variable
ag4masses/alphageometry/graph_test.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Unit tests for graph.py."""
17
+ import unittest
18
+
19
+ from absl.testing import absltest
20
+ import graph as gh
21
+ import numericals as nm
22
+ import problem as pr
23
+
24
+
25
+ MAX_LEVEL = 1000
26
+
27
+
28
+ class GraphTest(unittest.TestCase):
29
+
30
+ @classmethod
31
+ def setUpClass(cls):
32
+ super().setUpClass()
33
+
34
+ cls.defs = pr.Definition.from_txt_file('defs.txt', to_dict=True)
35
+ cls.rules = pr.Theorem.from_txt_file('rules.txt', to_dict=True)
36
+
37
+ # load a complex setup:
38
+ txt = 'a b c = triangle a b c; h = orthocenter a b c; h1 = foot a b c; h2 = foot b c a; h3 = foot c a b; g1 g2 g3 g = centroid g1 g2 g3 g a b c; o = circle a b c ? coll h g o' # pylint: disable=line-too-long
39
+ p = pr.Problem.from_txt(txt, translate=False)
40
+ cls.g, _ = gh.Graph.build_problem(p, GraphTest.defs)
41
+
42
+ def test_build_graph_points(self):
43
+ g = GraphTest.g
44
+
45
+ all_points = g.all_points()
46
+ all_names = [p.name for p in all_points]
47
+ self.assertCountEqual(
48
+ all_names,
49
+ ['a', 'b', 'c', 'g', 'h', 'o', 'g1', 'g2', 'g3', 'h1', 'h2', 'h3'],
50
+ )
51
+
52
+ def test_build_graph_predicates(self):
53
+ gr = GraphTest.g
54
+
55
+ a, b, c, g, h, o, g1, g2, g3, h1, h2, h3 = gr.names2points(
56
+ ['a', 'b', 'c', 'g', 'h', 'o', 'g1', 'g2', 'g3', 'h1', 'h2', 'h3']
57
+ )
58
+
59
+ # Explicit statements:
60
+ self.assertTrue(gr.check_cong([b, g1, g1, c]))
61
+ self.assertTrue(gr.check_cong([c, g2, g2, a]))
62
+ self.assertTrue(gr.check_cong([a, g3, g3, b]))
63
+ self.assertTrue(gr.check_perp([a, h1, b, c]))
64
+ self.assertTrue(gr.check_perp([b, h2, c, a]))
65
+ self.assertTrue(gr.check_perp([c, h3, a, b]))
66
+ self.assertTrue(gr.check_cong([o, a, o, b]))
67
+ self.assertTrue(gr.check_cong([o, b, o, c]))
68
+ self.assertTrue(gr.check_cong([o, a, o, c]))
69
+ self.assertTrue(gr.check_coll([a, g, g1]))
70
+ self.assertTrue(gr.check_coll([b, g, g2]))
71
+ self.assertTrue(gr.check_coll([g1, b, c]))
72
+ self.assertTrue(gr.check_coll([g2, c, a]))
73
+ self.assertTrue(gr.check_coll([g3, a, b]))
74
+ self.assertTrue(gr.check_perp([a, h, b, c]))
75
+ self.assertTrue(gr.check_perp([b, h, c, a]))
76
+
77
+ # These are NOT part of the premises:
78
+ self.assertFalse(gr.check_perp([c, h, a, b]))
79
+ self.assertFalse(gr.check_coll([c, g, g3]))
80
+
81
+ # These are automatically inferred by the graph datastructure:
82
+ self.assertTrue(gr.check_eqangle([a, h1, b, c, b, h2, c, a]))
83
+ self.assertTrue(gr.check_eqangle([a, h1, b, h2, b, c, c, a]))
84
+ self.assertTrue(gr.check_eqratio([b, g1, g1, c, c, g2, g2, a]))
85
+ self.assertTrue(gr.check_eqratio([b, g1, g1, c, o, a, o, b]))
86
+ self.assertTrue(gr.check_para([a, h, a, h1]))
87
+ self.assertTrue(gr.check_para([b, h, b, h2]))
88
+ self.assertTrue(gr.check_coll([a, h, h1]))
89
+ self.assertTrue(gr.check_coll([b, h, h2]))
90
+
91
+ def test_enumerate_colls(self):
92
+ g = GraphTest.g
93
+
94
+ for a, b, c in g.all_colls():
95
+ self.assertTrue(g.check_coll([a, b, c]))
96
+ self.assertTrue(nm.check_coll([a.num, b.num, c.num]))
97
+
98
+ def test_enumerate_paras(self):
99
+ g = GraphTest.g
100
+
101
+ for a, b, c, d in g.all_paras():
102
+ self.assertTrue(g.check_para([a, b, c, d]))
103
+ self.assertTrue(nm.check_para([a.num, b.num, c.num, d.num]))
104
+
105
+ def test_enumerate_perps(self):
106
+ g = GraphTest.g
107
+
108
+ for a, b, c, d in g.all_perps():
109
+ self.assertTrue(g.check_perp([a, b, c, d]))
110
+ self.assertTrue(nm.check_perp([a.num, b.num, c.num, d.num]))
111
+
112
+ def test_enumerate_congs(self):
113
+ g = GraphTest.g
114
+
115
+ for a, b, c, d in g.all_congs():
116
+ self.assertTrue(g.check_cong([a, b, c, d]))
117
+ self.assertTrue(nm.check_cong([a.num, b.num, c.num, d.num]))
118
+
119
+ def test_enumerate_eqangles(self):
120
+ g = GraphTest.g
121
+
122
+ for a, b, c, d, x, y, z, t in g.all_eqangles_8points():
123
+ self.assertTrue(g.check_eqangle([a, b, c, d, x, y, z, t]))
124
+ self.assertTrue(
125
+ nm.check_eqangle(
126
+ [a.num, b.num, c.num, d.num, x.num, y.num, z.num, t.num]
127
+ )
128
+ )
129
+
130
+ def test_enumerate_eqratios(self):
131
+ g = GraphTest.g
132
+
133
+ for a, b, c, d, x, y, z, t in g.all_eqratios_8points():
134
+ self.assertTrue(g.check_eqratio([a, b, c, d, x, y, z, t]))
135
+ self.assertTrue(
136
+ nm.check_eqratio(
137
+ [a.num, b.num, c.num, d.num, x.num, y.num, z.num, t.num]
138
+ )
139
+ )
140
+
141
+ def test_enumerate_cyclics(self):
142
+ g = GraphTest.g
143
+
144
+ for a, b, c, d, x, y, z, t in g.all_cyclics():
145
+ self.assertTrue(g.check_cyclic([a, b, c, d, x, y, z, t]))
146
+ self.assertTrue(nm.check_cyclic([a.num, b.num, c.num, d.num]))
147
+
148
+ def test_enumerate_midps(self):
149
+ g = GraphTest.g
150
+
151
+ for a, b, c in g.all_midps():
152
+ self.assertTrue(g.check_midp([a, b, c]))
153
+ self.assertTrue(nm.check_midp([a.num, b.num, c.num]))
154
+
155
+ def test_enumerate_circles(self):
156
+ g = GraphTest.g
157
+
158
+ for a, b, c, d in g.all_circles():
159
+ self.assertTrue(g.check_circle([a, b, c, d]))
160
+ self.assertTrue(nm.check_circle([a.num, b.num, c.num, d.num]))
161
+
162
+
163
+ if __name__ == '__main__':
164
+ absltest.main()
ag4masses/alphageometry/graph_utils.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Utilizations for graph representation.
17
+
18
+ Mainly for listing combinations and permutations of elements.
19
+ """
20
+
21
+ from geometry import Point
22
+
23
+
24
+ def _cross(elems1, elems2):
25
+ for e1 in elems1:
26
+ for e2 in elems2:
27
+ yield e1, e2
28
+
29
+
30
+ def cross(elems1, elems2):
31
+ return list(_cross(elems1, elems2))
32
+
33
+
34
+ def _comb2(elems):
35
+ if len(elems) < 2:
36
+ return
37
+ for i, e1 in enumerate(elems[:-1]):
38
+ for e2 in elems[i + 1 :]:
39
+ yield e1, e2
40
+
41
+
42
+ def comb2(elems):
43
+ return list(_comb2(elems))
44
+
45
+
46
+ def _comb3(elems):
47
+ if len(elems) < 3:
48
+ return
49
+ for i, e1 in enumerate(elems[:-2]):
50
+ for j, e2 in enumerate(elems[i + 1 : -1]):
51
+ for e3 in elems[i + j + 2 :]:
52
+ yield e1, e2, e3
53
+
54
+
55
+ def comb3(elems):
56
+ return list(_comb3(elems))
57
+
58
+
59
+ def _comb4(elems):
60
+ if len(elems) < 4:
61
+ return
62
+ for i, e1 in enumerate(elems[:-3]):
63
+ for j, e2 in enumerate(elems[i + 1 : -2]):
64
+ for e3, e4 in _comb2(elems[i + j + 2 :]):
65
+ yield e1, e2, e3, e4
66
+
67
+
68
+ def comb4(elems):
69
+ return list(_comb4(elems))
70
+
71
+
72
+ def _perm2(elems):
73
+ for e1, e2 in comb2(elems):
74
+ yield e1, e2
75
+ yield e2, e1
76
+
77
+
78
+ def perm2(elems):
79
+ return list(_perm2(elems))
80
+
81
+
82
+ def _all_4points(l1, l2):
83
+ p1s = l1.neighbors(Point)
84
+ p2s = l2.neighbors(Point)
85
+ for a, b in perm2(p1s):
86
+ for c, d in perm2(p2s):
87
+ yield a, b, c, d
88
+
89
+
90
+ def all_4points(l1, l2):
91
+ return list(_all_4points(l1, l2))
92
+
93
+
94
+ def _all_8points(l1, l2, l3, l4):
95
+ for a, b, c, d in all_4points(l1, l2):
96
+ for e, f, g, h in all_4points(l3, l4):
97
+ yield (a, b, c, d, e, f, g, h)
98
+
99
+
100
+ def all_8points(l1, l2, l3, l4):
101
+ return list(_all_8points(l1, l2, l3, l4))
102
+
103
+
104
+ def _perm3(elems):
105
+ for x in elems:
106
+ for y in elems:
107
+ if y == x:
108
+ continue
109
+ for z in elems:
110
+ if z not in (x, y):
111
+ yield x, y, z
112
+
113
+
114
+ def perm3(elems):
115
+ return list(_perm3(elems))
116
+
117
+
118
+ def _perm4(elems):
119
+ for x in elems:
120
+ for y in elems:
121
+ if y == x:
122
+ continue
123
+ for z in elems:
124
+ if z in (x, y):
125
+ continue
126
+ for t in elems:
127
+ if t not in (x, y, z):
128
+ yield x, y, z, t
129
+
130
+
131
+ def perm4(elems):
132
+ return list(_perm4(elems))
ag4masses/alphageometry/graph_utils_test.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Unit tests for graph_utils.py."""
17
+ import unittest
18
+
19
+ from absl.testing import absltest
20
+ import graph_utils as gu
21
+
22
+
23
+ class GraphUtilsTest(unittest.TestCase):
24
+
25
+ def test_cross(self):
26
+ self.assertEqual(gu.cross([], [1]), [])
27
+ self.assertEqual(gu.cross([1], []), [])
28
+ self.assertEqual(gu.cross([1], [2]), [(1, 2)])
29
+ self.assertEqual(gu.cross([1], [2, 3]), [(1, 2), (1, 3)])
30
+
31
+ e1 = [1, 2, 3]
32
+ e2 = [4, 5]
33
+ target = [(1, 4), (1, 5), (2, 4), (2, 5), (3, 4), (3, 5)]
34
+ self.assertEqual(gu.cross(e1, e2), target)
35
+
36
+ def test_comb2(self):
37
+ self.assertEqual(gu.comb2([]), [])
38
+ self.assertEqual(gu.comb2([1]), [])
39
+ self.assertEqual(gu.comb2([1, 2]), [(1, 2)])
40
+ self.assertEqual(gu.comb2([1, 2, 3]), [(1, 2), (1, 3), (2, 3)])
41
+
42
+ def test_comb3(self):
43
+ self.assertEqual(gu.comb3([]), [])
44
+ self.assertEqual(gu.comb3([1]), [])
45
+ self.assertEqual(gu.comb3([1, 2]), [])
46
+ self.assertEqual(gu.comb3([1, 2, 3]), [(1, 2, 3)])
47
+ self.assertEqual(
48
+ gu.comb3([1, 2, 3, 4]), [(1, 2, 3), (1, 2, 4), (1, 3, 4), (2, 3, 4)]
49
+ )
50
+
51
+ def test_comb4(self):
52
+ self.assertEqual(gu.comb4([]), [])
53
+ self.assertEqual(gu.comb4([1]), [])
54
+ self.assertEqual(gu.comb4([1, 2]), [])
55
+ self.assertEqual(gu.comb4([1, 2, 3]), [])
56
+ self.assertEqual(gu.comb4([1, 2, 3, 4]), [(1, 2, 3, 4)])
57
+ self.assertEqual(
58
+ gu.comb4([1, 2, 3, 4, 5]),
59
+ [(1, 2, 3, 4), (1, 2, 3, 5), (1, 2, 4, 5), (1, 3, 4, 5), (2, 3, 4, 5)],
60
+ )
61
+
62
+ def test_perm2(self):
63
+ self.assertEqual(gu.perm2([]), [])
64
+ self.assertEqual(gu.perm2([1]), [])
65
+ self.assertEqual(gu.perm2([1, 2]), [(1, 2), (2, 1)])
66
+ self.assertEqual(
67
+ gu.perm2([1, 2, 3]), [(1, 2), (2, 1), (1, 3), (3, 1), (2, 3), (3, 2)]
68
+ )
69
+
70
+ def test_perm3(self):
71
+ self.assertEqual(gu.perm3([]), [])
72
+ self.assertEqual(gu.perm3([1]), [])
73
+ self.assertEqual(gu.perm3([1, 2]), [])
74
+ self.assertEqual(
75
+ gu.perm3([1, 2, 3]),
76
+ [(1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1)],
77
+ )
78
+ self.assertEqual(
79
+ gu.perm3([1, 2, 3, 4]),
80
+ [
81
+ (1, 2, 3),
82
+ (1, 2, 4),
83
+ (1, 3, 2),
84
+ (1, 3, 4),
85
+ (1, 4, 2),
86
+ (1, 4, 3),
87
+ (2, 1, 3),
88
+ (2, 1, 4),
89
+ (2, 3, 1),
90
+ (2, 3, 4),
91
+ (2, 4, 1),
92
+ (2, 4, 3),
93
+ (3, 1, 2),
94
+ (3, 1, 4),
95
+ (3, 2, 1),
96
+ (3, 2, 4),
97
+ (3, 4, 1),
98
+ (3, 4, 2),
99
+ (4, 1, 2),
100
+ (4, 1, 3),
101
+ (4, 2, 1),
102
+ (4, 2, 3),
103
+ (4, 3, 1),
104
+ (4, 3, 2),
105
+ ],
106
+ )
107
+
108
+ def test_perm4(self):
109
+ self.assertEqual(gu.perm3([]), [])
110
+ self.assertEqual(gu.perm3([1]), [])
111
+ self.assertEqual(gu.perm3([1, 2]), [])
112
+ self.assertEqual(gu.perm4([1, 2, 3]), [])
113
+ self.assertEqual(
114
+ gu.perm4([1, 2, 3, 4]),
115
+ [
116
+ (1, 2, 3, 4),
117
+ (1, 2, 4, 3),
118
+ (1, 3, 2, 4),
119
+ (1, 3, 4, 2),
120
+ (1, 4, 2, 3),
121
+ (1, 4, 3, 2), # pylint: disable=line-too-long
122
+ (2, 1, 3, 4),
123
+ (2, 1, 4, 3),
124
+ (2, 3, 1, 4),
125
+ (2, 3, 4, 1),
126
+ (2, 4, 1, 3),
127
+ (2, 4, 3, 1), # pylint: disable=line-too-long
128
+ (3, 1, 2, 4),
129
+ (3, 1, 4, 2),
130
+ (3, 2, 1, 4),
131
+ (3, 2, 4, 1),
132
+ (3, 4, 1, 2),
133
+ (3, 4, 2, 1), # pylint: disable=line-too-long
134
+ (4, 1, 2, 3),
135
+ (4, 1, 3, 2),
136
+ (4, 2, 1, 3),
137
+ (4, 2, 3, 1),
138
+ (4, 3, 1, 2),
139
+ (4, 3, 2, 1),
140
+ ], # pylint: disable=line-too-long
141
+ )
142
+
143
+
144
+ if __name__ == '__main__':
145
+ absltest.main()
ag4masses/alphageometry/imo_ag_30.txt ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ translated_imo_2000_p1
2
+ a b = segment a b; g1 = on_tline g1 a a b; g2 = on_tline g2 b b a; m = on_circle m g1 a, on_circle m g2 b; n = on_circle n g1 a, on_circle n g2 b; c = on_pline c m a b, on_circle c g1 a; d = on_pline d m a b, on_circle d g2 b; e = on_line e a c, on_line e b d; p = on_line p a n, on_line p c d; q = on_line q b n, on_line q c d ? cong e p e q
3
+ translated_imo_2000_p6
4
+ a b c = triangle a b c; h = orthocenter h a b c; t1 t2 t3 i = incenter2 t1 t2 t3 i a b c; h1 = foot h1 a b c; h2 = foot h2 b c a; h3 = foot h3 c a b; x1 = reflect x1 h1 t1 t2; x2 = reflect x2 h2 t1 t2; y2 = reflect y2 h2 t2 t3; y3 = reflect y3 h3 t2 t3; z = on_line z x1 x2, on_line z y2 y3 ? cong i z i t1
5
+ translated_imo_2002_p2a
6
+ b c = segment b c; o = midpoint o b c; a = on_circle a o b; d = on_circle d o b, on_bline d a b; e = on_bline e o a, on_circle e o b; f = on_bline f o a, on_circle f o b; j = on_pline j o a d, on_line j a c ? eqangle e c e j e j e f
7
+ translated_imo_2002_p2b
8
+ b c = segment b c; o = midpoint o b c; a = on_circle a o b; d = on_circle d o b, on_bline d a b; e = on_bline e o a, on_circle e o b; f = on_bline f o a, on_circle f o b; j = on_pline j o a d, on_line j a c ? eqangle c e c j c j c f
9
+ translated_imo_2003_p4
10
+ a b c = triangle a b c; o = circle o a b c; b1 = on_circle b1 o a, on_bline b1 c a; d1 = on_circle d1 o a, on_bline d1 c a; x = on_line x b b1, on_line x a c; d = on_line d d1 x, on_circle d o a; p = foot p d b c; q = foot q d c a; r = foot r d a b ? cong p q q r
11
+ translated_imo_2004_p1
12
+ a b c = triangle a b c; o = midpoint o b c; m = on_circle m o b, on_line m a b; n = on_circle n o b, on_line n a c; r = angle_bisector r b a c, angle_bisector r m o n; o1 = circle o1 b m r; o2 = circle o2 c n r; p = on_circle p o1 r, on_circle p o2 r ? coll p b c
13
+ translated_imo_2004_p5
14
+ a b c = triangle a b c; o = circle o a b c; d = on_circle d o a; p = on_aline p b c a b d, on_aline p d c a d b ? cong a p c p
15
+ translated_imo_2005_p5
16
+ a b c = triangle a b c; d = eqdistance d a b c; e = on_line e b c; f = on_line f a d, eqdistance f d e b; p = on_line p a c, on_line p b d; q = on_line q e f, on_line q b d; r = on_line r e f, on_line r a c; o1 = circle o1 a p d; o2 = circle o2 b p c; m = on_circle m o1 p, on_circle m o2 p ? cyclic p q r m
17
+ translated_imo_2007_p4
18
+ a b c = triangle a b c; o = circle o a b c; r = on_circle r o a, on_bline r a b; l = midpoint l c a; k = midpoint k c b; p = on_line p o k, on_line p c r; q = on_line q o l, on_line q c r; l1 = foot l1 l c r; k1 = foot k1 k c r ? eqratio k k1 l l1 r q r p
19
+ translated_imo_2008_p1a
20
+ a b c = triangle a b c; h = orthocenter h a b c; d = midpoint d b c; e = midpoint e a c; f = midpoint f a b; a1 = on_circle a1 d h, on_line a1 b c; a2 = on_circle a2 d h, on_line a2 b c; b1 = on_circle b1 e h, on_line b1 c a; b2 = on_circle b2 e h, on_line b2 c a; c1 = on_circle c1 f h, on_line c1 a b; c2 = on_circle c2 f h, on_line c2 a b ? cyclic c1 c2 b1 b2
21
+ translated_imo_2008_p1b
22
+ a b c = triangle a b c; h = orthocenter h a b c; d = midpoint d b c; e = midpoint e a c; f = midpoint f a b; a1 = on_circle a1 d h, on_line a1 b c; a2 = on_circle a2 d h, on_line a2 b c; b1 = on_circle b1 e h, on_line b1 c a; b2 = on_circle b2 e h, on_line b2 c a; c1 = on_circle c1 f h, on_line c1 a b; c2 = on_circle c2 f h, on_line c2 a b ? cyclic c1 c2 b1 a1
23
+ translated_imo_2008_p6
24
+ x@4.96_-0.13 y@-1.0068968328888160_-1.2534881080682770 z@-2.8402847238575120_-4.9117762734006830 = triangle x y z; o = circle o x y z; w@6.9090049230038776_-1.3884003936987552 = on_circle w o x; a = on_tline a z o z, on_tline a x o x; b = on_tline b z o z, on_tline b w o w; c = on_tline c y o y, on_tline c w o w; d = on_tline d x o x, on_tline d y o y; i1 = incenter i1 a b c; i2 = incenter i2 a c d; f1 = foot f1 i1 a c; f2 = foot f2 i2 a c; q t p s = cc_tangent q t p s i1 f1 i2 f2; k = on_line k q t, on_line k p s ? cong o k o x
25
+ translated_imo_2009_p2
26
+ m l k = triangle m l k; w = circle w m l k; q = on_tline q m w m; p = mirror p q m; b = mirror b p k; c = mirror c q l; a = on_line a b q, on_line a c p; o = circle o a b c ? cong o p o q
27
+ translated_imo_2010_p2
28
+ a b c = triangle a b c; o = circle o a b c; i = incenter i a b c; d = on_line d a i, on_circle d o a; f = on_line f b c; e = on_aline e a c b a f, on_circle e o a; g = midpoint g i f; k = on_line k d g, on_line k e i ? cong o a o k
29
+ translated_imo_2010_p4
30
+ s c p = iso_triangle s c p; o = on_tline o c s c; a = on_circle a o c; b = on_circle b o c, on_line b s a; m = on_line m c p, on_circle m o c; l = on_line l b p, on_circle l o c; k = on_line k a p, on_circle k o c ? cong m k m l
31
+ translated_imo_2011_p6
32
+ a b c = triangle a b c; o = circle o a b c; p = on_circle p o a; q = on_tline q p o p; pa = reflect pa p b c; pb = reflect pb p c a; pc = reflect pc p a b; qa = reflect qa q b c; qb = reflect qb q c a; qc = reflect qc q a b; a1 = on_line a1 pb qb, on_line a1 pc qc; b1 = on_line b1 pa qa, on_line b1 pc qc; c1 = on_line c1 pa qa, on_line c1 pb qb; o1 = circle o1 a1 b1 c1; x = on_circle x o a, on_circle x o1 a1 ? coll x o o1
33
+ translated_imo_2012_p1
34
+ a b c = triangle a b c; m l k j = excenter2 m l k j a b c; f = on_line f m l, on_line f b j; g = on_line g m k, on_line g c j; s = on_line s f a, on_line s b c; t = on_line t g a, on_line t c b ? cong m s m t
35
+ translated_imo_2012_p5
36
+ c a b = r_triangle c a b; d = foot d c a b; x = on_line x c d; k = on_line k a x, on_circle k b c; l = on_line l b x, on_circle l a c; m = on_line m a l, on_line m b k ? cong m k m l
37
+ translated_imo_2013_p4
38
+ a b c = triangle a b c; h = orthocenter h a b c; m = on_line m h b, on_line m a c; n = on_line n h c, on_line n a b; w = on_line w b c; o1 = circle o1 b n w; o2 = circle o2 c m w; x = on_line x o1 w, on_circle x o1 w; y = on_line y o2 w, on_circle y o2 w ? coll x h y
39
+ translated_imo_2014_p4
40
+ a b c = triangle a b c; p = on_line p b c, on_aline p a b b c a; q = on_line q b c, on_aline q a c c b a; m = mirror m a p; n = mirror n a q; x = on_line x b m, on_line x c n; o = circle o a b c ? cong o x o a
41
+ translated_imo_2015_p3
42
+ a b c = triangle a b c; h = orthocenter h a b c; f = on_line f h a, on_line f b c; m = midpoint m b c; o = circle o a b c; q = on_dia q a h, on_circle q o a; k = on_dia k h q, on_circle k o a; o1 = circle o1 k q h; o2 = circle o2 f k m ? coll o1 o2 k
43
+ translated_imo_2015_p4
44
+ a b c = triangle a b c; o = circle o a b c; d = on_line d b c; e = on_line e b c, on_circle e a d; f = on_circle f o a, on_circle f a d; g = on_circle g o a, on_circle g a d; o1 = circle o1 f b d; o2 = circle o2 g c e; k = on_circle k o1 b, on_line k a b; l = on_circle l o2 c, on_line l a c; x = on_line x f k, on_line x l g ? coll x o a
45
+ translated_imo_2016_p1
46
+ a b z = triangle a b z; f = angle_bisector f b a z, on_bline f a b; c = on_tline c b f b, on_line c a f; d = on_line d a z, on_bline d a c; e = angle_mirror e c a d, on_bline e a d; m = midpoint m c f; x = parallelogram e a m x; y = on_line y f x, on_line y e m ? coll y b d
47
+ translated_imo_2017_p4
48
+ r s = segment r s; t = mirror t r s; o = on_bline o r s; j = on_circle j o s; o1 = circle o1 j s t; a = on_tline a r o r, on_circle a o1 s; b = on_tline b r o r, on_circle b o1 s; k = on_line k j a, on_circle k o s ? perp k t o1 t
49
+ translated_imo_2018_p1
50
+ a b c = triangle a b c; o = circle o a b c; d = on_line d a b; e = on_line e a c, on_circle e a d; f = on_bline f b d, on_circle f o a; g = on_bline g e c, on_circle g o a ? para d e f g
51
+ translated_imo_2019_p2
52
+ a b c = triangle; a1 = on_line b c; b1 = on_line a c; p = on_line a a1; q = on_line b b1, on_pline p a b; p1 = on_line p b1, eqangle3 p c a b c; q1 = on_line q a1, eqangle3 c q b c a ? cyclic p q p1 q1
53
+ translated_imo_2019_p6
54
+ a b c = triangle a b c; d e f i = incenter2 d e f i a b c; r = on_tline r d e f, on_circle r i d; p = on_line p r a, on_circle p i d; o1 = circle o1 p c e; o2 = circle o2 p b f; q = on_circle q o1 p, on_circle q o2 p; t = on_line t p q, on_line t i d ? perp a t a i
55
+ translated_imo_2020_p1
56
+ p a b = triangle p a b; x = angle_bisector p b a; y = angle_bisector p a b; z = on_aline z a p a b x; t = on_aline t p a p a z; d = on_aline d p t p b a, on_line a z; u = on_aline u b p b a y; v = on_aline v p b p b u; c = on_aline c p v p a b, on_line b u; o = angle_bisector a d p, angle_bisector p c b ? cong o a o b
57
+ translated_imo_2021_p3
58
+ a b c = triangle; d = angle_bisector b a c; e = on_aline d a d c b, on_line a c; f = on_aline d a d b c, on_line a b; x = on_bline b c, on_line a c; o1 = circle a d c; o2 = circle e x d; y = on_line e f, on_line b c ? coll o1 o2 y
59
+ translated_imo_2022_p4
60
+ b c = segment; d = free; e = eqdistance d b c; t = on_bline b d, on_bline c e; a = eqangle2 b t e; p = on_line a b, on_line c d; q = on_line a b, on_line c t; r = on_line a e, on_line c d; s = on_line a e, on_line d t ? cyclic p q r s
ag4masses/alphageometry/jgex_ag_231.txt ADDED
@@ -0,0 +1,462 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ examples/complete2/012/complete_004_6_GDD_FULL_81-109_101.gex
2
+ a b c = triangle a b c; o = circle o a b c; h = midpoint h c b; d = on_line d o h, on_line d a b; e = on_tline e c c o, on_tline e a a o ? cyclic a o e d
3
+ examples/complete2/012/complete_002_6_GDD_FULL_41-60_59.gex
4
+ a b c = triangle a b c; m = midpoint m b a; o = circle o a b c; n = on_line n o m, on_circle n o a ? eqangle c a c n c n c b
5
+ examples/complete2/012/complete_002_6_GDD_FULL_01-20_04.gex
6
+ a b c = triangle a b c; o = circle o a b c; d = on_circle d o a; q = midpoint q c b; s = midpoint s a d; j = midpoint j s q; m = mirror m o j; i = on_line i a d, on_line i b c ? perp s m b c
7
+ examples/complete2/012/complete_004_6_GDD_FULL_81-109_90.gex
8
+ a b c = triangle a b c; o = circle o a b c; d = on_circle d o a; g = foot g d a b; f = foot f d a c; c1 = on_circle c1 o d, on_line c1 d g; b1 = on_circle b1 o d, on_line b1 d f ? para c1 c b1 b
9
+ examples/complete2/012/complete_004_6_GDD_FULL_81-109_94.gex
10
+ a b c = triangle a b c; o = circle o a b c; d = on_circle d o a; p = on_circle p o a; f = foot f p a d; g = foot g p a b; h = foot h p b c; e = foot e p c d; i = on_line i f g, on_line i h e ? cyclic p g i h
11
+ examples/complete2/012/complete_003_6_GDD_FULL_21-40_37.gex
12
+ a b c = triangle a b c; h = orthocenter h a b c; o = circle o a b c; c1 = on_circle c1 o c, on_line c1 c h; a1 = on_circle a1 o a, on_line a1 a h ? cong b a1 b c1
13
+ examples/complete2/012/complete_003_6_GDD_FULL_21-40_22.gex
14
+ a b c = triangle a b c; o = circle o a b c; p = foot p o a c; q = foot q o a b; m = on_line m o q, on_circle m o a; n = on_line n o p, on_circle n o a; e = on_line e a c, on_line e n m; d = on_line d a b, on_line d n m ? eqangle d a d e e d e a
15
+ examples/complete2/012/complete_001_6_GDD_FULL_01-20_19.gex
16
+ a b c = triangle a b c; f = free f; p = circle p a b f; o = circle o a b c; e = on_circle e p a, on_line e a c; d = on_circle d o b, on_line d b f ? para c d e f
17
+ examples/complete2/012/complete_001_6_GDD_FULL_61-80_74.gex
18
+ a b c = triangle a b c; g = foot g c a b; o = circle o a b c; d = on_circle d o c, on_line d c g; e = foot e d a c; f = foot f d b c ? cyclic a e f b
19
+ examples/complete2/013/complete_002_6_GDD_FULL_41-60_49.gex
20
+ a b c = triangle a b c; p = midpoint p b a; q = midpoint q c b; d = on_tline d b a c; r = midpoint r d c; s = midpoint s a d; o = on_line o p r, on_line o q s ? cong o s o r
21
+ examples/complete2/013/complete_006_Other_ndgTest_70.gex
22
+ p a b = triangle p a b; o = midpoint o b a; a1 = on_line a1 p a, on_circle a1 o a; b1 = on_line b1 p b, on_circle b1 o a; o1 = circle o1 p a1 b1 ? perp o a1 a1 o1
23
+ examples/complete2/013/complete_001_6_GDD_FULL_01-20_16.gex
24
+ a b o = triangle a b o; m = on_line m a b; p = foot p m a o; q = foot q m b o; d = foot d b a o; c = foot c a b o; t = foot t q a o; k = foot k p b o; s = on_line s q t, on_line s p k ? perp o s p q
25
+ examples/complete2/013/complete_001_6_GDD_FULL_61-80_67.gex
26
+ m b c = triangle m b c; i = incenter i m b c; i_b = on_tline i_b c c i, on_line i_b b i; i_c = on_tline i_c b b i, on_line i_c c i; a = midpoint a i_b i_c; o = circumcenter o b i c ? perp a b b o
27
+ examples/complete2/013/complete_000_2_PWW_A018.gex
28
+ o a = segment o a; p = on_circle p a o; q = intersection_cc q a o p; r = lc_tangent r p a, on_circle r o p ? cong p q p r
29
+ examples/complete2/013/complete_004_6_GDD_FULL_81-109_88.gex
30
+ o x l = triangle o x l; a = foot a l o x; y = free y; b = foot b l o y; p = mirror p l a; q = mirror q l b; q1 = on_line q1 l a, on_line q1 o y; p1 = on_line p1 o x, on_line p1 l b ? cyclic o p q p1
31
+ examples/complete2/013/complete_003_6_GDD_FULL_21-40_24.gex
32
+ q r p = triangle q r p; o1 = circle o1 q r p; s = on_circle s o1 q; y = on_line y q s; o = circle o y p q; x = on_circle x o q; i = on_line i r s, on_line i y x ? eqangle i r i x p r p x
33
+ examples/complete2/013/complete_003_6_GDD_FULL_21-40_32.gex
34
+ b c r = triangle b c r; o = circle o b c r; s = on_circle s o b; a = on_line a b r, on_line a c s; m = foot m a r s; n = foot n a b c ? eqangle a b a m a n a c
35
+ examples/complete2/013/complete_002_6_GDD_FULL_41-60_54.gex
36
+ a b c = r_triangle a b c; d = foot d a b c; o = midpoint o c b; m = foot m b a o; g = on_line g b m, on_circle g o a; f = on_line f c a, on_line f b m; e = on_line e a d, on_line e b m ? cong e a e b
37
+ examples/complete2/013/complete_005_Other_ndg1_53.gex
38
+ a o = segment a o; b = on_circle b o a; c = on_line c a b; e = intersection_tt e b b o c c o; d = intersection_lt d c e a a o ? cong o e o d
39
+ examples/complete2/013/complete_002_6_GDD_FULL_41-60_56.gex
40
+ m a b = iso_triangle m a b; o = circle o a b m; d = on_line d m o, on_line d a b; e = on_tline e a a o, on_pline e m a o ? cong m e m d
41
+ examples/complete2/013/complete_002_6_GDD_FULL_41-60_52.gex
42
+ e c d = r_triangle e c d; o = midpoint o d c; a = on_tline a c c d, on_tline a e e o; f = on_line f c a, on_line f d e ? cong a e a f
43
+ examples/complete2/014/complete_008_7_Book_LLL_L053-1.gex
44
+ b c d = triangle b c d; e = foot e b c d; a = free a; f = foot f a c d; g = midpoint g b a ? cong f g g e
45
+ examples/complete2/014/complete_007_7_Book_LLL_L058-9.gex
46
+ a b = segment a b; c = on_bline c a b; d = on_tline d b a b; e = intersection_lt e c d a a b ? cong e c c d
47
+ examples/complete2/007/complete_003_6_GDD_FULL_more_E015-6.gex
48
+ a b c = triangle a b c; d = midpoint d a c; e = midpoint e b a; f = midpoint f c b; g = on_pline g d a f, on_pline g f a c ? para c e g b
49
+ examples/complete2/007/complete_003_6_GDD_FULL_more_E022-9.gex
50
+ a b c = triangle a b c; d = circumcenter d b a c; e = on_line e a c, angle_bisector e c b a; f = intersection_lc f e d b; g = on_tline g f d f ? para f g a c
51
+ examples/complete2/007/complete_012_7_Book_00EE_02_E028-2.gex
52
+ a b c = triangle a b c; d e = square a c d e; g f = square c b g f ? perp d b f a
53
+ examples/complete2/007/complete_012_7_Book_00EE_05_E051-22.gex
54
+ a b = segment a b; c = midpoint c b a; d = on_tline d b a b; e = on_line e a d, on_circle e c a; f = on_pline f e a b, on_circle f c e; g = foot g d a f ? cong a f f g
55
+ examples/complete2/007/complete_005_Other_other_E075-25-sss.gex
56
+ a b c = triangle a b c; d = midpoint d c b; e = midpoint e a c; f = midpoint f b a; g = foot g a b c ? eqangle d f d e g f g e
57
+ examples/complete2/007/complete_001_6_GDD_FULL_01-20_01.gex
58
+ a b c = triangle a b c; d = foot d c a b; e = foot e b a c; f = midpoint f c b; g = midpoint g d e ? perp f g d e
59
+ examples/complete2/007/complete_000_2_PWW_B016x.gex
60
+ a b c = triangle a b c; d = midpoint d b c; e = midpoint e c a; f = midpoint f b a; g = parallelogram d a e g ? cong c f g b
61
+ examples/complete2/007/complete_001_6_GDD_FULL_61-80_66.gex
62
+ a b c = triangle a b c; d = foot d c a b; e = on_tline e a b c, on_line e c d; f = midpoint f a e; g = midpoint g c b ? perp d g d f
63
+ examples/complete2/007/complete_016_7_Book_00EE_06_E051-30.gex
64
+ a b = segment a b; c = midpoint c b a; d = on_circle d c a; e = lc_tangent e d c, on_line e a b; f = angle_bisector f d e a, on_line f a d; g = on_line g e f, on_line g b d ? cong d f d g
65
+ examples/complete2/007/complete_016_7_Book_00EE_06_E051-24.gex
66
+ a b = segment a b; c = midpoint c b a; d = on_circle d c a; e = on_line e b d; f = circle f d c e; g = on_pline g e a b, on_circle g f c ? cong g e c b
67
+ examples/complete2/007/complete_013_7_Book_00EE_11_E077-37.gex
68
+ a b = segment a b; c = midpoint c b a; d = on_circle d c b; e = foot e d a b; f = lc_tangent f d c; g = on_line g d f ? eqangle d f d a d a d e
69
+ examples/complete2/007/complete_013_7_Book_00EE_07_E059-54-1.gex
70
+ a b = segment a b; c = midpoint c b a; d = mirror d c b; e = on_circle e c a, on_circle e b c; f = on_tline f b a b, on_line f a e; g = on_line g b f, on_line g d e ? cong e g g f
71
+ examples/complete2/007/complete_008_ex-gao_ex160_e201f.gex
72
+ a b c = triangle a b c; d e = square a b d e; f = foot f a b c; g = on_line g a f, eqdistance g a b c ? perp g b c d
73
+ examples/complete2/000/complete_016_ex-gao_gao_M_M020-52.gex
74
+ a b = segment a b; c = on_circle c a b; d = on_circle d a b; e = on_circle e a b, on_pline e d b c ? cong d c e b
75
+ examples/complete2/000/complete_010_Other_gao_L_L190-7.gex
76
+ a b = segment a b; c = nsquare c b a; d = psquare d a b; e = on_line e b d; f = foot f e b c; g = foot g e d c ? cong e a g f
77
+ examples/complete2/000/complete_007_7_Book_LLL_L017-11.gex
78
+ c a = segment c a; b = on_tline b c c a; d = foot d c a b ? eqangle a c a d c b c d
79
+ examples/complete2/000/complete_016_ex-gao_gao_M_M024-94.gex
80
+ a b c = triangle a b c; d = on_circle d a b, on_circle d c b; e = on_line e d a, on_circle e a d; f = on_line f d c, on_circle f c d ? coll e b f
81
+ examples/complete2/000/complete_007_7_Book_LLL_L054-2-1.gex
82
+ a b = segment a b; c = on_bline c a b; d = on_line d a c; e = eqdistance e b a d, on_line e b c; f = on_line f a b, on_line f d e; g = on_line g b c, on_pline g d a b ? cong d f f e
83
+ examples/complete2/000/complete_007_7_Book_LLL_L057-1-1.gex
84
+ a b = segment a b; m = midpoint m a b; c = on_circle c m a; d = angle_mirror d a b c; e = midpoint e a d; f = on_line f b d, on_line f a c ? para c e b d
85
+ examples/complete2/000/complete_016_ex-gao_gao_M_M021-64.gex
86
+ a b = segment a b; d = midpoint d b a; c = on_circle c b a, on_circle c a b; e = on_line e a c, on_circle e d a; f = on_circle f d a, on_line f b c ? cong a e e f
87
+ examples/complete2/000/complete_007_7_Book_LLL_L057-3-2.gex
88
+ a b = segment a b; c = on_bline c a b; e = midpoint e a c; d = on_circle d a c, on_line d a c; f = midpoint f b d ? cong b e b f
89
+ examples/complete2/000/complete_004_6_GDD_FULL_81-109_95.gex
90
+ a b c = triangle a b c; a1 = midpoint a1 c b; f = circle f a b c; s = on_aline s a b c a a1, on_line s b c; p = on_circle p f a, on_line p a a1; q = on_circle q f a, on_line q a s ? para b c p q
91
+ examples/complete2/000/complete_001_6_GDD_FULL_01-20_02.gex
92
+ a b c = triangle a b c; a1 = midpoint a1 c b; b1 = midpoint b1 c a; c1 = midpoint c1 b a; o = circle o a b c ? perp o a1 b1 c1
93
+ examples/complete2/000/complete_004_6_GDD_FULL_81-109_96.gex
94
+ a b c = triangle a b c; a1 = midpoint a1 c b; n = on_line n a a1; g = foot g n a b; h = foot h n a c; s = on_aline s a b c a a1, on_line s b c ? perp g h a s
95
+ examples/complete2/000/complete_007_7_Book_LLL_L194-2.gex
96
+ a b = segment a b; c = on_bline c a b; d = on_bline d a b; e = on_line e c d, on_line e a b ? cong a e e b
97
+ examples/complete2/000/complete_017_ex-gao_gao_L_L022-1.gex
98
+ a b = segment a b; c = on_bline c a b; d = on_line d a c; e = on_circle e c d, on_line e b c ? cong a e b d
99
+ examples/complete2/000/complete_016_ex-gao_gao_M_M09-14.gex
100
+ c b = segment c b; d = midpoint d c b; a = free a; e = midpoint e b a; f = midpoint f c a; g = on_line g a d, on_line g e f ? cong e g g f
101
+ examples/complete2/009/complete_014_7_Book_00EE_09_E071-4.gex
102
+ a b = segment a b; c = midpoint c b a; d = on_circle d c a; e = lc_tangent e d c, on_line e a b; f = foot f a d e ? eqangle a f a d a d a b
103
+ examples/complete2/009/complete_013_7_Book_00EE_10_E072-13.gex
104
+ a b c = triangle a b c; d = foot d b a c; e = foot e a b c; f = foot f b d e ? eqangle b a b d b c b f
105
+ examples/complete2/009/complete_014_7_Book_00EE_09_E071-2.gex
106
+ a b c = triangle a b c; d = midpoint d c b; e = foot e b a c; f = foot f c a b ? eqangle a b a c e f e d
107
+ examples/complete2/009/complete_014_7_Book_00EE_09_E071-1.gex
108
+ a b c = triangle a b c; e = on_line e a b, on_circle e a c; d = angle_bisector d b a c, on_line d b c; f = on_pline f e b c, on_line f a c ? eqangle e d e c e c e f
109
+ examples/complete2/009/complete_017_ex-gao_ex160_4_e10.gex
110
+ a b c d = isquare a b c d; e = on_line e b d, on_circle e b c; f = on_tline f e b d, on_line f d c ? cong e d c f
111
+ examples/complete2/009/complete_003_6_GDD_FULL_more_E022-12.gex
112
+ a b c = triangle a b c; e = circumcenter e a b c; d = on_line d a b, angle_bisector d a c b; f = on_tline f c c e, on_pline f d a c ? cong c f d b
113
+ examples/complete2/009/complete_001_6_GDD_FULL_61-80_69.gex
114
+ d a b = r_triangle d a b; c = midpoint c b a; e = circle e a c d; f = circle f b d c ? perp e d d f
115
+ examples/complete2/009/complete_012_7_Book_00EE_05_E051-19.gex
116
+ a b c = triangle a b c; d = circumcenter d a c b; e = on_line e b c; f = on_circle f d a, angle_bisector f a c e ? cong a f f b
117
+ examples/complete2/009/complete_016_7_Book_00EE_06_E051-32.gex
118
+ a b c = triangle a b c; d = eq_triangle d a b; e = eq_triangle e a c; f = eq_triangle f c b ? para e d c f
119
+ examples/complete2/009/complete_013_7_Book_00EE_10_E074-23.gex
120
+ a b c = triangle a b c; d = foot d a b c; e = circumcenter e b a c; f = angle_bisector f b a c, on_circle f e a ? eqangle e a a f f a a d
121
+ examples/complete2/009/complete_011_Other_Auxiliary_aux2_trapezoid.gex
122
+ a b c d = trapezoid a b c d; e = midpoint e d a; f = on_pline f e a b, on_line f b c ? midp f b c
123
+ examples/complete2/009/complete_016_7_Book_00EE_06_E057-37.gex
124
+ a b c = triangle a b c; d = eq_triangle d a b; e = eq_triangle e a c; f = parallelogram c e d f ? cong b f f c
125
+ examples/complete2/008/complete_004_6_GDD_FULL_81-109_100.gex
126
+ a c = segment a c; b = eq_triangle b c a; e = mirror e c b; d = mirror d b e; f = foot f d a b ? perp a c c f
127
+ examples/complete2/008/complete_005_Other_ndgs_02.gex
128
+ b a c = triangle b a c; d = foot d b a c; e = foot e c a b; f = intersection_ll f b d c e ? perp b c a f
129
+ examples/complete2/008/complete_008_ex-gao_ex160_205.gex
130
+ c a b = r_triangle c a b; d = midpoint d c a; f = midpoint f c b; e = on_line e a b, on_circle e d c ? perp d e e f
131
+ examples/complete2/008/complete_015_7_Book_00EE_08_E061-62.gex
132
+ a b = segment a b; c = on_circle c a b; e = on_circle e a b; d = on_circle d a b, on_circle d b c; f = on_circle f b c, on_line f c e ? cong e d e f
133
+ examples/complete2/008/complete_015_7_Book_00EE_06_E051-31.gex
134
+ a b c = triangle a b c; d = parallelogram a b c d; e = eq_triangle e a b; f = eq_triangle f b c ? cong d e d f
135
+ examples/complete2/008/complete_011_7_Book_00EE_03_E037-22.gex
136
+ c a b = risos c a b; e = midpoint e b a; d = on_line d a b, on_circle d b c; f = on_line f a c, on_circle f c e ? perp a c f d
137
+ examples/complete2/008/complete_011_7_Book_00EE_03_E037-21.gex
138
+ a b = segment a b; c = on_circle c a b; d = lc_tangent d c a, on_line d a b; e = on_line e a b, on_circle e a b; f = on_pline f a c e, on_line f c d ? perp f b a b
139
+ examples/complete2/008/complete_011_7_Book_00EE_04_E051-5.gex
140
+ c a = segment c a; b = eq_triangle b c a; d = circumcenter d c a b; e = on_pline e d a c, on_line e a b; f = on_pline f d b c, on_line f a b ? cong a e e f
141
+ examples/complete2/008/complete_003_6_GDD_FULL_more_E009-1.gex
142
+ a c = segment a c; b = on_tline b c a c; d = on_dia d b a, on_circle d a c; e = on_line e b c, on_circle e a b; f = on_line f b d, on_circle f a b ? para c d e f
143
+ examples/complete2/008/complete_011_7_Book_00EE_03_E039-28.gex
144
+ a b = segment a b; c = on_circle c a b; d = on_circle d a b, on_circle d c b; e = mirror e d c; f = on_circle f a b, on_line f b e ? coll d a f
145
+ examples/complete2/008/complete_011_7_Book_00EE_03_E040-28-1.gex
146
+ c a b = iso_triangle c a b; d = on_line d b c; e = circle e a b d; f = on_circle f e a, on_line f a c ? para a b f d
147
+ examples/complete2/008/complete_018_ex-gao_ex160_4_004.gex
148
+ b a c = triangle b a c; d = on_line d b c, on_circle d a b; e = on_tline e c a c, on_tline e b a b; f = on_tline f d a d, on_line f c e ? cong e c c f
149
+ examples/complete2/008/complete_014_7_Book_00EE_07_E059-50.gex
150
+ a b = segment a b; c = on_circle c a b; d = on_circle d a b; e = circle e c a d; f = on_line f b c, on_circle f e a ? cong d f f b
151
+ examples/complete2/008/complete_013_7_Book_00EE_07_E057-44.gex
152
+ c a b = iso_triangle c a b; d = foot d a b c; e = foot e b a c; f = on_line f a d, on_line f b e ? cong f a f b
153
+ examples/complete2/001/complete_006_7_Book_LLL_L046-16.gex
154
+ a b = segment a b; c = on_line c a b; d = on_circle d c a, on_circle d a c; e = on_aline e b a d c a, on_aline e c a d a b; f = on_line f c d, on_line f a e; g = on_line g b d, on_line g c e ? cong c f c g
155
+ examples/complete2/001/complete_016_ex-gao_gao_M_M010-32.gex
156
+ b c a = triangle b c a; d = on_pline d a b c, on_pline d c a b; e = on_line e b c; f = on_line f a d, on_pline f e a b; g = on_line g a e, on_line g b f; h = on_line h c f, on_line h d e ? para h g d a
157
+ examples/complete2/001/complete_016_ex-gao_gao_M_M010-26.gex
158
+ b d = segment b d; e = midpoint e b d; c = free c; a = on_pline a d b c, on_pline a b d c; f = on_line f c d; g = on_line g a b, on_line g e f; h = on_line h e f, on_line h a d; i = on_line i b c, on_line i e f ? cong f h g i
159
+ examples/complete2/001/complete_016_ex-gao_gao_C_C101.gex
160
+ a b c = triangle a b c; e = foot e a b c; f = foot f c a b; d = on_bline d a c, on_bline d a b; g = on_line g c f, on_line g a e; h = on_line h c f, on_circle h d c ? cong g f f h
161
+ examples/complete2/001/complete_016_ex-gao_gao_C_C100.gex
162
+ a c = segment a c; b = on_tline b c c a; e = on_circle e b c; d = on_circle d a c, on_circle d b c; f = on_line f c e, on_circle f a c; g = on_line g e b, on_circle g b e ? coll d f g
163
+ examples/complete2/001/complete_016_ex-gao_gao_L_L182-6.gex
164
+ a b c = triangle a b c; e = midpoint e b c; d = on_line d a b; f = midpoint f d c; g = midpoint g b a; h = midpoint h g f; i = on_line i a b, on_line i e h ? cong a i i d
165
+ examples/complete2/001/complete_016_ex-gao_gao_C_C111.gex
166
+ a d c = triangle a d c; b = on_pline b a d c; e = on_line e a d; f = on_line f a c, on_pline f e a b; g = on_line g b d, on_line g e f; h = on_line h b c, on_line h e f ? cong e f g h
167
+ examples/complete2/001/complete_016_ex-gao_gao_L_L025-5.gex
168
+ a b = segment a b; c = on_bline c a b; d = on_line d a c; e = on_circle e c d, on_line e b c; f = on_line f b d, on_line f a e ? eqangle a c c f f c c b
169
+ examples/complete2/001/complete_017_ex-gao_gao_L_L189-2.gex
170
+ a b = segment a b; c = on_bline c a b; e = midpoint e c a; f = midpoint f b c; d = on_pline d b a c, on_pline d a b c; g = midpoint g d b; h = midpoint h a d ? perp h e e f
171
+ examples/complete2/001/complete_016_ex-gao_gao_L_L182-5.gex
172
+ c d a = triangle c d a; b = on_pline b c d a, on_pline b a d c; e = on_line e c d; f = on_line f a b, on_pline f c a e; g = on_line g b e, on_line g c f; h = on_line h d f, on_line h a e ? cong g e f h
173
+ examples/complete2/001/complete_017_ex-gao_gao_L_L189-1.gex
174
+ a b = segment a b; c = on_tline c b a b; d = on_tline d c b c, on_tline d a a b; e = midpoint e c d; f = midpoint f b c; g = midpoint g a b; h = midpoint h a d ? cong h g h e
175
+ examples/complete2/001/complete_016_ex-gao_gao_C_C109.gex
176
+ b d a = triangle b d a; c = on_pline c d a b; e = on_line e b d, on_line e a c; f = on_line f a d, on_pline f e a b; g = on_line g b c, on_line g e f ? cong f e e g
177
+ examples/complete2/001/complete_016_ex-gao_gao_L_LL153-1.gex
178
+ c a d = triangle c a d; e = foot e c a d; b = free b; f = foot f b a d; g = midpoint g c b; h = midpoint h e f ? cong g e g f
179
+ examples/complete2/001/complete_010_Other_gao_Y_yL182-4.gex
180
+ c d = segment c d; e = midpoint e c d; a = free a; b = on_pline b c d a, on_pline b a d c; f = midpoint f a b; g = on_line g a c, on_line g b e; h = on_line h d f, on_line h a c ? cong a h h g
181
+ examples/complete2/006/complete_012_7_Book_00EE_02_E028-3.gex
182
+ c a b = risos c a b; d = midpoint d b a; e = on_line e b c; f = circle f d b e; g = on_line g a e, on_circle g f b ? perp c g a e
183
+ examples/complete2/006/complete_003_6_GDD_FULL_more_E022-11.gex
184
+ a b c = triangle a b c; d = circumcenter d a b c; f = foot f d a b; e = on_tline e c c d, on_tline e b b d; g = on_line g d f, on_line g a c ? para g e a b
185
+ examples/complete2/006/complete_010_Other_Auxiliary_aux2_e04f.gex
186
+ a b c d = trapezoid a b c d; e = midpoint e c a; f = midpoint f d b; g = on_line g e f, on_line g a d ? midp g a d
187
+ examples/complete2/006/complete_004_6_GDD_FULL_81-109_98.gex
188
+ a b c = triangle a b c; e = on_line e a b; d = circle d a b c; f = on_circle f d a, on_aline f c b a c e; g = on_circle g d c, on_line g c e ? para a b g f
189
+ examples/complete2/006/complete_001_6_GDD_FULL_61-80_72.gex
190
+ a b c = triangle a b c; d = circle d a b c; e = on_circle e d a; f = foot f e a c; g = foot g e a b ? simtri e f g e c b
191
+ examples/complete2/006/complete_013_7_Book_00EE_11_E075-26.gex
192
+ a b = segment a b; c = mirror c a b; d = mirror d b c; e = midpoint e c b; f = on_circle f e c, on_dia f a e; g = on_line g a f ? eqangle b f a f c f e f
193
+ examples/complete2/006/complete_015_7_Book_00EE_06_E057-38.gex
194
+ c a b = r_triangle c a b; d = foot d c a b; e = angle_bisector e c a b, on_line e b c; g = foot g e a b; f = on_line f c d, on_line f a e ? cong c e c f
195
+ examples/complete2/006/complete_014_7_Book_00EE_07_E059-47.gex
196
+ a b c d = rectangle a b c d; e = on_line e b d, on_line e a c; f = midpoint f e d; g = midpoint g e a ? cong f c g b
197
+ examples/complete2/006/complete_014_7_Book_00EE_07_E059-53.gex
198
+ a b c = triangle a b c; d = circle d c a b; e = circle e c d b; f = on_line f a b, on_circle f e b; g = on_line g a c, on_circle g e b ? cong g b g a
199
+ examples/complete2/006/complete_003_6_GDD_FULL_more_E023-15.gex
200
+ a b c d = quadrangle a b c d; e = on_line e a c; g = on_pline g e a b, on_line g b c; f = on_pline f e a d, on_line f c d ? para b d g f
201
+ examples/complete2/011/complete_002_6_GDD_FULL_01-20_12.gex
202
+ a b c = triangle a b c; o = circle o a b c; d = on_tline d b a c, on_circle d o a; f = midpoint f b a; e = on_line e a c, on_line e b d ? perp f e c d
203
+ examples/complete2/011/complete_002_6_GDD_FULL_01-20_05.gex
204
+ a b c = triangle a b c; h = orthocenter h a b c; o = circumcenter o a b c; c1 = circumcenter c1 a b h; b1 = circumcenter b1 a h c; a1 = circumcenter a1 b h c ? perp a1 o b1 c1
205
+ examples/complete2/011/complete_003_6_GDD_FULL_21-40_34.gex
206
+ a b c = triangle a b c; h = orthocenter h a b c; o = circle o h b c; p = on_tline p h c h, on_circle p o b ? para a h b p
207
+ examples/complete2/011/complete_004_6_GDD_FULL_81-109_99.gex
208
+ a b c = triangle a b c; m = free m; n = on_aline n a c b a m; q = foot q m a b; p = foot p m a c ? perp a n p q
209
+ examples/complete2/011/complete_003_6_GDD_FULL_21-40_35.gex
210
+ a b c = triangle a b c; d = foot d c a b; e = foot e b a c; o = circle o a b c; k = on_circle k o c, on_line k c d; h = on_line h c d, on_line h b e ? cong a k a h
211
+ examples/complete2/011/complete_003_6_GDD_FULL_21-40_31.gex
212
+ a b c = triangle a b c; c1 = midpoint c1 b a; b1 = midpoint b1 c a; o = circle o a b c; p = on_line p o c1, on_line p a c; q = on_line q o b1, on_line q a b ? cyclic q b c p
213
+ examples/complete2/011/complete_002_6_GDD_FULL_41-60_41.gex
214
+ a b c = triangle a b c; i = incenter i a b c; y = foot y i a c; l = foot l i b c; x = foot x b a i ? coll x y l
215
+ examples/complete2/011/complete_002_6_GDD_FULL_41-60_43.gex
216
+ c a b = r_triangle c a b; f e = square a b f e; p = on_line p b e, on_line p a f ? eqangle c a c p c p c b
217
+ examples/complete2/011/complete_002_6_GDD_FULL_41-60_51.gex
218
+ a b c = triangle a b c; o = circle o a b c; d = on_tline d b a c, on_circle d o a; e = on_circle e o d, on_line e d o ? para b e a c
219
+ examples/complete2/011/complete_002_6_GDD_FULL_41-60_44.gex
220
+ a b c = triangle a b c; o = circle o a b c; d = on_circle d o a; a1 = on_tline a1 a a b, on_line a1 c d; c1 = on_tline c1 c c d, on_line c1 a b ? para d b a1 c1
221
+ examples/complete2/010/complete_004_6_GDD_FULL_21-40_29.gex
222
+ a b c = triangle a b c; d = foot d a b c; q = foot q d a b; p = foot p d a c ? cyclic b q p c
223
+ examples/complete2/010/complete_002_6_GDD_FULL_01-20_10.gex
224
+ a b c d = quadrangle a b c d; e = on_line e b c, on_line e a d; o1 = circle o1 c d e; o = circle o e b a; p = on_line p c d, on_line p a b; q = on_circle q o1 c, on_circle q o a ? cyclic p d q a
225
+ examples/complete2/010/complete_013_7_Book_00EE_10_E072-15.gex
226
+ b c a = triangle b c a; d = lc_tangent d c a, lc_tangent d b a; e = on_circle e a c, on_dia e a d ? eqangle b e b a b a b c
227
+ examples/complete2/010/complete_011_7_Book_00EE_04_E051-6.gex
228
+ a b c = triangle a b c; d = eq_triangle d c a; e = eq_triangle e b a ? cong d e c b
229
+ examples/complete2/010/complete_012_7_Book_00EE_05_E051-20.gex
230
+ a b = segment a b; d = midpoint d a b; c = on_tline c b a b; e = on_line e a c, on_circle e d b; f = lc_tangent f e d, on_line f b c ? cong f c f b
231
+ examples/complete2/010/complete_011_7_Book_00EE_03_E037-20.gex
232
+ a b = segment a b; c = midpoint c a b; d = on_circle d c a; e = lc_tangent e d c, angle_mirror e b a d ? perp a e e d
233
+ examples/complete2/010/complete_012_7_Book_00EE_11_E076-32.gex
234
+ c a b = r_triangle c a b; d = midpoint d b c; e = foot e c a d ? eqangle a b b c d e e b
235
+ examples/complete2/010/complete_000_3_JAR_JAR02-new_fig214.gex
236
+ a b c = triangle a b c; d = intersection_pp d a b c c a b; e = intersection_ll e a c b d ? cong a e e c
237
+ examples/complete2/010/complete_003_6_GDD_FULL_more_E021-3.gex
238
+ a b = segment a b; c = on_circle c a b; e = intersection_lc e a a c; d = on_tline d c a c, on_tline d b a b ? para a d b e
239
+ examples/complete2/010/complete_013_7_Book_00EE_10_E074-22.gex
240
+ a b = segment a b; c = on_circle c a b; d = on_line d a c; e = on_line e a b, on_circle e a d; f = on_line f b d, on_line f c e ? eqangle a b a f a f a c
241
+ examples/complete2/010/complete_001_6_GDD_FULL_01-20_20.gex
242
+ a b c = triangle a b c; d = foot d a b c; e = foot e b a c; h = on_line h a d, on_line h b e; g = foot g h a b ? eqangle g e g h g h g d
243
+ examples/complete2/010/complete_002_6_GDD_FULL_41-60_57.gex
244
+ a b c = triangle a b c; d = foot d a b c; o = midpoint o a d; e = on_line e a b, on_circle e o d; f = on_line f a c, on_circle f o d ? cyclic b c e f
245
+ examples/complete2/010/complete_010_Other_Auxiliary_ye_aux_ppara.gex
246
+ a b c d = eq_trapezoid a b c d; e = on_pline e b a d, on_line e c d ? eqangle a d a b b a b c
247
+ examples/complete2/003/complete_003_6_GDD_FULL_more_E013-3.gex
248
+ a b = segment a b; d = on_tline d b a b; e = on_circle e a b; f = on_line f d e, on_circle f a b; g = midpoint g e f; c = on_circle c a b, on_dia c d a; h = intersection_lc h g a c ? para e f b h
249
+ examples/complete2/003/complete_005_Other_ndgs_01.gex
250
+ b c a = triangle b c a; d = intersection_cc d b a c; e = on_circle e b c; g = intersection_lc g e a d; f = on_circle f b c; h = intersection_lc h f a c ? para g h e f
251
+ examples/complete2/003/complete_013_7_Book_00EE_10_E072-12.gex
252
+ b a c = triangle b a c; d = on_circle d a b, on_circle d c b; e = on_tline e d b d, on_circle e a b; f = on_circle f c d, on_line f d e; h = on_circle h a b, on_line h b f; g = on_circle g c b, on_line g b e ? eqangle d h d b d b d g
253
+ examples/complete2/003/complete_010_Other_Auxiliary_ye_aux_wang3.gex
254
+ a b c d = isquare a b c d; f = angle_bisector f a d b, on_line f a c; g = foot g c d f; e = on_line e b d, on_line e a c; h = on_line h c g, on_line h a d; i = on_line i b d, on_line i c g; x = midpoint x a h ? cong a x e i
255
+ examples/complete2/003/complete_003_6_GDD_FULL_more_E022-8.gex
256
+ b a c = triangle b a c; d = on_circle d a b, on_circle d c b; e = on_circle e c b; f = intersection_lc f e a d; g = intersection_lc g e a b; h = on_tline h e c e ? para h e g f
257
+ examples/complete2/003/complete_008_ex-gao_ex160_206.gex
258
+ a b c = triangle a b c; e d = square b a e d; f g = square a c f g; h = on_line h b e, on_line h a d; i = on_pline i e a g, on_pline i g a e ? perp c h h i
259
+ examples/complete2/003/complete_013_7_Book_00EE_11_E077-38.gex
260
+ a b c = triangle a b c; d = circumcenter d a b c; e = lc_tangent e b d; f = angle_bisector f e b c, on_circle f d a; g = foot g f b c; h = foot h f b e ? eqangle b a a f f a a c
261
+ examples/complete2/003/complete_004_6_GDD_FULL_81-109_84.gex
262
+ a b c = triangle a b c; d = midpoint d b a; e = midpoint e c b; f = midpoint f a c; g = circle g d e f; h = on_tline h e e g, on_line h a b; i = on_line i e h, on_line i a c ? cyclic b h c i
263
+ examples/complete2/003/complete_003_6_GDD_FULL_more_E022-10.gex
264
+ a b = segment a b; c = on_circle c a b; e = on_circle e a b; d = on_circle d a b; f = on_line f c e, on_line f b d; g = circumcenter g e f b; h = on_tline h f f g ? para h f c d
265
+ examples/complete2/003/complete_011_7_Book_00EE_03_E037-25.gex
266
+ a b = segment a b; c = midpoint c b a; d = on_tline d c a b, on_circle d c a; e = on_circle e c d, on_line e d c; f = on_line f a b; g = on_line g c d, on_circle g c f; h = on_circle h c e, on_line h e f; i = on_line i b g, on_circle i c a ? perp e h b i
267
+ examples/complete2/003/complete_016_7_Book_00EE_06_E051-25.gex
268
+ a b c = triangle a b c; d = foot d b a c; e = foot e c a b; g = circumcenter g b c a; h = intersection_lc h g g a; f = on_line f b d, on_line f c e; i = on_line i b c, on_line i f h ? cong f i i h
269
+ examples/complete2/003/complete_013_7_Book_00EE_10_E074-20.gex
270
+ a b c = triangle a b c; d = on_line d a b; e = foot e d b c; f = foot f e a b; g = on_line g b c; h = foot h g a b; i = foot i h b c ? eqangle d g d h f i f d
271
+ examples/complete2/003/complete_017_ex-gao_ex160_4_003.gex
272
+ a b = segment a b; c = on_circle c a b; e = on_line e b c; d = on_tline d c a c, on_tline d b a b; f = on_tline f e a e, on_line f c d; g = on_line g e f, on_circle g a b; h = on_line h e f, on_line h b d ? cong c f h b
273
+ examples/complete2/003/complete_015_7_Book_00EE_08_E059-56.gex
274
+ a b c = triangle a b c; d = foot d b a c; e = foot e a b c; f = on_line f b d, on_line f a e; g = circle g b f a; h = on_line h b c, on_circle h g a; i = on_line i a c, on_circle i g a ? cong f c f i
275
+ examples/complete2/003/complete_014_7_Book_00EE_07_E059-52.gex
276
+ a b = segment a b; d = on_circle d a b; c = on_tline c a a b, on_circle c a b; e = on_tline e c a c, on_tline e b a b; f = lc_tangent f d a, on_line f c e; g = on_line g b e, on_line g d f; h = on_line h c e, on_line h b d ? cong h e d g
277
+ examples/complete2/004/complete_002_6_GDD_FULL_01-20_13.gex
278
+ a b c = triangle a b c; d = parallelogram a b c d; e = foot e b a c; f = foot f a b d; g = foot g d a c; h = foot h c b d ? para e f g h
279
+ examples/complete2/004/complete_006_Other_Auxiliary_E092-5.gex
280
+ a b c = triangle a b c; d = parallelogram a b c d; e = angle_bisector e d a b, on_dia e a d; f = foot f b a e; g = foot g c b f; h = on_line h d e, on_line h c g ? para e g a b
281
+ examples/complete2/004/complete_004_6_GDD_FULL_81-109_86.gex
282
+ a b c = triangle a b c; d = midpoint d c a; e = midpoint e b c; f = midpoint f a b; g = angle_bisector g a b c, on_line g d f; h = on_line h b g, on_line h d e ? cong d g d h
283
+ examples/complete2/004/complete_011_7_Book_00EE_03_E037-26.gex
284
+ a b c d = isquare a b c d; e = on_line e b c; g = on_line g d c, on_line g a e; f = on_line f b d, on_line f a e; h = circle h g e c ? perp f c c h
285
+ examples/complete2/004/complete_016_7_Book_00EE_06_E051-27.gex
286
+ a b = segment a b; c = midpoint c b a; d = on_circle d c a; e = angle_bisector e d c a, on_circle e c a; f = foot f e a b; g = intersection_ll g a d e f; h = intersection_ll h a d b e ? cong a g g e
287
+ examples/complete2/004/complete_001_6_GDD_FULL_61-80_73.gex
288
+ a b c = triangle a b c; d = circle d a b c; e = on_circle e d a; f = foot f e a c; g = foot g e a b; h = on_circle h d e, on_line h e g ? para g f h c
289
+ examples/complete2/004/complete_014_7_Book_00EE_07_E057-42.gex
290
+ a b c = triangle a b c; d = midpoint d a c; e = midpoint e b a; f = midpoint f c b; g = on_line g a b; h = on_pline h d f g, on_line h a b ? cong h a g e
291
+ examples/complete2/005/complete_005_Other_ndgs_03.gex
292
+ b a c = triangle b a c; d = foot d b a c; e = foot e c a b; f = midpoint f c b; g = foot g f d e ? cong g e g d
293
+ examples/complete2/005/complete_000_rebuilt_example_9point.gex
294
+ a b c = triangle a b c; d = foot d a b c; e = midpoint e b a; f = midpoint f c b; g = midpoint g a c; o = circumcenter o e f g ? cyclic d g e f
295
+ examples/complete2/005/complete_013_7_Book_00EE_11_E081-2.gex
296
+ a b = segment a b; d = on_circle d a b; c = on_circle c a b, on_circle c b d; e = foot e d b c; f = intersection_lc f e a d; g = on_line g d e, on_circle g e f ? cong f c g d
297
+ examples/complete2/005/complete_002_6_GDD_FULL_41-60_58.gex
298
+ a b c = triangle a b c; d = circle d a b c; e = on_circle e d a; f = on_line f a b, on_line f c e; g = on_pline g f a e, on_line g b c ? eqangle f g f b c f c b
299
+ examples/complete2/005/complete_016_7_Book_00EE_06_E051-26.gex
300
+ a b = segment a b; c = on_circle c a b; e = on_line e b c; d = lc_tangent d b a, lc_tangent d c a; f = on_tline f e a e, on_line f c d; g = on_line g e f, on_line g b d ? cong g e e f
301
+ examples/complete2/005/complete_001_6_GDD_FULL_61-80_61.gex
302
+ a b c = triangle a b c; e = midpoint e b a; d = circle d a b c; f = on_line f d e; g = on_line g b c, on_circle g f a ? simtri a d f a c g
303
+ examples/complete2/005/complete_017_ex-gao_ex160_4_e03a_lratio.gex
304
+ c a b = iso_triangle c a b; e = midpoint e b c; f = on_line f a b, on_circle f e b; d x = trisegment d x c b; g = on_line g c f, on_line g a d ? cong c g g f
305
+ examples/complete2/005/complete_008_ex-gao_ex160_e122.gex
306
+ a b = segment a b; c = on_circle c a b; d = on_circle d a b; e = on_circle e a b, on_pline e d b c; f = on_circle f a b; g = on_circle g a b, on_pline g d c f ? para g b e f
307
+ examples/complete2/002/complete_007_7_Book_LLL_yL251-1.gex
308
+ a d b = triangle a d b; c = free c; e = on_line e a d; f = on_line f a b; g = on_line g a c; h = on_line h a d; i = on_line i a c, on_pline i h e g; j = on_line j a b, on_pline j i f g ? para e f h j
309
+ examples/complete2/002/complete_017_ex-gao_ex160_4_e12.gex
310
+ a b c d = quadrangle a b c d; e = midpoint e c b; f = midpoint f d c; g = midpoint g a d; h = midpoint h b a; i = on_line i e g, on_line i f h ? midp i h f
311
+ examples/complete2/002/complete_013_7_Book_00EE_10_E073-18.gex
312
+ a b c = triangle a b c; d = parallelogram a b c d; e = on_line e b d; f = foot f e a b; g = foot g e b c; h = on_line h e f, on_line h c d; i = on_line i e g, on_line i a d ? para h i g f
313
+ examples/complete2/002/complete_011_7_Book_00EE_03_E043-3.gex
314
+ a b = segment a b; c = on_circle c a b; f = on_circle f a b; e = on_line e b c, on_dia e f a; g = intersection_lc g e a f; d = lc_tangent d b a, lc_tangent d c a; h = on_line h c d, on_line h e f; i = on_line i e f, on_line i b d ? cong c h b i
315
+ examples/complete2/002/complete_012_7_Book_00EE_02_E028-2-1.gex
316
+ a b c = triangle a b c; e d = square a c e d; f g = square c b f g; h = midpoint h a e; i = midpoint i b g; j = midpoint j a b ? perp h j j i
317
+ examples/complete2/002/complete_008_ex-gao_ex160_e124.gex
318
+ b a c = triangle b a c; e = on_circle e a b; d = on_circle d a b, on_circle d c b; h = on_tline h e a e, on_circle h c b; g = on_circle g c d, on_line g d e; f = on_circle f c b, on_line f b e; i = on_circle i c h, on_line i h e ? para g f h i
319
+ examples/complete2/000/complete_004_6_GDD_FULL_81-109_106.gex
320
+ q4 q1 q3 q0 q2 = pentagon q4 q1 q3 q0 q2; p0 = on_line p0 q4 q1, on_line p0 q0 q2; p4 = on_line p4 q4 q1, on_line p4 q3 q0; p3 = on_line p3 q3 q0, on_line p3 q4 q2; p2 = on_line p2 q1 q3, on_line p2 q4 q2; p1 = on_line p1 q1 q3, on_line p1 q0 q2; o0 = circle o0 q0 p0 p4; o1 = circle o1 p1 q1 p0; o4 = circle o4 p4 p3 q4; o3 = circle o3 p3 p2 q3; o2 = circle o2 p1 p2 q2; m0 = on_circle m0 o0 q0, on_circle m0 o1 q1; m4 = on_circle m4 o0 q0, on_circle m4 o4 q4; m3 = on_circle m3 o4 q4, on_circle m3 o3 q3; m2 = on_circle m2 o3 q3, on_circle m2 o2 q2; m1 = on_circle m1 o2 q2, on_circle m1 o1 q1 ? cyclic m4 m3 m2 m1
321
+ examples/complete2/unsolved2/complete_010_Other_Auxiliary_ye_aux_think2.gex
322
+ c a b = iso_triangle c a b; d = on_line d a c; e = on_line e b c, eqdistance e b d a; f = on_line f a b, on_line f d e; g = on_pline g f a c, on_line g b c ? midp f d e
323
+ examples/complete2/unsolved2/complete_012_7_Book_00EE_02_E023-21.gex
324
+ a b = segment a b; c = on_tline c b a b; d = on_circle d a b; f = midpoint f c b; g = on_line g d f, on_circle g a b; h = intersection_lc h c a g; e = on_line e c d, on_circle e a b ? para b c h e
325
+ examples/complete2/unsolved2/complete_006_7_Book_LLL_yL252-6.gex
326
+ a c d = triangle a c d; b = on_pline b c d a, on_pline b a d c; e = on_line e a c; g = on_line g a b, on_pline g e a d; f = on_line f a d, on_pline f e c d; h = on_line h e g, on_line h c d; i = on_line i b c, on_line i e f ? para f g h i
327
+ examples/complete2/unsolved2/complete_015_7_Book_00EE_08_E059-59.gex
328
+ a b c = triangle a b c; d = angle_bisector d b a c; e = on_pline e c b d, on_pline e b c d; f = on_line f b e, on_line f a c; g = on_line g c e, on_line g a b ? cong b g c f
329
+ examples/complete2/unsolved2/complete_013_7_Book_00EE_10_E072-16.gex
330
+ a b c = triangle a b c; d = parallelogram a b c d; e = on_line e c d; f = on_line f a d, eqdistance f c a e; g = on_line g a e, on_line g c f ? eqangle g a g b g b g c
331
+ examples/complete2/unsolved2/complete_003_6_GDD_FULL_more_E023-19.gex
332
+ c a b = r_triangle c a b; d = foot d c a b; e = on_line e c d, angle_bisector e b a c; f = on_line f a b, angle_bisector f d c b; g = on_line g b c, on_line g a e ? para e f c b
333
+ examples/complete2/unsolved2/complete_010_Other_Auxiliary_ye_aux_ll43.gex
334
+ a b = segment a b; c = on_dia c a b, on_bline c a b; d = midpoint d a c; e = foot e c b d; f = on_line f c e, on_line f a b ? eqangle d c d b d f d a
335
+ examples/complete2/unsolved2/complete_010_Other_Auxiliary_aux2_22.gex
336
+ c a b = iso_triangle c a b; d = on_line d a c; e = on_line e b c, eqdistance e b a d; f = on_line f a b, on_line f d e ? cong d f e f
337
+ examples/complete2/unsolved2/complete_014_7_Book_00EE_09_E066-04.gex
338
+ a b = segment a b; c = lc_tangent c b a; d = midpoint d b c; e = on_circle e a b; f = on_line f d e, on_circle f a b ? eqangle e c c d d f f c
339
+ examples/complete2/unsolved2/complete_014_7_Book_00EE_08_E061-66.gex
340
+ a b = segment a b; c = s_angle b a c 60; d = foot d a b c; e = foot e b a c; g = circumcenter g b c a; f = on_line f a d, on_line f b e ? cong a f a g
341
+ examples/complete2/unsolved2/complete_011_7_Book_00EE_03_E037-24.gex
342
+ a b c = triangle a b c; d = circle d b a c; e = lc_tangent e a d, on_line e b c; f = angle_bisector f b e a, on_line f a b; g = on_line g a c, on_line g e f; h = angle_bisector h b a c, on_line h b c ? perp f e a h
343
+ examples/complete2/unsolved2/complete_014_7_Book_00EE_08_E061-65.gex
344
+ a b c d = isquare a b c d; e = s_angle c d e 15, s_angle d c e -15; f = reflect f e a c ? contri e a b a b e
345
+ examples/complete2/unsolved2/complete_012_7_Book_00EE_11_E076-31.gex
346
+ a b c = triangle a b c; d = angle_bisector d c b a, on_line d a c; e = angle_bisector e a c b, on_line e a b; f = on_line f d e, on_line f b c; g = on_line g a b ? eqangle a g a f a f a c
347
+ examples/complete2/unsolved2/complete_010_Other_Auxiliary_ye_aux_y1.gex
348
+ a b c = triangle a b c; d = angle_bisector d a b c, on_dia d b c; e = angle_bisector e b a c, on_dia e a c ? para d e a b
349
+ examples/complete2/unsolved2/complete_004_6_GDD_FULL_21-40_40.gex
350
+ a b c = triangle a b c; i = incenter i a b c; e = on_pline e i a b, on_line e a c ? cong e i e a
351
+ examples/complete2/unsolved2/complete_014_7_Book_00EE_09_E069-8.gex
352
+ a b c = triangle a b c; d = parallelogram a b c d; e = eqangle2 e d a b ? eqangle d a a e e c c d
353
+ examples/complete2/unsolved2/complete_011_7_Book_00EE_04_E051-9.gex
354
+ a b = segment a b; c = s_angle b a c 30; d = mirror d b c; e = foot e d a b ? cong d e a c
355
+ examples/complete2/unsolved2/complete_003_6_GDD_FULL_21-40_27.gex
356
+ a b c = triangle a b c; h = orthocenter h a b c; o = circumcenter o a b c; o3 = circumcenter o3 a h b; o1 = circumcenter o1 b h c; o2 = circumcenter o2 c h a ? cong h o1 h o2
357
+ examples/complete2/unsolved2/complete_017_ex-gao_ex160_4_e08.gex
358
+ a b c = triangle a b c; d = on_line d b c, angle_bisector d b a c; e = on_pline e d a c, on_line e a b; f = on_pline f e b c, on_line f a c ? cong e a f c
359
+ examples/complete2/unsolved2/complete_015_7_Book_00EE_06_E051-29.gex
360
+ a b = segment a b; c = on_circle c a b; d = on_circle d a b; e = on_circle e a b; f = on_line f b e, on_line f c d; g = on_line g d e, on_pline g f b c; h = on_circle h a b, on_dia h g a ? cong f g g h
361
+ examples/complete2/unsolved2/complete_002_6_GDD_FULL_41-60_42.gex
362
+ a b c = triangle a b c; d = incenter d a b c; e = foot e a b c; f = foot f b a d; g = foot g c a d; h = midpoint h c b ? cyclic e f g h
363
+ examples/complete2/unsolved2/complete_014_7_Book_00EE_08_E061-63f.gex
364
+ a b = segment a b; c = midpoint c b a; d = s_angle b a d 30, on_circle d c a; e = lc_tangent e d c, on_line e a b ? cong d a d e
365
+ examples/complete2/unsolved2/complete_007_7_Book_LLL_yL198-1.gex
366
+ c d = segment c d; e = midpoint e c d; b = free b; f = midpoint f b c; a = eqdistance a d c b, on_pline a b d c; g = midpoint g a b; h = midpoint h d a ? cong h e e f
367
+ examples/complete2/unsolved/complete_015_7_Book_00EE_08_E061-61.gex
368
+ a b = segment a b; c = on_circle c a b; d = on_circle d a b; e = lc_tangent e c a, lc_tangent e d a; f = lc_tangent f b a, on_line f c e; h = on_pline h c b f, on_line h b d; g = on_line g d e, on_line g b f; i = on_line i b e, on_line i c h ? cong c i i h
369
+ examples/complete2/unsolved/complete_008_ex-gao_ex160_204.gex
370
+ a b c = triangle a b c; d = circumcenter d a b c; e = on_circle e d a; f = on_line f a b, on_line f c e; h = on_line h b c, angle_bisector h a f c; g = on_line g a e, on_line g b c; i = on_line i a b, angle_bisector i a g b; j = on_line j f h, on_line j g i; k = on_line k a e, on_line k f h ? perp g i f h
371
+ examples/complete2/unsolved/complete_005_Other_unsolved_65.gex
372
+ a b c = triangle a b c; e = on_line e a b; f = on_pline f e b c, on_line f a c; d = circle d a b c; g = circle g a e f ? coll a g d
373
+ examples/complete2/unsolved/complete_008_ex-gao_ex160_005.gex
374
+ a b c = triangle a b c; d = on_line d b c, angle_bisector d b a c; e = on_line e a b; f = on_line f a c, eqdistance f c b e; g = midpoint g f e; h = midpoint h c b ? para g h a d
375
+ examples/complete2/unsolved/complete_006_Other_ndgTest_65.gex
376
+ a b c = triangle a b c; e = on_line e a b; f = intersection_lp f a c e c b; d = circle d a b c; g = circle g a e f ? coll a g d
377
+ examples/complete2/unsolved/complete_005_Other_unsolved_E051-7.gex
378
+ a b c = triangle a b c; d = on_line d a b; e = angle_bisector e c b a, on_line e a c; f = on_pline f e a b, on_line f b c; g = angle_bisector g c b d, on_line g e f ? cong e f f g
379
+ examples/complete2/unsolved/complete_005_Other_unsolved_E046-10.gex
380
+ a b c d = isquare a b c d; e = midpoint e b a; f = on_line f a b; g = on_tline g e d e, angle_bisector g c b f ? cong d e e g
381
+ examples/complete2/unsolved/ex-gao_ex160_103.gex
382
+ c b y = triangle c b y; a = foot a c b y; x = angle_bisector x c b y; d = foot d a b c; e = on_line e b x, on_line e c a; f = foot f e b c; g = on_line g b x, on_line g a d ? cong f g f e
383
+ examples/complete2/unsolved/ex-gao_ex160_104.gex
384
+ c a y = triangle c a y; b = foot b c a y; x = angle_bisector x c a y; e = foot e b a c; d = on_line d a x, on_line d c b; f = on_line f a x, on_line f b e; g = on_line g c b, on_pline g f a c ? cong b d c g
385
+ examples/complete2/unsolved/complete_005_Other_unsolved_109f.gex
386
+ a b d = triangle a b d; e = on_line e a d; c = on_line c a b; f = on_line f b e, on_line f c d; g = circle g d e f; h = circle h a c d; i = circle i b c f; j = circle j b a e ? cyclic i j h g
387
+ examples/complete2/unsolved/complete_005_Other_unsolved_E046-7.gex
388
+ a b c = triangle a b c; d = midpoint d c a; e = angle_bisector e b a d, on_line e b d; f = on_pline f b c e, on_line f a c ? cong b a c f
389
+ examples/complete2/unsolved/complete_008_ex-gao_ex160_e121.gex
390
+ a b = segment a b; d = midpoint d b a; c = on_tline c a a b; e = on_circle e c a; f = on_line f d e, on_circle f c a; g = on_circle g c f, on_line g f b; h = on_line h b e, on_circle h c a ? para a b g h
391
+ examples/complete2/unsolved/complete_018_ex-gao_ex160_4_010.gex
392
+ a b c d = isquare a b c d; e = mirror e a b; f = midpoint f b a; g = on_tline g f d f, angle_bisector g c b e; h = foot h g a b ? cong d f g f
393
+ examples/complete2/unsolved/complete_005_Other_unsolved_82.gex
394
+ a b c = triangle a b c; o = incenter o a b c; i = foot i c a o; e = on_tline e a a o; j = foot j c a e; l = foot l c b o ? coll i l j
395
+ examples/complete2/unsolved/complete_014_7_Book_00EE_07_E057-41.gex
396
+ a b c = triangle a b c; d = foot d c a b; e = free e; f = on_circle f c e; g = on_line g d f, on_circle g c e; h = on_line h d e, on_circle h c e; i = on_line i f h, on_line i a b; j = on_line j e g, on_line j a b ? cong j d d i
397
+ examples/complete2/unsolved/complete_015_7_Book_00EE_08_E059-55.gex
398
+ a b = segment a b; c = on_circle c a b; d = lc_tangent d b a, lc_tangent d c a; e = on_circle e a b; f = on_line f d e, on_circle f a b; g = angle_bisector g f b e, on_line g d e ? cong d b d g
399
+ examples/complete2/unsolved/complete_005_Other_unsolved_E073-17.gex
400
+ a b c = triangle a b c; o = circumcenter o a b c; p = lc_tangent p a o, on_line p b c; d = on_circle d p a; e = intersection_lc e d o b; f = intersection_lc f d o c ? para e f p d
401
+ examples/complete2/unsolved/complete_015_7_Book_00EE_06_E056-33.gex
402
+ a b = segment a b; c = on_tline c a a b, on_circle c a b; e = s_angle b a e 60, on_circle e a b; d = s_angle b a d 30, on_circle d a b; f = on_line f a e, on_line f b c; g = on_line g a d, on_line g b c ? cong c f g b
403
+ examples/complete2/unsolved/complete_005_Other_unsolved_E074-24.gex
404
+ a b = segment a b; c = on_tline c a a b; d = foot d a b c; e = midpoint e d a; f = on_line f b e, on_line f a c; g = foot g f b c; h = midpoint h c a; i = on_tline i f a c, on_circle i h a ? cong f i f g
405
+ examples/complete2/unsolved1/complete_008_7_Book_LLL_L057-3.gex
406
+ a b = segment a b; c = on_bline c a b; e = midpoint e a c; d = mirror d c a; f = midpoint f b d ? cong e b f b
407
+ examples/complete2/unsolved1/complete_006_7_Book_LLL_L046-17.gex
408
+ c a b = risos c a b; d = midpoint d b a; e = on_line e a b; f = foot f e a c; g = foot g e b c ? cong d f d g
409
+ examples/complete2/unsolved1/complete_008_7_Book_LLL_L057-2.gex
410
+ c a b = triangle a b c; d = angle_bisector d b a c; e = foot e c a d; f = intersection_lp f a c e a b ? cong f e f c
411
+ examples/complete2/unsolved1/complete_008_ex-gao_ex160_e102.gex
412
+ a b c = triangle a b c; d = on_line d a c, angle_bisector d c b a; e = on_line e a b, angle_bisector e b c a; f = foot f a c e; g = foot g a b d ? para g f b c
413
+ examples/complete2/unsolved1/complete_012_7_Book_00EE_11_E075-27f.gex
414
+ a b c = triangle a b c; d = foot d c a b; e = on_line e c d; f = on_line f b e, on_line f a c; g = on_line g a e, on_line g b c ? eqangle d f d c d c d g
415
+ examples/complete2/unsolved1/complete_006_7_Book_LLL_L091-13.gex
416
+ b c d = triangle b c d; e = midpoint e c d; a = eqdistance a d c b, on_pline a b d c; f = midpoint f b a ? perp a b e f
417
+ examples/complete2/unsolved1/complete_010_Other_gao_Y_yL157-1.gex
418
+ a b d = triangle a b d; e = midpoint e d a; c = angle_bisector c a b d, on_dia c a b ? para c e b d
419
+ examples/complete2/unsolved1/complete_008_7_Book_LLL_L055-5.gex
420
+ d b a = triangle d b a; c = angle_bisector c d a b, angle_bisector c d b a; e = on_line e b c, on_tline e d b c; f = on_line f a c, on_tline f d a c ? para e f a b
421
+ examples/complete2/unsolved1/complete_013_7_Book_00EE_11_E075-29.gex
422
+ a b = segment a b; d = midpoint d b a; f = on_circle f d a; c = intersection_lt c a b f d f; e = midpoint e c a; g = on_tline g b a b, on_circle g e a ? eqangle f c f g g f g c
423
+ examples/complete2/unsolved1/complete_012_7_Book_00EE_05_E051-14-1.gex
424
+ a b c d = quadrangle a b c d; e = midpoint e b a; f = foot f a c d; g = foot g b c d ? cong e f e g
425
+ examples/complete2/unsolved1/complete_007_7_Book_LLL_L057-3-1.gex
426
+ a b = segment a b; c = on_bline c a b; e = midpoint e a c; f = mirror f b e; d = on_circle d a c, on_line d a c ? cong f b b d
427
+ examples/complete2/unsolved1/complete_001_6_GDD_FULL_61-80_80.gex
428
+ a b c = triangle a b c; o = circle o a b c; u = angle_bisector u b a c, on_line u b c; t = on_tline t a a o, on_line t b c ? cong t a t u
429
+ examples/complete2/unsolved1/complete_008_ex-gao_ex160_e213.gex
430
+ a b c = triangle a b c; d = midpoint d a b; e = midpoint e c a; f = on_line f d e, angle_bisector f c b a ? perp a f b f
431
+ examples/complete2/unsolved1/complete_011_7_Book_00EE_04_E051-2.gex
432
+ a b c = triangle a b c; d = on_line d a b; e = on_pline e d b c, on_line e a c; f = on_line f c d, on_line f b e; g = on_line g a f, on_line g b c ? midp g b c
433
+ examples/complete2/unsolved1/complete_007_7_Book_LLL_L043-5.gex
434
+ a b = segment a b; c = on_circle c a b; d = on_circle d a b; e = on_circle e a b; f = intersection_ll f b c d e; g = intersection_ll g c d b e; h = angle_bisector h d f b; i = angle_bisector i c g b; j = intersection_ll j f h g i ? perp g i f h
435
+ examples/complete2/unsolved1/complete_001_6_GDD_FULL_61-80_71.gex
436
+ a b c = triangle a b c; o = circle o a b c; e = on_pline e a b c, on_circle e o a; f = foot f e a b; g = foot g e a c ? para f g a o
437
+ examples/complete2/unsolved1/complete_007_7_Book_LLL_L043-5-1.gex
438
+ a b = segment a b; c = on_circle c a b; d = on_circle d a b; e = on_circle e a b; f = intersection_ll f b c d e; g = intersection_ll g c d b e; h = angle_bisector h d f b; i = angle_bisector i c g b; j = intersection_ll j f h g i; k = intersection_ll k d e g i; l = intersection_ll l g i b c ? perp g i f h
439
+ examples/complete2/unsolved1/complete_011_7_Book_00EE_04_E051-8.gex
440
+ a b c = triangle a b c; d = angle_bisector d a c b, on_line d a b; e = on_pline e d b c, on_line e a c; f = on_pline f e a b, on_line f b c ? cong c e f b
441
+ examples/complete2/unsolved1/complete_013_7_Book_00EE_10_E072-8.gex
442
+ a b c = triangle a b c; d = angle_bisector d b a c, on_line d b c; f = on_line f b c, on_bline f a d; e = on_bline e a d, on_line e a d ? eqangle a b a f c f c a
443
+ examples/complete2/unsolved1/complete_003_6_GDD_FULL_more_E023-14.gex
444
+ a b c = triangle a b c; d = midpoint d b a; e = angle_bisector e c d a, on_line e a c; f = angle_bisector f c d b, on_line f c b ? para e f a b
445
+ examples/complete2/unsolved1/complete_010_Other_gao_Y_yL182-1.gex
446
+ a c d = triangle a c d; b = on_pline b c d a, on_pline b a d c; e = on_line e a c; f = shift f c a e ? para d e f b
447
+ examples/complete2/unsolved1/complete_008_ex-gao_ex160_e120.gex
448
+ a b = segment a b; c = midpoint c b a; d = on_circle d c a; e = on_tline e a a b, on_tline e d c d; f = on_tline f b a b, on_line f d e; g = on_line g b e, on_line g a f ? para d g a e
449
+ new_unsolved/0.gex
450
+ c d b = triangle c d b; e = midpoint e c d; a = eqdistance a d c b, on_pline a b d c; f = midpoint f b a ? perp a b e f
451
+ new_unsolved/1.gex
452
+ a b c d = eq_trapezoid a b c d ? eqangle a d a b b a b c
453
+ examples/complete2/unsolved1/complete_009_Other_paper_Thebault_t5.gex
454
+ a b c = triangle a b c; d = circle d a b c; e = on_line e b c; f g h i = 2l1c f g h i a b e d; j k l m = 2l1c j k l m a c e d; n = incenter n a b c ? coll m n i
455
+ examples/complete2/unsolved/complete_013_7_Book_00EE_10_E072-11.gex
456
+ a b = segment b a; c = on_line c a b; d = on_circle d a b; e = on_circle e a b; g = on_line g d e, on_circle g c b; f = on_line f d e, on_circle f c b ? eqangle b e b f b g b d
457
+ examples/complete2/unsolved2/complete_015_7_Book_00EE_06_E051-28.gex
458
+ b c = segment b c; a = on_tline a b b c; d = on_circle d c b; e g = e5128 e g a b c d ? cong a g g b
459
+ examples/complete2/unsolved2/complete_010_Other_Auxiliary_ye_aux_think.gex
460
+ c a b = iso_triangle c a b; d e f = 3peq d e f c a b ? cong d a b e
461
+ examples/complete2/unsolved/morley.gex
462
+ a b c = triangle a b c; d e = trisect d e b a c; f g = trisect f g c b a; h i = trisect h i a c b; j = intersection_ll j b f c i; k = intersection_ll k a e c h; l = intersection_ll l a d b g ? cong j l j k
ag4masses/alphageometry/lm_inference.py ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Wrapper for language modeling inference implemented in Meliad."""
17
+ from typing import Any, Dict
18
+
19
+ import jax
20
+ import models # pylint: disable=unused-import
21
+ import t5.data
22
+ from transformer import inference_utils
23
+
24
+
25
+ np = jax.numpy
26
+
27
+
28
+ Trainer = inference_utils.Trainer
29
+
30
+ MetricsOutput = Dict[str, Any] # Metrics output by model.
31
+
32
+
33
+ parse_gin_configuration = inference_utils.parse_gin_configuration
34
+
35
+
36
+ class LanguageModelInference:
37
+ """Meliad wrapper for LM inference."""
38
+
39
+ def __init__(self, vocab_path: str, load_dir: str, mode='beam_search'):
40
+ self.vocab = t5.data.SentencePieceVocabulary(vocab_path)
41
+
42
+ # This task won't be pulling from a dataset.
43
+ def null_iter_fn() -> None:
44
+ return None
45
+
46
+ process_summaries_f = inference_utils.models.process_summaries_function(
47
+ self.vocab
48
+ )
49
+
50
+ trainer = inference_utils.training_loop.Trainer(
51
+ get_training_dataset_iterator=null_iter_fn,
52
+ get_test_dataset_iterator=None,
53
+ pretty_print_input_function=None,
54
+ process_summaries_function=process_summaries_f,
55
+ load_dir=load_dir,
56
+ workdir='', # Don't log or save checkpoints.
57
+ replicate_mode=False,
58
+ ) # Run on a single device at batch size 1.
59
+ self.trainer = trainer
60
+
61
+ # Create and initialize the model.
62
+ (tstate, _, imodel, prngs) = trainer.initialize_model()
63
+ self.imodel = imodel
64
+ self.batch_size = imodel.task_config.batch_size
65
+
66
+ self.n = imodel.num_heads
67
+ self.h = imodel.head_size
68
+
69
+ # Create an inference task.
70
+ writers = {}
71
+ self.task = trainer.create_training_task(mode, imodel, prngs, writers) # pylint: disable=too-many-function-args
72
+
73
+ # Register any additional actions.
74
+ # Actions are cleared first for use with colab.
75
+ inference_utils.training_loop.clear_interstep_callbacks()
76
+ inference_utils.training_loop.register_interstep_callbacks()
77
+ self.tstate = tstate
78
+
79
+ # some default parameters.
80
+ eos = [0] * 1024
81
+ for idx in self.encode_list(['.', ';']):
82
+ eos[idx] = 1
83
+
84
+ self.eos = np.array(eos, dtype=np.bfloat16)
85
+ self.mask = jax.numpy.ones([1024], dtype=np.bfloat16)
86
+
87
+ def decode(self, ids: list[int]) -> str:
88
+ return self.vocab.decode(ids)
89
+
90
+ def decode_list(self, tokens: list[int]) -> list[str]:
91
+ return [self.decode([tok]) for tok in tokens]
92
+
93
+ def encode(self, inputs_str: str) -> list[int]:
94
+ return self.vocab.encode(inputs_str)
95
+
96
+ def encode_list(self, inputs_strs: list[str]) -> list[int]:
97
+ result = [self.vocab.encode(x) for x in inputs_strs]
98
+ assert all([len(x) == 1 for x in result]), [
99
+ self.decode(x) for x in result if len(x) != 1
100
+ ]
101
+ return [x[0] for x in result]
102
+
103
+ def call(
104
+ self,
105
+ inputs: np.ndarray,
106
+ dstate: tuple[dict[str, np.ndarray], ...] = None,
107
+ eos: np.ndarray = None,
108
+ mask: np.ndarray = None,
109
+ ) -> MetricsOutput:
110
+ """Call the meliad model."""
111
+ batch_size, length = inputs.shape
112
+ inputs = jax.numpy.pad(inputs, [(0, 0), (0, 1024 - length)])
113
+
114
+ if eos is None:
115
+ eos = self.eos
116
+ if mask is None:
117
+ mask = self.mask
118
+
119
+ x = {'targets': inputs, 'length': length, 'eos': eos, 'mask': mask}
120
+
121
+ if dstate is not None:
122
+ x['start_of_sequence'] = jax.numpy.array([False] * batch_size)
123
+ else:
124
+ dstate = tuple(
125
+ [{ # this dummy value will never be used.
126
+ 'current_index': np.array([0] * batch_size, dtype=np.int32),
127
+ 'keys': np.zeros(
128
+ (batch_size, 2048, self.n, self.h), dtype=np.bfloat16
129
+ ),
130
+ 'values': np.zeros(
131
+ (batch_size, 2048, self.n, self.h), dtype=np.bfloat16
132
+ ),
133
+ 'recurrent_kvq': None,
134
+ 'relative_position_bias': np.zeros(
135
+ (batch_size, self.n, 1, 1024), dtype=np.bfloat16
136
+ ),
137
+ }]
138
+ * 12
139
+ )
140
+ x['start_of_sequence'] = jax.numpy.array([True] * batch_size)
141
+
142
+ x['dstate'] = dstate
143
+ _, metrics_np = self.task.run_step(self.tstate, x, 0)
144
+ return metrics_np
145
+
146
+ def beam_decode(
147
+ self,
148
+ inputs: str,
149
+ eos_tokens: np.ndarray = None,
150
+ mask_tokens: np.ndarray = None,
151
+ dstate: dict[str, np.ndarray] = None,
152
+ ) -> MetricsOutput:
153
+ """Beam search."""
154
+ inputs = jax.numpy.array([self.vocab.encode(inputs)] * self.batch_size)
155
+
156
+ eos = self.eos
157
+ if eos_tokens is not None:
158
+ eos_ids = self.encode_list(eos_tokens)
159
+ eos = np.array(
160
+ [1 if idx in eos_ids else 0 for idx in range(1024)], dtype=np.bfloat16
161
+ ).reshape((1, 1, 1024))
162
+
163
+ mask = self.mask
164
+ if mask_tokens is not None:
165
+ mask_ids = self.encode_list(mask_tokens)
166
+ mask = np.array(
167
+ [0 if idx in mask_ids else 1 for idx in range(1024)],
168
+ dtype=np.bfloat16,
169
+ ).reshape((1, 1, 1024))
170
+
171
+ metrics_np = self.call(inputs, dstate=dstate, eos=eos, mask=mask)
172
+
173
+ finished_seqs = metrics_np['finished_seqs']
174
+ finished_scores = metrics_np['finished_scores']
175
+
176
+ seqs = []
177
+ scores = []
178
+ for seq, score in zip(finished_seqs, finished_scores):
179
+ seq = self.decode(seq[1:])
180
+ seqs.append(seq)
181
+ scores.append(score)
182
+
183
+ return {
184
+ 'finished_seqs': finished_seqs,
185
+ 'finished_scores': finished_scores,
186
+ 'seqs_str': seqs,
187
+ 'scores': scores,
188
+ 'dstate': metrics_np['dstate'],
189
+ }
ag4masses/alphageometry/lm_inference_test.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Unit tests for lm_inference.py."""
17
+ import os
18
+ import unittest
19
+
20
+ from absl import flags
21
+ from absl.testing import absltest
22
+ import lm_inference as lm
23
+
24
+
25
+ _DATA_PATH = flags.DEFINE_string('data_path', '', 'path to ckpt and vocab.')
26
+ _MELIAD_PATH = flags.DEFINE_string(
27
+ 'meliad_path', '', 'path to meliad repository.'
28
+ ) # pylint: disable=line-too-long
29
+
30
+
31
+ class LmInferenceTest(unittest.TestCase):
32
+
33
+ @classmethod
34
+ def setUpClass(cls):
35
+ super().setUpClass()
36
+ gin_file = [
37
+ 'base_htrans.gin',
38
+ 'size/medium_150M.gin',
39
+ 'options/positions_t5.gin',
40
+ 'options/lr_cosine_decay.gin',
41
+ 'options/seq_1024_nocache.gin',
42
+ 'geometry_150M_generate.gin',
43
+ ]
44
+
45
+ gin_param = [
46
+ 'DecoderOnlyLanguageModelGenerate.output_token_losses=True',
47
+ 'TransformerTaskConfig.batch_size=2',
48
+ 'TransformerTaskConfig.sequence_length=128',
49
+ 'Trainer.restore_state_variables=False',
50
+ ]
51
+
52
+ gin_search_paths = [
53
+ os.path.join(_MELIAD_PATH.value, 'transformer/configs'),
54
+ os.getcwd(),
55
+ ]
56
+
57
+ vocab_path = os.path.join(_DATA_PATH.value, 'geometry.757.model')
58
+
59
+ lm.parse_gin_configuration(gin_file, gin_param, gin_paths=gin_search_paths)
60
+
61
+ cls.loaded_lm = lm.LanguageModelInference(
62
+ vocab_path, _DATA_PATH.value, mode='beam_search'
63
+ )
64
+
65
+ def test_lm_decode(self):
66
+ outputs = LmInferenceTest.loaded_lm.beam_decode(
67
+ '{S} a : ; b : ; c : ; d : T a b c d 00 T a c b d 01 ? T a d b c'
68
+ ' {F1} x00',
69
+ eos_tokens=[';'],
70
+ )
71
+ self.assertEqual(
72
+ outputs['seqs_str'],
73
+ ['e : D a b c e 02 D a c b e 03 ;', 'e : C a c e 02 C b d e 03 ;'],
74
+ )
75
+
76
+ def test_lm_score_may_fail_numerically_for_external_meliad(self):
77
+ outputs = LmInferenceTest.loaded_lm.beam_decode(
78
+ '{S} a : ; b : ; c : ; d : T a b c d 00 T a c b d 01 ? T a d b c'
79
+ ' {F1} x00',
80
+ eos_tokens=[';'],
81
+ )
82
+ self.assertEqual(
83
+ outputs['scores'],
84
+ [-1.18607294559478759765625, -1.10228693485260009765625],
85
+ )
86
+
87
+
88
+ if __name__ == '__main__':
89
+ absltest.main()
ag4masses/alphageometry/models.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Transformer language model generate mode."""
17
+
18
+ from typing import Any, Tuple
19
+ import beam_search
20
+ import decoder_stack
21
+ import gin
22
+ import jax
23
+ import jax.numpy as jnp
24
+ from transformer import models
25
+
26
+
27
+ @gin.configurable
28
+ class DecoderOnlyLanguageModelGenerate(models.DecoderOnlyLanguageModel):
29
+ """Decoder only language modeling in inference mode."""
30
+
31
+ decoder_factory = decoder_stack.DecoderStackGenerate
32
+
33
+ num_heads: int = gin.REQUIRED
34
+ head_size: int = gin.REQUIRED
35
+
36
+ def get_fake_input(self) -> dict[str, Any]:
37
+ fake_input_dict = super().get_fake_input()
38
+ b = self.task_config.batch_size
39
+ n = self.num_heads
40
+ h = self.head_size
41
+ fake_input_dict.update({
42
+ 'dstate': tuple(
43
+ [{
44
+ 'current_index': jnp.array([0] * b, dtype=jnp.int32),
45
+ 'keys': jnp.zeros((b, 2048, n, h), dtype=jnp.bfloat16),
46
+ 'values': jnp.zeros((b, 2048, n, h), dtype=jnp.bfloat16),
47
+ 'recurrent_kvq': None,
48
+ 'relative_position_bias': jnp.zeros(
49
+ (b, n, 1, 1024), dtype=jnp.bfloat16
50
+ ),
51
+ }]
52
+ * 12
53
+ ),
54
+ 'eos': jnp.zeros([1024], dtype=jnp.bfloat16),
55
+ 'mask': jnp.ones([1024], dtype=jnp.bfloat16),
56
+ 'length': 1,
57
+ 'temperature': 1.0,
58
+ })
59
+ return fake_input_dict
60
+
61
+ def __call__(self, inputs: ...) -> tuple[Any, dict[str, Any]]:
62
+ # Make sure this code is not used on untested cases.
63
+ if self.mode not in ['init', 'beam_search']:
64
+ raise ValueError(f'{type(self)} cannot do mode {self.mode}')
65
+ if self.decoder.supports_generate():
66
+ raise ValueError(f'{type(self)}.decoder cannot supports_generate()')
67
+
68
+ self.decoder(
69
+ input_tokens=inputs['targets'][:, 0:1],
70
+ target_tokens=None,
71
+ start_of_sequence=inputs['start_of_sequence'],
72
+ )
73
+
74
+ b = inputs['targets'].shape[0]
75
+ no_start_of_seq = jnp.array([False] * b, dtype=jnp.bool_)
76
+
77
+ # This fn is used in both beam_search or topk_sampling.
78
+ def tokens_to_logits_fn(
79
+ input_token: jnp.ndarray, dstate: tuple[dict[str, jnp.ndarray], ...]
80
+ ) -> tuple[jnp.ndarray, tuple[dict[str, jnp.ndarray], ...]]:
81
+ (logits, dstate, _) = self.decoder(
82
+ input_tokens=input_token,
83
+ target_tokens=None,
84
+ start_of_sequence=no_start_of_seq,
85
+ decoder_state=dstate,
86
+ )
87
+ return logits[:, -1, :], dstate
88
+
89
+ last_token = jax.lax.dynamic_slice_in_dim(
90
+ inputs['targets'], inputs['length'] - 1, 1, axis=1
91
+ )
92
+
93
+ # last token is used to seed beam_search
94
+ inputs['targets'] = inputs['targets'][:, 0:-1]
95
+ dstate = jax.lax.cond(
96
+ inputs['start_of_sequence'][0],
97
+ lambda: self.generate(inputs)[0],
98
+ lambda: inputs['dstate'],
99
+ )
100
+
101
+ # Then we run beam search, init with last_token & dstate.
102
+ finished_seqs, finished_scores, dstate = beam_search.beam_search_flat(
103
+ last_token,
104
+ dstate,
105
+ tokens_to_logits_fn,
106
+ max_decode_len=512,
107
+ eos=inputs['eos'].reshape((1, 1, -1)),
108
+ mask=inputs['mask'].reshape((1, 1, -1)),
109
+ )
110
+
111
+ return 0.0, {
112
+ 'finished_seqs': finished_seqs,
113
+ 'finished_scores': finished_scores,
114
+ 'dstate': dstate,
115
+ }
116
+
117
+ def generate(
118
+ self, inputs: ...
119
+ ) -> tuple[tuple[dict[str, jnp.ndarray, ...], ...], jnp.ndarray]:
120
+ """Generate an output sequence.
121
+
122
+ Args:
123
+ inputs: the same as argument to _call_.
124
+
125
+ Returns:
126
+ An array of generated tokens of shape (batch_size, sequence_length).
127
+ """
128
+ input_tokens = inputs['targets'] # [b,seq_len]
129
+ start_of_sequence = inputs['start_of_sequence'] # [b]
130
+ target_tokens = jnp.pad(input_tokens[:, 1:], [(0, 0), (0, 1)])
131
+ batch_size = target_tokens.shape[0]
132
+
133
+ # Assuming all sequences start at the same time.
134
+ start0 = inputs['start_of_sequence'][0]
135
+ dstate = jax.lax.cond(
136
+ start0,
137
+ lambda: self.decoder.init_decoder_state_vanilla( # pylint: disable=g-long-lambda
138
+ 1024, start_of_sequence
139
+ ),
140
+ lambda: inputs['dstate'],
141
+ )
142
+
143
+ first_token = input_tokens[:, 0:1]
144
+ no_start_of_seq = jnp.array([False] * batch_size, dtype=jnp.bool_)
145
+ temperature = 1
146
+ if 'temperature' in inputs:
147
+ temperature = inputs['temperature']
148
+
149
+ num_steps = inputs['length']
150
+ if self.mode == 'beam_search':
151
+ num_steps -= 1
152
+
153
+ def cond_fn(scan_state) -> jnp.bool_:
154
+ _, _, i, _ = scan_state
155
+ return i < num_steps
156
+
157
+ def loop_fn(scan_state: Any) -> Tuple[Any, Any, Any, Any]:
158
+ (dstate, input_token, i, _) = scan_state
159
+
160
+ (logits, dstate, _) = self.decoder(
161
+ input_tokens=input_token,
162
+ target_tokens=None,
163
+ start_of_sequence=no_start_of_seq,
164
+ decoder_state=dstate,
165
+ )
166
+
167
+ logits = logits / temperature
168
+ output_token = jax.lax.dynamic_slice_in_dim(target_tokens, i, 1, axis=1)
169
+
170
+ return (dstate, output_token, i + 1, logits)
171
+
172
+ # Scan over the sequence length.
173
+ dummy_logits = jnp.zeros((batch_size, 1, 1024))
174
+ initial_scan_state = (dstate, first_token, 0, dummy_logits)
175
+ dstate, _, _, logits = jax.lax.while_loop(
176
+ cond_fn, loop_fn, initial_scan_state
177
+ )
178
+ return dstate, logits
ag4masses/alphageometry/numericals.py ADDED
@@ -0,0 +1,1923 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Numerical representation of geometry."""
17
+ from __future__ import annotations
18
+
19
+ import math
20
+ from typing import Any, Optional, Union
21
+
22
+ import geometry as gm
23
+ import matplotlib
24
+ from matplotlib import pyplot as plt
25
+ import matplotlib.colors as mcolors
26
+ import numpy as np
27
+ from numpy.random import uniform as unif # pylint: disable=g-importing-member
28
+
29
+
30
+ matplotlib.use('TkAgg')
31
+
32
+
33
+ ATOM = 1e-12
34
+
35
+
36
+ # Some variables are there for better code reading.
37
+ # pylint: disable=unused-assignment
38
+ # pylint: disable=unused-argument
39
+ # pylint: disable=unused-variable
40
+
41
+ # Naming in geometry is a little different
42
+ # we stick to geometry naming to better read the code.
43
+ # pylint: disable=invalid-name
44
+
45
+
46
+ class Point:
47
+ """Numerical point."""
48
+
49
+ def __init__(self, x, y):
50
+ self.x = x
51
+ self.y = y
52
+
53
+ def __lt__(self, other: Point) -> bool:
54
+ return (self.x, self.y) < (other.x, other.y)
55
+
56
+ def __gt__(self, other: Point) -> bool:
57
+ return (self.x, self.y) > (other.x, other.y)
58
+
59
+ def __add__(self, p: Point) -> Point:
60
+ return Point(self.x + p.x, self.y + p.y)
61
+
62
+ def __sub__(self, p: Point) -> Point:
63
+ return Point(self.x - p.x, self.y - p.y)
64
+
65
+ def __mul__(self, f: float) -> Point:
66
+ return Point(self.x * f, self.y * f)
67
+
68
+ def __rmul__(self, f: float) -> Point:
69
+ return self * f
70
+
71
+ def __truediv__(self, f: float) -> Point:
72
+ return Point(self.x / f, self.y / f)
73
+
74
+ def __floordiv__(self, f: float) -> Point:
75
+ div = self / f # true div
76
+ return Point(int(div.x), int(div.y))
77
+
78
+ def __str__(self) -> str:
79
+ return 'P({},{})'.format(self.x, self.y)
80
+
81
+ def close(self, point: Point, tol: float = 1e-12) -> bool:
82
+ return abs(self.x - point.x) < tol and abs(self.y - point.y) < tol
83
+
84
+ def midpoint(self, p: Point) -> Point:
85
+ return Point(0.5 * (self.x + p.x), 0.5 * (self.y + p.y))
86
+
87
+ def distance(self, p: Union[Point, Line, Circle]) -> float:
88
+ if isinstance(p, Line):
89
+ return p.distance(self)
90
+ if isinstance(p, Circle):
91
+ return abs(p.radius - self.distance(p.center))
92
+ dx = self.x - p.x
93
+ dy = self.y - p.y
94
+ return np.sqrt(dx * dx + dy * dy)
95
+
96
+ def distance2(self, p: Point) -> float:
97
+ if isinstance(p, Line):
98
+ return p.distance(self)
99
+ dx = self.x - p.x
100
+ dy = self.y - p.y
101
+ return dx * dx + dy * dy
102
+
103
+ def rotatea(self, ang: float) -> Point:
104
+ sinb, cosb = np.sin(ang), np.cos(ang)
105
+ return self.rotate(sinb, cosb)
106
+
107
+ def rotate(self, sinb: float, cosb: float) -> Point:
108
+ x, y = self.x, self.y
109
+ return Point(x * cosb - y * sinb, x * sinb + y * cosb)
110
+
111
+ def flip(self) -> Point:
112
+ return Point(-self.x, self.y)
113
+
114
+ def perpendicular_line(self, line: Line) -> Line:
115
+ return line.perpendicular_line(self)
116
+
117
+ def foot(self, line: Line) -> Point:
118
+ if isinstance(line, Line):
119
+ l = line.perpendicular_line(self)
120
+ return line_line_intersection(l, line)
121
+ elif isinstance(line, Circle):
122
+ c, r = line.center, line.radius
123
+ return c + (self - c) * r / self.distance(c)
124
+ raise ValueError('Dropping foot to weird type {}'.format(type(line)))
125
+
126
+ def parallel_line(self, line: Line) -> Line:
127
+ return line.parallel_line(self)
128
+
129
+ def norm(self) -> float:
130
+ return np.sqrt(self.x**2 + self.y**2)
131
+
132
+ def cos(self, other: Point) -> float:
133
+ x, y = self.x, self.y
134
+ a, b = other.x, other.y
135
+ return (x * a + y * b) / self.norm() / other.norm()
136
+
137
+ def dot(self, other: Point) -> float:
138
+ return self.x * other.x + self.y * other.y
139
+
140
+ def sign(self, line: Line) -> int:
141
+ return line.sign(self)
142
+
143
+ def is_same(self, other: Point) -> bool:
144
+ return self.distance(other) <= ATOM
145
+
146
+
147
+ class Line:
148
+ """Numerical line."""
149
+
150
+ def __init__(
151
+ self,
152
+ p1: Point = None,
153
+ p2: Point = None,
154
+ coefficients: tuple[int, int, int] = None,
155
+ ):
156
+ if p1 is None and p2 is None and coefficients is None:
157
+ self.coefficients = None, None, None
158
+ return
159
+
160
+ a, b, c = coefficients or (
161
+ p1.y - p2.y,
162
+ p2.x - p1.x,
163
+ p1.x * p2.y - p2.x * p1.y,
164
+ )
165
+
166
+ # Make sure a is always positive (or always negative for that matter)
167
+ # With a == 0, Assuming a = +epsilon > 0
168
+ # Then b such that ax + by = 0 with y>0 should be negative.
169
+ if a < 0.0 or a == 0.0 and b > 0.0:
170
+ a, b, c = -a, -b, -c
171
+
172
+ self.coefficients = a, b, c
173
+
174
+ def parallel_line(self, p: Point) -> Line:
175
+ a, b, _ = self.coefficients
176
+ return Line(coefficients=(a, b, -a * p.x - b * p.y)) # pylint: disable=invalid-unary-operand-type
177
+
178
+ def perpendicular_line(self, p: Point) -> Line:
179
+ a, b, _ = self.coefficients
180
+ return Line(p, p + Point(a, b))
181
+
182
+ def greater_than(self, other: Line) -> bool:
183
+ a, b, _ = self.coefficients
184
+ x, y, _ = other.coefficients
185
+ # b/a > y/x
186
+ return b * x > a * y
187
+
188
+ def __gt__(self, other: Line) -> bool:
189
+ return self.greater_than(other)
190
+
191
+ def __lt__(self, other: Line) -> bool:
192
+ return other.greater_than(self)
193
+
194
+ def same(self, other: Line) -> bool:
195
+ a, b, c = self.coefficients
196
+ x, y, z = other.coefficients
197
+ return close_enough(a * y, b * x) and close_enough(b * z, c * y)
198
+
199
+ def equal(self, other: Line) -> bool:
200
+ a, b, _ = self.coefficients
201
+ x, y, _ = other.coefficients
202
+ # b/a == y/x
203
+ return b * x == a * y
204
+
205
+ def less_than(self, other: Line) -> bool:
206
+ a, b, _ = self.coefficients
207
+ x, y, _ = other.coefficients
208
+ # b/a > y/x
209
+ return b * x < a * y
210
+
211
+ def intersect(self, obj: Union[Line, Circle]) -> tuple[Point, ...]:
212
+ if isinstance(obj, Line):
213
+ return line_line_intersection(self, obj)
214
+ if isinstance(obj, Circle):
215
+ return line_circle_intersection(self, obj)
216
+
217
+ def distance(self, p: Point) -> float:
218
+ a, b, c = self.coefficients
219
+ return abs(self(p.x, p.y)) / math.sqrt(a * a + b * b)
220
+
221
+ def __call__(self, x: Point, y: Point = None) -> float:
222
+ if isinstance(x, Point) and y is None:
223
+ return self(x.x, x.y)
224
+ a, b, c = self.coefficients
225
+ return x * a + y * b + c
226
+
227
+ def is_parallel(self, other: Line) -> bool:
228
+ a, b, _ = self.coefficients
229
+ x, y, _ = other.coefficients
230
+ return abs(a * y - b * x) < ATOM
231
+
232
+ def is_perp(self, other: Line) -> bool:
233
+ a, b, _ = self.coefficients
234
+ x, y, _ = other.coefficients
235
+ return abs(a * x + b * y) < ATOM
236
+
237
+ def cross(self, other: Line) -> float:
238
+ a, b, _ = self.coefficients
239
+ x, y, _ = other.coefficients
240
+ return a * y - b * x
241
+
242
+ def dot(self, other: Line) -> float:
243
+ a, b, _ = self.coefficients
244
+ x, y, _ = other.coefficients
245
+ return a * x + b * y
246
+
247
+ def point_at(self, x: float = None, y: float = None) -> Optional[Point]:
248
+ """Get a point on line closest to (x, y)."""
249
+ a, b, c = self.coefficients
250
+ # ax + by + c = 0
251
+ if x is None and y is not None:
252
+ if a != 0:
253
+ return Point((-c - b * y) / a, y) # pylint: disable=invalid-unary-operand-type
254
+ else:
255
+ return None
256
+ elif x is not None and y is None:
257
+ if b != 0:
258
+ return Point(x, (-c - a * x) / b) # pylint: disable=invalid-unary-operand-type
259
+ else:
260
+ return None
261
+ elif x is not None and y is not None:
262
+ if a * x + b * y + c == 0.0:
263
+ return Point(x, y)
264
+ return None
265
+
266
+ def diff_side(self, p1: Point, p2: Point) -> Optional[bool]:
267
+ d1 = self(p1.x, p1.y)
268
+ d2 = self(p2.x, p2.y)
269
+ if d1 == 0 or d2 == 0:
270
+ return None
271
+ return d1 * d2 < 0
272
+
273
+ def same_side(self, p1: Point, p2: Point) -> Optional[bool]:
274
+ d1 = self(p1.x, p1.y)
275
+ d2 = self(p2.x, p2.y)
276
+ if d1 == 0 or d2 == 0:
277
+ return None
278
+ return d1 * d2 > 0
279
+
280
+ def sign(self, point: Point) -> int:
281
+ s = self(point.x, point.y)
282
+ if s > 0:
283
+ return 1
284
+ elif s < 0:
285
+ return -1
286
+ return 0
287
+
288
+ def is_same(self, other: Line) -> bool:
289
+ a, b, c = self.coefficients
290
+ x, y, z = other.coefficients
291
+ return abs(a * y - b * x) <= ATOM and abs(b * z - c * y) <= ATOM
292
+
293
+ def sample_within(self, points: list[Point], n: int = 5) -> list[Point]:
294
+ """Sample a point within the boundary of points."""
295
+ center = sum(points, Point(0.0, 0.0)) * (1.0 / len(points))
296
+ radius = max([p.distance(center) for p in points])
297
+ if close_enough(center.distance(self), radius):
298
+ center = center.foot(self)
299
+ a, b = line_circle_intersection(self, Circle(center.foot(self), radius))
300
+
301
+ result = None
302
+ best = -1.0
303
+ for _ in range(n):
304
+ rand = unif(0.0, 1.0)
305
+ x = a + (b - a) * rand
306
+ mind = min([x.distance(p) for p in points])
307
+ if mind > best:
308
+ best = mind
309
+ result = x
310
+
311
+ return [result]
312
+
313
+
314
+ class InvalidLineIntersectError(Exception):
315
+ pass
316
+
317
+
318
+ class HalfLine(Line):
319
+ """Numerical ray."""
320
+
321
+ def __init__(self, tail: Point, head: Point): # pylint: disable=super-init-not-called
322
+ self.line = Line(tail, head)
323
+ self.coefficients = self.line.coefficients
324
+ self.tail = tail
325
+ self.head = head
326
+
327
+ def intersect(self, obj: Union[Line, HalfLine, Circle, HoleCircle]) -> Point:
328
+ if isinstance(obj, (HalfLine, Line)):
329
+ return line_line_intersection(self.line, obj)
330
+
331
+ exclude = [self.tail]
332
+ if isinstance(obj, HoleCircle):
333
+ exclude += [obj.hole]
334
+
335
+ a, b = line_circle_intersection(self.line, obj)
336
+ if any([a.close(x) for x in exclude]):
337
+ return b
338
+ if any([b.close(x) for x in exclude]):
339
+ return a
340
+
341
+ v = self.head - self.tail
342
+ va = a - self.tail
343
+ vb = b - self.tail
344
+ if v.dot(va) > 0:
345
+ return a
346
+ if v.dot(vb) > 0:
347
+ return b
348
+ raise InvalidLineIntersectError()
349
+
350
+ def sample_within(self, points: list[Point], n: int = 5) -> list[Point]:
351
+ center = sum(points, Point(0.0, 0.0)) * (1.0 / len(points))
352
+ radius = max([p.distance(center) for p in points])
353
+ if close_enough(center.distance(self.line), radius):
354
+ center = center.foot(self)
355
+ a, b = line_circle_intersection(self, Circle(center.foot(self), radius))
356
+
357
+ if (a - self.tail).dot(self.head - self.tail) > 0:
358
+ a, b = self.tail, a
359
+ else:
360
+ a, b = self.tail, b # pylint: disable=self-assigning-variable
361
+
362
+ result = None
363
+ best = -1.0
364
+ for _ in range(n):
365
+ x = a + (b - a) * unif(0.0, 1.0)
366
+ mind = min([x.distance(p) for p in points])
367
+ if mind > best:
368
+ best = mind
369
+ result = x
370
+
371
+ return [result]
372
+
373
+
374
+ def _perpendicular_bisector(p1: Point, p2: Point) -> Line:
375
+ midpoint = (p1 + p2) * 0.5
376
+ return Line(midpoint, midpoint + Point(p2.y - p1.y, p1.x - p2.x))
377
+
378
+
379
+ def same_sign(
380
+ a: Point, b: Point, c: Point, d: Point, e: Point, f: Point
381
+ ) -> bool:
382
+ a, b, c, d, e, f = map(lambda p: p.sym, [a, b, c, d, e, f])
383
+ ab, cb = a - b, c - b
384
+ de, fe = d - e, f - e
385
+ return (ab.x * cb.y - ab.y * cb.x) * (de.x * fe.y - de.y * fe.x) > 0
386
+
387
+
388
+ class Circle:
389
+ """Numerical circle."""
390
+
391
+ def __init__(
392
+ self,
393
+ center: Optional[Point] = None,
394
+ radius: Optional[float] = None,
395
+ p1: Optional[Point] = None,
396
+ p2: Optional[Point] = None,
397
+ p3: Optional[Point] = None,
398
+ ):
399
+ if not center:
400
+ if not (p1 and p2 and p3):
401
+ self.center = self.radius = self.r2 = None
402
+ return
403
+ # raise ValueError('Circle without center need p1 p2 p3')
404
+
405
+ l12 = _perpendicular_bisector(p1, p2)
406
+ l23 = _perpendicular_bisector(p2, p3)
407
+ center = line_line_intersection(l12, l23)
408
+
409
+ self.center = center
410
+ self.a, self.b = center.x, center.y
411
+
412
+ if not radius:
413
+ if not (p1 or p2 or p3):
414
+ raise ValueError('Circle needs radius or p1 or p2 or p3')
415
+ p = p1 or p2 or p3
416
+ self.r2 = (self.a - p.x) ** 2 + (self.b - p.y) ** 2
417
+ self.radius = math.sqrt(self.r2)
418
+ else:
419
+ self.radius = radius
420
+ self.r2 = radius * radius
421
+
422
+ def intersect(self, obj: Union[Line, Circle]) -> tuple[Point, ...]:
423
+ if isinstance(obj, Line):
424
+ return obj.intersect(self)
425
+ if isinstance(obj, Circle):
426
+ return circle_circle_intersection(self, obj)
427
+
428
+ def sample_within(self, points: list[Point], n: int = 5) -> list[Point]:
429
+ """Sample a point within the boundary of points."""
430
+ result = None
431
+ best = -1.0
432
+ for _ in range(n):
433
+ ang = unif(0.0, 2.0) * np.pi
434
+ x = self.center + Point(np.cos(ang), np.sin(ang)) * self.radius
435
+ mind = min([x.distance(p) for p in points])
436
+ if mind > best:
437
+ best = mind
438
+ result = x
439
+
440
+ return [result]
441
+
442
+
443
+ class HoleCircle(Circle):
444
+ """Numerical circle with a missing point."""
445
+
446
+ def __init__(self, center: Point, radius: float, hole: Point):
447
+ super().__init__(center, radius)
448
+ self.hole = hole
449
+
450
+ def intersect(self, obj: Union[Line, HalfLine, Circle, HoleCircle]) -> Point:
451
+ if isinstance(obj, Line):
452
+ a, b = line_circle_intersection(obj, self)
453
+ if a.close(self.hole):
454
+ return b
455
+ return a
456
+ if isinstance(obj, HalfLine):
457
+ return obj.intersect(self)
458
+ if isinstance(obj, Circle):
459
+ a, b = circle_circle_intersection(obj, self)
460
+ if a.close(self.hole):
461
+ return b
462
+ return a
463
+ if isinstance(obj, HoleCircle):
464
+ a, b = circle_circle_intersection(obj, self)
465
+ if a.close(self.hole) or a.close(obj.hole):
466
+ return b
467
+ return a
468
+
469
+
470
+ def solve_quad(a: float, b: float, c: float) -> tuple[float, float]:
471
+ """Solve a x^2 + bx + c = 0."""
472
+ a = 2 * a
473
+ d = b * b - 2 * a * c
474
+ if d < 0:
475
+ return None # the caller should expect this result.
476
+
477
+ y = math.sqrt(d)
478
+ return (-b - y) / a, (-b + y) / a
479
+
480
+
481
+ def circle_circle_intersection(c1: Circle, c2: Circle) -> tuple[Point, Point]:
482
+ """Returns a pair of Points as intersections of c1 and c2."""
483
+ # circle 1: (x0, y0), radius r0
484
+ # circle 2: (x1, y1), radius r1
485
+ x0, y0, r0 = c1.a, c1.b, c1.radius
486
+ x1, y1, r1 = c2.a, c2.b, c2.radius
487
+
488
+ d = math.sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2)
489
+ if d == 0:
490
+ raise InvalidQuadSolveError()
491
+
492
+ a = (r0**2 - r1**2 + d**2) / (2 * d)
493
+ h = r0**2 - a**2
494
+ if h < 0:
495
+ raise InvalidQuadSolveError()
496
+ h = np.sqrt(h)
497
+ x2 = x0 + a * (x1 - x0) / d
498
+ y2 = y0 + a * (y1 - y0) / d
499
+ x3 = x2 + h * (y1 - y0) / d
500
+ y3 = y2 - h * (x1 - x0) / d
501
+ x4 = x2 - h * (y1 - y0) / d
502
+ y4 = y2 + h * (x1 - x0) / d
503
+
504
+ return Point(x3, y3), Point(x4, y4)
505
+
506
+
507
+ class InvalidQuadSolveError(Exception):
508
+ pass
509
+
510
+
511
+ def line_circle_intersection(line: Line, circle: Circle) -> tuple[Point, Point]:
512
+ """Returns a pair of points as intersections of line and circle."""
513
+ a, b, c = line.coefficients
514
+ r = float(circle.radius)
515
+ center = circle.center
516
+ p, q = center.x, center.y
517
+
518
+ if b == 0:
519
+ x = -c / a
520
+ x_p = x - p
521
+ x_p2 = x_p * x_p
522
+ y = solve_quad(1, -2 * q, q * q + x_p2 - r * r)
523
+ if y is None:
524
+ raise InvalidQuadSolveError()
525
+ y1, y2 = y
526
+ return (Point(x, y1), Point(x, y2))
527
+
528
+ if a == 0:
529
+ y = -c / b
530
+ y_q = y - q
531
+ y_q2 = y_q * y_q
532
+ x = solve_quad(1, -2 * p, p * p + y_q2 - r * r)
533
+ if x is None:
534
+ raise InvalidQuadSolveError()
535
+ x1, x2 = x
536
+ return (Point(x1, y), Point(x2, y))
537
+
538
+ c_ap = c + a * p
539
+ a2 = a * a
540
+ y = solve_quad(
541
+ a2 + b * b, 2 * (b * c_ap - a2 * q), c_ap * c_ap + a2 * (q * q - r * r)
542
+ )
543
+ if y is None:
544
+ raise InvalidQuadSolveError()
545
+ y1, y2 = y
546
+
547
+ return Point(-(b * y1 + c) / a, y1), Point(-(b * y2 + c) / a, y2)
548
+
549
+
550
+ def _check_between(a: Point, b: Point, c: Point) -> bool:
551
+ """Whether a is between b & c."""
552
+ return (a - b).dot(c - b) > 0 and (a - c).dot(b - c) > 0
553
+
554
+
555
+ def circle_segment_intersect(
556
+ circle: Circle, p1: Point, p2: Point
557
+ ) -> list[Point]:
558
+ l = Line(p1, p2)
559
+ px, py = line_circle_intersection(l, circle)
560
+
561
+ result = []
562
+ if _check_between(px, p1, p2):
563
+ result.append(px)
564
+ if _check_between(py, p1, p2):
565
+ result.append(py)
566
+ return result
567
+
568
+
569
+ def line_segment_intersection(l: Line, A: Point, B: Point) -> Point: # pylint: disable=invalid-name
570
+ a, b, c = l.coefficients
571
+ x1, y1, x2, y2 = A.x, A.y, B.x, B.y
572
+ dx, dy = x2 - x1, y2 - y1
573
+ alpha = (-c - a * x1 - b * y1) / (a * dx + b * dy)
574
+ return Point(x1 + alpha * dx, y1 + alpha * dy)
575
+
576
+
577
+ def line_line_intersection(l1: Line, l2: Line) -> Point:
578
+ a1, b1, c1 = l1.coefficients
579
+ a2, b2, c2 = l2.coefficients
580
+ # a1x + b1y + c1 = 0
581
+ # a2x + b2y + c2 = 0
582
+ d = a1 * b2 - a2 * b1
583
+ if d == 0:
584
+ raise InvalidLineIntersectError
585
+ return Point((c2 * b1 - c1 * b2) / d, (c1 * a2 - c2 * a1) / d)
586
+
587
+
588
+ def check_too_close(
589
+ newpoints: list[Point], points: list[Point], tol: int = 0.1
590
+ ) -> bool:
591
+ if not points:
592
+ return False
593
+ avg = sum(points, Point(0.0, 0.0)) * 1.0 / len(points)
594
+ mindist = min([p.distance(avg) for p in points])
595
+ for p0 in newpoints:
596
+ for p1 in points:
597
+ if p0.distance(p1) < tol * mindist:
598
+ return True
599
+ return False
600
+
601
+
602
+ def check_too_far(
603
+ newpoints: list[Point], points: list[Point], tol: int = 4
604
+ ) -> bool:
605
+ if len(points) < 2:
606
+ return False
607
+ avg = sum(points, Point(0.0, 0.0)) * 1.0 / len(points)
608
+ maxdist = max([p.distance(avg) for p in points])
609
+ for p in newpoints:
610
+ if p.distance(avg) > maxdist * tol:
611
+ return True
612
+ return False
613
+
614
+
615
+ def check_aconst(args: list[Point]) -> bool:
616
+ a, b, c, d, num, den = args
617
+ d = d + a - c
618
+ ang = ang_between(a, b, d)
619
+ if ang < 0:
620
+ ang += np.pi
621
+ return close_enough(ang, num * np.pi / den)
622
+
623
+
624
+ def check(name: str, args: list[Union[gm.Point, Point]]) -> bool:
625
+ """Numerical check."""
626
+ if name == 'eqangle6':
627
+ name = 'eqangle'
628
+ elif name == 'eqratio6':
629
+ name = 'eqratio'
630
+ elif name in ['simtri2', 'simtri*']:
631
+ name = 'simtri'
632
+ elif name in ['contri2', 'contri*']:
633
+ name = 'contri'
634
+ elif name == 'para':
635
+ name = 'para_or_coll'
636
+ elif name == 'on_line':
637
+ name = 'coll'
638
+ elif name in ['rcompute', 'acompute']:
639
+ return True
640
+ elif name in ['fixl', 'fixc', 'fixb', 'fixt', 'fixp']:
641
+ return True
642
+
643
+ fn_name = 'check_' + name
644
+ if fn_name not in globals():
645
+ return None
646
+
647
+ fun = globals()['check_' + name]
648
+ args = [p.num if isinstance(p, gm.Point) else p for p in args]
649
+ return fun(args)
650
+
651
+
652
+ def check_circle(points: list[Point]) -> bool:
653
+ if len(points) != 4:
654
+ return False
655
+ o, a, b, c = points
656
+ oa, ob, oc = o.distance(a), o.distance(b), o.distance(c)
657
+ return close_enough(oa, ob) and close_enough(ob, oc)
658
+
659
+
660
+ def check_coll(points: list[Point]) -> bool:
661
+ a, b = points[:2]
662
+ l = Line(a, b)
663
+ for p in points[2:]:
664
+ if abs(l(p.x, p.y)) > ATOM:
665
+ return False
666
+ return True
667
+
668
+
669
+ def check_ncoll(points: list[Point]) -> bool:
670
+ return not check_coll(points)
671
+
672
+
673
+ def check_sameside(points: list[Point]) -> bool:
674
+ b, a, c, y, x, z = points
675
+ # whether b is to the same side of a & c as y is to x & z
676
+ ba = b - a
677
+ bc = b - c
678
+ yx = y - x
679
+ yz = y - z
680
+ return ba.dot(bc) * yx.dot(yz) > 0
681
+
682
+
683
+ def check_para_or_coll(points: list[Point]) -> bool:
684
+ return check_para(points) or check_coll(points)
685
+
686
+
687
+ def check_para(points: list[Point]) -> bool:
688
+ a, b, c, d = points
689
+ ab = Line(a, b)
690
+ cd = Line(c, d)
691
+ if ab.same(cd):
692
+ return False
693
+ return ab.is_parallel(cd)
694
+
695
+
696
+ def check_perp(points: list[Point]) -> bool:
697
+ a, b, c, d = points
698
+ ab = Line(a, b)
699
+ cd = Line(c, d)
700
+ return ab.is_perp(cd)
701
+
702
+
703
+ def check_cyclic(points: list[Point]) -> bool:
704
+ points = list(set(points))
705
+ (a, b, c, *ps) = points
706
+ circle = Circle(p1=a, p2=b, p3=c)
707
+ for d in ps:
708
+ if not close_enough(d.distance(circle.center), circle.radius):
709
+ return False
710
+ return True
711
+
712
+
713
+ def bring_together(
714
+ a: Point, b: Point, c: Point, d: Point
715
+ ) -> tuple[Point, Point, Point, Point]:
716
+ ab = Line(a, b)
717
+ cd = Line(c, d)
718
+ x = line_line_intersection(ab, cd)
719
+ unit = Circle(center=x, radius=1.0)
720
+ y, _ = line_circle_intersection(ab, unit)
721
+ z, _ = line_circle_intersection(cd, unit)
722
+ return x, y, x, z
723
+
724
+
725
+ def same_clock(
726
+ a: Point, b: Point, c: Point, d: Point, e: Point, f: Point
727
+ ) -> bool:
728
+ ba = b - a
729
+ cb = c - b
730
+ ed = e - d
731
+ fe = f - e
732
+ return (ba.x * cb.y - ba.y * cb.x) * (ed.x * fe.y - ed.y * fe.x) > 0
733
+
734
+
735
+ def check_const_angle(points: list[Point]) -> bool:
736
+ """Check if the angle is equal to the given constant."""
737
+ a, b, c, d, m, n = points
738
+ a, b, c, d = bring_together(a, b, c, d)
739
+ ba = b - a
740
+ dc = d - c
741
+
742
+ a3 = np.arctan2(ba.y, ba.x)
743
+ a4 = np.arctan2(dc.y, dc.x)
744
+ y = a3 - a4
745
+
746
+ return close_enough(m / n % 1, y / np.pi % 1)
747
+
748
+
749
+ def check_eqangle(points: list[Point]) -> bool:
750
+ """Check if 8 points make 2 equal angles."""
751
+ a, b, c, d, e, f, g, h = points
752
+
753
+ ab = Line(a, b)
754
+ cd = Line(c, d)
755
+ ef = Line(e, f)
756
+ gh = Line(g, h)
757
+
758
+ if ab.is_parallel(cd):
759
+ return ef.is_parallel(gh)
760
+ if ef.is_parallel(gh):
761
+ return ab.is_parallel(cd)
762
+
763
+ a, b, c, d = bring_together(a, b, c, d)
764
+ e, f, g, h = bring_together(e, f, g, h)
765
+
766
+ ba = b - a
767
+ dc = d - c
768
+ fe = f - e
769
+ hg = h - g
770
+
771
+ sameclock = (ba.x * dc.y - ba.y * dc.x) * (fe.x * hg.y - fe.y * hg.x) > 0
772
+ if not sameclock:
773
+ ba = ba * -1.0
774
+
775
+ a1 = np.arctan2(fe.y, fe.x)
776
+ a2 = np.arctan2(hg.y, hg.x)
777
+ x = a1 - a2
778
+
779
+ a3 = np.arctan2(ba.y, ba.x)
780
+ a4 = np.arctan2(dc.y, dc.x)
781
+ y = a3 - a4
782
+
783
+ xy = (x - y) % (2 * np.pi)
784
+ return close_enough(xy, 0, tol=1e-11) or close_enough(
785
+ xy, 2 * np.pi, tol=1e-11
786
+ )
787
+
788
+
789
+ def check_eqratio(points: list[Point]) -> bool:
790
+ a, b, c, d, e, f, g, h = points
791
+ ab = a.distance(b)
792
+ cd = c.distance(d)
793
+ ef = e.distance(f)
794
+ gh = g.distance(h)
795
+ return close_enough(ab * gh, cd * ef)
796
+
797
+
798
+ def check_cong(points: list[Point]) -> bool:
799
+ a, b, c, d = points
800
+ return close_enough(a.distance(b), c.distance(d))
801
+
802
+
803
+ def check_midp(points: list[Point]) -> bool:
804
+ a, b, c = points
805
+ return check_coll(points) and close_enough(a.distance(b), a.distance(c))
806
+
807
+
808
+ def check_simtri(points: list[Point]) -> bool:
809
+ """Check if 6 points make a pair of similar triangles."""
810
+ a, b, c, x, y, z = points
811
+ ab = a.distance(b)
812
+ bc = b.distance(c)
813
+ ca = c.distance(a)
814
+ xy = x.distance(y)
815
+ yz = y.distance(z)
816
+ zx = z.distance(x)
817
+ tol = 1e-9
818
+ return close_enough(ab * yz, bc * xy, tol) and close_enough(
819
+ bc * zx, ca * yz, tol
820
+ )
821
+
822
+
823
+ def check_contri(points: list[Point]) -> bool:
824
+ a, b, c, x, y, z = points
825
+ ab = a.distance(b)
826
+ bc = b.distance(c)
827
+ ca = c.distance(a)
828
+ xy = x.distance(y)
829
+ yz = y.distance(z)
830
+ zx = z.distance(x)
831
+ tol = 1e-9
832
+ return (
833
+ close_enough(ab, xy, tol)
834
+ and close_enough(bc, yz, tol)
835
+ and close_enough(ca, zx, tol)
836
+ )
837
+
838
+
839
+ def check_ratio(points: list[Point]) -> bool:
840
+ a, b, c, d, m, n = points
841
+ ab = a.distance(b)
842
+ cd = c.distance(d)
843
+ return close_enough(ab * n, cd * m)
844
+
845
+
846
+ def draw_angle(
847
+ ax: matplotlib.axes.Axes,
848
+ head: Point,
849
+ p1: Point,
850
+ p2: Point,
851
+ color: Any = 'red',
852
+ alpha: float = 0.5,
853
+ frac: float = 1.0,
854
+ ) -> None:
855
+ """Draw an angle on plt ax."""
856
+ d1 = p1 - head
857
+ d2 = p2 - head
858
+
859
+ a1 = np.arctan2(float(d1.y), float(d1.x))
860
+ a2 = np.arctan2(float(d2.y), float(d2.x))
861
+ a1, a2 = a1 * 180 / np.pi, a2 * 180 / np.pi
862
+ a1, a2 = a1 % 360, a2 % 360
863
+
864
+ if a1 > a2:
865
+ a1, a2 = a2, a1
866
+
867
+ if a2 - a1 > 180:
868
+ a1, a2 = a2, a1
869
+
870
+ b1, b2 = a1, a2
871
+ if b1 > b2:
872
+ b2 += 360
873
+ d = b2 - b1
874
+ # if d >= 90:
875
+ # return
876
+
877
+ scale = min(2.0, 90 / d)
878
+ scale = max(scale, 0.4)
879
+ fov = matplotlib.patches.Wedge(
880
+ (float(head.x), float(head.y)),
881
+ unif(0.075, 0.125) * scale * frac,
882
+ a1,
883
+ a2,
884
+ color=color,
885
+ alpha=alpha,
886
+ )
887
+ ax.add_artist(fov)
888
+
889
+
890
+ def naming_position(
891
+ ax: matplotlib.axes.Axes, p: Point, lines: list[Line], circles: list[Circle]
892
+ ) -> tuple[float, float]:
893
+ """Figure out a good naming position on the drawing."""
894
+ _ = ax
895
+ r = 0.08
896
+ c = Circle(center=p, radius=r)
897
+ avoid = []
898
+ for p1, p2 in lines:
899
+ try:
900
+ avoid.extend(circle_segment_intersect(c, p1, p2))
901
+ except InvalidQuadSolveError:
902
+ continue
903
+ for x in circles:
904
+ try:
905
+ avoid.extend(circle_circle_intersection(c, x))
906
+ except InvalidQuadSolveError:
907
+ continue
908
+
909
+ if not avoid:
910
+ return [p.x + 0.01, p.y + 0.01]
911
+
912
+ angs = sorted([ang_of(p, a) for a in avoid])
913
+ angs += [angs[0] + 2 * np.pi]
914
+ angs = [(angs[i + 1] - a, a) for i, a in enumerate(angs[:-1])]
915
+
916
+ d, a = max(angs)
917
+ ang = a + d / 2
918
+
919
+ name_pos = p + Point(np.cos(ang), np.sin(ang)) * r
920
+
921
+ x, y = (name_pos.x - r / 1.5, name_pos.y - r / 1.5)
922
+ return x, y
923
+
924
+
925
+ def draw_point(
926
+ ax: matplotlib.axes.Axes,
927
+ p: Point,
928
+ name: str,
929
+ lines: list[Line],
930
+ circles: list[Circle],
931
+ color: Any = 'white',
932
+ size: float = 15,
933
+ ) -> None:
934
+ """draw a point."""
935
+ ax.scatter(p.x, p.y, color=color, s=size)
936
+
937
+ if color == 'white':
938
+ color = 'lightgreen'
939
+ else:
940
+ color = 'grey'
941
+
942
+ name = name.upper()
943
+ if len(name) > 1:
944
+ name = name[0] + '_' + name[1:]
945
+
946
+ ax.annotate(
947
+ name, naming_position(ax, p, lines, circles), color=color, fontsize=15
948
+ )
949
+
950
+
951
+ def _draw_line(
952
+ ax: matplotlib.axes.Axes,
953
+ p1: Point,
954
+ p2: Point,
955
+ color: Any = 'white',
956
+ lw: float = 1.2,
957
+ alpha: float = 0.8,
958
+ ) -> None:
959
+ """Draw a line in matplotlib."""
960
+ ls = '-'
961
+ if color == '--':
962
+ color = 'black'
963
+ ls = '--'
964
+
965
+ lx, ly = (p1.x, p2.x), (p1.y, p2.y)
966
+ ax.plot(lx, ly, color=color, lw=lw, alpha=alpha, ls=ls)
967
+
968
+
969
+ def draw_line(
970
+ ax: matplotlib.axes.Axes, line: Line, color: Any = 'white'
971
+ ) -> tuple[Point, Point]:
972
+ """Draw a line."""
973
+ points = line.neighbors(gm.Point)
974
+ if len(points) <= 1:
975
+ return
976
+
977
+ points = [p.num for p in points]
978
+ p1, p2 = points[:2]
979
+
980
+ pmin, pmax = (p1, 0.0), (p2, (p2 - p1).dot(p2 - p1))
981
+
982
+ for p in points[2:]:
983
+ v = (p - p1).dot(p2 - p1)
984
+ if v < pmin[1]:
985
+ pmin = p, v
986
+ if v > pmax[1]:
987
+ pmax = p, v
988
+
989
+ p1, p2 = pmin[0], pmax[0]
990
+ _draw_line(ax, p1, p2, color=color)
991
+ return p1, p2
992
+
993
+
994
+ def _draw_circle(
995
+ ax: matplotlib.axes.Axes, c: Circle, color: Any = 'cyan', lw: float = 1.2
996
+ ) -> None:
997
+ ls = '-'
998
+ if color == '--':
999
+ color = 'black'
1000
+ ls = '--'
1001
+
1002
+ ax.add_patch(
1003
+ plt.Circle(
1004
+ (c.center.x, c.center.y),
1005
+ c.radius,
1006
+ color=color,
1007
+ alpha=0.8,
1008
+ fill=False,
1009
+ lw=lw,
1010
+ ls=ls,
1011
+ )
1012
+ )
1013
+
1014
+
1015
+ def draw_circle(
1016
+ ax: matplotlib.axes.Axes, circle: Circle, color: Any = 'cyan'
1017
+ ) -> Circle:
1018
+ """Draw a circle."""
1019
+ if circle.num is not None:
1020
+ circle = circle.num
1021
+ else:
1022
+ points = circle.neighbors(gm.Point)
1023
+ if len(points) <= 2:
1024
+ return
1025
+ points = [p.num for p in points]
1026
+ p1, p2, p3 = points[:3]
1027
+ circle = Circle(p1=p1, p2=p2, p3=p3)
1028
+
1029
+ _draw_circle(ax, circle, color)
1030
+ return circle
1031
+
1032
+
1033
+ def mark_segment(
1034
+ ax: matplotlib.axes.Axes, p1: Point, p2: Point, color: Any, alpha: float
1035
+ ) -> None:
1036
+ _ = alpha
1037
+ x, y = (p1.x + p2.x) / 2, (p1.y + p2.y) / 2
1038
+ ax.scatter(x, y, color=color, alpha=1.0, marker='o', s=50)
1039
+
1040
+
1041
+ def highlight_angle(
1042
+ ax: matplotlib.axes.Axes,
1043
+ a: Point,
1044
+ b: Point,
1045
+ c: Point,
1046
+ d: Point,
1047
+ color: Any,
1048
+ alpha: float,
1049
+ ) -> None:
1050
+ """Highlight an angle between ab and cd with (color, alpha)."""
1051
+ try:
1052
+ a, b, c, d = bring_together(a, b, c, d)
1053
+ except: # pylint: disable=bare-except
1054
+ return
1055
+ draw_angle(ax, a, b, d, color=color, alpha=alpha, frac=1.0)
1056
+
1057
+
1058
+ def highlight(
1059
+ ax: matplotlib.axes.Axes,
1060
+ name: str,
1061
+ args: list[gm.Point],
1062
+ lcolor: Any,
1063
+ color1: Any,
1064
+ color2: Any,
1065
+ ) -> None:
1066
+ """Draw highlights."""
1067
+ args = list(map(lambda x: x.num if isinstance(x, gm.Point) else x, args))
1068
+
1069
+ if name == 'cyclic':
1070
+ a, b, c, d = args
1071
+ _draw_circle(ax, Circle(p1=a, p2=b, p3=c), color=color1, lw=2.0)
1072
+ if name == 'coll':
1073
+ a, b, c = args
1074
+ a, b = max(a, b, c), min(a, b, c)
1075
+ _draw_line(ax, a, b, color=color1, lw=2.0)
1076
+ if name == 'para':
1077
+ a, b, c, d = args
1078
+ _draw_line(ax, a, b, color=color1, lw=2.0)
1079
+ _draw_line(ax, c, d, color=color2, lw=2.0)
1080
+ if name == 'eqangle':
1081
+ a, b, c, d, e, f, g, h = args
1082
+
1083
+ x = line_line_intersection(Line(a, b), Line(c, d))
1084
+ if b.distance(x) > a.distance(x):
1085
+ a, b = b, a
1086
+ if d.distance(x) > c.distance(x):
1087
+ c, d = d, c
1088
+ a, b, d = x, a, c
1089
+
1090
+ y = line_line_intersection(Line(e, f), Line(g, h))
1091
+ if f.distance(y) > e.distance(y):
1092
+ e, f = f, e
1093
+ if h.distance(y) > g.distance(y):
1094
+ g, h = h, g
1095
+ e, f, h = y, e, g
1096
+
1097
+ _draw_line(ax, a, b, color=lcolor, lw=2.0)
1098
+ _draw_line(ax, a, d, color=lcolor, lw=2.0)
1099
+ _draw_line(ax, e, f, color=lcolor, lw=2.0)
1100
+ _draw_line(ax, e, h, color=lcolor, lw=2.0)
1101
+ if color1 == '--':
1102
+ color1 = 'red'
1103
+ draw_angle(ax, a, b, d, color=color1, alpha=0.5)
1104
+ if color2 == '--':
1105
+ color2 = 'red'
1106
+ draw_angle(ax, e, f, h, color=color2, alpha=0.5)
1107
+ if name == 'perp':
1108
+ a, b, c, d = args
1109
+ _draw_line(ax, a, b, color=color1, lw=2.0)
1110
+ _draw_line(ax, c, d, color=color1, lw=2.0)
1111
+ if name == 'ratio':
1112
+ a, b, c, d, m, n = args
1113
+ _draw_line(ax, a, b, color=color1, lw=2.0)
1114
+ _draw_line(ax, c, d, color=color2, lw=2.0)
1115
+ if name == 'cong':
1116
+ a, b, c, d = args
1117
+ _draw_line(ax, a, b, color=color1, lw=2.0)
1118
+ _draw_line(ax, c, d, color=color2, lw=2.0)
1119
+ if name == 'midp':
1120
+ m, a, b = args
1121
+ _draw_line(ax, a, m, color=color1, lw=2.0, alpha=0.5)
1122
+ _draw_line(ax, b, m, color=color2, lw=2.0, alpha=0.5)
1123
+ if name == 'eqratio':
1124
+ a, b, c, d, m, n, p, q = args
1125
+ _draw_line(ax, a, b, color=color1, lw=2.0, alpha=0.5)
1126
+ _draw_line(ax, c, d, color=color2, lw=2.0, alpha=0.5)
1127
+ _draw_line(ax, m, n, color=color1, lw=2.0, alpha=0.5)
1128
+ _draw_line(ax, p, q, color=color2, lw=2.0, alpha=0.5)
1129
+
1130
+
1131
+ HCOLORS = None
1132
+
1133
+
1134
+ def _draw(
1135
+ ax: matplotlib.axes.Axes,
1136
+ points: list[gm.Point],
1137
+ lines: list[gm.Line],
1138
+ circles: list[gm.Circle],
1139
+ goal: Any,
1140
+ equals: list[tuple[Any, Any]],
1141
+ highlights: list[tuple[str, list[gm.Point]]],
1142
+ ):
1143
+ """Draw everything."""
1144
+ colors = ['red', 'green', 'blue', 'orange', 'magenta', 'purple']
1145
+ pcolor = 'black'
1146
+ lcolor = 'black'
1147
+ ccolor = 'grey'
1148
+ if get_theme() == 'dark':
1149
+ pcolor, lcolor, ccolor = 'white', 'white', 'cyan'
1150
+ elif get_theme() == 'light':
1151
+ pcolor, lcolor, ccolor = 'black', 'black', 'blue'
1152
+ elif get_theme() == 'grey':
1153
+ pcolor, lcolor, ccolor = 'black', 'black', 'grey'
1154
+ colors = ['grey']
1155
+
1156
+ line_boundaries = []
1157
+ for l in lines:
1158
+ p1, p2 = draw_line(ax, l, color=lcolor)
1159
+ line_boundaries.append((p1, p2))
1160
+ circles = [draw_circle(ax, c, color=ccolor) for c in circles]
1161
+
1162
+ for p in points:
1163
+ draw_point(ax, p.num, p.name, line_boundaries, circles, color=pcolor)
1164
+
1165
+ if equals:
1166
+ for i, segs in enumerate(equals['segments']):
1167
+ color = colors[i % len(colors)]
1168
+ for a, b in segs:
1169
+ mark_segment(ax, a, b, color, 0.5)
1170
+
1171
+ for i, angs in enumerate(equals['angles']):
1172
+ color = colors[i % len(colors)]
1173
+ for a, b, c, d in angs:
1174
+ highlight_angle(ax, a, b, c, d, color, 0.5)
1175
+
1176
+ if highlights:
1177
+ global HCOLORS
1178
+ if HCOLORS is None:
1179
+ HCOLORS = [k for k in mcolors.TABLEAU_COLORS.keys() if 'red' not in k]
1180
+
1181
+ for i, (name, args) in enumerate(highlights):
1182
+ color_i = HCOLORS[i % len(HCOLORS)]
1183
+ highlight(ax, name, args, 'black', color_i, color_i)
1184
+
1185
+ if goal:
1186
+ name, args = goal
1187
+ lcolor = color1 = color2 = 'red'
1188
+ highlight(ax, name, args, lcolor, color1, color2)
1189
+
1190
+
1191
+ THEME = 'dark'
1192
+
1193
+
1194
+ def set_theme(theme) -> None:
1195
+ global THEME
1196
+ THEME = theme
1197
+
1198
+
1199
+ def get_theme() -> str:
1200
+ return THEME
1201
+
1202
+
1203
+ def draw(
1204
+ points: list[gm.Point],
1205
+ lines: list[gm.Line],
1206
+ circles: list[gm.Circle],
1207
+ segments: list[gm.Segment],
1208
+ goal: Any = None,
1209
+ highlights: list[tuple[str, list[gm.Point]]] = None,
1210
+ equals: list[tuple[Any, Any]] = None,
1211
+ block: bool = True,
1212
+ save_to: str = None,
1213
+ theme: str = 'light',
1214
+ ) -> None:
1215
+ """Draw everything on the same canvas."""
1216
+ plt.close()
1217
+ imsize = 512 / 100
1218
+ fig, ax = plt.subplots(figsize=(imsize, imsize), dpi=100)
1219
+
1220
+ set_theme(theme)
1221
+
1222
+ if get_theme() == 'dark':
1223
+ ax.set_facecolor((0.0, 0.0, 0.0))
1224
+ else:
1225
+ ax.set_facecolor((1.0, 1.0, 1.0))
1226
+
1227
+ _draw(ax, points, lines, circles, goal, equals, highlights)
1228
+
1229
+ plt.axis('equal')
1230
+ fig.subplots_adjust(left=0, right=1, top=1, bottom=0, wspace=0, hspace=0)
1231
+ if points:
1232
+ xmin = min([p.num.x for p in points])
1233
+ xmax = max([p.num.x for p in points])
1234
+ ymin = min([p.num.y for p in points])
1235
+ ymax = max([p.num.y for p in points])
1236
+ plt.margins((xmax - xmin) * 0.1, (ymax - ymin) * 0.1)
1237
+ if save_to:
1238
+ plt.savefig(save_to)
1239
+ # plt.show(block=block)
1240
+
1241
+
1242
+
1243
+ def close_enough(a: float, b: float, tol: float = 1e-12) -> bool:
1244
+ return abs(a - b) < tol
1245
+
1246
+
1247
+ def assert_close_enough(a: float, b: float, tol: float = 1e-12) -> None:
1248
+ assert close_enough(a, b, tol), f'|{a}-{b}| = {abs(a-b)} >= {tol}'
1249
+
1250
+
1251
+ def ang_of(tail: Point, head: Point) -> float:
1252
+ vector = head - tail
1253
+ arctan = np.arctan2(vector.y, vector.x) % (2 * np.pi)
1254
+ return arctan
1255
+
1256
+
1257
+ def ang_between(tail: Point, head1: Point, head2: Point) -> float:
1258
+ ang1 = ang_of(tail, head1)
1259
+ ang2 = ang_of(tail, head2)
1260
+ diff = ang1 - ang2
1261
+ # return diff % (2*np.pi)
1262
+ if diff > np.pi:
1263
+ return diff - 2 * np.pi
1264
+ if diff < -np.pi:
1265
+ return 2 * np.pi + diff
1266
+ return diff
1267
+
1268
+
1269
+ def head_from(tail: Point, ang: float, length: float = 1) -> Point:
1270
+ vector = Point(np.cos(ang) * length, np.sin(ang) * length)
1271
+ return tail + vector
1272
+
1273
+
1274
+ def random_points(n: int = 3) -> list[Point]:
1275
+ return [Point(unif(-1, 1), unif(-1, 1)) for _ in range(n)]
1276
+
1277
+
1278
+ def random_rfss(*points: list[Point]) -> list[Point]:
1279
+ """Random rotate-flip-scale-shift a point cloud."""
1280
+ # center point cloud.
1281
+ average = sum(points, Point(0.0, 0.0)) * (1.0 / len(points))
1282
+ points = [p - average for p in points]
1283
+
1284
+ # rotate
1285
+ ang = unif(0.0, 2 * np.pi)
1286
+ sin, cos = np.sin(ang), np.cos(ang)
1287
+ # scale and shift
1288
+ scale = unif(0.5, 2.0)
1289
+ shift = Point(unif(-1, 1), unif(-1, 1))
1290
+ points = [p.rotate(sin, cos) * scale + shift for p in points]
1291
+
1292
+ # randomly flip
1293
+ if np.random.rand() < 0.5:
1294
+ points = [p.flip() for p in points]
1295
+
1296
+ return points
1297
+
1298
+
1299
+ def reduce(
1300
+ objs: list[Union[Point, Line, Circle, HalfLine, HoleCircle]],
1301
+ existing_points: list[Point],
1302
+ ) -> list[Point]:
1303
+ """Reduce intersecting objects into one point of intersections."""
1304
+ if all(isinstance(o, Point) for o in objs):
1305
+ return objs
1306
+
1307
+ elif len(objs) == 1:
1308
+ return objs[0].sample_within(existing_points)
1309
+
1310
+ elif len(objs) == 2:
1311
+ a, b = objs
1312
+ result = a.intersect(b)
1313
+ if isinstance(result, Point):
1314
+ return [result]
1315
+ a, b = result
1316
+ a_close = any([a.close(x) for x in existing_points])
1317
+ if a_close:
1318
+ return [b]
1319
+ b_close = any([b.close(x) for x in existing_points])
1320
+ if b_close:
1321
+ return [a]
1322
+ return [np.random.choice([a, b])]
1323
+
1324
+ else:
1325
+ raise ValueError(f'Cannot reduce {objs}')
1326
+
1327
+
1328
+ def sketch(
1329
+ name: str, args: list[Union[Point, gm.Point]]
1330
+ ) -> list[Union[Point, Line, Circle, HalfLine, HoleCircle]]:
1331
+ fun = globals()['sketch_' + name]
1332
+ args = [p.num if isinstance(p, gm.Point) else p for p in args]
1333
+ out = fun(args)
1334
+
1335
+ # out can be one or multiple {Point/Line/HalfLine}
1336
+ if isinstance(out, (tuple, list)):
1337
+ return list(out)
1338
+ return [out]
1339
+
1340
+
1341
+ def sketch_on_opline(args: tuple[gm.Point, ...]) -> HalfLine:
1342
+ a, b = args
1343
+ return HalfLine(a, a + a - b)
1344
+
1345
+
1346
+ def sketch_on_hline(args: tuple[gm.Point, ...]) -> HalfLine:
1347
+ a, b = args
1348
+ return HalfLine(a, b)
1349
+
1350
+
1351
+ def sketch_ieq_triangle(args: tuple[gm.Point, ...]) -> tuple[Point, ...]:
1352
+ a = Point(0.0, 0.0)
1353
+ b = Point(1.0, 0.0)
1354
+
1355
+ c, _ = Circle(a, p1=b).intersect(Circle(b, p1=a))
1356
+ return a, b, c
1357
+
1358
+
1359
+ def sketch_incenter2(args: tuple[gm.Point, ...]) -> tuple[Point, ...]:
1360
+ a, b, c = args
1361
+ l1 = sketch_bisect([b, a, c])
1362
+ l2 = sketch_bisect([a, b, c])
1363
+ i = line_line_intersection(l1, l2)
1364
+ x = i.foot(Line(b, c))
1365
+ y = i.foot(Line(c, a))
1366
+ z = i.foot(Line(a, b))
1367
+ return x, y, z, i
1368
+
1369
+
1370
+ def sketch_excenter2(args: tuple[gm.Point, ...]) -> tuple[Point, ...]:
1371
+ a, b, c = args
1372
+ l1 = sketch_bisect([b, a, c])
1373
+ l2 = sketch_exbisect([a, b, c])
1374
+ i = line_line_intersection(l1, l2)
1375
+ x = i.foot(Line(b, c))
1376
+ y = i.foot(Line(c, a))
1377
+ z = i.foot(Line(a, b))
1378
+ return x, y, z, i
1379
+
1380
+
1381
+ def sketch_centroid(args: tuple[gm.Point, ...]) -> tuple[Point, ...]:
1382
+ a, b, c = args
1383
+ x = (b + c) * 0.5
1384
+ y = (c + a) * 0.5
1385
+ z = (a + b) * 0.5
1386
+ i = line_line_intersection(Line(a, x), Line(b, y))
1387
+ return x, y, z, i
1388
+
1389
+
1390
+ def sketch_ninepoints(args: tuple[gm.Point, ...]) -> tuple[Point, ...]:
1391
+ a, b, c = args
1392
+ x = (b + c) * 0.5
1393
+ y = (c + a) * 0.5
1394
+ z = (a + b) * 0.5
1395
+ c = Circle(p1=x, p2=y, p3=z)
1396
+ return x, y, z, c.center
1397
+
1398
+
1399
+ def sketch_2l1c(args: tuple[gm.Point, ...]) -> tuple[Point, ...]:
1400
+ """Sketch a circle touching two lines and another circle."""
1401
+ a, b, c, p = args
1402
+ bc, ac = Line(b, c), Line(a, c)
1403
+ circle = Circle(p, p1=a)
1404
+
1405
+ d, d_ = line_circle_intersection(p.perpendicular_line(bc), circle)
1406
+ if bc.diff_side(d_, a):
1407
+ d = d_
1408
+
1409
+ e, e_ = line_circle_intersection(p.perpendicular_line(ac), circle)
1410
+ if ac.diff_side(e_, b):
1411
+ e = e_
1412
+
1413
+ df = d.perpendicular_line(Line(p, d))
1414
+ ef = e.perpendicular_line(Line(p, e))
1415
+ f = line_line_intersection(df, ef)
1416
+
1417
+ g, g_ = line_circle_intersection(Line(c, f), circle)
1418
+ if bc.same_side(g_, a):
1419
+ g = g_
1420
+
1421
+ b_ = c + (b - c) / b.distance(c)
1422
+ a_ = c + (a - c) / a.distance(c)
1423
+ m = (a_ + b_) * 0.5
1424
+ x = line_line_intersection(Line(c, m), Line(p, g))
1425
+ return x.foot(ac), x.foot(bc), g, x
1426
+
1427
+
1428
+ def sketch_3peq(args: tuple[gm.Point, ...]) -> tuple[Point, ...]:
1429
+ a, b, c = args
1430
+ ab, bc, ca = Line(a, b), Line(b, c), Line(c, a)
1431
+
1432
+ z = b + (c - b) * np.random.uniform(-0.5, 1.5)
1433
+
1434
+ z_ = z * 2 - c
1435
+ l = z_.parallel_line(ca)
1436
+ x = line_line_intersection(l, ab)
1437
+ y = z * 2 - x
1438
+ return x, y, z
1439
+
1440
+
1441
+ def try_to_sketch_intersect(
1442
+ name1: str,
1443
+ args1: list[Union[gm.Point, Point]],
1444
+ name2: str,
1445
+ args2: list[Union[gm.Point, Point]],
1446
+ existing_points: list[Point],
1447
+ ) -> Optional[Point]:
1448
+ """Try to sketch an intersection between two objects."""
1449
+ obj1 = sketch(name1, args1)[0]
1450
+ obj2 = sketch(name2, args2)[0]
1451
+
1452
+ if isinstance(obj1, Line) and isinstance(obj2, Line):
1453
+ fn = line_line_intersection
1454
+ elif isinstance(obj1, Circle) and isinstance(obj2, Circle):
1455
+ fn = circle_circle_intersection
1456
+ else:
1457
+ fn = line_circle_intersection
1458
+ if isinstance(obj2, Line) and isinstance(obj1, Circle):
1459
+ obj1, obj2 = obj2, obj1
1460
+
1461
+ try:
1462
+ x = fn(obj1, obj2)
1463
+ except: # pylint: disable=bare-except
1464
+ return None
1465
+
1466
+ if isinstance(x, Point):
1467
+ return x
1468
+
1469
+ x1, x2 = x
1470
+
1471
+ close1 = check_too_close([x1], existing_points)
1472
+ far1 = check_too_far([x1], existing_points)
1473
+ if not close1 and not far1:
1474
+ return x1
1475
+ close2 = check_too_close([x2], existing_points)
1476
+ far2 = check_too_far([x2], existing_points)
1477
+ if not close2 and not far2:
1478
+ return x2
1479
+
1480
+ return None
1481
+
1482
+
1483
+ def sketch_acircle(args: tuple[gm.Point, ...]) -> Circle:
1484
+ a, b, c, d, f = args
1485
+ de = sketch_aline([c, a, b, f, d])
1486
+ fe = sketch_aline([a, c, b, d, f])
1487
+ e = line_line_intersection(de, fe)
1488
+ return Circle(p1=d, p2=e, p3=f)
1489
+
1490
+
1491
+ def sketch_aline(args: tuple[gm.Point, ...]) -> HalfLine:
1492
+ """Sketch the construction aline."""
1493
+ A, B, C, D, E = args
1494
+ ab = A - B
1495
+ cb = C - B
1496
+ de = D - E
1497
+
1498
+ dab = A.distance(B)
1499
+ ang_ab = np.arctan2(ab.y / dab, ab.x / dab)
1500
+
1501
+ dcb = C.distance(B)
1502
+ ang_bc = np.arctan2(cb.y / dcb, cb.x / dcb)
1503
+
1504
+ dde = D.distance(E)
1505
+ ang_de = np.arctan2(de.y / dde, de.x / dde)
1506
+
1507
+ ang_ex = ang_de + ang_bc - ang_ab
1508
+ X = E + Point(np.cos(ang_ex), np.sin(ang_ex))
1509
+ return HalfLine(E, X)
1510
+
1511
+
1512
+ def sketch_amirror(args: tuple[gm.Point, ...]) -> HalfLine:
1513
+ """Sketch the angle mirror."""
1514
+ A, B, C = args # pylint: disable=invalid-name
1515
+ ab = A - B
1516
+ cb = C - B
1517
+
1518
+ dab = A.distance(B)
1519
+ ang_ab = np.arctan2(ab.y / dab, ab.x / dab)
1520
+ dcb = C.distance(B)
1521
+ ang_bc = np.arctan2(cb.y / dcb, cb.x / dcb)
1522
+
1523
+ ang_bx = 2 * ang_bc - ang_ab
1524
+ X = B + Point(np.cos(ang_bx), np.sin(ang_bx)) # pylint: disable=invalid-name
1525
+ return HalfLine(B, X)
1526
+
1527
+
1528
+ def sketch_bisect(args: tuple[gm.Point, ...]) -> Line:
1529
+ a, b, c = args
1530
+ ab = a.distance(b)
1531
+ bc = b.distance(c)
1532
+ x = b + (c - b) * (ab / bc)
1533
+ m = (a + x) * 0.5
1534
+ return Line(b, m)
1535
+
1536
+
1537
+ def sketch_exbisect(args: tuple[gm.Point, ...]) -> Line:
1538
+ a, b, c = args
1539
+ return sketch_bisect(args).perpendicular_line(b)
1540
+
1541
+
1542
+ def sketch_bline(args: tuple[gm.Point, ...]) -> Line:
1543
+ a, b = args
1544
+ m = (a + b) * 0.5
1545
+ return m.perpendicular_line(Line(a, b))
1546
+
1547
+
1548
+ def sketch_dia(args: tuple[gm.Point, ...]) -> Circle:
1549
+ a, b = args
1550
+ return Circle((a + b) * 0.5, p1=a)
1551
+
1552
+
1553
+ def sketch_tangent(args: tuple[gm.Point, ...]) -> tuple[Point, Point]:
1554
+ a, o, b = args
1555
+ dia = sketch_dia([a, o])
1556
+ return circle_circle_intersection(Circle(o, p1=b), dia)
1557
+
1558
+
1559
+ def sketch_circle(args: tuple[gm.Point, ...]) -> Circle:
1560
+ a, b, c = args
1561
+ return Circle(center=a, radius=b.distance(c))
1562
+
1563
+
1564
+ def sketch_cc_tangent(args: tuple[gm.Point, ...]) -> tuple[Point, ...]:
1565
+ """Sketch tangents to two circles."""
1566
+ o, a, w, b = args
1567
+ ra, rb = o.distance(a), w.distance(b)
1568
+
1569
+ ow = Line(o, w)
1570
+ if close_enough(ra, rb):
1571
+ oo = ow.perpendicular_line(o)
1572
+ oa = Circle(o, ra)
1573
+ x, z = line_circle_intersection(oo, oa)
1574
+ y = x + w - o
1575
+ t = z + w - o
1576
+ return x, y, z, t
1577
+
1578
+ swap = rb > ra
1579
+ if swap:
1580
+ o, a, w, b = w, b, o, a
1581
+ ra, rb = rb, ra
1582
+
1583
+ oa = Circle(o, ra)
1584
+ q = o + (w - o) * ra / (ra - rb)
1585
+
1586
+ x, z = circle_circle_intersection(sketch_dia([o, q]), oa)
1587
+ y = w.foot(Line(x, q))
1588
+ t = w.foot(Line(z, q))
1589
+
1590
+ if swap:
1591
+ x, y, z, t = y, x, t, z
1592
+
1593
+ return x, y, z, t
1594
+
1595
+
1596
+ def sketch_hcircle(args: tuple[gm.Point, ...]) -> HoleCircle:
1597
+ a, b = args
1598
+ return HoleCircle(center=a, radius=a.distance(b), hole=b)
1599
+
1600
+
1601
+ def sketch_e5128(args: tuple[gm.Point, ...]) -> tuple[Point, Point]:
1602
+ a, b, c, d = args
1603
+ ad = Line(a, d)
1604
+
1605
+ g = (a + b) * 0.5
1606
+ de = Line(d, g)
1607
+
1608
+ e, f = line_circle_intersection(de, Circle(c, p1=b))
1609
+
1610
+ if e.distance(d) < f.distance(d):
1611
+ e = f
1612
+ return e, g
1613
+
1614
+
1615
+ def sketch_eq_quadrangle(args: tuple[gm.Point, ...]) -> tuple[Point, ...]:
1616
+ """Sketch quadrangle with two equal opposite sides."""
1617
+ a = Point(0.0, 0.0)
1618
+ b = Point(1.0, 0.0)
1619
+
1620
+ length = np.random.uniform(0.5, 2.0)
1621
+ ang = np.random.uniform(np.pi / 3, np.pi * 2 / 3)
1622
+ d = head_from(a, ang, length)
1623
+
1624
+ ang = ang_of(b, d)
1625
+ ang = np.random.uniform(ang / 10, ang / 9)
1626
+ c = head_from(b, ang, length)
1627
+ a, b, c, d = random_rfss(a, b, c, d)
1628
+ return a, b, c, d
1629
+
1630
+
1631
+ def sketch_eq_trapezoid(args: tuple[gm.Point, ...]) -> tuple[Point, ...]:
1632
+ a = Point(0.0, 0.0)
1633
+ b = Point(1.0, 0.0)
1634
+ l = unif(0.5, 2.0)
1635
+
1636
+ height = unif(0.5, 2.0)
1637
+ c = Point(0.5 + l / 2.0, height)
1638
+ d = Point(0.5 - l / 2.0, height)
1639
+
1640
+ a, b, c, d = random_rfss(a, b, c, d)
1641
+ return a, b, c, d
1642
+
1643
+
1644
+ def sketch_eqangle2(args: tuple[gm.Point, ...]) -> Point:
1645
+ """Sketch the def eqangle2."""
1646
+ a, b, c = args
1647
+
1648
+ d = c * 2 - b
1649
+
1650
+ ba = b.distance(a)
1651
+ bc = b.distance(c)
1652
+ l = ba * ba / bc
1653
+
1654
+ if unif(0.0, 1.0) < 0.5:
1655
+ be = min(l, bc)
1656
+ be = unif(be * 0.1, be * 0.9)
1657
+ else:
1658
+ be = max(l, bc)
1659
+ be = unif(be * 1.1, be * 1.5)
1660
+
1661
+ e = b + (c - b) * (be / bc)
1662
+ y = b + (a - b) * (be / l)
1663
+ return line_line_intersection(Line(c, y), Line(a, e))
1664
+
1665
+
1666
+ def sketch_eqangle3(args: tuple[gm.Point, ...]) -> Circle:
1667
+ a, b, d, e, f = args
1668
+ de = d.distance(e)
1669
+ ef = e.distance(f)
1670
+ ab = b.distance(a)
1671
+ ang_ax = ang_of(a, b) + ang_between(e, d, f)
1672
+ x = head_from(a, ang_ax, length=de / ef * ab)
1673
+ return Circle(p1=a, p2=b, p3=x)
1674
+
1675
+
1676
+ def sketch_eqdia_quadrangle(args: tuple[gm.Point, ...]) -> tuple[Point, ...]:
1677
+ """Sketch quadrangle with two equal diagonals."""
1678
+ m = unif(0.3, 0.7)
1679
+ n = unif(0.3, 0.7)
1680
+ a = Point(-m, 0.0)
1681
+ c = Point(1 - m, 0.0)
1682
+ b = Point(0.0, -n)
1683
+ d = Point(0.0, 1 - n)
1684
+
1685
+ ang = unif(-0.25 * np.pi, 0.25 * np.pi)
1686
+ sin, cos = np.sin(ang), np.cos(ang)
1687
+ b = b.rotate(sin, cos)
1688
+ d = d.rotate(sin, cos)
1689
+ a, b, c, d = random_rfss(a, b, c, d)
1690
+ return a, b, c, d
1691
+
1692
+
1693
+ def sketch_free(args: tuple[gm.Point, ...]) -> Point:
1694
+ return random_points(1)[0]
1695
+
1696
+
1697
+ def sketch_isos(args: tuple[gm.Point, ...]) -> tuple[Point, ...]:
1698
+ base = unif(0.5, 1.5)
1699
+ height = unif(0.5, 1.5)
1700
+
1701
+ b = Point(-base / 2, 0.0)
1702
+ c = Point(base / 2, 0.0)
1703
+ a = Point(0.0, height)
1704
+ a, b, c = random_rfss(a, b, c)
1705
+ return a, b, c
1706
+
1707
+
1708
+ def sketch_line(args: tuple[gm.Point, ...]) -> Line:
1709
+ a, b = args
1710
+ return Line(a, b)
1711
+
1712
+
1713
+ def sketch_cyclic(args: tuple[gm.Point, ...]) -> Circle:
1714
+ a, b, c = args
1715
+ return Circle(p1=a, p2=b, p3=c)
1716
+
1717
+
1718
+ def sketch_hline(args: tuple[gm.Point, ...]) -> HalfLine:
1719
+ a, b = args
1720
+ return HalfLine(a, b)
1721
+
1722
+
1723
+ def sketch_midp(args: tuple[gm.Point, ...]) -> Point:
1724
+ a, b = args
1725
+ return (a + b) * 0.5
1726
+
1727
+
1728
+ def sketch_pentagon(args: tuple[gm.Point, ...]) -> tuple[Point, ...]:
1729
+ points = [Point(1.0, 0.0)]
1730
+ ang = 0.0
1731
+
1732
+ for i in range(4):
1733
+ ang += (2 * np.pi - ang) / (5 - i) * unif(0.5, 1.5)
1734
+ point = Point(np.cos(ang), np.sin(ang))
1735
+ points.append(point)
1736
+
1737
+ a, b, c, d, e = points # pylint: disable=unbalanced-tuple-unpacking
1738
+ a, b, c, d, e = random_rfss(a, b, c, d, e)
1739
+ return a, b, c, d, e
1740
+
1741
+
1742
+ def sketch_pline(args: tuple[gm.Point, ...]) -> Line:
1743
+ a, b, c = args
1744
+ return a.parallel_line(Line(b, c))
1745
+
1746
+
1747
+ def sketch_pmirror(args: tuple[gm.Point, ...]) -> Point:
1748
+ a, b = args
1749
+ return b * 2 - a
1750
+
1751
+
1752
+ def sketch_quadrangle(args: tuple[gm.Point, ...]) -> tuple[Point, ...]:
1753
+ """Sketch a random quadrangle."""
1754
+ m = unif(0.3, 0.7)
1755
+ n = unif(0.3, 0.7)
1756
+
1757
+ a = Point(-m, 0.0)
1758
+ c = Point(1 - m, 0.0)
1759
+ b = Point(0.0, -unif(0.25, 0.75))
1760
+ d = Point(0.0, unif(0.25, 0.75))
1761
+
1762
+ ang = unif(-0.25 * np.pi, 0.25 * np.pi)
1763
+ sin, cos = np.sin(ang), np.cos(ang)
1764
+ b = b.rotate(sin, cos)
1765
+ d = d.rotate(sin, cos)
1766
+ a, b, c, d = random_rfss(a, b, c, d)
1767
+ return a, b, c, d
1768
+
1769
+
1770
+ def sketch_r_trapezoid(args: tuple[gm.Point, ...]) -> tuple[Point, ...]:
1771
+ a = Point(0.0, 1.0)
1772
+ d = Point(0.0, 0.0)
1773
+ b = Point(unif(0.5, 1.5), 1.0)
1774
+ c = Point(unif(0.5, 1.5), 0.0)
1775
+ a, b, c, d = random_rfss(a, b, c, d)
1776
+ return a, b, c, d
1777
+
1778
+
1779
+ def sketch_r_triangle(args: tuple[gm.Point, ...]) -> tuple[Point, ...]:
1780
+ a = Point(0.0, 0.0)
1781
+ b = Point(0.0, unif(0.5, 2.0))
1782
+ c = Point(unif(0.5, 2.0), 0.0)
1783
+ a, b, c = random_rfss(a, b, c)
1784
+ return a, b, c
1785
+
1786
+
1787
+ def sketch_rectangle(args: tuple[gm.Point, ...]) -> tuple[Point, ...]:
1788
+ a = Point(0.0, 0.0)
1789
+ b = Point(0.0, 1.0)
1790
+ l = unif(0.5, 2.0)
1791
+ c = Point(l, 1.0)
1792
+ d = Point(l, 0.0)
1793
+ a, b, c, d = random_rfss(a, b, c, d)
1794
+ return a, b, c, d
1795
+
1796
+
1797
+ def sketch_reflect(args: tuple[gm.Point, ...]) -> Point:
1798
+ a, b, c = args
1799
+ m = a.foot(Line(b, c))
1800
+ return m * 2 - a
1801
+
1802
+
1803
+ def sketch_risos(args: tuple[gm.Point, ...]) -> tuple[Point, ...]:
1804
+ a = Point(0.0, 0.0)
1805
+ b = Point(0.0, 1.0)
1806
+ c = Point(1.0, 0.0)
1807
+ a, b, c = random_rfss(a, b, c)
1808
+ return a, b, c
1809
+
1810
+
1811
+ def sketch_rotaten90(args: tuple[gm.Point, ...]) -> Point:
1812
+ a, b = args
1813
+ ang = -np.pi / 2
1814
+ return a + (b - a).rotate(np.sin(ang), np.cos(ang))
1815
+
1816
+
1817
+ def sketch_rotatep90(args: tuple[gm.Point, ...]) -> Point:
1818
+ a, b = args
1819
+ ang = np.pi / 2
1820
+ return a + (b - a).rotate(np.sin(ang), np.cos(ang))
1821
+
1822
+
1823
+ def sketch_s_angle(args: tuple[gm.Point, ...]) -> HalfLine:
1824
+ a, b, y = args
1825
+ ang = y / 180 * np.pi
1826
+ x = b + (a - b).rotatea(ang)
1827
+ return HalfLine(b, x)
1828
+
1829
+
1830
+ def sketch_segment(args: tuple[gm.Point, ...]) -> tuple[Point, Point]:
1831
+ a, b = random_points(2)
1832
+ return a, b
1833
+
1834
+
1835
+ def sketch_shift(args: tuple[gm.Point, ...]) -> Point:
1836
+ a, b, c = args
1837
+ return c + (b - a)
1838
+
1839
+
1840
+ def sketch_square(args: tuple[gm.Point, ...]) -> tuple[Point, Point]:
1841
+ a, b = args
1842
+ c = b + (a - b).rotatea(-np.pi / 2)
1843
+ d = a + (b - a).rotatea(np.pi / 2)
1844
+ return c, d
1845
+
1846
+
1847
+ def sketch_isquare(args: tuple[gm.Point, ...]) -> tuple[Point, ...]:
1848
+ a = Point(0.0, 0.0)
1849
+ b = Point(1.0, 0.0)
1850
+ c = Point(1.0, 1.0)
1851
+ d = Point(0.0, 1.0)
1852
+ a, b, c, d = random_rfss(a, b, c, d)
1853
+ return a, b, c, d
1854
+
1855
+
1856
+ def sketch_tline(args: tuple[gm.Point, ...]) -> Line:
1857
+ a, b, c = args
1858
+ return a.perpendicular_line(Line(b, c))
1859
+
1860
+
1861
+ def sketch_trapezoid(args: tuple[gm.Point, ...]) -> tuple[Point, ...]:
1862
+ d = Point(0.0, 0.0)
1863
+ c = Point(1.0, 0.0)
1864
+
1865
+ base = unif(0.5, 2.0)
1866
+ height = unif(0.5, 2.0)
1867
+ a = Point(unif(0.2, 0.5), height)
1868
+ b = Point(a.x + base, height)
1869
+ a, b, c, d = random_rfss(a, b, c, d)
1870
+ return a, b, c, d
1871
+
1872
+
1873
+ def sketch_triangle(args: tuple[gm.Point, ...]) -> tuple[Point, ...]:
1874
+ a = Point(0.0, 0.0)
1875
+ b = Point(1.0, 0.0)
1876
+ ac = unif(0.5, 2.0)
1877
+ ang = unif(0.2, 0.8) * np.pi
1878
+ c = head_from(a, ang, ac)
1879
+ return a, b, c
1880
+
1881
+
1882
+ def sketch_triangle12(args: tuple[gm.Point, ...]) -> tuple[Point, ...]:
1883
+ b = Point(0.0, 0.0)
1884
+ c = Point(unif(1.5, 2.5), 0.0)
1885
+ a, _ = circle_circle_intersection(Circle(b, 1.0), Circle(c, 2.0))
1886
+ a, b, c = random_rfss(a, b, c)
1887
+ return a, b, c
1888
+
1889
+
1890
+ def sketch_trisect(args: tuple[gm.Point, ...]) -> tuple[Point, Point]:
1891
+ """Sketch two trisectors of an angle."""
1892
+ a, b, c = args
1893
+ ang1 = ang_of(b, a)
1894
+ ang2 = ang_of(b, c)
1895
+
1896
+ swap = 0
1897
+ if ang1 > ang2:
1898
+ ang1, ang2 = ang2, ang1
1899
+ swap += 1
1900
+
1901
+ if ang2 - ang1 > np.pi:
1902
+ ang1, ang2 = ang2, ang1 + 2 * np.pi
1903
+ swap += 1
1904
+
1905
+ angx = ang1 + (ang2 - ang1) / 3
1906
+ angy = ang2 - (ang2 - ang1) / 3
1907
+
1908
+ x = b + Point(np.cos(angx), np.sin(angx))
1909
+ y = b + Point(np.cos(angy), np.sin(angy))
1910
+
1911
+ ac = Line(a, c)
1912
+ x = line_line_intersection(Line(b, x), ac)
1913
+ y = line_line_intersection(Line(b, y), ac)
1914
+
1915
+ if swap == 1:
1916
+ return y, x
1917
+ return x, y
1918
+
1919
+
1920
+ def sketch_trisegment(args: tuple[gm.Point, ...]) -> tuple[Point, Point]:
1921
+ a, b = args
1922
+ x, y = a + (b - a) * (1.0 / 3), a + (b - a) * (2.0 / 3)
1923
+ return x, y
ag4masses/alphageometry/numericals_test.py ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Unit testing for the geometry numericals code."""
17
+
18
+ import unittest
19
+
20
+ from absl.testing import absltest
21
+ import numericals as nm
22
+
23
+ np = nm.np
24
+
25
+ unif = nm.unif
26
+ Point = nm.Point
27
+ Line = nm.Line
28
+ Circle = nm.Circle
29
+ HalfLine = nm.HalfLine
30
+
31
+ line_circle_intersection = nm.line_circle_intersection
32
+ line_line_intersection = nm.line_line_intersection
33
+
34
+ check_coll = nm.check_coll
35
+ check_eqangle = nm.check_eqangle
36
+
37
+ random_points = nm.random_points
38
+ ang_between = nm.ang_between
39
+ head_from = nm.head_from
40
+
41
+
42
+ class NumericalTest(unittest.TestCase):
43
+
44
+ def test_sketch_ieq_triangle(self):
45
+ a, b, c = nm.sketch_ieq_triangle([])
46
+ self.assertAlmostEqual(a.distance(b), b.distance(c))
47
+ self.assertAlmostEqual(c.distance(a), b.distance(c))
48
+
49
+ def test_sketch_2l1c(self):
50
+ p = nm.Point(0.0, 0.0)
51
+ pi = np.pi
52
+ anga = unif(-0.4 * pi, 0.4 * pi)
53
+ a = Point(np.cos(anga), np.sin(anga))
54
+ angb = unif(0.6 * pi, 1.4 * pi)
55
+ b = Point(np.cos(angb), np.sin(angb))
56
+
57
+ angc = unif(anga + 0.05 * pi, angb - 0.05 * pi)
58
+ c = Point(np.cos(angc), np.sin(angc)) * unif(0.2, 0.8)
59
+
60
+ x, y, z, i = nm.sketch_2l1c([a, b, c, p])
61
+ self.assertTrue(check_coll([x, c, a]))
62
+ self.assertTrue(check_coll([y, c, b]))
63
+ self.assertAlmostEqual(z.distance(p), 1.0)
64
+ self.assertTrue(check_coll([p, i, z]))
65
+ self.assertTrue(Line(i, x).is_perp(Line(c, a)))
66
+ self.assertTrue(Line(i, y).is_perp(Line(c, b)))
67
+ self.assertAlmostEqual(i.distance(x), i.distance(y))
68
+ self.assertAlmostEqual(i.distance(x), i.distance(z))
69
+
70
+ def test_sketch_3peq(self):
71
+ a, b, c = random_points(3)
72
+ x, y, z = nm.sketch_3peq([a, b, c])
73
+
74
+ self.assertTrue(check_coll([a, b, x]))
75
+ self.assertTrue(check_coll([a, c, y]))
76
+ self.assertTrue(check_coll([b, c, z]))
77
+ self.assertTrue(check_coll([x, y, z]))
78
+ self.assertAlmostEqual(z.distance(x), z.distance(y))
79
+
80
+ def test_sketch_aline(self):
81
+ a, b, c, d, e = random_points(5)
82
+ ex = nm.sketch_aline([a, b, c, d, e])
83
+ self.assertIsInstance(ex, HalfLine)
84
+ self.assertEqual(ex.tail, e)
85
+ x = ex.head
86
+ self.assertAlmostEqual(ang_between(b, a, c), ang_between(e, d, x))
87
+
88
+ def test_sketch_amirror(self):
89
+ a, b, c = random_points(3)
90
+ bx = nm.sketch_amirror([a, b, c])
91
+ self.assertIsInstance(bx, HalfLine)
92
+ assert bx.tail == b
93
+ x = bx.head
94
+
95
+ ang1 = ang_between(b, a, c)
96
+ ang2 = ang_between(b, c, x)
97
+ self.assertAlmostEqual(ang1, ang2)
98
+
99
+ def test_sketch_bisect(self):
100
+ a, b, c = random_points(3)
101
+ line = nm.sketch_bisect([a, b, c])
102
+ self.assertAlmostEqual(b.distance(line), 0.0)
103
+
104
+ l = a.perpendicular_line(line)
105
+ x = line_line_intersection(l, Line(b, c))
106
+ self.assertAlmostEqual(a.distance(line), x.distance(line))
107
+
108
+ d, _ = line_circle_intersection(line, Circle(b, radius=1))
109
+ ang1 = ang_between(b, a, d)
110
+ ang2 = ang_between(b, d, c)
111
+ self.assertAlmostEqual(ang1, ang2)
112
+
113
+ def test_sketch_bline(self):
114
+ a, b = random_points(2)
115
+ l = nm.sketch_bline([a, b])
116
+ self.assertTrue(Line(a, b).is_perp(l))
117
+ self.assertAlmostEqual(a.distance(l), b.distance(l))
118
+
119
+ def test_sketch_cc_tangent(self):
120
+ o = Point(0.0, 0.0)
121
+ w = Point(1.0, 0.0)
122
+
123
+ ra = unif(0.0, 0.6)
124
+ rb = unif(0.4, 1.0)
125
+
126
+ a = unif(0.0, np.pi)
127
+ b = unif(0.0, np.pi)
128
+
129
+ a = o + ra * Point(np.cos(a), np.sin(a))
130
+ b = w + rb * Point(np.sin(b), np.cos(b))
131
+
132
+ x, y, z, t = nm.sketch_cc_tangent([o, a, w, b])
133
+ xy = Line(x, y)
134
+ zt = Line(z, t)
135
+ self.assertAlmostEqual(o.distance(xy), o.distance(a))
136
+ self.assertAlmostEqual(o.distance(zt), o.distance(a))
137
+ self.assertAlmostEqual(w.distance(xy), w.distance(b))
138
+ self.assertAlmostEqual(w.distance(zt), w.distance(b))
139
+
140
+ def test_sketch_circle(self):
141
+ a, b, c = random_points(3)
142
+ circle = nm.sketch_circle([a, b, c])
143
+ self.assertAlmostEqual(circle.center.distance(a), 0.0)
144
+ self.assertAlmostEqual(circle.radius, b.distance(c))
145
+
146
+ def test_sketch_e5128(self):
147
+ b = Point(0.0, 0.0)
148
+ c = Point(0.0, 1.0)
149
+ ang = unif(-np.pi / 2, 3 * np.pi / 2)
150
+ d = head_from(c, ang, 1.0)
151
+ a = Point(unif(0.5, 2.0), 0.0)
152
+
153
+ e, g = nm.sketch_e5128([a, b, c, d])
154
+ ang1 = ang_between(a, b, d)
155
+ ang2 = ang_between(e, a, g)
156
+ self.assertAlmostEqual(ang1, ang2)
157
+
158
+ def test_sketch_eq_quadrangle(self):
159
+ a, b, c, d = nm.sketch_eq_quadrangle([])
160
+ self.assertAlmostEqual(a.distance(d), c.distance(b))
161
+ ac = Line(a, c)
162
+ assert ac.diff_side(b, d), (ac(b), ac(d))
163
+ bd = Line(b, d)
164
+ assert bd.diff_side(a, c), (bd(a), bd(c))
165
+
166
+ def test_sketch_eq_trapezoid(self):
167
+ a, b, c, d = nm.sketch_eq_trapezoid([])
168
+ assert Line(a, b).is_parallel(Line(c, d))
169
+ self.assertAlmostEqual(a.distance(d), b.distance(c))
170
+
171
+ def test_sketch_eqangle3(self):
172
+ points = random_points(5)
173
+ x = nm.sketch_eqangle3(points).sample_within(points)[0]
174
+ a, b, d, e, f = points
175
+ self.assertTrue(check_eqangle([x, a, x, b, d, e, d, f]))
176
+
177
+ def test_sketch_eqangle2(self):
178
+ a, b, c = random_points(3)
179
+ x = nm.sketch_eqangle2([a, b, c])
180
+ ang1 = ang_between(a, b, x)
181
+ ang2 = ang_between(c, x, b)
182
+ self.assertAlmostEqual(ang1, ang2)
183
+
184
+ def test_sketch_edia_quadrangle(self):
185
+ a, b, c, d = nm.sketch_eqdia_quadrangle([])
186
+ assert Line(a, c).diff_side(b, d)
187
+ assert Line(b, d).diff_side(a, c)
188
+ self.assertAlmostEqual(a.distance(c), b.distance(d))
189
+
190
+ def test_sketch_isos(self):
191
+ a, b, c = nm.sketch_isos([])
192
+ self.assertAlmostEqual(a.distance(b), a.distance(c))
193
+ self.assertAlmostEqual(ang_between(b, a, c), ang_between(c, b, a))
194
+
195
+ def test_sketch_quadrange(self):
196
+ a, b, c, d = nm.sketch_quadrangle([])
197
+ self.assertTrue(Line(a, c).diff_side(b, d))
198
+ self.assertTrue(Line(b, d).diff_side(a, c))
199
+
200
+ def test_sketch_r_trapezoid(self):
201
+ a, b, c, d = nm.sketch_r_trapezoid([])
202
+ self.assertTrue(Line(a, b).is_perp(Line(a, d)))
203
+ self.assertTrue(Line(a, b).is_parallel(Line(c, d)))
204
+ self.assertTrue(Line(a, c).diff_side(b, d))
205
+ self.assertTrue(Line(b, d).diff_side(a, c))
206
+
207
+ def test_sketch_r_triangle(self):
208
+ a, b, c = nm.sketch_r_triangle([])
209
+ self.assertTrue(Line(a, b).is_perp(Line(a, c)))
210
+
211
+ def test_sketch_rectangle(self):
212
+ a, b, c, d = nm.sketch_rectangle([])
213
+ self.assertTrue(Line(a, b).is_perp(Line(b, c)))
214
+ self.assertTrue(Line(b, c).is_perp(Line(c, d)))
215
+ self.assertTrue(Line(c, d).is_perp(Line(d, a)))
216
+
217
+ def test_sketch_reflect(self):
218
+ a, b, c = random_points(3)
219
+ x = nm.sketch_reflect([a, b, c])
220
+ self.assertTrue(Line(a, x).is_perp(Line(b, c)))
221
+ self.assertAlmostEqual(x.distance(Line(b, c)), a.distance(Line(b, c)))
222
+
223
+ def test_sketch_risos(self):
224
+ a, b, c = nm.sketch_risos([])
225
+ self.assertAlmostEqual(a.distance(b), a.distance(c))
226
+ self.assertTrue(Line(a, b).is_perp(Line(a, c)))
227
+
228
+ def test_sketch_rotaten90(self):
229
+ a, b = random_points(2)
230
+ x = nm.sketch_rotaten90([a, b])
231
+ self.assertAlmostEqual(a.distance(x), a.distance(b))
232
+ self.assertTrue(Line(a, x).is_perp(Line(a, b)))
233
+ d = Point(0.0, 0.0)
234
+ e = Point(0.0, 1.0)
235
+ f = Point(1.0, 0.0)
236
+ self.assertAlmostEqual(ang_between(d, e, f), ang_between(a, b, x))
237
+
238
+ def test_sketch_rotatep90(self):
239
+ a, b = random_points(2)
240
+ x = nm.sketch_rotatep90([a, b])
241
+ self.assertAlmostEqual(a.distance(x), a.distance(b))
242
+ self.assertTrue(Line(a, x).is_perp(Line(a, b)))
243
+ d = Point(0.0, 0.0)
244
+ e = Point(0.0, 1.0)
245
+ f = Point(1.0, 0.0)
246
+ self.assertAlmostEqual(ang_between(d, f, e), ang_between(a, b, x))
247
+
248
+ def test_sketch_s_angle(self):
249
+ a, b = random_points(2)
250
+ y = unif(0.0, np.pi)
251
+ bx = nm.sketch_s_angle([a, b, y / np.pi * 180])
252
+ self.assertIsInstance(bx, HalfLine)
253
+ self.assertEqual(bx.tail, b)
254
+ x = bx.head
255
+
256
+ d = Point(1.0, 0.0)
257
+ e = Point(0.0, 0.0)
258
+ f = Point(np.cos(y), np.sin(y))
259
+ self.assertAlmostEqual(ang_between(e, d, f), ang_between(b, a, x))
260
+
261
+ def test_sketch_shift(self):
262
+ a, b, c = random_points(3)
263
+ x = nm.sketch_shift([a, b, c])
264
+ self.assertTrue((b - a).close(x - c))
265
+
266
+ def test_sketch_square(self):
267
+ a, b = random_points(2)
268
+ c, d = nm.sketch_square([a, b])
269
+ self.assertTrue(Line(a, b).is_perp(Line(b, c)))
270
+ self.assertTrue(Line(b, c).is_perp(Line(c, d)))
271
+ self.assertTrue(Line(c, d).is_perp(Line(d, a)))
272
+ self.assertAlmostEqual(a.distance(b), b.distance(c))
273
+
274
+ def test_sketch_isquare(self):
275
+ a, b, c, d = nm.sketch_isquare([])
276
+ self.assertTrue(Line(a, b).is_perp(Line(b, c)))
277
+ self.assertTrue(Line(b, c).is_perp(Line(c, d)))
278
+ self.assertTrue(Line(c, d).is_perp(Line(d, a)))
279
+ self.assertAlmostEqual(a.distance(b), b.distance(c))
280
+
281
+ def test_sketch_trapezoid(self):
282
+ a, b, c, d = nm.sketch_trapezoid([])
283
+ self.assertTrue(Line(a, b).is_parallel(Line(c, d)))
284
+ self.assertTrue(Line(a, c).diff_side(b, d))
285
+ self.assertTrue(Line(b, d).diff_side(a, c))
286
+
287
+ def test_sketch_triangle(self):
288
+ a, b, c = nm.sketch_triangle([])
289
+ self.assertFalse(check_coll([a, b, c]))
290
+
291
+ def test_sketch_triangle12(self):
292
+ a, b, c = nm.sketch_triangle12([])
293
+ self.assertAlmostEqual(a.distance(b) * 2, a.distance(c))
294
+
295
+ def test_sketch_trisect(self):
296
+ a, b, c = random_points(3)
297
+ x, y = nm.sketch_trisect([a, b, c])
298
+ self.assertAlmostEqual(ang_between(b, a, x), ang_between(b, x, y))
299
+ self.assertAlmostEqual(ang_between(b, x, y), ang_between(b, y, c))
300
+ self.assertAlmostEqual(ang_between(b, a, x) * 3, ang_between(b, a, c))
301
+
302
+ def test_sketch_trisegment(self):
303
+ a, b = random_points(2)
304
+ x, y = nm.sketch_trisegment([a, b])
305
+ self.assertAlmostEqual(
306
+ a.distance(x) + x.distance(y) + y.distance(b), a.distance(b)
307
+ )
308
+ self.assertAlmostEqual(a.distance(x), x.distance(y))
309
+ self.assertAlmostEqual(x.distance(y), y.distance(b))
310
+
311
+
312
+ if __name__ == '__main__':
313
+ absltest.main()
ag4masses/alphageometry/pretty.py ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Utilities for string manipulation in the DSL."""
17
+
18
+ MAP_SYMBOL = {
19
+ 'T': 'perp',
20
+ 'P': 'para',
21
+ 'D': 'cong',
22
+ 'S': 'simtri',
23
+ 'I': 'circle',
24
+ 'M': 'midp',
25
+ 'O': 'cyclic',
26
+ 'C': 'coll',
27
+ '^': 'eqangle',
28
+ '/': 'eqratio',
29
+ '%': 'eqratio',
30
+ '=': 'contri',
31
+ 'X': 'collx',
32
+ 'A': 'acompute',
33
+ 'R': 'rcompute',
34
+ 'Q': 'fixc',
35
+ 'E': 'fixl',
36
+ 'V': 'fixb',
37
+ 'H': 'fixt',
38
+ 'Z': 'fixp',
39
+ 'Y': 'ind',
40
+ }
41
+
42
+
43
+ def map_symbol(c: str) -> str:
44
+ return MAP_SYMBOL[c]
45
+
46
+
47
+ def map_symbol_inv(c: str) -> str:
48
+ return {v: k for k, v in MAP_SYMBOL.items()}[c]
49
+
50
+
51
+ def _gcd(x: int, y: int) -> int:
52
+ while y:
53
+ x, y = y, x % y
54
+ return x
55
+
56
+
57
+ def simplify(n: int, d: int) -> tuple[int, int]:
58
+ g = _gcd(n, d)
59
+ return (n // g, d // g)
60
+
61
+
62
+ def pretty2r(a: str, b: str, c: str, d: str) -> str:
63
+ if b in (c, d):
64
+ a, b = b, a
65
+
66
+ if a == d:
67
+ c, d = d, c
68
+
69
+ return f'{a} {b} {c} {d}'
70
+
71
+
72
+ def pretty2a(a: str, b: str, c: str, d: str) -> str:
73
+ if b in (c, d):
74
+ a, b = b, a
75
+
76
+ if a == d:
77
+ c, d = d, c
78
+
79
+ return f'{a} {b} {c} {d}'
80
+
81
+
82
+ def pretty_angle(a: str, b: str, c: str, d: str) -> str:
83
+ if b in (c, d):
84
+ a, b = b, a
85
+ if a == d:
86
+ c, d = d, c
87
+
88
+ if a == c:
89
+ return f'\u2220{b}{a}{d}'
90
+ return f'\u2220({a}{b}-{c}{d})'
91
+
92
+
93
+ def pretty_nl(name: str, args: list[str]) -> str:
94
+ """Natural lang formatting a predicate."""
95
+ if name == 'aconst':
96
+ a, b, c, d, y = args
97
+ return f'{pretty_angle(a, b, c, d)} = {y}'
98
+ if name == 'rconst':
99
+ a, b, c, d, y = args
100
+ return f'{a}{b}:{c}{d} = {y}'
101
+ if name == 'acompute':
102
+ a, b, c, d = args
103
+ return f'{pretty_angle(a, b, c, d)}'
104
+ if name in ['coll', 'C']:
105
+ return '' + ','.join(args) + ' are collinear'
106
+ if name == 'collx':
107
+ return '' + ','.join(list(set(args))) + ' are collinear'
108
+ if name in ['cyclic', 'O']:
109
+ return '' + ','.join(args) + ' are concyclic'
110
+ if name in ['midp', 'midpoint', 'M']:
111
+ x, a, b = args
112
+ return f'{x} is midpoint of {a}{b}'
113
+ if name in ['eqangle', 'eqangle6', '^']:
114
+ a, b, c, d, e, f, g, h = args
115
+ return f'{pretty_angle(a, b, c, d)} = {pretty_angle(e, f, g, h)}'
116
+ if name in ['eqratio', 'eqratio6', '/']:
117
+ return '{}{}:{}{} = {}{}:{}{}'.format(*args)
118
+ if name == 'eqratio3':
119
+ a, b, c, d, o, o = args # pylint: disable=redeclared-assigned-name
120
+ return f'S {o} {a} {b} {o} {c} {d}'
121
+ if name in ['cong', 'D']:
122
+ a, b, c, d = args
123
+ return f'{a}{b} = {c}{d}'
124
+ if name in ['perp', 'T']:
125
+ if len(args) == 2: # this is algebraic derivation.
126
+ ab, cd = args # ab = 'd( ... )'
127
+ return f'{ab} \u27c2 {cd}'
128
+ a, b, c, d = args
129
+ return f'{a}{b} \u27c2 {c}{d}'
130
+ if name in ['para', 'P']:
131
+ if len(args) == 2: # this is algebraic derivation.
132
+ ab, cd = args # ab = 'd( ... )'
133
+ return f'{ab} \u2225 {cd}'
134
+ a, b, c, d = args
135
+ return f'{a}{b} \u2225 {c}{d}'
136
+ if name in ['simtri2', 'simtri', 'simtri*']:
137
+ a, b, c, x, y, z = args
138
+ return f'\u0394{a}{b}{c} is similar to \u0394{x}{y}{z}'
139
+ if name in ['contri2', 'contri', 'contri*']:
140
+ a, b, c, x, y, z = args
141
+ return f'\u0394{a}{b}{c} is congruent to \u0394{x}{y}{z}'
142
+ if name in ['circle', 'I']:
143
+ o, a, b, c = args
144
+ return f'{o} is the circumcenter of \\Delta {a}{b}{c}'
145
+ if name == 'foot':
146
+ a, b, c, d = args
147
+ return f'{a} is the foot of {b} on {c}{d}'
148
+
149
+
150
+ def pretty(txt: str) -> str:
151
+ """Pretty formating a predicate string."""
152
+ if isinstance(txt, str):
153
+ txt = txt.split(' ')
154
+ name, *args = txt
155
+ if name == 'ind':
156
+ return 'Y ' + ' '.join(args)
157
+ if name in ['fixc', 'fixl', 'fixb', 'fixt', 'fixp']:
158
+ return map_symbol_inv(name) + ' ' + ' '.join(args)
159
+ if name == 'acompute':
160
+ a, b, c, d = args
161
+ return 'A ' + ' '.join(args)
162
+ if name == 'rcompute':
163
+ a, b, c, d = args
164
+ return 'R ' + ' '.join(args)
165
+ if name == 'aconst':
166
+ a, b, c, d, y = args
167
+ return f'^ {pretty2a(a, b, c, d)} {y}'
168
+ if name == 'rconst':
169
+ a, b, c, d, y = args
170
+ return f'/ {pretty2r(a, b, c, d)} {y}'
171
+ if name == 'coll':
172
+ return 'C ' + ' '.join(args)
173
+ if name == 'collx':
174
+ return 'X ' + ' '.join(args)
175
+ if name == 'cyclic':
176
+ return 'O ' + ' '.join(args)
177
+ if name in ['midp', 'midpoint']:
178
+ x, a, b = args
179
+ return f'M {x} {a} {b}'
180
+ if name == 'eqangle':
181
+ a, b, c, d, e, f, g, h = args
182
+ return f'^ {pretty2a(a, b, c, d)} {pretty2a(e, f, g, h)}'
183
+ if name == 'eqratio':
184
+ a, b, c, d, e, f, g, h = args
185
+ return f'/ {pretty2r(a, b, c, d)} {pretty2r(e, f, g, h)}'
186
+ if name == 'eqratio3':
187
+ a, b, c, d, o, o = args # pylint: disable=redeclared-assigned-name
188
+ return f'S {o} {a} {b} {o} {c} {d}'
189
+ if name == 'cong':
190
+ a, b, c, d = args
191
+ return f'D {a} {b} {c} {d}'
192
+ if name == 'perp':
193
+ if len(args) == 2: # this is algebraic derivation.
194
+ ab, cd = args # ab = 'd( ... )'
195
+ return f'T {ab} {cd}'
196
+ a, b, c, d = args
197
+ return f'T {a} {b} {c} {d}'
198
+ if name == 'para':
199
+ if len(args) == 2: # this is algebraic derivation.
200
+ ab, cd = args # ab = 'd( ... )'
201
+ return f'P {ab} {cd}'
202
+ a, b, c, d = args
203
+ return f'P {a} {b} {c} {d}'
204
+ if name in ['simtri2', 'simtri', 'simtri*']:
205
+ a, b, c, x, y, z = args
206
+ return f'S {a} {b} {c} {x} {y} {z}'
207
+ if name in ['contri2', 'contri', 'contri*']:
208
+ a, b, c, x, y, z = args
209
+ return f'= {a} {b} {c} {x} {y} {z}'
210
+ if name == 'circle':
211
+ o, a, b, c = args
212
+ return f'I {o} {a} {b} {c}'
213
+ if name == 'foot':
214
+ a, b, c, d = args
215
+ return f'F {a} {b} {c} {d}'
216
+ return ' '.join(txt)
ag4masses/alphageometry/problem.py ADDED
@@ -0,0 +1,1133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Implements objects to represent problems, theorems, proofs, traceback."""
17
+
18
+ from __future__ import annotations
19
+
20
+ from collections import defaultdict # pylint: disable=g-importing-member
21
+ from typing import Any
22
+
23
+ import geometry as gm
24
+ import pretty as pt
25
+
26
+
27
+ # pylint: disable=protected-access
28
+ # pylint: disable=unused-variable
29
+ # pylint: disable=unused-argument
30
+ # pylint: disable=unused-assignment
31
+
32
+
33
+ def reshape(l: list[Any], n: int = 1) -> list[list[Any]]:
34
+ assert len(l) % n == 0
35
+ columns = [[] for i in range(n)]
36
+ for i, x in enumerate(l):
37
+ columns[i % n].append(x)
38
+ return zip(*columns)
39
+
40
+
41
+ def isint(x: str) -> bool:
42
+ try:
43
+ int(x)
44
+ return True
45
+ except: # pylint: disable=bare-except
46
+ return False
47
+
48
+
49
+ class Construction:
50
+ """One predicate."""
51
+
52
+ @classmethod
53
+ def from_txt(cls, data: str) -> Construction:
54
+ data = data.split(' ')
55
+ return Construction(data[0], data[1:])
56
+
57
+ def __init__(self, name: str, args: list[str]):
58
+ self.name = name
59
+ self.args = args
60
+
61
+ def translate(self, mapping: dict[str, str]) -> Construction:
62
+ args = [a if isint(a) else mapping[a] for a in self.args]
63
+ return Construction(self.name, args)
64
+
65
+ def txt(self) -> str:
66
+ return ' '.join([self.name] + list(self.args))
67
+
68
+
69
+ class Clause:
70
+ """One construction (>= 1 predicate)."""
71
+
72
+ @classmethod
73
+ def from_txt(cls, data: str) -> Clause:
74
+ if data == ' =':
75
+ return Clause([], [])
76
+ points, constructions = data.split(' = ')
77
+ return Clause(
78
+ points.split(' '),
79
+ [Construction.from_txt(c) for c in constructions.split(', ')],
80
+ )
81
+
82
+ def __init__(self, points: list[str], constructions: list[Construction]):
83
+ self.points = []
84
+ self.nums = []
85
+
86
+ for p in points:
87
+ num = None
88
+ if isinstance(p, str) and '@' in p:
89
+ p, num = p.split('@')
90
+ x, y = num.split('_')
91
+ num = float(x), float(y)
92
+ self.points.append(p)
93
+ self.nums.append(num)
94
+
95
+ self.constructions = constructions
96
+
97
+ def translate(self, mapping: dict[str, str]) -> Clause:
98
+ points0 = []
99
+ for p in self.points:
100
+ pcount = len(mapping) + 1
101
+ name = chr(96 + pcount)
102
+ if name > 'z': # pcount = 26 -> name = 'z'
103
+ name = chr(97 + (pcount - 1) % 26) + str((pcount - 1) // 26)
104
+
105
+ p0 = mapping.get(p, name)
106
+ mapping[p] = p0
107
+ points0.append(p0)
108
+ return Clause(points0, [c.translate(mapping) for c in self.constructions])
109
+
110
+ def add(self, name: str, args: list[str]) -> None:
111
+ self.constructions.append(Construction(name, args))
112
+
113
+ def txt(self) -> str:
114
+ return (
115
+ ' '.join(self.points)
116
+ + ' = '
117
+ + ', '.join(c.txt() for c in self.constructions)
118
+ )
119
+
120
+
121
+ def _gcd(x: int, y: int) -> int:
122
+ while y:
123
+ x, y = y, x % y
124
+ return x
125
+
126
+
127
+ def simplify(n: int, d: int) -> tuple[int, int]:
128
+ g = _gcd(n, d)
129
+ return (n // g, d // g)
130
+
131
+
132
+ def compare_fn(dep: Dependency) -> tuple[Dependency, str]:
133
+ return (dep, pt.pretty(dep))
134
+
135
+
136
+ def sort_deps(deps: list[Dependency]) -> list[Dependency]:
137
+ return sorted(deps, key=compare_fn)
138
+
139
+
140
+ class Problem:
141
+ """Describe one problem to solve."""
142
+
143
+ @classmethod
144
+ def from_txt_file(
145
+ cls, fname: str, to_dict: bool = False, translate: bool = True
146
+ ):
147
+ """Load a problem from a text file."""
148
+ with open(fname, 'r') as f:
149
+ lines = f.read().split('\n')
150
+
151
+ lines = [l for l in lines if l]
152
+ data = [
153
+ cls.from_txt(url + '\n' + problem, translate)
154
+ for (url, problem) in reshape(lines, 2)
155
+ ]
156
+ if to_dict:
157
+ return cls.to_dict(data)
158
+ return data
159
+
160
+ @classmethod
161
+ def from_txt(cls, data: str, translate: bool = True) -> Problem:
162
+ """Load a problem from a str object."""
163
+ url = ''
164
+ if '\n' in data:
165
+ url, data = data.split('\n')
166
+
167
+ if ' ? ' in data:
168
+ clauses, goal = data.split(' ? ')
169
+ goal = Construction.from_txt(goal)
170
+ else:
171
+ clauses, goal = data, None
172
+
173
+ clauses = clauses.split('; ')
174
+ problem = Problem(
175
+ url=url, clauses=[Clause.from_txt(c) for c in clauses], goal=goal
176
+ )
177
+ if translate:
178
+ return problem.translate()
179
+ return problem
180
+
181
+ @classmethod
182
+ def to_dict(cls, data: list[Problem]) -> dict[str, Problem]:
183
+ return {p.url: p for p in data}
184
+
185
+ def __init__(self, url: str, clauses: list[Clause], goal: Construction):
186
+ self.url = url
187
+ self.clauses = clauses
188
+ self.goal = goal
189
+
190
+ def copy(self) -> Problem:
191
+ return Problem(self.url, list(self.clauses), self.goal)
192
+
193
+ def translate(self) -> Problem: # to single-char point names
194
+ """Translate point names into alphabetical."""
195
+ mapping = {}
196
+ clauses = []
197
+
198
+ for clause in self.clauses:
199
+ clauses.append(clause.translate(mapping))
200
+
201
+ if self.goal:
202
+ goal = self.goal.translate(mapping)
203
+ else:
204
+ goal = self.goal
205
+
206
+ p = Problem(self.url, clauses, goal)
207
+ p.mapping = mapping
208
+ return p
209
+
210
+ def txt(self) -> str:
211
+ return (
212
+ '; '.join([c.txt() for c in self.clauses]) + ' ? ' + self.goal.txt()
213
+ if self.goal
214
+ else ''
215
+ )
216
+
217
+ def setup_str_from_problem(self, definitions: list[Definition]) -> str:
218
+ """Construct the <theorem_premises> string from Problem object."""
219
+ ref = 0
220
+
221
+ string = []
222
+ for clause in self.clauses:
223
+ group = {}
224
+ p2deps = defaultdict(list)
225
+ for c in clause.constructions:
226
+ cdef = definitions[c.name]
227
+
228
+ if len(c.args) != len(cdef.construction.args):
229
+ assert len(c.args) + len(clause.points) == len(cdef.construction.args)
230
+ c.args = clause.points + c.args
231
+
232
+ mapping = dict(zip(cdef.construction.args, c.args))
233
+ for points, bs in cdef.basics:
234
+ points = tuple([mapping[x] for x in points])
235
+ for p in points:
236
+ group[p] = points
237
+
238
+ for b in bs:
239
+ args = [mapping[a] for a in b.args]
240
+ name = b.name
241
+ if b.name in ['s_angle', 'aconst']:
242
+ x, y, z, v = args
243
+ name = 'aconst'
244
+ v = int(v)
245
+
246
+ if v < 0:
247
+ v = -v
248
+ x, z = z, x
249
+
250
+ m, n = simplify(int(v), 180)
251
+ args = [y, z, y, x, f'{m}pi/{n}']
252
+
253
+ p2deps[points].append(hashed_txt(name, args))
254
+
255
+ for k, v in p2deps.items():
256
+ p2deps[k] = sort_deps(v)
257
+
258
+ points = clause.points
259
+ while points:
260
+ p = points[0]
261
+ gr = group[p]
262
+ points = [x for x in points if x not in gr]
263
+
264
+ deps_str = []
265
+ for dep in p2deps[gr]:
266
+ ref_str = '{:02}'.format(ref)
267
+ dep_str = pt.pretty(dep)
268
+
269
+ if dep[0] == 'aconst':
270
+ m, n = map(int, dep[-1].split('pi/'))
271
+ mn = f'{m}. pi / {n}.'
272
+ dep_str = ' '.join(dep_str.split()[:-1] + [mn])
273
+
274
+ deps_str.append(dep_str + ' ' + ref_str)
275
+ ref += 1
276
+
277
+ string.append(' '.join(gr) + ' : ' + ' '.join(deps_str))
278
+
279
+ string = '{S} ' + ' ; '.join([s.strip() for s in string])
280
+ goal = self.goal
281
+ string += ' ? ' + pt.pretty([goal.name] + goal.args)
282
+ return string
283
+
284
+
285
+ def parse_rely(s: str) -> dict[str, str]:
286
+ result = {}
287
+ if not s:
288
+ return result
289
+ s = [x.strip() for x in s.split(',')]
290
+ for x in s:
291
+ a, b = x.split(':')
292
+ a, b = a.strip().split(), b.strip().split()
293
+ result.update({m: b for m in a})
294
+ return result
295
+
296
+
297
+ class Definition:
298
+ """Definitions of construction statements."""
299
+
300
+ @classmethod
301
+ def from_txt_file(cls, fname: str, to_dict: bool = False) -> Definition:
302
+ with open(fname, 'r') as f:
303
+ lines = f.read()
304
+ return cls.from_string(lines, to_dict)
305
+
306
+ @classmethod
307
+ def from_string(cls, string: str, to_dict: bool = False) -> Definition:
308
+ lines = string.split('\n')
309
+ data = [cls.from_txt('\n'.join(group)) for group in reshape(lines, 6)]
310
+ if to_dict:
311
+ return cls.to_dict(data)
312
+ return data
313
+
314
+ @classmethod
315
+ def to_dict(cls, data: list[Definition]) -> dict[str, Definition]:
316
+ return {d.construction.name: d for d in data}
317
+
318
+ @classmethod
319
+ def from_txt(cls, data: str) -> Definition:
320
+ """Load definitions from a str object."""
321
+ construction, rely, deps, basics, numerics, _ = data.split('\n')
322
+ basics = [] if not basics else [b.strip() for b in basics.split(';')]
323
+
324
+ levels = []
325
+ for bs in basics:
326
+ if ':' in bs:
327
+ points, bs = bs.split(':')
328
+ points = points.strip().split()
329
+ else:
330
+ points = []
331
+ if bs.strip():
332
+ bs = [Construction.from_txt(b.strip()) for b in bs.strip().split(',')]
333
+ else:
334
+ bs = []
335
+ levels.append((points, bs))
336
+
337
+ numerics = [] if not numerics else numerics.split(', ')
338
+
339
+ return Definition(
340
+ construction=Construction.from_txt(construction),
341
+ rely=parse_rely(rely),
342
+ deps=Clause.from_txt(deps),
343
+ basics=levels,
344
+ numerics=[Construction.from_txt(c) for c in numerics],
345
+ )
346
+
347
+ def __init__(
348
+ self,
349
+ construction: Construction,
350
+ rely: dict[str, str],
351
+ deps: Clause,
352
+ basics: list[tuple[list[str], list[Construction]]],
353
+ numerics: list[Construction],
354
+ ):
355
+ self.construction = construction
356
+ self.rely = rely
357
+ self.deps = deps
358
+ self.basics = basics
359
+ self.numerics = numerics
360
+
361
+ args = set()
362
+ for num in numerics:
363
+ args.update(num.args)
364
+
365
+ self.points = []
366
+ self.args = []
367
+ for p in self.construction.args:
368
+ if p in args:
369
+ self.args.append(p)
370
+ else:
371
+ self.points.append(p)
372
+
373
+
374
+ class Theorem:
375
+ """Deduction rule."""
376
+
377
+ @classmethod
378
+ def from_txt_file(cls, fname: str, to_dict: bool = False) -> Theorem:
379
+ with open(fname, 'r') as f:
380
+ theorems = f.read()
381
+ return cls.from_string(theorems, to_dict)
382
+
383
+ @classmethod
384
+ def from_string(cls, string: str, to_dict: bool = False) -> Theorem:
385
+ """Load deduction rule from a str object."""
386
+ theorems = string.split('\n')
387
+ theorems = [l for l in theorems if l and not l.startswith('#')]
388
+ theorems = [cls.from_txt(l) for l in theorems]
389
+
390
+ for i, th in enumerate(theorems):
391
+ th.rule_name = 'r{:02}'.format(i)
392
+
393
+ if to_dict:
394
+ result = {}
395
+ for t in theorems:
396
+ if t.name in result:
397
+ t.name += '_'
398
+ result[t.rule_name] = t
399
+
400
+ return result
401
+
402
+ return theorems
403
+
404
+ @classmethod
405
+ def from_txt(cls, data: str) -> Theorem:
406
+ premises, conclusion = data.split(' => ')
407
+ premises = premises.split(', ')
408
+ conclusion = conclusion.split(', ')
409
+ return Theorem(
410
+ premise=[Construction.from_txt(p) for p in premises],
411
+ conclusion=[Construction.from_txt(c) for c in conclusion],
412
+ )
413
+
414
+ def __init__(
415
+ self, premise: list[Construction], conclusion: list[Construction]
416
+ ):
417
+ if len(conclusion) != 1:
418
+ raise ValueError('Cannot have more than one conclusion')
419
+ self.name = '_'.join([p.name for p in premise + conclusion])
420
+ self.premise = premise
421
+ self.conclusion = conclusion
422
+ self.is_arg_reduce = False
423
+
424
+ assert len(self.conclusion) == 1
425
+ con = self.conclusion[0]
426
+
427
+ if con.name in [
428
+ 'eqratio3',
429
+ 'midp',
430
+ 'contri',
431
+ 'simtri',
432
+ 'contri2',
433
+ 'simtri2',
434
+ 'simtri*',
435
+ 'contri*',
436
+ ]:
437
+ return
438
+
439
+ prem_args = set(sum([p.args for p in self.premise], []))
440
+ con_args = set(con.args)
441
+ if len(prem_args) <= len(con_args):
442
+ self.is_arg_reduce = True
443
+
444
+ def txt(self) -> str:
445
+ premise_txt = ', '.join([clause.txt() for clause in self.premise])
446
+ conclusion_txt = ', '.join([clause.txt() for clause in self.conclusion])
447
+ return f'{premise_txt} => {conclusion_txt}'
448
+
449
+ def conclusion_name_args(
450
+ self, mapping: dict[str, gm.Point]
451
+ ) -> tuple[str, list[gm.Point]]:
452
+ mapping = {arg: p for arg, p in mapping.items() if isinstance(arg, str)}
453
+ c = self.conclusion[0]
454
+ args = [mapping[a] for a in c.args]
455
+ return c.name, args
456
+
457
+
458
+ def why_eqratio(
459
+ d1: gm.Direction,
460
+ d2: gm.Direction,
461
+ d3: gm.Direction,
462
+ d4: gm.Direction,
463
+ level: int,
464
+ ) -> list[Dependency]:
465
+ """Why two ratios are equal, returns a Dependency objects."""
466
+ all12 = list(gm.all_ratios(d1, d2, level))
467
+ all34 = list(gm.all_ratios(d3, d4, level))
468
+
469
+ min_why = None
470
+ for ang12, d1s, d2s in all12:
471
+ for ang34, d3s, d4s in all34:
472
+ why0 = gm.why_equal(ang12, ang34, level)
473
+ if why0 is None:
474
+ continue
475
+ d1_, d2_ = ang12._l
476
+ d3_, d4_ = ang34._l
477
+ why1 = gm.bfs_backtrack(d1, [d1_], d1s)
478
+ why2 = gm.bfs_backtrack(d2, [d2_], d2s)
479
+ why3 = gm.bfs_backtrack(d3, [d3_], d3s)
480
+ why4 = gm.bfs_backtrack(d4, [d4_], d4s)
481
+ why = why0 + why1 + why2 + why3 + why4
482
+ if min_why is None or len(why) < len(min_why[0]):
483
+ min_why = why, ang12, ang34, why0, why1, why2, why3, why4
484
+
485
+ if min_why is None:
486
+ return None
487
+
488
+ _, ang12, ang34, why0, why1, why2, why3, why4 = min_why
489
+ d1_, d2_ = ang12._l
490
+ d3_, d4_ = ang34._l
491
+
492
+ if d1 == d1_ and d2 == d2_ and d3 == d3_ and d4 == d4_:
493
+ return why0
494
+
495
+ (a_, b_), (c_, d_) = d1_._obj.points, d2_._obj.points
496
+ (e_, f_), (g_, h_) = d3_._obj.points, d4_._obj.points
497
+ deps = []
498
+ if why0:
499
+ dep = Dependency('eqratio', [a_, b_, c_, d_, e_, f_, g_, h_], '', level)
500
+ dep.why = why0
501
+ deps.append(dep)
502
+
503
+ (a, b), (c, d) = d1._obj.points, d2._obj.points
504
+ (e, f), (g, h) = d3._obj.points, d4._obj.points
505
+ for why, (x, y), (x_, y_) in zip(
506
+ [why1, why2, why3, why4],
507
+ [(a, b), (c, d), (e, f), (g, h)],
508
+ [(a_, b_), (c_, d_), (e_, f_), (g_, h_)],
509
+ ):
510
+ if why:
511
+ dep = Dependency('cong', [x, y, x_, y_], '', level)
512
+ dep.why = why
513
+ deps.append(dep)
514
+
515
+ return deps
516
+
517
+
518
+ def why_eqangle(
519
+ d1: gm.Direction,
520
+ d2: gm.Direction,
521
+ d3: gm.Direction,
522
+ d4: gm.Direction,
523
+ level: int,
524
+ verbose: bool = False,
525
+ ) -> list[Dependency]:
526
+ """Why two angles are equal, returns a Dependency objects."""
527
+ all12 = list(gm.all_angles(d1, d2, level))
528
+ all34 = list(gm.all_angles(d3, d4, level))
529
+
530
+ min_why = None
531
+ for ang12, d1s, d2s in all12:
532
+ for ang34, d3s, d4s in all34:
533
+ why0 = gm.why_equal(ang12, ang34, level)
534
+ if why0 is None:
535
+ continue
536
+ d1_, d2_ = ang12._d
537
+ d3_, d4_ = ang34._d
538
+ why1 = gm.bfs_backtrack(d1, [d1_], d1s)
539
+ why2 = gm.bfs_backtrack(d2, [d2_], d2s)
540
+ why3 = gm.bfs_backtrack(d3, [d3_], d3s)
541
+ why4 = gm.bfs_backtrack(d4, [d4_], d4s)
542
+ why = why0 + why1 + why2 + why3 + why4
543
+ if min_why is None or len(why) < len(min_why[0]):
544
+ min_why = why, ang12, ang34, why0, why1, why2, why3, why4
545
+
546
+ if min_why is None:
547
+ return None
548
+
549
+ _, ang12, ang34, why0, why1, why2, why3, why4 = min_why
550
+ why0 = gm.why_equal(ang12, ang34, level)
551
+ d1_, d2_ = ang12._d
552
+ d3_, d4_ = ang34._d
553
+
554
+ if d1 == d1_ and d2 == d2_ and d3 == d3_ and d4 == d4_:
555
+ return (d1_, d2_, d3_, d4_), why0
556
+
557
+ (a_, b_), (c_, d_) = d1_._obj.points, d2_._obj.points
558
+ (e_, f_), (g_, h_) = d3_._obj.points, d4_._obj.points
559
+ deps = []
560
+ if why0:
561
+ dep = Dependency('eqangle', [a_, b_, c_, d_, e_, f_, g_, h_], '', None)
562
+ dep.why = why0
563
+ deps.append(dep)
564
+
565
+ (a, b), (c, d) = d1._obj.points, d2._obj.points
566
+ (e, f), (g, h) = d3._obj.points, d4._obj.points
567
+ for why, d_xy, (x, y), d_xy_, (x_, y_) in zip(
568
+ [why1, why2, why3, why4],
569
+ [d1, d2, d3, d4],
570
+ [(a, b), (c, d), (e, f), (g, h)],
571
+ [d1_, d2_, d3_, d4_],
572
+ [(a_, b_), (c_, d_), (e_, f_), (g_, h_)],
573
+ ):
574
+ xy, xy_ = d_xy._obj, d_xy_._obj
575
+ if why:
576
+ if xy == xy_:
577
+ name = 'collx'
578
+ else:
579
+ name = 'para'
580
+ dep = Dependency(name, [x_, y_, x, y], '', None)
581
+ dep.why = why
582
+ deps.append(dep)
583
+
584
+ return (d1_, d2_, d3_, d4_), deps
585
+
586
+
587
+ CONSTRUCTION_RULE = 'c0'
588
+
589
+
590
+ class EmptyDependency:
591
+ """Empty dependency predicate ready to get filled up."""
592
+
593
+ def __init__(self, level: int, rule_name: str):
594
+ self.level = level
595
+ self.rule_name = rule_name or ''
596
+ self.empty = True
597
+ self.why = []
598
+ self.trace = None
599
+
600
+ def populate(self, name: str, args: list[gm.Point]) -> Dependency:
601
+ dep = Dependency(name, args, self.rule_name, self.level)
602
+ dep.trace2 = self.trace
603
+ dep.why = list(self.why)
604
+ return dep
605
+
606
+ def copy(self) -> EmptyDependency:
607
+ other = EmptyDependency(self.level, self.rule_name)
608
+ other.why = list(self.why)
609
+ return other
610
+
611
+ def extend(
612
+ self,
613
+ g: Any,
614
+ name0: str,
615
+ args0: list[gm.Point],
616
+ name: str,
617
+ args: list[gm.Point],
618
+ ) -> EmptyDependency:
619
+ """Extend the dependency list by (name, args)."""
620
+ dep0 = self.populate(name0, args0)
621
+ deps = EmptyDependency(level=self.level, rule_name=None)
622
+ dep = Dependency(name, args, None, deps.level)
623
+ deps.why = [dep0, dep.why_me_or_cache(g, None)]
624
+ return deps
625
+
626
+ def extend_many(
627
+ self,
628
+ g: Any,
629
+ name0: str,
630
+ args0: list[gm.Point],
631
+ name_args: list[tuple[str, list[gm.Point]]],
632
+ ) -> EmptyDependency:
633
+ """Extend the dependency list by many name_args."""
634
+ if not name_args:
635
+ return self
636
+ dep0 = self.populate(name0, args0)
637
+ deps = EmptyDependency(level=self.level, rule_name=None)
638
+ deps.why = [dep0]
639
+ for name, args in name_args:
640
+ dep = Dependency(name, args, None, deps.level)
641
+ deps.why += [dep.why_me_or_cache(g, None)]
642
+ return deps
643
+
644
+
645
+ def maybe_make_equal_pairs(
646
+ a: gm.Point,
647
+ b: gm.Point,
648
+ c: gm.Point,
649
+ d: gm.Point,
650
+ m: gm.Point,
651
+ n: gm.Point,
652
+ p: gm.Point,
653
+ q: gm.Point,
654
+ ab: gm.Line,
655
+ mn: gm.Line,
656
+ g: Any,
657
+ level: int,
658
+ ) -> list[Dependency]:
659
+ """Make a-b:c-d==m-n:p-q in case a-b==m-n or c-d==p-q."""
660
+ if ab != mn:
661
+ return
662
+ why = []
663
+ eqname = 'para' if isinstance(ab, gm.Line) else 'cong'
664
+ colls = [a, b, m, n]
665
+ if len(set(colls)) > 2 and eqname == 'para':
666
+ dep = Dependency('collx', colls, None, level)
667
+ dep.why_me(g, level)
668
+ why += [dep]
669
+
670
+ dep = Dependency(eqname, [c, d, p, q], None, level)
671
+ dep.why_me(g, level)
672
+ why += [dep]
673
+ return why
674
+
675
+
676
+ class Dependency(Construction):
677
+ """Dependency is a predicate that other predicates depend on."""
678
+
679
+ def __init__(
680
+ self, name: str, args: list[gm.Point], rule_name: str, level: int
681
+ ):
682
+ super().__init__(name, args)
683
+ self.rule_name = rule_name or ''
684
+ self.level = level
685
+ self.why = []
686
+
687
+ self._stat = None
688
+ self.trace = None
689
+
690
+ def _find(self, dep_hashed: tuple[str, ...]) -> Dependency:
691
+ for w in self.why:
692
+ f = w._find(dep_hashed)
693
+ if f:
694
+ return f
695
+ if w.hashed() == dep_hashed:
696
+ return w
697
+
698
+ def remove_loop(self) -> Dependency:
699
+ f = self._find(self.hashed())
700
+ if f:
701
+ return f
702
+ return self
703
+
704
+ def copy(self) -> Dependency:
705
+ dep = Dependency(self.name, self.args, self.rule_name, self.level)
706
+ dep.trace = self.trace
707
+ dep.why = list(self.why)
708
+ return dep
709
+
710
+ def why_me_or_cache(self, g: Any, level: int) -> Dependency:
711
+ if self.hashed() in g.cache:
712
+ return g.cache[self.hashed()]
713
+ self.why_me(g, level)
714
+ return self
715
+
716
+ def populate(self, name: str, args: list[gm.Point]) -> Dependency:
717
+ assert self.rule_name == CONSTRUCTION_RULE, self.rule_name
718
+ dep = Dependency(self.name, self.args, self.rule_name, self.level)
719
+ dep.why = list(self.why)
720
+ return dep
721
+
722
+ def why_me(self, g: Any, level: int) -> None:
723
+ """Figure out the dependencies predicates of self."""
724
+ name, args = self.name, self.args
725
+
726
+ hashed_me = hashed(name, args)
727
+ if hashed_me in g.cache:
728
+ dep = g.cache[hashed_me]
729
+ self.why = dep.why
730
+ self.rule_name = dep.rule_name
731
+ return
732
+
733
+ if self.name == 'para':
734
+ a, b, c, d = self.args
735
+ if {a, b} == {c, d}:
736
+ self.why = []
737
+ return
738
+
739
+ ab = g._get_line(a, b)
740
+ cd = g._get_line(c, d)
741
+ if ab == cd:
742
+ if {a, b} == {c, d}:
743
+ self.why = []
744
+ self.rule_name = ''
745
+ return
746
+ dep = Dependency('coll', list({a, b, c, d}), 't??', None)
747
+ self.why = [dep.why_me_or_cache(g, level)]
748
+ return
749
+
750
+ for (x, y), xy in zip([(a, b), (c, d)], [ab, cd]):
751
+ x_, y_ = xy.points
752
+ if {x, y} == {x_, y_}:
753
+ continue
754
+ d = Dependency('collx', [x, y, x_, y_], None, level)
755
+ self.why += [d.why_me_or_cache(g, level)]
756
+
757
+ whypara = g.why_equal(ab, cd, None)
758
+ self.why += whypara
759
+
760
+ elif self.name == 'midp':
761
+ m, a, b = self.args
762
+ ma = g._get_segment(m, a)
763
+ mb = g._get_segment(m, b)
764
+ dep = Dependency('coll', [m, a, b], None, None).why_me_or_cache(g, None)
765
+ self.why = [dep] + g.why_equal(ma, mb, level)
766
+
767
+ elif self.name == 'perp':
768
+ a, b, c, d = self.args
769
+ ab = g._get_line(a, b)
770
+ cd = g._get_line(c, d)
771
+ for (x, y), xy in zip([(a, b), (c, d)], [ab, cd]):
772
+ x_, y_ = xy.points
773
+ if {x, y} == {x_, y_}:
774
+ continue
775
+ d = Dependency('collx', [x, y, x_, y_], None, level)
776
+ self.why += [d.why_me_or_cache(g, level)]
777
+
778
+ _, why = why_eqangle(ab._val, cd._val, cd._val, ab._val, level)
779
+ a, b = ab.points
780
+ c, d = cd.points
781
+
782
+ if hashed(self.name, [a, b, c, d]) != self.hashed():
783
+ d = Dependency(self.name, [a, b, c, d], None, level)
784
+ d.why = why
785
+ why = [d]
786
+
787
+ self.why += why
788
+
789
+ elif self.name == 'cong':
790
+ a, b, c, d = self.args
791
+ ab = g._get_segment(a, b)
792
+ cd = g._get_segment(c, d)
793
+
794
+ self.why = g.why_equal(ab, cd, level)
795
+
796
+ elif self.name == 'coll':
797
+ _, why = gm.line_of_and_why(self.args, level)
798
+ self.why = why
799
+
800
+ elif self.name == 'collx':
801
+ if g.check_coll(self.args):
802
+ args = list(set(self.args))
803
+ hashed_me = hashed('coll', args)
804
+ if hashed_me in g.cache:
805
+ dep = g.cache[hashed_me]
806
+ self.why = [dep]
807
+ self.rule_name = ''
808
+ return
809
+ _, self.why = gm.line_of_and_why(args, level)
810
+ else:
811
+ self.name = 'para'
812
+ self.why_me(g, level)
813
+
814
+ elif self.name == 'cyclic':
815
+ _, why = gm.circle_of_and_why(self.args, level)
816
+ self.why = why
817
+
818
+ elif self.name == 'circle':
819
+ o, a, b, c = self.args
820
+ oa = g._get_segment(o, a)
821
+ ob = g._get_segment(o, b)
822
+ oc = g._get_segment(o, c)
823
+ self.why = g.why_equal(oa, ob, level) + g.why_equal(oa, oc, level)
824
+
825
+ elif self.name in ['eqangle', 'eqangle6']:
826
+ a, b, c, d, m, n, p, q = self.args
827
+
828
+ ab, why1 = g.get_line_thru_pair_why(a, b)
829
+ cd, why2 = g.get_line_thru_pair_why(c, d)
830
+ mn, why3 = g.get_line_thru_pair_why(m, n)
831
+ pq, why4 = g.get_line_thru_pair_why(p, q)
832
+
833
+ if ab is None or cd is None or mn is None or pq is None:
834
+ if {a, b} == {m, n}:
835
+ d = Dependency('para', [c, d, p, q], None, level)
836
+ self.why = [d.why_me_or_cache(g, level)]
837
+ if {a, b} == {c, d}:
838
+ d = Dependency('para', [p, q, m, n], None, level)
839
+ self.why = [d.why_me_or_cache(g, level)]
840
+ if {c, d} == {p, q}:
841
+ d = Dependency('para', [a, b, m, n], None, level)
842
+ self.why = [d.why_me_or_cache(g, level)]
843
+ if {p, q} == {m, n}:
844
+ d = Dependency('para', [a, b, c, d], None, level)
845
+ self.why = [d.why_me_or_cache(g, level)]
846
+ return
847
+
848
+ for (x, y), xy, whyxy in zip(
849
+ [(a, b), (c, d), (m, n), (p, q)],
850
+ [ab, cd, mn, pq],
851
+ [why1, why2, why3, why4],
852
+ ):
853
+ x_, y_ = xy.points
854
+ if {x, y} == {x_, y_}:
855
+ continue
856
+ d = Dependency('collx', [x, y, x_, y_], None, level)
857
+ d.why = whyxy
858
+ self.why += [d]
859
+
860
+ a, b = ab.points
861
+ c, d = cd.points
862
+ m, n = mn.points
863
+ p, q = pq.points
864
+ diff = hashed(self.name, [a, b, c, d, m, n, p, q]) != self.hashed()
865
+
866
+ whyeqangle = None
867
+ if ab._val and cd._val and mn._val and pq._val:
868
+ whyeqangle = why_eqangle(ab._val, cd._val, mn._val, pq._val, level)
869
+
870
+ if whyeqangle:
871
+ (dab, dcd, dmn, dpq), whyeqangle = whyeqangle
872
+ if diff:
873
+ d = Dependency('eqangle', [a, b, c, d, m, n, p, q], None, level)
874
+ d.why = whyeqangle
875
+ whyeqangle = [d]
876
+ self.why += whyeqangle
877
+
878
+ else:
879
+ if (ab == cd and mn == pq) or (ab == mn and cd == pq):
880
+ self.why += []
881
+ elif ab == mn:
882
+ self.why += maybe_make_equal_pairs(
883
+ a, b, c, d, m, n, p, q, ab, mn, g, level
884
+ )
885
+ elif cd == pq:
886
+ self.why += maybe_make_equal_pairs(
887
+ c, d, a, b, p, q, m, n, cd, pq, g, level
888
+ )
889
+ elif ab == cd:
890
+ self.why += maybe_make_equal_pairs(
891
+ a, b, m, n, c, d, p, q, ab, cd, g, level
892
+ )
893
+ elif mn == pq:
894
+ self.why += maybe_make_equal_pairs(
895
+ m, n, a, b, p, q, c, d, mn, pq, g, level
896
+ )
897
+ elif g.is_equal(ab, mn) or g.is_equal(cd, pq):
898
+ dep1 = Dependency('para', [a, b, m, n], None, level)
899
+ dep1.why_me(g, level)
900
+ dep2 = Dependency('para', [c, d, p, q], None, level)
901
+ dep2.why_me(g, level)
902
+ self.why += [dep1, dep2]
903
+ elif g.is_equal(ab, cd) or g.is_equal(mn, pq):
904
+ dep1 = Dependency('para', [a, b, c, d], None, level)
905
+ dep1.why_me(g, level)
906
+ dep2 = Dependency('para', [m, n, p, q], None, level)
907
+ dep2.why_me(g, level)
908
+ self.why += [dep1, dep2]
909
+ elif ab._val and cd._val and mn._val and pq._val:
910
+ self.why = why_eqangle(ab._val, cd._val, mn._val, pq._val, level)
911
+
912
+ elif self.name in ['eqratio', 'eqratio6']:
913
+ a, b, c, d, m, n, p, q = self.args
914
+ ab = g._get_segment(a, b)
915
+ cd = g._get_segment(c, d)
916
+ mn = g._get_segment(m, n)
917
+ pq = g._get_segment(p, q)
918
+
919
+ if ab is None or cd is None or mn is None or pq is None:
920
+ if {a, b} == {m, n}:
921
+ d = Dependency('cong', [c, d, p, q], None, level)
922
+ self.why = [d.why_me_or_cache(g, level)]
923
+ if {a, b} == {c, d}:
924
+ d = Dependency('cong', [p, q, m, n], None, level)
925
+ self.why = [d.why_me_or_cache(g, level)]
926
+ if {c, d} == {p, q}:
927
+ d = Dependency('cong', [a, b, m, n], None, level)
928
+ self.why = [d.why_me_or_cache(g, level)]
929
+ if {p, q} == {m, n}:
930
+ d = Dependency('cong', [a, b, c, d], None, level)
931
+ self.why = [d.why_me_or_cache(g, level)]
932
+ return
933
+
934
+ if ab._val and cd._val and mn._val and pq._val:
935
+ self.why = why_eqratio(ab._val, cd._val, mn._val, pq._val, level)
936
+
937
+ if self.why is None:
938
+ self.why = []
939
+ if (ab == cd and mn == pq) or (ab == mn and cd == pq):
940
+ self.why = []
941
+ elif ab == mn:
942
+ self.why += maybe_make_equal_pairs(
943
+ a, b, c, d, m, n, p, q, ab, mn, g, level
944
+ )
945
+ elif cd == pq:
946
+ self.why += maybe_make_equal_pairs(
947
+ c, d, a, b, p, q, m, n, cd, pq, g, level
948
+ )
949
+ elif ab == cd:
950
+ self.why += maybe_make_equal_pairs(
951
+ a, b, m, n, c, d, p, q, ab, cd, g, level
952
+ )
953
+ elif mn == pq:
954
+ self.why += maybe_make_equal_pairs(
955
+ m, n, a, b, p, q, c, d, mn, pq, g, level
956
+ )
957
+ elif g.is_equal(ab, mn) or g.is_equal(cd, pq):
958
+ dep1 = Dependency('cong', [a, b, m, n], None, level)
959
+ dep1.why_me(g, level)
960
+ dep2 = Dependency('cong', [c, d, p, q], None, level)
961
+ dep2.why_me(g, level)
962
+ self.why += [dep1, dep2]
963
+ elif g.is_equal(ab, cd) or g.is_equal(mn, pq):
964
+ dep1 = Dependency('cong', [a, b, c, d], None, level)
965
+ dep1.why_me(g, level)
966
+ dep2 = Dependency('cong', [m, n, p, q], None, level)
967
+ dep2.why_me(g, level)
968
+ self.why += [dep1, dep2]
969
+ elif ab._val and cd._val and mn._val and pq._val:
970
+ self.why = why_eqangle(ab._val, cd._val, mn._val, pq._val, level)
971
+
972
+ elif self.name in ['diff', 'npara', 'nperp', 'ncoll', 'sameside']:
973
+ self.why = []
974
+
975
+ elif self.name == 'simtri':
976
+ a, b, c, x, y, z = self.args
977
+ dep1 = Dependency('eqangle', [a, b, a, c, x, y, x, z], '', level)
978
+ dep1.why_me(g, level)
979
+ dep2 = Dependency('eqangle', [b, a, b, c, y, x, y, z], '', level)
980
+ dep2.why_me(g, level)
981
+ self.rule_name = 'r34'
982
+ self.why = [dep1, dep2]
983
+
984
+ elif self.name == 'contri':
985
+ a, b, c, x, y, z = self.args
986
+ dep1 = Dependency('cong', [a, b, x, y], '', level)
987
+ dep1.why_me(g, level)
988
+ dep2 = Dependency('cong', [b, c, y, z], '', level)
989
+ dep2.why_me(g, level)
990
+ dep3 = Dependency('cong', [c, a, z, x], '', level)
991
+ dep3.why_me(g, level)
992
+ self.rule_name = 'r32'
993
+ self.why = [dep1, dep2, dep3]
994
+
995
+ elif self.name == 'ind':
996
+ pass
997
+
998
+ elif self.name == 'aconst':
999
+ a, b, c, d, ang0 = self.args
1000
+
1001
+ measure = ang0._val
1002
+
1003
+ for ang in measure.neighbors(gm.Angle):
1004
+ if ang == ang0:
1005
+ continue
1006
+ d1, d2 = ang._d
1007
+ l1, l2 = d1._obj, d2._obj
1008
+ (a1, b1), (c1, d1) = l1.points, l2.points
1009
+
1010
+ if not g.check_para_or_coll([a, b, a1, b1]) or not g.check_para_or_coll(
1011
+ [c, d, c1, d1]
1012
+ ):
1013
+ continue
1014
+
1015
+ self.why = []
1016
+ for args in [(a, b, a1, b1), (c, d, c1, d1)]:
1017
+ if g.check_coll(args):
1018
+ if len(set(args)) > 2:
1019
+ dep = Dependency('coll', args, None, None)
1020
+ self.why.append(dep.why_me_or_cache(g, level))
1021
+ else:
1022
+ dep = Dependency('para', args, None, None)
1023
+ self.why.append(dep.why_me_or_cache(g, level))
1024
+
1025
+ self.why += gm.why_equal(ang, ang0)
1026
+ break
1027
+
1028
+ elif self.name == 'rconst':
1029
+ a, b, c, d, rat0 = self.args
1030
+
1031
+ val = rat0._val
1032
+
1033
+ for rat in val.neighbors(gm.Ratio):
1034
+ if rat == rat0:
1035
+ continue
1036
+ l1, l2 = rat._l
1037
+ s1, s2 = l1._obj, l2._obj
1038
+ (a1, b1), (c1, d1) = list(s1.points), list(s2.points)
1039
+
1040
+ if not g.check_cong([a, b, a1, b1]) or not g.check_cong([c, d, c1, d1]):
1041
+ continue
1042
+
1043
+ self.why = []
1044
+ for args in [(a, b, a1, b1), (c, d, c1, d1)]:
1045
+ if len(set(args)) > 2:
1046
+ dep = Dependency('cong', args, None, None)
1047
+ self.why.append(dep.why_me_or_cache(g, level))
1048
+
1049
+ self.why += gm.why_equal(rat, rat0)
1050
+ break
1051
+
1052
+ else:
1053
+ raise ValueError('Not recognize', self.name)
1054
+
1055
+ def hashed(self, rename: bool = False) -> tuple[str, ...]:
1056
+ return hashed(self.name, self.args, rename=rename)
1057
+
1058
+
1059
+ def hashed(
1060
+ name: str, args: list[gm.Point], rename: bool = False
1061
+ ) -> tuple[str, ...]:
1062
+ if name == 's_angle':
1063
+ args = [p.name if not rename else p.new_name for p in args[:-1]] + [
1064
+ str(args[-1])
1065
+ ]
1066
+ else:
1067
+ args = [p.name if not rename else p.new_name for p in args]
1068
+ return hashed_txt(name, args)
1069
+
1070
+
1071
+ def hashed_txt(name: str, args: list[str]) -> tuple[str, ...]:
1072
+ """Return a tuple unique to name and args upto arg permutation equivariant."""
1073
+
1074
+ if name in ['const', 'aconst', 'rconst']:
1075
+ a, b, c, d, y = args
1076
+ a, b = sorted([a, b])
1077
+ c, d = sorted([c, d])
1078
+ return name, a, b, c, d, y
1079
+
1080
+ if name in ['npara', 'nperp', 'para', 'cong', 'perp', 'collx']:
1081
+ a, b, c, d = args
1082
+
1083
+ a, b = sorted([a, b])
1084
+ c, d = sorted([c, d])
1085
+ (a, b), (c, d) = sorted([(a, b), (c, d)])
1086
+
1087
+ return (name, a, b, c, d)
1088
+
1089
+ if name in ['midp', 'midpoint']:
1090
+ a, b, c = args
1091
+ b, c = sorted([b, c])
1092
+ return (name, a, b, c)
1093
+
1094
+ if name in ['coll', 'cyclic', 'ncoll', 'diff', 'triangle']:
1095
+ return (name,) + tuple(sorted(list(set(args))))
1096
+
1097
+ if name == 'circle':
1098
+ x, a, b, c = args
1099
+ return (name, x) + tuple(sorted([a, b, c]))
1100
+
1101
+ if name in ['eqangle', 'eqratio', 'eqangle6', 'eqratio6']:
1102
+ a, b, c, d, e, f, g, h = args
1103
+ a, b = sorted([a, b])
1104
+ c, d = sorted([c, d])
1105
+ e, f = sorted([e, f])
1106
+ g, h = sorted([g, h])
1107
+ if tuple(sorted([a, b, e, f])) > tuple(sorted([c, d, g, h])):
1108
+ a, b, e, f, c, d, g, h = c, d, g, h, a, b, e, f
1109
+ if (a, b, c, d) > (e, f, g, h):
1110
+ a, b, c, d, e, f, g, h = e, f, g, h, a, b, c, d
1111
+
1112
+ if name == 'eqangle6':
1113
+ name = 'eqangle'
1114
+ if name == 'eqratio6':
1115
+ name = 'eqratio'
1116
+ return (name,) + (a, b, c, d, e, f, g, h)
1117
+
1118
+ if name in ['contri', 'simtri', 'simtri2', 'contri2', 'contri*', 'simtri*']:
1119
+ a, b, c, x, y, z = args
1120
+ (a, x), (b, y), (c, z) = sorted([(a, x), (b, y), (c, z)], key=sorted)
1121
+ (a, b, c), (x, y, z) = sorted([(a, b, c), (x, y, z)], key=sorted)
1122
+ return (name, a, b, c, x, y, z)
1123
+
1124
+ if name in ['eqratio3']:
1125
+ a, b, c, d, o, o = args # pylint: disable=redeclared-assigned-name
1126
+ (a, c), (b, d) = sorted([(a, c), (b, d)], key=sorted)
1127
+ (a, b), (c, d) = sorted([(a, b), (c, d)], key=sorted)
1128
+ return (name, a, b, c, d, o, o)
1129
+
1130
+ if name in ['sameside', 's_angle']:
1131
+ return (name,) + tuple(args)
1132
+
1133
+ raise ValueError(f'Not recognize {name} to hash.')
ag4masses/alphageometry/problem_test.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Unit tests for problem.py."""
17
+ import unittest
18
+
19
+ from absl.testing import absltest
20
+ import problem as pr
21
+
22
+
23
+ class ProblemTest(unittest.TestCase):
24
+
25
+ @classmethod
26
+ def setUpClass(cls):
27
+ super().setUpClass()
28
+ cls.defs = pr.Definition.from_txt_file('defs.txt', to_dict=True)
29
+
30
+ def test_orthocenter_no_translate(self):
31
+ txt = 'a b c = triangle a b c; h = on_tline h b a c, on_tline h c a b ? perp a h b c' # pylint: disable=line-too-long
32
+
33
+ # read the txt into pr.Problem object, do not change the name of points:
34
+ p = pr.Problem.from_txt(txt, translate=False)
35
+
36
+ # This is fed into the LM, translating from constructive to constrained:
37
+ setup_str = p.setup_str_from_problem(ProblemTest.defs)
38
+
39
+ self.assertEqual(
40
+ setup_str,
41
+ '{S} a : ; b : ; c : ; h : T a b c h 00 T a c b h 01 ? T a h b c',
42
+ )
43
+
44
+ def test_orthocenter_translate(self):
45
+ txt = 'a b c = triangle a b c; h = on_tline h b a c, on_tline h c a b ? perp a h b c' # pylint: disable=line-too-long
46
+
47
+ # Read the txt into pr.Problem object, change h -> d to match
48
+ # training data distribution.
49
+ p = pr.Problem.from_txt(txt, translate=True)
50
+
51
+ # This is fed into the LM, translating from constructive to constrained:
52
+ setup_str = p.setup_str_from_problem(ProblemTest.defs)
53
+
54
+ self.assertEqual(
55
+ setup_str,
56
+ '{S} a : ; b : ; c : ; d : T a b c d 00 T a c b d 01 ? T a d b c',
57
+ )
58
+
59
+
60
+ if __name__ == '__main__':
61
+ absltest.main()
ag4masses/alphageometry/rules.txt ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ perp A B C D, perp C D E F, ncoll A B E => para A B E F
2
+ cong O A O B, cong O B O C, cong O C O D => cyclic A B C D
3
+ eqangle A B P Q C D P Q => para A B C D
4
+ cyclic A B P Q => eqangle P A P B Q A Q B
5
+ eqangle6 P A P B Q A Q B, ncoll P Q A B => cyclic A B P Q
6
+ cyclic A B C P Q R, eqangle C A C B R P R Q => cong A B P Q
7
+ midp E A B, midp F A C => para E F B C
8
+ para A B C D, coll O A C, coll O B D => eqratio3 A B C D O O
9
+ perp A B C D, perp E F G H, npara A B E F => eqangle A B E F C D G H
10
+ eqangle a b c d m n p q, eqangle c d e f p q r u => eqangle a b e f m n r u
11
+ eqratio a b c d m n p q, eqratio c d e f p q r u => eqratio a b e f m n r u
12
+ eqratio6 d b d c a b a c, coll d b c, ncoll a b c => eqangle6 a b a d a d a c
13
+ eqangle6 a b a d a d a c, coll d b c, ncoll a b c => eqratio6 d b d c a b a c
14
+ cong O A O B, ncoll O A B => eqangle O A A B A B O B
15
+ eqangle6 A O A B B A B O, ncoll O A B => cong O A O B
16
+ circle O A B C, perp O A A X => eqangle A X A B C A C B
17
+ circle O A B C, eqangle A X A B C A C B => perp O A A X
18
+ circle O A B C, midp M B C => eqangle A B A C O B O M
19
+ circle O A B C, coll M B C, eqangle A B A C O B O M => midp M B C
20
+ perp A B B C, midp M A C => cong A M B M
21
+ circle O A B C, coll O A C => perp A B B C
22
+ cyclic A B C D, para A B C D => eqangle A D C D C D C B
23
+ midp M A B, perp O M A B => cong O A O B
24
+ cong A P B P, cong A Q B Q => perp A B P Q
25
+ cong A P B P, cong A Q B Q, cyclic A B P Q => perp P A A Q
26
+ midp M A B, midp M C D => para A C B D
27
+ midp M A B, para A C B D, para A D B C => midp M C D
28
+ eqratio O A A C O B B D, coll O A C, coll O B D, ncoll A B C, sameside A O C B O D => para A B C D
29
+ para A B A C => coll A B C
30
+ midp M A B, midp N C D => eqratio M A A B N C C D
31
+ eqangle A B P Q C D U V, perp P Q U V => perp A B C D
32
+ eqratio A B P Q C D U V, cong P Q U V => cong A B C D
33
+ cong A B P Q, cong B C Q R, cong C A R P, ncoll A B C => contri* A B C P Q R
34
+ cong A B P Q, cong B C Q R, eqangle6 B A B C Q P Q R, ncoll A B C => contri* A B C P Q R
35
+ eqangle6 B A B C Q P Q R, eqangle6 C A C B R P R Q, ncoll A B C => simtri A B C P Q R
36
+ eqangle6 B A B C Q R Q P, eqangle6 C A C B R Q R P, ncoll A B C => simtri2 A B C P Q R
37
+ eqangle6 B A B C Q P Q R, eqangle6 C A C B R P R Q, ncoll A B C, cong A B P Q => contri A B C P Q R
38
+ eqangle6 B A B C Q R Q P, eqangle6 C A C B R Q R P, ncoll A B C, cong A B P Q => contri2 A B C P Q R
39
+ eqratio6 B A B C Q P Q R, eqratio6 C A C B R P R Q, ncoll A B C => simtri* A B C P Q R
40
+ eqratio6 B A B C Q P Q R, eqangle6 B A B C Q P Q R, ncoll A B C => simtri* A B C P Q R
41
+ eqratio6 B A B C Q P Q R, eqratio6 C A C B R P R Q, ncoll A B C, cong A B P Q => contri* A B C P Q R
42
+ para a b c d, coll m a d, coll n b c, eqratio6 m a m d n b n c, sameside m a d n b c => para m n a b
43
+ para a b c d, coll m a d, coll n b c, para m n a b => eqratio6 m a m d n b n c
ag4masses/alphageometry/trace_back.py ADDED
@@ -0,0 +1,374 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Implements DAG-level traceback."""
17
+
18
+ from typing import Any
19
+
20
+ import geometry as gm
21
+ import pretty as pt
22
+ import problem
23
+
24
+
25
+ pretty = pt.pretty
26
+
27
+
28
+ def point_levels(
29
+ setup: list[problem.Dependency], existing_points: list[gm.Point]
30
+ ) -> list[tuple[set[gm.Point], list[problem.Dependency]]]:
31
+ """Reformat setup into levels of point constructions."""
32
+ levels = []
33
+ for con in setup:
34
+ plevel = max([p.plevel for p in con.args if isinstance(p, gm.Point)])
35
+
36
+ while len(levels) - 1 < plevel:
37
+ levels.append((set(), []))
38
+
39
+ for p in con.args:
40
+ if not isinstance(p, gm.Point):
41
+ continue
42
+ if existing_points and p in existing_points:
43
+ continue
44
+
45
+ levels[p.plevel][0].add(p)
46
+
47
+ cons = levels[plevel][1]
48
+ cons.append(con)
49
+
50
+ return [(p, c) for p, c in levels if p or c]
51
+
52
+
53
+ def point_log(
54
+ setup: list[problem.Dependency],
55
+ ref_id: dict[tuple[str, ...], int],
56
+ existing_points=list[gm.Point],
57
+ ) -> list[tuple[list[gm.Point], list[problem.Dependency]]]:
58
+ """Reformat setup into groups of point constructions."""
59
+ log = []
60
+
61
+ levels = point_levels(setup, existing_points)
62
+
63
+ for points, cons in levels:
64
+ for con in cons:
65
+ if con.hashed() not in ref_id:
66
+ ref_id[con.hashed()] = len(ref_id)
67
+
68
+ log.append((points, cons))
69
+
70
+ return log
71
+
72
+
73
+ def setup_to_levels(
74
+ setup: list[problem.Dependency],
75
+ ) -> list[list[problem.Dependency]]:
76
+ """Reformat setup into levels of point constructions."""
77
+ levels = []
78
+ for d in setup:
79
+ plevel = max([p.plevel for p in d.args if isinstance(p, gm.Point)])
80
+ while len(levels) - 1 < plevel:
81
+ levels.append([])
82
+
83
+ levels[plevel].append(d)
84
+
85
+ levels = [lvl for lvl in levels if lvl]
86
+ return levels
87
+
88
+
89
+ def separate_dependency_difference(
90
+ query: problem.Dependency,
91
+ log: list[tuple[list[problem.Dependency], list[problem.Dependency]]],
92
+ ) -> tuple[
93
+ list[tuple[list[problem.Dependency], list[problem.Dependency]]],
94
+ list[problem.Dependency],
95
+ list[problem.Dependency],
96
+ set[gm.Point],
97
+ set[gm.Point],
98
+ ]:
99
+ """Identify and separate the dependency difference."""
100
+ setup = []
101
+ log_, log = log, []
102
+ for prems, cons in log_:
103
+ if not prems:
104
+ setup.extend(cons)
105
+ continue
106
+ cons_ = []
107
+ for con in cons:
108
+ if con.rule_name == 'c0':
109
+ setup.append(con)
110
+ else:
111
+ cons_.append(con)
112
+ if not cons_:
113
+ continue
114
+
115
+ prems = [p for p in prems if p.name != 'ind']
116
+ log.append((prems, cons_))
117
+
118
+ points = set(query.args)
119
+ queue = list(query.args)
120
+ i = 0
121
+ while i < len(queue):
122
+ q = queue[i]
123
+ i += 1
124
+ if not isinstance(q, gm.Point):
125
+ continue
126
+ for p in q.rely_on:
127
+ if p not in points:
128
+ points.add(p)
129
+ queue.append(p)
130
+
131
+ setup_, setup, aux_setup, aux_points = setup, [], [], set()
132
+ for con in setup_:
133
+ if con.name == 'ind':
134
+ continue
135
+ elif any([p not in points for p in con.args if isinstance(p, gm.Point)]):
136
+ aux_setup.append(con)
137
+ aux_points.update(
138
+ [p for p in con.args if isinstance(p, gm.Point) and p not in points]
139
+ )
140
+ else:
141
+ setup.append(con)
142
+
143
+ return log, setup, aux_setup, points, aux_points
144
+
145
+
146
+ def recursive_traceback(
147
+ query: problem.Dependency,
148
+ ) -> list[tuple[list[problem.Dependency], list[problem.Dependency]]]:
149
+ """Recursively traceback from the query, i.e. the conclusion."""
150
+ visited = set()
151
+ log = []
152
+ stack = []
153
+
154
+ def read(q: problem.Dependency) -> None:
155
+ q = q.remove_loop()
156
+ hashed = q.hashed()
157
+ if hashed in visited:
158
+ return
159
+
160
+ if hashed[0] in ['ncoll', 'npara', 'nperp', 'diff', 'sameside']:
161
+ return
162
+
163
+ nonlocal stack
164
+
165
+ stack.append(hashed)
166
+ prems = []
167
+
168
+ if q.rule_name != problem.CONSTRUCTION_RULE:
169
+ all_deps = []
170
+ dep_names = set()
171
+ for d in q.why:
172
+ if d.hashed() in dep_names:
173
+ continue
174
+ dep_names.add(d.hashed())
175
+ all_deps.append(d)
176
+
177
+ for d in all_deps:
178
+ h = d.hashed()
179
+ if h not in visited:
180
+ read(d)
181
+ if h in visited:
182
+ prems.append(d)
183
+
184
+ visited.add(hashed)
185
+ hashs = sorted([d.hashed() for d in prems])
186
+ found = False
187
+ for ps, qs in log:
188
+ if sorted([d.hashed() for d in ps]) == hashs:
189
+ qs += [q]
190
+ found = True
191
+ break
192
+ if not found:
193
+ log.append((prems, [q]))
194
+
195
+ stack.pop(-1)
196
+
197
+ read(query)
198
+
199
+ # post process log: separate multi-conclusion lines
200
+ log_, log = log, []
201
+ for ps, qs in log_:
202
+ for q in qs:
203
+ log.append((ps, [q]))
204
+
205
+ return log
206
+
207
+
208
+ def collx_to_coll_setup(
209
+ setup: list[problem.Dependency],
210
+ ) -> list[problem.Dependency]:
211
+ """Convert collx to coll in setups."""
212
+ result = []
213
+ for level in setup_to_levels(setup):
214
+ hashs = set()
215
+ for dep in level:
216
+ if dep.name == 'collx':
217
+ dep.name = 'coll'
218
+ dep.args = list(set(dep.args))
219
+
220
+ if dep.hashed() in hashs:
221
+ continue
222
+ hashs.add(dep.hashed())
223
+ result.append(dep)
224
+
225
+ return result
226
+
227
+
228
+ def collx_to_coll(
229
+ setup: list[problem.Dependency],
230
+ aux_setup: list[problem.Dependency],
231
+ log: list[tuple[list[problem.Dependency], list[problem.Dependency]]],
232
+ ) -> tuple[
233
+ list[problem.Dependency],
234
+ list[problem.Dependency],
235
+ list[tuple[list[problem.Dependency], list[problem.Dependency]]],
236
+ ]:
237
+ """Convert collx to coll and dedup."""
238
+ setup = collx_to_coll_setup(setup)
239
+ aux_setup = collx_to_coll_setup(aux_setup)
240
+
241
+ con_set = set([p.hashed() for p in setup + aux_setup])
242
+ log_, log = log, []
243
+ for prems, cons in log_:
244
+ prem_set = set()
245
+ prems_, prems = prems, []
246
+ for p in prems_:
247
+ if p.name == 'collx':
248
+ p.name = 'coll'
249
+ p.args = list(set(p.args))
250
+ if p.hashed() in prem_set:
251
+ continue
252
+ prem_set.add(p.hashed())
253
+ prems.append(p)
254
+
255
+ cons_, cons = cons, []
256
+ for c in cons_:
257
+ if c.name == 'collx':
258
+ c.name = 'coll'
259
+ c.args = list(set(c.args))
260
+ if c.hashed() in con_set:
261
+ continue
262
+ con_set.add(c.hashed())
263
+ cons.append(c)
264
+
265
+ if not cons or not prems:
266
+ continue
267
+
268
+ log.append((prems, cons))
269
+
270
+ return setup, aux_setup, log
271
+
272
+
273
+ def get_logs(
274
+ query: problem.Dependency, g: Any, merge_trivials: bool = False
275
+ ) -> tuple[
276
+ list[problem.Dependency],
277
+ list[problem.Dependency],
278
+ list[tuple[list[problem.Dependency], list[problem.Dependency]]],
279
+ set[gm.Point],
280
+ ]:
281
+ """Given a DAG and conclusion N, return the premise, aux, proof."""
282
+ query = query.why_me_or_cache(g, query.level)
283
+ log = recursive_traceback(query)
284
+ log, setup, aux_setup, setup_points, _ = separate_dependency_difference(
285
+ query, log
286
+ )
287
+
288
+ setup, aux_setup, log = collx_to_coll(setup, aux_setup, log)
289
+
290
+ setup, aux_setup, log = shorten_and_shave(
291
+ setup, aux_setup, log, merge_trivials
292
+ )
293
+
294
+ return setup, aux_setup, log, setup_points
295
+
296
+
297
+ def shorten_and_shave(
298
+ setup: list[problem.Dependency],
299
+ aux_setup: list[problem.Dependency],
300
+ log: list[tuple[list[problem.Dependency], list[problem.Dependency]]],
301
+ merge_trivials: bool = False,
302
+ ) -> tuple[
303
+ list[problem.Dependency],
304
+ list[problem.Dependency],
305
+ list[tuple[list[problem.Dependency], list[problem.Dependency]]],
306
+ ]:
307
+ """Shorten the proof by removing unused predicates."""
308
+ log, _ = shorten_proof(log, merge_trivials=merge_trivials)
309
+
310
+ all_prems = sum([list(prems) for prems, _ in log], [])
311
+ all_prems = set([p.hashed() for p in all_prems])
312
+ setup = [d for d in setup if d.hashed() in all_prems]
313
+ aux_setup = [d for d in aux_setup if d.hashed() in all_prems]
314
+ return setup, aux_setup, log
315
+
316
+
317
+ def join_prems(
318
+ con: problem.Dependency,
319
+ con2prems: dict[tuple[str, ...], list[problem.Dependency]],
320
+ expanded: set[tuple[str, ...]],
321
+ ) -> list[problem.Dependency]:
322
+ """Join proof steps with the same premises."""
323
+ h = con.hashed()
324
+ if h in expanded or h not in con2prems:
325
+ return [con]
326
+
327
+ result = []
328
+ for p in con2prems[h]:
329
+ result += join_prems(p, con2prems, expanded)
330
+ return result
331
+
332
+
333
+ def shorten_proof(
334
+ log: list[tuple[list[problem.Dependency], list[problem.Dependency]]],
335
+ merge_trivials: bool = False,
336
+ ) -> tuple[
337
+ list[tuple[list[problem.Dependency], list[problem.Dependency]]],
338
+ dict[tuple[str, ...], list[problem.Dependency]],
339
+ ]:
340
+ """Join multiple trivials proof steps into one."""
341
+ pops = set()
342
+ con2prem = {}
343
+ for prems, cons in log:
344
+ assert len(cons) == 1
345
+ con = cons[0]
346
+ if con.rule_name == '': # pylint: disable=g-explicit-bool-comparison
347
+ con2prem[con.hashed()] = prems
348
+ elif not merge_trivials:
349
+ # except for the ones that are premises to non-trivial steps.
350
+ pops.update({p.hashed() for p in prems})
351
+
352
+ for p in pops:
353
+ if p in con2prem:
354
+ con2prem.pop(p)
355
+
356
+ expanded = set()
357
+ log2 = []
358
+ for i, (prems, cons) in enumerate(log):
359
+ con = cons[0]
360
+ if i < len(log) - 1 and con.hashed() in con2prem:
361
+ continue
362
+
363
+ hashs = set()
364
+ new_prems = []
365
+
366
+ for p in sum([join_prems(p, con2prem, expanded) for p in prems], []):
367
+ if p.hashed() not in hashs:
368
+ new_prems.append(p)
369
+ hashs.add(p.hashed())
370
+
371
+ log2 += [(new_prems, [con])]
372
+ expanded.add(con.hashed())
373
+
374
+ return log2, con2prem
ag4masses/alphageometry/trace_back_test.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2023 DeepMind Technologies Limited
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Unit testing for the trace_back code."""
17
+
18
+ import unittest
19
+
20
+ from absl.testing import absltest
21
+ import ddar
22
+ import graph as gh
23
+ import problem as pr
24
+ import trace_back as tb
25
+
26
+
27
+ class TracebackTest(unittest.TestCase):
28
+
29
+ @classmethod
30
+ def setUpClass(cls):
31
+ super().setUpClass()
32
+ cls.defs = pr.Definition.from_txt_file('defs.txt', to_dict=True)
33
+ cls.rules = pr.Theorem.from_txt_file('rules.txt', to_dict=True)
34
+
35
+ def test_orthocenter_dependency_difference(self):
36
+ txt = 'a b c = triangle a b c; d = on_tline d b a c, on_tline d c a b; e = on_line e a c, on_line e b d ? perp a d b c' # pylint: disable=line-too-long
37
+ p = pr.Problem.from_txt(txt)
38
+ g, _ = gh.Graph.build_problem(p, TracebackTest.defs)
39
+
40
+ ddar.solve(g, TracebackTest.rules, p)
41
+
42
+ goal_args = g.names2nodes(p.goal.args)
43
+ query = pr.Dependency(p.goal.name, goal_args, None, None)
44
+
45
+ setup, aux, _, _ = tb.get_logs(query, g, merge_trivials=False)
46
+
47
+ # Convert each predicates to its hash string:
48
+ setup = [p.hashed() for p in setup]
49
+ aux = [p.hashed() for p in aux]
50
+
51
+ self.assertCountEqual(
52
+ setup, [('perp', 'a', 'c', 'b', 'd'), ('perp', 'a', 'b', 'c', 'd')]
53
+ )
54
+
55
+ self.assertCountEqual(
56
+ aux, [('coll', 'a', 'c', 'e'), ('coll', 'b', 'd', 'e')]
57
+ )
58
+
59
+
60
+ if __name__ == '__main__':
61
+ absltest.main()