Skip to content

Modules

Modules

Core orchestration for geometric search.

Engine coordinates the deductive database (discrete inference rules) with the algebraic system (symbolic equation solving) against a mutable State. The two systems alternate until a goal is solved, a fixed point is reached, or a depth limit is hit.

Between

Bases: Relation

p1 lies between p2 and p3 (on the same line).

Source code in pyeuclid/formalization/relation.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
class Between(Relation):
    """p1 lies between p2 and p3 (on the same line)."""

    def __init__(self, p1: Point, p2: Point, p3: Point):
        """
        p1 is between p2 and p3.

        Args:
            p1 (Point): Middle point.
            p2 (Point): Endpoint 1.
            p3 (Point): Endpoint 2.
        """
        super().__init__()
        p2, p3 = sort_points(p2, p3)
        self.p1, self.p2, self.p3 = p1, p2, p3

    def permutations(self):
        """Enumerate equivalent point orderings for the between relation.

        Returns:
            list[tuple[Point, Point, Point]]: Permitted permutations.
        """
        return [(self.p1, self.p2, self.p3), (self.p1, self.p3, self.p2)]

__init__(p1, p2, p3)

p1 is between p2 and p3.

Parameters:

Name Type Description Default
p1 Point

Middle point.

required
p2 Point

Endpoint 1.

required
p3 Point

Endpoint 2.

required
Source code in pyeuclid/formalization/relation.py
170
171
172
173
174
175
176
177
178
179
180
181
def __init__(self, p1: Point, p2: Point, p3: Point):
    """
    p1 is between p2 and p3.

    Args:
        p1 (Point): Middle point.
        p2 (Point): Endpoint 1.
        p3 (Point): Endpoint 2.
    """
    super().__init__()
    p2, p3 = sort_points(p2, p3)
    self.p1, self.p2, self.p3 = p1, p2, p3

permutations()

Enumerate equivalent point orderings for the between relation.

Returns:

Type Description

list[tuple[Point, Point, Point]]: Permitted permutations.

Source code in pyeuclid/formalization/relation.py
183
184
185
186
187
188
189
def permutations(self):
    """Enumerate equivalent point orderings for the between relation.

    Returns:
        list[tuple[Point, Point, Point]]: Permitted permutations.
    """
    return [(self.p1, self.p2, self.p3), (self.p1, self.p3, self.p2)]

Collinear

Bases: Relation

Points p1,p2,p3 are collinear.

Source code in pyeuclid/formalization/relation.py
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
class Collinear(Relation):
    """Points p1,p2,p3 are collinear."""

    def __init__(self, p1, p2, p3):
        """
        Args:
            p1 (Point)
            p2 (Point)
            p3 (Point)
        """
        super().__init__()
        self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

    def permutations(self):
        """Enumerate collinearity argument permutations.

        Returns:
            itertools.permutations: All orderings of the three points.
        """
        return itertools.permutations([self.p1, self.p2, self.p3])

__init__(p1, p2, p3)

Source code in pyeuclid/formalization/relation.py
248
249
250
251
252
253
254
255
256
def __init__(self, p1, p2, p3):
    """
    Args:
        p1 (Point)
        p2 (Point)
        p3 (Point)
    """
    super().__init__()
    self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

permutations()

Enumerate collinearity argument permutations.

Returns:

Type Description

itertools.permutations: All orderings of the three points.

Source code in pyeuclid/formalization/relation.py
258
259
260
261
262
263
264
def permutations(self):
    """Enumerate collinearity argument permutations.

    Returns:
        itertools.permutations: All orderings of the three points.
    """
    return itertools.permutations([self.p1, self.p2, self.p3])

Concyclic

Bases: Relation

All given points lie on the same circle.

Source code in pyeuclid/formalization/relation.py
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
class Concyclic(Relation):
    """All given points lie on the same circle."""

    def __init__(self, *ps: Point):
        """
        Args:
            *ps (Point): Points to be tested for concyclicity.
        """
        super().__init__()
        self.ps = list(sort_points(*ps))

    def permutations(self):
        """Enumerate symmetric permutations of the point set.

        Returns:
            itertools.permutations: All orderings of the points.
        """
        return itertools.permutations(self.ps)

__init__(*ps)

Parameters:

Name Type Description Default
*ps Point

Points to be tested for concyclicity.

()
Source code in pyeuclid/formalization/relation.py
376
377
378
379
380
381
382
def __init__(self, *ps: Point):
    """
    Args:
        *ps (Point): Points to be tested for concyclicity.
    """
    super().__init__()
    self.ps = list(sort_points(*ps))

permutations()

Enumerate symmetric permutations of the point set.

Returns:

Type Description

itertools.permutations: All orderings of the points.

Source code in pyeuclid/formalization/relation.py
384
385
386
387
388
389
390
def permutations(self):
    """Enumerate symmetric permutations of the point set.

    Returns:
        itertools.permutations: All orderings of the points.
    """
    return itertools.permutations(self.ps)

Congruent

Bases: Relation

Triangles (p1,p2,p3) and (p4,p5,p6) are congruent.

Source code in pyeuclid/formalization/relation.py
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
class Congruent(Relation):
    """Triangles (p1,p2,p3) and (p4,p5,p6) are congruent."""

    def __init__(
        self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
    ):
        """
        Args:
            p1, p2, p3 (Point): First triangle vertices.
            p4, p5, p6 (Point): Second triangle vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

    def definition(self):
        """Congruence expressed as equal side lengths and non-collinearity.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) - Length(self.p4, self.p5),
            Length(self.p2, self.p3) - Length(self.p5, self.p6),
            Length(self.p1, self.p3) - Length(self.p4, self.p6),
            NotCollinear(self.p1, self.p2, self.p3),
        ]

__init__(p1, p2, p3, p4, p5, p6)

Parameters:

Name Type Description Default
p1, p2, p3 Point

First triangle vertices.

required
p4, p5, p6 Point

Second triangle vertices.

required
Source code in pyeuclid/formalization/relation.py
319
320
321
322
323
324
325
326
327
328
def __init__(
    self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
):
    """
    Args:
        p1, p2, p3 (Point): First triangle vertices.
        p4, p5, p6 (Point): Second triangle vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

definition()

Congruence expressed as equal side lengths and non-collinearity.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
330
331
332
333
334
335
336
337
338
339
340
341
def definition(self):
    """Congruence expressed as equal side lengths and non-collinearity.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) - Length(self.p4, self.p5),
        Length(self.p2, self.p3) - Length(self.p5, self.p6),
        Length(self.p1, self.p3) - Length(self.p4, self.p6),
        NotCollinear(self.p1, self.p2, self.p3),
    ]

Different

Bases: Relation

All provided points must be pairwise distinct.

Source code in pyeuclid/formalization/relation.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
class Different(Relation):
    """All provided points must be pairwise distinct."""

    def __init__(self, *ps: list[Point]):
        """
        Args:
            *ps (Point): Points that must be distinct.
        """
        super().__init__()
        self.ps = ps

    def definition(self):
        """Expand to pairwise inequality relations.

        Returns:
            list[Relation]: Negated equalities for every point pair.
        """
        return [Not(Equal(self.ps[i], self.ps[j])) for i in range(len(self.ps)) for j in range(i + 1, len(self.ps))]

__init__(*ps)

Parameters:

Name Type Description Default
*ps Point

Points that must be distinct.

()
Source code in pyeuclid/formalization/relation.py
150
151
152
153
154
155
156
def __init__(self, *ps: list[Point]):
    """
    Args:
        *ps (Point): Points that must be distinct.
    """
    super().__init__()
    self.ps = ps

definition()

Expand to pairwise inequality relations.

Returns:

Type Description

list[Relation]: Negated equalities for every point pair.

Source code in pyeuclid/formalization/relation.py
158
159
160
161
162
163
164
def definition(self):
    """Expand to pairwise inequality relations.

    Returns:
        list[Relation]: Negated equalities for every point pair.
    """
    return [Not(Equal(self.ps[i], self.ps[j])) for i in range(len(self.ps)) for j in range(i + 1, len(self.ps))]

Engine

Coordinate deductive and algebraic reasoning over a State.

Typical usage::

state = State()
db = DeductiveDatabase(state)
alg = AlgebraicSystem(state)
engine = Engine(state, db, alg)
engine.search()
Source code in pyeuclid/engine/engine.py
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
class Engine:
    """Coordinate deductive and algebraic reasoning over a `State`.

    Typical usage::

        state = State()
        db = DeductiveDatabase(state)
        alg = AlgebraicSystem(state)
        engine = Engine(state, db, alg)
        engine.search()
    """

    def __init__(self, state, deductive_database, algebraic_system):
        """Create an engine.

        Args:
            state (State): Shared state tracking relations, equations, and goal.
            deductive_database (DeductiveDatabase): Applies inference rules.
            algebraic_system (AlgebraicSystem): Solves symbolic constraints.
        """
        self.state = state
        self.deductive_database = deductive_database
        self.algebraic_system = algebraic_system

    def search(self, depth=9999):
        """Run alternating algebraic + deductive steps until solved or closed.

        Args:
            depth (int): Maximum additional reasoning depth to explore before returning.

        The method:
            1) solves current algebraic constraints,
            2) alternates deductive and algebraic steps, incrementing depth,
            3) stops early if the goal is satisfied or the deductive database reports closure.

        Returns:
            None
        """
        self.algebraic_system.run()
        for _ in range(self.state.current_depth, self.state.current_depth + depth):
            if self.state.complete() is not None:
                break

            self.state.current_depth += 1

            self.deductive_database.run()

            if self.deductive_database.closure:
                break

            if self.state.complete() is not None:
                break

            self.algebraic_system.run()

    def step(self, conditions, conclusions=[]):
        """Apply a single interactive step constrained to given conditions.

        The method temporarily restricts the state to a subset of relations,
        verifies `conditions`, runs one search depth, then checks `conclusions`.
        It restores the previous state afterward, allowing interactive/human-in-the-loop
        experimentation without polluting the main search trace.

        Args:
            conditions (Iterable): Relations/equations that must hold.
            conclusions (Iterable): Relations/equations expected to be derivable.

        Raises:
            AssertionError: If `conditions` is empty.
            Exception: If any condition cannot be verified or any conclusion fails.

        Returns:
            None
        """
        assert len(conditions) > 0
        relations_bak = self.state.relations
        equations_bak = self.state.equations
        lengths_bak = self.state.lengths
        angles_bak = self.state.angles
        points_bak = self.state.points

        diagrammatic_relations = (Between, SameSide, Collinear)

        try:
            self.algebraic_system.solve_equation()
            for condition in conditions:
                if not self.state.check_conditions(condition):
                    raise Exception(f"Condition {condition} is not verified")
            diagrammatic_relations = [item for item in self.state.relations if isinstance(item, diagrammatic_relations)]

            self.state.add_relations(conditions)

            for relation in diagrammatic_relations:
                if all([point in self.state.points for point in relation.get_points()]):
                    self.state.add_relation(relation)

            self.search(depth=1)

            for conclusion in conclusions:
                if not self.state.check_conditions(conclusion):
                    raise Exception(f"Conclusion {conclusion} is not verified")

            self.state.points = points_bak
            self.state.lengths = lengths_bak
            self.state.angles = angles_bak
            new_relations = [item for item in self.state.relations if not item in conditions]
            new_equations = [item for item in self.state.equations if not item in conditions]
            self.state.relations = relations_bak
            self.state.equations = equations_bak
            self.state.add_relations(new_relations + new_equations)
            self.state.solutions = self.state.solutions[:-1]
            self.algebraic_system.solve_equation()

        except Exception as e:
            self.state.points = points_bak
            self.state.lengths = lengths_bak
            self.state.angles = angles_bak
            self.state.relations = relations_bak
            self.state.equations = equations_bak
            raise e

__init__(state, deductive_database, algebraic_system)

Create an engine.

Parameters:

Name Type Description Default
state State

Shared state tracking relations, equations, and goal.

required
deductive_database DeductiveDatabase

Applies inference rules.

required
algebraic_system AlgebraicSystem

Solves symbolic constraints.

required
Source code in pyeuclid/engine/engine.py
24
25
26
27
28
29
30
31
32
33
34
def __init__(self, state, deductive_database, algebraic_system):
    """Create an engine.

    Args:
        state (State): Shared state tracking relations, equations, and goal.
        deductive_database (DeductiveDatabase): Applies inference rules.
        algebraic_system (AlgebraicSystem): Solves symbolic constraints.
    """
    self.state = state
    self.deductive_database = deductive_database
    self.algebraic_system = algebraic_system

search(depth=9999)

Run alternating algebraic + deductive steps until solved or closed.

Parameters:

Name Type Description Default
depth int

Maximum additional reasoning depth to explore before returning.

9999
The method

1) solves current algebraic constraints, 2) alternates deductive and algebraic steps, incrementing depth, 3) stops early if the goal is satisfied or the deductive database reports closure.

Returns:

Type Description

None

Source code in pyeuclid/engine/engine.py
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def search(self, depth=9999):
    """Run alternating algebraic + deductive steps until solved or closed.

    Args:
        depth (int): Maximum additional reasoning depth to explore before returning.

    The method:
        1) solves current algebraic constraints,
        2) alternates deductive and algebraic steps, incrementing depth,
        3) stops early if the goal is satisfied or the deductive database reports closure.

    Returns:
        None
    """
    self.algebraic_system.run()
    for _ in range(self.state.current_depth, self.state.current_depth + depth):
        if self.state.complete() is not None:
            break

        self.state.current_depth += 1

        self.deductive_database.run()

        if self.deductive_database.closure:
            break

        if self.state.complete() is not None:
            break

        self.algebraic_system.run()

step(conditions, conclusions=[])

Apply a single interactive step constrained to given conditions.

The method temporarily restricts the state to a subset of relations, verifies conditions, runs one search depth, then checks conclusions. It restores the previous state afterward, allowing interactive/human-in-the-loop experimentation without polluting the main search trace.

Parameters:

Name Type Description Default
conditions Iterable

Relations/equations that must hold.

required
conclusions Iterable

Relations/equations expected to be derivable.

[]

Raises:

Type Description
AssertionError

If conditions is empty.

Exception

If any condition cannot be verified or any conclusion fails.

Returns:

Type Description

None

Source code in pyeuclid/engine/engine.py
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
def step(self, conditions, conclusions=[]):
    """Apply a single interactive step constrained to given conditions.

    The method temporarily restricts the state to a subset of relations,
    verifies `conditions`, runs one search depth, then checks `conclusions`.
    It restores the previous state afterward, allowing interactive/human-in-the-loop
    experimentation without polluting the main search trace.

    Args:
        conditions (Iterable): Relations/equations that must hold.
        conclusions (Iterable): Relations/equations expected to be derivable.

    Raises:
        AssertionError: If `conditions` is empty.
        Exception: If any condition cannot be verified or any conclusion fails.

    Returns:
        None
    """
    assert len(conditions) > 0
    relations_bak = self.state.relations
    equations_bak = self.state.equations
    lengths_bak = self.state.lengths
    angles_bak = self.state.angles
    points_bak = self.state.points

    diagrammatic_relations = (Between, SameSide, Collinear)

    try:
        self.algebraic_system.solve_equation()
        for condition in conditions:
            if not self.state.check_conditions(condition):
                raise Exception(f"Condition {condition} is not verified")
        diagrammatic_relations = [item for item in self.state.relations if isinstance(item, diagrammatic_relations)]

        self.state.add_relations(conditions)

        for relation in diagrammatic_relations:
            if all([point in self.state.points for point in relation.get_points()]):
                self.state.add_relation(relation)

        self.search(depth=1)

        for conclusion in conclusions:
            if not self.state.check_conditions(conclusion):
                raise Exception(f"Conclusion {conclusion} is not verified")

        self.state.points = points_bak
        self.state.lengths = lengths_bak
        self.state.angles = angles_bak
        new_relations = [item for item in self.state.relations if not item in conditions]
        new_equations = [item for item in self.state.equations if not item in conditions]
        self.state.relations = relations_bak
        self.state.equations = equations_bak
        self.state.add_relations(new_relations + new_equations)
        self.state.solutions = self.state.solutions[:-1]
        self.algebraic_system.solve_equation()

    except Exception as e:
        self.state.points = points_bak
        self.state.lengths = lengths_bak
        self.state.angles = angles_bak
        self.state.relations = relations_bak
        self.state.equations = equations_bak
        raise e

Equal

Bases: Relation

Point equality relation.

Source code in pyeuclid/formalization/relation.py
89
90
91
92
93
94
95
96
97
98
class Equal(Relation):
    """Point equality relation."""

    def __init__(self, v1: Point, v2: Point):
        super().__init__()
        self.v1, self.v2 = sort_points(v1, v2)

    def permutations(self):
        """Enumerate equivalent orderings of the two points."""
        return [(self.v1, self.v2), (self.v2, self.v1)]

permutations()

Enumerate equivalent orderings of the two points.

Source code in pyeuclid/formalization/relation.py
96
97
98
def permutations(self):
    """Enumerate equivalent orderings of the two points."""
    return [(self.v1, self.v2), (self.v2, self.v1)]

Lt

Bases: Relation

Source code in pyeuclid/formalization/relation.py
81
82
83
84
85
86
class Lt(Relation):
    def __init__(self, v1: Point, v2: Point):
        """Ordering helper used to canonicalize inference rule assignments."""
        super().__init__()
        self.v1 = v1
        self.v2 = v2

__init__(v1, v2)

Ordering helper used to canonicalize inference rule assignments.

Source code in pyeuclid/formalization/relation.py
82
83
84
85
86
def __init__(self, v1: Point, v2: Point):
    """Ordering helper used to canonicalize inference rule assignments."""
    super().__init__()
    self.v1 = v1
    self.v2 = v2

Midpoint

Bases: Relation

p1 is the midpoint of segment p2p3.

Source code in pyeuclid/formalization/relation.py
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
class Midpoint(Relation):
    """p1 is the midpoint of segment p2p3."""

    def __init__(self, p1: Point, p2: Point, p3: Point):
        """
        Args:
            p1 (Point): Candidate midpoint.
            p2 (Point): Segment endpoint.
            p3 (Point): Segment endpoint.
        """
        super().__init__()
        self.p1 = p1
        self.p2, self.p3 = sort_points(p2, p3)

    def definition(self):
        """Midpoint expressed via equal lengths, collinearity, and betweenness.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) - Length(self.p1, self.p3),
            Collinear(self.p1, self.p2, self.p3),
            Different(self.p2, self.p3),
            Between(self.p1, self.p2, self.p3),
        ]

__init__(p1, p2, p3)

Parameters:

Name Type Description Default
p1 Point

Candidate midpoint.

required
p2 Point

Segment endpoint.

required
p3 Point

Segment endpoint.

required
Source code in pyeuclid/formalization/relation.py
291
292
293
294
295
296
297
298
299
300
def __init__(self, p1: Point, p2: Point, p3: Point):
    """
    Args:
        p1 (Point): Candidate midpoint.
        p2 (Point): Segment endpoint.
        p3 (Point): Segment endpoint.
    """
    super().__init__()
    self.p1 = p1
    self.p2, self.p3 = sort_points(p2, p3)

definition()

Midpoint expressed via equal lengths, collinearity, and betweenness.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
302
303
304
305
306
307
308
309
310
311
312
313
def definition(self):
    """Midpoint expressed via equal lengths, collinearity, and betweenness.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) - Length(self.p1, self.p3),
        Collinear(self.p1, self.p2, self.p3),
        Different(self.p2, self.p3),
        Between(self.p1, self.p2, self.p3),
    ]

NotCollinear

Bases: Relation

Points p1,p2,p3 are not collinear.

Source code in pyeuclid/formalization/relation.py
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
class NotCollinear(Relation):
    """Points p1,p2,p3 are not collinear."""

    def __init__(self, p1, p2, p3):
        """
        Args:
            p1 (Point)
            p2 (Point)
            p3 (Point)
        """
        super().__init__()
        self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

    def definition(self):
        """Expand non-collinearity into primitive constraints."""
        return [
            Not(Collinear(self.p1, self.p2, self.p3)),
            Different(self.p1, self.p2, self.p3)
        ]

__init__(p1, p2, p3)

Source code in pyeuclid/formalization/relation.py
270
271
272
273
274
275
276
277
278
def __init__(self, p1, p2, p3):
    """
    Args:
        p1 (Point)
        p2 (Point)
        p3 (Point)
    """
    super().__init__()
    self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

definition()

Expand non-collinearity into primitive constraints.

Source code in pyeuclid/formalization/relation.py
280
281
282
283
284
285
def definition(self):
    """Expand non-collinearity into primitive constraints."""
    return [
        Not(Collinear(self.p1, self.p2, self.p3)),
        Different(self.p1, self.p2, self.p3)
    ]

OppositeSide

Bases: Relation

Points p1,p2 lie on opposite sides of the line (p3,p4).

Source code in pyeuclid/formalization/relation.py
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
class OppositeSide(Relation):
    """Points p1,p2 lie on opposite sides of the line (p3,p4)."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1 (Point): First query point.
            p2 (Point): Second query point.
            p3 (Point): Line endpoint 1.
            p4 (Point): Line endpoint 2.
        """
        super().__init__()
        self.p1, self.p2 = sort_points(p1, p2)
        self.p3, self.p4 = sort_points(p3, p4)

    def definition(self):
        """Logical expansion expressing opposite-side constraints.

        Returns:
            list[Relation]: Primitive relations defining opposite sides.
        """
        return [
            Not(Collinear(self.p1, self.p3, self.p4)),
            Not(Collinear(self.p2, self.p3, self.p4)),
            Not(SameSide(self.p1, self.p2, self.p3, self.p4)),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1 Point

First query point.

required
p2 Point

Second query point.

required
p3 Point

Line endpoint 1.

required
p4 Point

Line endpoint 2.

required
Source code in pyeuclid/formalization/relation.py
220
221
222
223
224
225
226
227
228
229
230
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1 (Point): First query point.
        p2 (Point): Second query point.
        p3 (Point): Line endpoint 1.
        p4 (Point): Line endpoint 2.
    """
    super().__init__()
    self.p1, self.p2 = sort_points(p1, p2)
    self.p3, self.p4 = sort_points(p3, p4)

definition()

Logical expansion expressing opposite-side constraints.

Returns:

Type Description

list[Relation]: Primitive relations defining opposite sides.

Source code in pyeuclid/formalization/relation.py
232
233
234
235
236
237
238
239
240
241
242
def definition(self):
    """Logical expansion expressing opposite-side constraints.

    Returns:
        list[Relation]: Primitive relations defining opposite sides.
    """
    return [
        Not(Collinear(self.p1, self.p3, self.p4)),
        Not(Collinear(self.p2, self.p3, self.p4)),
        Not(SameSide(self.p1, self.p2, self.p3, self.p4)),
    ]

Parallel

Bases: Relation

Segments (p1,p2) and (p3,p4) are parallel.

Source code in pyeuclid/formalization/relation.py
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
class Parallel(Relation):
    """Segments (p1,p2) and (p3,p4) are parallel."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1, p2 (Point): First segment endpoints.
            p3, p4 (Point): Second segment endpoints.
        """
        super().__init__()
        p1, p2 = sort_points(p1, p2)
        p3, p4 = sort_points(p3, p4)
        self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

    def permutations(self):
        """Enumerate symmetric endpoint permutations preserving segment groups.

        Returns:
            list[tuple[Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p1, self.p2, self.p4, self.p3),
            (self.p2, self.p1, self.p3, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
            (self.p3, self.p4, self.p1, self.p2),
            (self.p4, self.p3, self.p1, self.p2),
            (self.p3, self.p4, self.p2, self.p1),
            (self.p4, self.p3, self.p2, self.p1),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1, p2 Point

First segment endpoints.

required
p3, p4 Point

Second segment endpoints.

required
Source code in pyeuclid/formalization/relation.py
396
397
398
399
400
401
402
403
404
405
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1, p2 (Point): First segment endpoints.
        p3, p4 (Point): Second segment endpoints.
    """
    super().__init__()
    p1, p2 = sort_points(p1, p2)
    p3, p4 = sort_points(p3, p4)
    self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

permutations()

Enumerate symmetric endpoint permutations preserving segment groups.

Returns:

Type Description

list[tuple[Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
def permutations(self):
    """Enumerate symmetric endpoint permutations preserving segment groups.

    Returns:
        list[tuple[Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p1, self.p2, self.p4, self.p3),
        (self.p2, self.p1, self.p3, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
        (self.p3, self.p4, self.p1, self.p2),
        (self.p4, self.p3, self.p1, self.p2),
        (self.p3, self.p4, self.p2, self.p1),
        (self.p4, self.p3, self.p2, self.p1),
    ]

Pentagon

Bases: Relation

Points form a cyclically ordered pentagon.

Source code in pyeuclid/formalization/relation.py
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
class Pentagon(Relation):
    """Points form a cyclically ordered pentagon."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point):
        """
        Args:
            p1, p2, p3, p4, p5 (Point): Vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5 = sort_cyclic_points(p1, p2, p3, p4, p5)

    def permutations(self):
        """Enumerate cyclic and reversed vertex orderings.

        Returns:
            list[tuple[Point, Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4, self.p5),
            (self.p2, self.p3, self.p4, self.p5, self.p1),
            (self.p3, self.p4, self.p5, self.p1, self.p2),
            (self.p4, self.p5, self.p1, self.p2, self.p3),
            (self.p5, self.p1, self.p2, self.p3, self.p4),
            (self.p5, self.p4, self.p3, self.p2, self.p1),
            (self.p4, self.p3, self.p2, self.p1, self.p5),
            (self.p3, self.p2, self.p1, self.p5, self.p4),
            (self.p2, self.p1, self.p5, self.p4, self.p3),
            (self.p1, self.p5, self.p4, self.p3, self.p2),
        ]

__init__(p1, p2, p3, p4, p5)

Parameters:

Name Type Description Default
p1, p2, p3, p4, p5 Point

Vertices.

required
Source code in pyeuclid/formalization/relation.py
500
501
502
503
504
505
506
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point):
    """
    Args:
        p1, p2, p3, p4, p5 (Point): Vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5 = sort_cyclic_points(p1, p2, p3, p4, p5)

permutations()

Enumerate cyclic and reversed vertex orderings.

Returns:

Type Description

list[tuple[Point, Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
def permutations(self):
    """Enumerate cyclic and reversed vertex orderings.

    Returns:
        list[tuple[Point, Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4, self.p5),
        (self.p2, self.p3, self.p4, self.p5, self.p1),
        (self.p3, self.p4, self.p5, self.p1, self.p2),
        (self.p4, self.p5, self.p1, self.p2, self.p3),
        (self.p5, self.p1, self.p2, self.p3, self.p4),
        (self.p5, self.p4, self.p3, self.p2, self.p1),
        (self.p4, self.p3, self.p2, self.p1, self.p5),
        (self.p3, self.p2, self.p1, self.p5, self.p4),
        (self.p2, self.p1, self.p5, self.p4, self.p3),
        (self.p1, self.p5, self.p4, self.p3, self.p2),
    ]

Perpendicular

Bases: Relation

Segments (p1,p2) and (p3,p4) are perpendicular.

Source code in pyeuclid/formalization/relation.py
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
class Perpendicular(Relation):
    """Segments (p1,p2) and (p3,p4) are perpendicular."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1, p2 (Point): First segment endpoints.
            p3, p4 (Point): Second segment endpoints.
        """
        super().__init__()
        p1, p2 = sort_points(p1, p2)
        p3, p4 = sort_points(p3, p4)
        self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

    def permutations(self):
        """Enumerate symmetric endpoint permutations preserving segment groups.

        Returns:
            list[tuple[Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p1, self.p2, self.p4, self.p3),
            (self.p2, self.p1, self.p3, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
            (self.p3, self.p4, self.p1, self.p2),
            (self.p4, self.p3, self.p1, self.p2),
            (self.p3, self.p4, self.p2, self.p1),
            (self.p4, self.p3, self.p2, self.p1),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1, p2 Point

First segment endpoints.

required
p3, p4 Point

Second segment endpoints.

required
Source code in pyeuclid/formalization/relation.py
428
429
430
431
432
433
434
435
436
437
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1, p2 (Point): First segment endpoints.
        p3, p4 (Point): Second segment endpoints.
    """
    super().__init__()
    p1, p2 = sort_points(p1, p2)
    p3, p4 = sort_points(p3, p4)
    self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

permutations()

Enumerate symmetric endpoint permutations preserving segment groups.

Returns:

Type Description

list[tuple[Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
def permutations(self):
    """Enumerate symmetric endpoint permutations preserving segment groups.

    Returns:
        list[tuple[Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p1, self.p2, self.p4, self.p3),
        (self.p2, self.p1, self.p3, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
        (self.p3, self.p4, self.p1, self.p2),
        (self.p4, self.p3, self.p1, self.p2),
        (self.p3, self.p4, self.p2, self.p1),
        (self.p4, self.p3, self.p2, self.p1),
    ]

Point

Named geometric point used throughout the formalization.

Source code in pyeuclid/formalization/relation.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Point:
    """Named geometric point used throughout the formalization."""

    def __init__(self, name: str):
        """Create a point.

        Args:
            name (str): Identifier (underscores are not allowed).
        """
        self.name = name
        assert "_" not in name

    def __str__(self):
        return self.name

    def __eq__(self, other):
        return str(self) == str(other)

    def __repr__(self):
        return str(self)

    def __hash__(self):
        return hash(str(self))

__init__(name)

Create a point.

Parameters:

Name Type Description Default
name str

Identifier (underscores are not allowed).

required
Source code in pyeuclid/formalization/relation.py
16
17
18
19
20
21
22
23
def __init__(self, name: str):
    """Create a point.

    Args:
        name (str): Identifier (underscores are not allowed).
    """
    self.name = name
    assert "_" not in name

Quadrilateral

Bases: Relation

Points form a cyclically ordered quadrilateral.

Source code in pyeuclid/formalization/relation.py
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
class Quadrilateral(Relation):
    """Points form a cyclically ordered quadrilateral."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1, p2, p3, p4 (Point): Vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4 = sort_cyclic_points(p1, p2, p3, p4)

    def permutations(self):
        """Enumerate cyclic and reversed vertex orderings.

        Returns:
            list[tuple[Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p2, self.p3, self.p4, self.p1),
            (self.p3, self.p4, self.p1, self.p2),
            (self.p4, self.p1, self.p2, self.p3),
            (self.p4, self.p3, self.p2, self.p1),
            (self.p3, self.p2, self.p1, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
            (self.p1, self.p4, self.p3, self.p2),
        ]

    def definition(self):
        """Opposite sides must lie on opposite sides of diagonals.

        Returns:
            list[Relation]: Primitive opposite-side relations.
        """
        return [
            OppositeSide(self.p1, self.p3, self.p2, self.p4),
            OppositeSide(self.p2, self.p4, self.p1, self.p3),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1, p2, p3, p4 Point

Vertices.

required
Source code in pyeuclid/formalization/relation.py
460
461
462
463
464
465
466
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1, p2, p3, p4 (Point): Vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4 = sort_cyclic_points(p1, p2, p3, p4)

definition()

Opposite sides must lie on opposite sides of diagonals.

Returns:

Type Description

list[Relation]: Primitive opposite-side relations.

Source code in pyeuclid/formalization/relation.py
485
486
487
488
489
490
491
492
493
494
def definition(self):
    """Opposite sides must lie on opposite sides of diagonals.

    Returns:
        list[Relation]: Primitive opposite-side relations.
    """
    return [
        OppositeSide(self.p1, self.p3, self.p2, self.p4),
        OppositeSide(self.p2, self.p4, self.p1, self.p3),
    ]

permutations()

Enumerate cyclic and reversed vertex orderings.

Returns:

Type Description

list[tuple[Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
def permutations(self):
    """Enumerate cyclic and reversed vertex orderings.

    Returns:
        list[tuple[Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p2, self.p3, self.p4, self.p1),
        (self.p3, self.p4, self.p1, self.p2),
        (self.p4, self.p1, self.p2, self.p3),
        (self.p4, self.p3, self.p2, self.p1),
        (self.p3, self.p2, self.p1, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
        (self.p1, self.p4, self.p3, self.p2),
    ]

Relation

Base class for logical relations over points.

Source code in pyeuclid/formalization/relation.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class Relation:
    """Base class for logical relations over points."""

    def __init__(self):
        self.negated = False

    def negate(self):
        """Toggle negation flag in-place."""
        self.negated = not self.negated

    def get_points(self):
        """Return all point instances contained in the relation."""
        points = []
        for v in vars(self).values():
            if isinstance(v, Point):
                points.append(v)
            elif isinstance(v, list):
                for p in v:
                    if isinstance(p, Point):
                        points.append(p)
        return points

    def __str__(self):
        """Readable representation, prefixed with Not() when negated."""
        class_name = self.__class__.__name__
        points = self.get_points()
        args_name = ",".join([p.name for p in points])

        if not self.negated:
            return f"{class_name}({args_name})"
        else:
            return f"Not({class_name}({args_name}))"

    def __repr__(self):
        return str(self)

    def __eq__(self, other):
        return str(self) == str(other)

    def __hash__(self):
        return hash(str(self))

__str__()

Readable representation, prefixed with Not() when negated.

Source code in pyeuclid/formalization/relation.py
60
61
62
63
64
65
66
67
68
69
def __str__(self):
    """Readable representation, prefixed with Not() when negated."""
    class_name = self.__class__.__name__
    points = self.get_points()
    args_name = ",".join([p.name for p in points])

    if not self.negated:
        return f"{class_name}({args_name})"
    else:
        return f"Not({class_name}({args_name}))"

get_points()

Return all point instances contained in the relation.

Source code in pyeuclid/formalization/relation.py
48
49
50
51
52
53
54
55
56
57
58
def get_points(self):
    """Return all point instances contained in the relation."""
    points = []
    for v in vars(self).values():
        if isinstance(v, Point):
            points.append(v)
        elif isinstance(v, list):
            for p in v:
                if isinstance(p, Point):
                    points.append(p)
    return points

negate()

Toggle negation flag in-place.

Source code in pyeuclid/formalization/relation.py
44
45
46
def negate(self):
    """Toggle negation flag in-place."""
    self.negated = not self.negated

SameSide

Bases: Relation

Points p1,p2 lie on the same side of the line (p3,p4).

Source code in pyeuclid/formalization/relation.py
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
class SameSide(Relation):
    """Points p1,p2 lie on the same side of the line (p3,p4)."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1 (Point): First query point.
            p2 (Point): Second query point.
            p3 (Point): Line endpoint 1.
            p4 (Point): Line endpoint 2.
        """
        super().__init__()
        self.p1, self.p2 = sort_points(p1, p2)
        self.p3, self.p4 = sort_points(p3, p4)

    def permutations(self):
        """Enumerate symmetric orderings for same-side tests."""
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p1, self.p2, self.p4, self.p3),
            (self.p2, self.p1, self.p3, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1 Point

First query point.

required
p2 Point

Second query point.

required
p3 Point

Line endpoint 1.

required
p4 Point

Line endpoint 2.

required
Source code in pyeuclid/formalization/relation.py
195
196
197
198
199
200
201
202
203
204
205
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1 (Point): First query point.
        p2 (Point): Second query point.
        p3 (Point): Line endpoint 1.
        p4 (Point): Line endpoint 2.
    """
    super().__init__()
    self.p1, self.p2 = sort_points(p1, p2)
    self.p3, self.p4 = sort_points(p3, p4)

permutations()

Enumerate symmetric orderings for same-side tests.

Source code in pyeuclid/formalization/relation.py
207
208
209
210
211
212
213
214
def permutations(self):
    """Enumerate symmetric orderings for same-side tests."""
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p1, self.p2, self.p4, self.p3),
        (self.p2, self.p1, self.p3, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
    ]

Similar

Bases: Relation

Triangles (p1,p2,p3) and (p4,p5,p6) are similar.

Source code in pyeuclid/formalization/relation.py
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
class Similar(Relation):
    """Triangles (p1,p2,p3) and (p4,p5,p6) are similar."""

    def __init__(
        self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
    ):
        """
        Args:
            p1, p2, p3 (Point): First triangle vertices.
            p4, p5, p6 (Point): Second triangle vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

    def definition(self):
        """Similarity expressed via length ratios and non-collinearity.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) / Length(self.p4, self.p5)
            - Length(self.p2, self.p3) / Length(self.p5, self.p6),
            Length(self.p1, self.p2) / Length(self.p4, self.p5)
            - Length(self.p3, self.p1) / Length(self.p6, self.p4),
            NotCollinear(self.p1, self.p2, self.p3),
        ]

__init__(p1, p2, p3, p4, p5, p6)

Parameters:

Name Type Description Default
p1, p2, p3 Point

First triangle vertices.

required
p4, p5, p6 Point

Second triangle vertices.

required
Source code in pyeuclid/formalization/relation.py
347
348
349
350
351
352
353
354
355
356
def __init__(
    self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
):
    """
    Args:
        p1, p2, p3 (Point): First triangle vertices.
        p4, p5, p6 (Point): Second triangle vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

definition()

Similarity expressed via length ratios and non-collinearity.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
358
359
360
361
362
363
364
365
366
367
368
369
370
def definition(self):
    """Similarity expressed via length ratios and non-collinearity.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) / Length(self.p4, self.p5)
        - Length(self.p2, self.p3) / Length(self.p5, self.p6),
        Length(self.p1, self.p2) / Length(self.p4, self.p5)
        - Length(self.p3, self.p1) / Length(self.p6, self.p4),
        NotCollinear(self.p1, self.p2, self.p3),
    ]

Similar4P

Bases: Relation

Two quadrilaterals are similar.

Source code in pyeuclid/formalization/relation.py
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
class Similar4P(Relation):
    """Two quadrilaterals are similar."""

    def __init__(
        self,
        p1: Point,
        p2: Point,
        p3: Point,
        p4: Point,
        p5: Point,
        p6: Point,
        p7: Point,
        p8: Point,
    ):
        """
        Args:
            p1..p4 (Point): First quadrilateral.
            p5..p8 (Point): Second quadrilateral.
        """
        super().__init__()
        point_map_12 = {p1: p5, p2: p6, p3: p7, p4: p8}
        point_map_21 = {p5: p1, p6: p2, p7: p3, p8: p4}

        sorted_1 = sort_cyclic_points(p1, p2, p3, p4)
        sorted_2 = sort_cyclic_points(p5, p6, p7, p8)
        if compare_names(sorted_1, sorted_2) == 0:
            self.p1, self.p2, self.p3, self.p4 = sorted_1
            self.p5,self.p6,self.p7,self.p8 = point_map_12[self.p1], point_map_12[
                self.p2], point_map_12[self.p3], point_map_12[self.p4]
        else:
            self.p1, self.p2, self.p3, self.p4 = sorted_2
            self.p5, self.p6, self.p7, self.p8 = point_map_21[self.p1], point_map_21[
                self.p2], point_map_21[self.p3], point_map_21[self.p4]

    def definition(self):
        """Similarity expressed via side ratios and angle equalities.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) / Length(self.p5, self.p6)
            - Length(self.p2, self.p3) / Length(self.p6, self.p7),
            Length(self.p1, self.p2) / Length(self.p5, self.p6)
            - Length(self.p3, self.p4) / Length(self.p7, self.p8),
            Length(self.p1, self.p2) / Length(self.p5, self.p6)
            - Length(self.p4, self.p1) / Length(self.p8, self.p5),
            Angle(self.p1, self.p2, self.p3) - Angle(self.p5, self.p6, self.p7),
            Angle(self.p2, self.p3, self.p4) - Angle(self.p6, self.p7, self.p8),
            Angle(self.p3, self.p4, self.p1) - Angle(self.p7, self.p8, self.p5),
            Angle(self.p4, self.p1, self.p2) - Angle(self.p8, self.p5, self.p6),
        ]

__init__(p1, p2, p3, p4, p5, p6, p7, p8)

Parameters:

Name Type Description Default
p1..p4 Point

First quadrilateral.

required
p5..p8 Point

Second quadrilateral.

required
Source code in pyeuclid/formalization/relation.py
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
def __init__(
    self,
    p1: Point,
    p2: Point,
    p3: Point,
    p4: Point,
    p5: Point,
    p6: Point,
    p7: Point,
    p8: Point,
):
    """
    Args:
        p1..p4 (Point): First quadrilateral.
        p5..p8 (Point): Second quadrilateral.
    """
    super().__init__()
    point_map_12 = {p1: p5, p2: p6, p3: p7, p4: p8}
    point_map_21 = {p5: p1, p6: p2, p7: p3, p8: p4}

    sorted_1 = sort_cyclic_points(p1, p2, p3, p4)
    sorted_2 = sort_cyclic_points(p5, p6, p7, p8)
    if compare_names(sorted_1, sorted_2) == 0:
        self.p1, self.p2, self.p3, self.p4 = sorted_1
        self.p5,self.p6,self.p7,self.p8 = point_map_12[self.p1], point_map_12[
            self.p2], point_map_12[self.p3], point_map_12[self.p4]
    else:
        self.p1, self.p2, self.p3, self.p4 = sorted_2
        self.p5, self.p6, self.p7, self.p8 = point_map_21[self.p1], point_map_21[
            self.p2], point_map_21[self.p3], point_map_21[self.p4]

definition()

Similarity expressed via side ratios and angle equalities.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
def definition(self):
    """Similarity expressed via side ratios and angle equalities.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) / Length(self.p5, self.p6)
        - Length(self.p2, self.p3) / Length(self.p6, self.p7),
        Length(self.p1, self.p2) / Length(self.p5, self.p6)
        - Length(self.p3, self.p4) / Length(self.p7, self.p8),
        Length(self.p1, self.p2) / Length(self.p5, self.p6)
        - Length(self.p4, self.p1) / Length(self.p8, self.p5),
        Angle(self.p1, self.p2, self.p3) - Angle(self.p5, self.p6, self.p7),
        Angle(self.p2, self.p3, self.p4) - Angle(self.p6, self.p7, self.p8),
        Angle(self.p3, self.p4, self.p1) - Angle(self.p7, self.p8, self.p5),
        Angle(self.p4, self.p1, self.p2) - Angle(self.p8, self.p5, self.p6),
    ]

Similar5P

Bases: Relation

Two pentagons are similar.

Source code in pyeuclid/formalization/relation.py
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
class Similar5P(Relation):
    """Two pentagons are similar."""

    def __init__(
        self,
        p1: Point,
        p2: Point,
        p3: Point,
        p4: Point,
        p5: Point,
        p6: Point,
        p7: Point,
        p8: Point,
        p9: Point,
        p10: Point,
    ):
        """
        Args:
            p1..p5 (Point): First pentagon.
            p6..p10 (Point): Second pentagon.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5, self.p6, self.p7, self.p8, self.p9, self.p10 = p1, p2, p3, p4, p5, p6, p7, p8, p9, p10

    def definition(self):
        """Similarity expressed via consecutive side ratios and angle equalities.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) / Length(self.p6, self.p7)
            - Length(self.p2, self.p3) / Length(self.p7, self.p8),
            Length(self.p2, self.p3) / Length(self.p7, self.p8)
            - Length(self.p3, self.p4) / Length(self.p8, self.p9),
            Length(self.p3, self.p4) / Length(self.p8, self.p9)
            - Length(self.p4, self.p5) / Length(self.p9, self.p10),
            Length(self.p4, self.p5) / Length(self.p9, self.p10)
            - Length(self.p5, self.p1) / Length(self.p10, self.p6),
            Length(self.p5, self.p1) / Length(self.p10, self.p6)
            - Length(self.p1, self.p2) / Length(self.p6, self.p7),
            Angle(self.p1, self.p2, self.p3) - Angle(self.p6, self.p7, self.p8),
            Angle(self.p2, self.p3, self.p4) - Angle(self.p7, self.p8, self.p9),
            Angle(self.p3, self.p4, self.p5) - Angle(self.p8, self.p9, self.p10),
            Angle(self.p4, self.p5, self.p1) - Angle(self.p9, self.p10, self.p6),
            Angle(self.p5, self.p1, self.p2) - Angle(self.p10, self.p6, self.p7),
        ]

__init__(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10)

Parameters:

Name Type Description Default
p1..p5 Point

First pentagon.

required
p6..p10 Point

Second pentagon.

required
Source code in pyeuclid/formalization/relation.py
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
def __init__(
    self,
    p1: Point,
    p2: Point,
    p3: Point,
    p4: Point,
    p5: Point,
    p6: Point,
    p7: Point,
    p8: Point,
    p9: Point,
    p10: Point,
):
    """
    Args:
        p1..p5 (Point): First pentagon.
        p6..p10 (Point): Second pentagon.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5, self.p6, self.p7, self.p8, self.p9, self.p10 = p1, p2, p3, p4, p5, p6, p7, p8, p9, p10

definition()

Similarity expressed via consecutive side ratios and angle equalities.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
def definition(self):
    """Similarity expressed via consecutive side ratios and angle equalities.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) / Length(self.p6, self.p7)
        - Length(self.p2, self.p3) / Length(self.p7, self.p8),
        Length(self.p2, self.p3) / Length(self.p7, self.p8)
        - Length(self.p3, self.p4) / Length(self.p8, self.p9),
        Length(self.p3, self.p4) / Length(self.p8, self.p9)
        - Length(self.p4, self.p5) / Length(self.p9, self.p10),
        Length(self.p4, self.p5) / Length(self.p9, self.p10)
        - Length(self.p5, self.p1) / Length(self.p10, self.p6),
        Length(self.p5, self.p1) / Length(self.p10, self.p6)
        - Length(self.p1, self.p2) / Length(self.p6, self.p7),
        Angle(self.p1, self.p2, self.p3) - Angle(self.p6, self.p7, self.p8),
        Angle(self.p2, self.p3, self.p4) - Angle(self.p7, self.p8, self.p9),
        Angle(self.p3, self.p4, self.p5) - Angle(self.p8, self.p9, self.p10),
        Angle(self.p4, self.p5, self.p1) - Angle(self.p9, self.p10, self.p6),
        Angle(self.p5, self.p1, self.p2) - Angle(self.p10, self.p6, self.p7),
    ]

Angle(p1, p2, p3)

Symbolic angle at p2.

Returns:

Type Description

sympy.Symbol: Non-negative angle symbol.

Source code in pyeuclid/formalization/relation.py
108
109
110
111
112
113
114
115
def Angle(p1: Point, p2: Point, p3: Point):
    """Symbolic angle at p2.

    Returns:
        sympy.Symbol: Non-negative angle symbol.
    """
    p1, p3 = sort_points(p1, p3)
    return Symbol(f"Angle_{p1}_{p2}_{p3}", non_negative=True)

Area(*ps)

Symbolic polygonal area over an ordered point cycle.

Returns:

Type Description

sympy.Symbol: Positive area symbol.

Source code in pyeuclid/formalization/relation.py
128
129
130
131
132
133
134
135
def Area(*ps: list[Point]):
    """Symbolic polygonal area over an ordered point cycle.

    Returns:
        sympy.Symbol: Positive area symbol.
    """
    ps = sort_cyclic_points(*ps)
    return Symbol("_".join(["Area"] + [str(item) for item in ps]), positive=True)

Length(p1, p2)

Symbolic length between two points.

Returns:

Type Description

sympy.Symbol: Positive length symbol.

Source code in pyeuclid/formalization/relation.py
118
119
120
121
122
123
124
125
def Length(p1: Point, p2: Point):
    """Symbolic length between two points.

    Returns:
        sympy.Symbol: Positive length symbol.
    """
    p1, p2 = sort_points(p1, p2)
    return Symbol(f"Length_{str(p1)}_{str(p2)}", positive=True)

Not(p)

Return a negated shallow copy of a relation.

Source code in pyeuclid/formalization/relation.py
101
102
103
104
105
def Not(p: Relation) -> Relation:
    """Return a negated shallow copy of a relation."""
    other = copy.copy(p)
    other.negate()
    return other

Variable(name)

Free symbolic variable placeholder.

Returns:

Type Description

sympy.Symbol: Dimensionless variable symbol.

Source code in pyeuclid/formalization/relation.py
138
139
140
141
142
143
144
def Variable(name: str):
    """Free symbolic variable placeholder.

    Returns:
        sympy.Symbol: Dimensionless variable symbol.
    """
    return Symbol(f"Variable_{name}")

Inference rules and registry for the deductive database.

Between

Bases: Relation

p1 lies between p2 and p3 (on the same line).

Source code in pyeuclid/formalization/relation.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
class Between(Relation):
    """p1 lies between p2 and p3 (on the same line)."""

    def __init__(self, p1: Point, p2: Point, p3: Point):
        """
        p1 is between p2 and p3.

        Args:
            p1 (Point): Middle point.
            p2 (Point): Endpoint 1.
            p3 (Point): Endpoint 2.
        """
        super().__init__()
        p2, p3 = sort_points(p2, p3)
        self.p1, self.p2, self.p3 = p1, p2, p3

    def permutations(self):
        """Enumerate equivalent point orderings for the between relation.

        Returns:
            list[tuple[Point, Point, Point]]: Permitted permutations.
        """
        return [(self.p1, self.p2, self.p3), (self.p1, self.p3, self.p2)]

__init__(p1, p2, p3)

p1 is between p2 and p3.

Parameters:

Name Type Description Default
p1 Point

Middle point.

required
p2 Point

Endpoint 1.

required
p3 Point

Endpoint 2.

required
Source code in pyeuclid/formalization/relation.py
170
171
172
173
174
175
176
177
178
179
180
181
def __init__(self, p1: Point, p2: Point, p3: Point):
    """
    p1 is between p2 and p3.

    Args:
        p1 (Point): Middle point.
        p2 (Point): Endpoint 1.
        p3 (Point): Endpoint 2.
    """
    super().__init__()
    p2, p3 = sort_points(p2, p3)
    self.p1, self.p2, self.p3 = p1, p2, p3

permutations()

Enumerate equivalent point orderings for the between relation.

Returns:

Type Description

list[tuple[Point, Point, Point]]: Permitted permutations.

Source code in pyeuclid/formalization/relation.py
183
184
185
186
187
188
189
def permutations(self):
    """Enumerate equivalent point orderings for the between relation.

    Returns:
        list[tuple[Point, Point, Point]]: Permitted permutations.
    """
    return [(self.p1, self.p2, self.p3), (self.p1, self.p3, self.p2)]

Collinear

Bases: Relation

Points p1,p2,p3 are collinear.

Source code in pyeuclid/formalization/relation.py
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
class Collinear(Relation):
    """Points p1,p2,p3 are collinear."""

    def __init__(self, p1, p2, p3):
        """
        Args:
            p1 (Point)
            p2 (Point)
            p3 (Point)
        """
        super().__init__()
        self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

    def permutations(self):
        """Enumerate collinearity argument permutations.

        Returns:
            itertools.permutations: All orderings of the three points.
        """
        return itertools.permutations([self.p1, self.p2, self.p3])

__init__(p1, p2, p3)

Source code in pyeuclid/formalization/relation.py
248
249
250
251
252
253
254
255
256
def __init__(self, p1, p2, p3):
    """
    Args:
        p1 (Point)
        p2 (Point)
        p3 (Point)
    """
    super().__init__()
    self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

permutations()

Enumerate collinearity argument permutations.

Returns:

Type Description

itertools.permutations: All orderings of the three points.

Source code in pyeuclid/formalization/relation.py
258
259
260
261
262
263
264
def permutations(self):
    """Enumerate collinearity argument permutations.

    Returns:
        itertools.permutations: All orderings of the three points.
    """
    return itertools.permutations([self.p1, self.p2, self.p3])

Concyclic

Bases: Relation

All given points lie on the same circle.

Source code in pyeuclid/formalization/relation.py
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
class Concyclic(Relation):
    """All given points lie on the same circle."""

    def __init__(self, *ps: Point):
        """
        Args:
            *ps (Point): Points to be tested for concyclicity.
        """
        super().__init__()
        self.ps = list(sort_points(*ps))

    def permutations(self):
        """Enumerate symmetric permutations of the point set.

        Returns:
            itertools.permutations: All orderings of the points.
        """
        return itertools.permutations(self.ps)

__init__(*ps)

Parameters:

Name Type Description Default
*ps Point

Points to be tested for concyclicity.

()
Source code in pyeuclid/formalization/relation.py
376
377
378
379
380
381
382
def __init__(self, *ps: Point):
    """
    Args:
        *ps (Point): Points to be tested for concyclicity.
    """
    super().__init__()
    self.ps = list(sort_points(*ps))

permutations()

Enumerate symmetric permutations of the point set.

Returns:

Type Description

itertools.permutations: All orderings of the points.

Source code in pyeuclid/formalization/relation.py
384
385
386
387
388
389
390
def permutations(self):
    """Enumerate symmetric permutations of the point set.

    Returns:
        itertools.permutations: All orderings of the points.
    """
    return itertools.permutations(self.ps)

Congruent

Bases: Relation

Triangles (p1,p2,p3) and (p4,p5,p6) are congruent.

Source code in pyeuclid/formalization/relation.py
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
class Congruent(Relation):
    """Triangles (p1,p2,p3) and (p4,p5,p6) are congruent."""

    def __init__(
        self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
    ):
        """
        Args:
            p1, p2, p3 (Point): First triangle vertices.
            p4, p5, p6 (Point): Second triangle vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

    def definition(self):
        """Congruence expressed as equal side lengths and non-collinearity.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) - Length(self.p4, self.p5),
            Length(self.p2, self.p3) - Length(self.p5, self.p6),
            Length(self.p1, self.p3) - Length(self.p4, self.p6),
            NotCollinear(self.p1, self.p2, self.p3),
        ]

__init__(p1, p2, p3, p4, p5, p6)

Parameters:

Name Type Description Default
p1, p2, p3 Point

First triangle vertices.

required
p4, p5, p6 Point

Second triangle vertices.

required
Source code in pyeuclid/formalization/relation.py
319
320
321
322
323
324
325
326
327
328
def __init__(
    self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
):
    """
    Args:
        p1, p2, p3 (Point): First triangle vertices.
        p4, p5, p6 (Point): Second triangle vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

definition()

Congruence expressed as equal side lengths and non-collinearity.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
330
331
332
333
334
335
336
337
338
339
340
341
def definition(self):
    """Congruence expressed as equal side lengths and non-collinearity.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) - Length(self.p4, self.p5),
        Length(self.p2, self.p3) - Length(self.p5, self.p6),
        Length(self.p1, self.p3) - Length(self.p4, self.p6),
        NotCollinear(self.p1, self.p2, self.p3),
    ]

Different

Bases: Relation

All provided points must be pairwise distinct.

Source code in pyeuclid/formalization/relation.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
class Different(Relation):
    """All provided points must be pairwise distinct."""

    def __init__(self, *ps: list[Point]):
        """
        Args:
            *ps (Point): Points that must be distinct.
        """
        super().__init__()
        self.ps = ps

    def definition(self):
        """Expand to pairwise inequality relations.

        Returns:
            list[Relation]: Negated equalities for every point pair.
        """
        return [Not(Equal(self.ps[i], self.ps[j])) for i in range(len(self.ps)) for j in range(i + 1, len(self.ps))]

__init__(*ps)

Parameters:

Name Type Description Default
*ps Point

Points that must be distinct.

()
Source code in pyeuclid/formalization/relation.py
150
151
152
153
154
155
156
def __init__(self, *ps: list[Point]):
    """
    Args:
        *ps (Point): Points that must be distinct.
    """
    super().__init__()
    self.ps = ps

definition()

Expand to pairwise inequality relations.

Returns:

Type Description

list[Relation]: Negated equalities for every point pair.

Source code in pyeuclid/formalization/relation.py
158
159
160
161
162
163
164
def definition(self):
    """Expand to pairwise inequality relations.

    Returns:
        list[Relation]: Negated equalities for every point pair.
    """
    return [Not(Equal(self.ps[i], self.ps[j])) for i in range(len(self.ps)) for j in range(i + 1, len(self.ps))]

Equal

Bases: Relation

Point equality relation.

Source code in pyeuclid/formalization/relation.py
89
90
91
92
93
94
95
96
97
98
class Equal(Relation):
    """Point equality relation."""

    def __init__(self, v1: Point, v2: Point):
        super().__init__()
        self.v1, self.v2 = sort_points(v1, v2)

    def permutations(self):
        """Enumerate equivalent orderings of the two points."""
        return [(self.v1, self.v2), (self.v2, self.v1)]

permutations()

Enumerate equivalent orderings of the two points.

Source code in pyeuclid/formalization/relation.py
96
97
98
def permutations(self):
    """Enumerate equivalent orderings of the two points."""
    return [(self.v1, self.v2), (self.v2, self.v1)]

InferenceRule

Base class for all geometric inference rules.

Subclasses implement condition() and conclusion(), each returning relations/equations. The register decorator wraps these to expand definitions and filter zero expressions before the deductive database uses them.

Source code in pyeuclid/engine/inference_rule.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
class InferenceRule:
    """Base class for all geometric inference rules.

    Subclasses implement `condition()` and `conclusion()`, each returning
    relations/equations. The `register` decorator wraps these to expand
    definitions and filter zero expressions before the deductive database uses
    them.
    """

    def __init__(self):
        pass

    def condition(self):
        """Return premises (relations/equations) required to trigger the rule."""

    def conclusion(self):
        """Return relations/equations that are added when the rule fires."""

    def get_entities_in_condition(self):
        entities = set()
        for i in self.condition()[2]:
            entities = entities.union(set(i.get_entities()))
        return entities

    def degenerate(self):
        return False

    def get_entities_in_conclusion(self):
        entities = set()
        for i in self.conclusion()[2]:
            entities = entities.union(set(i.get_entities()))
        return entities

    def __str__(self):
        class_name = self.__class__.__name__
        content = []
        for key, value in vars(self).items():
            if key.startswith("_") or key == "depth":
                continue
            if not isinstance(value, Iterable):
                content.append(str(value))
            else:
                content.append(','.join(str(i) for i in value))
        attributes = ','.join(content)
        return f"{class_name}({attributes})"

    def __repr__(self):
        return str(self)

    def __eq__(self, other):
        return str(self) == str(other)

    def __hash__(self):
        return hash(str(self))

conclusion()

Return relations/equations that are added when the rule fires.

Source code in pyeuclid/engine/inference_rule.py
60
61
def conclusion(self):
    """Return relations/equations that are added when the rule fires."""

condition()

Return premises (relations/equations) required to trigger the rule.

Source code in pyeuclid/engine/inference_rule.py
57
58
def condition(self):
    """Return premises (relations/equations) required to trigger the rule."""

Lt

Bases: Relation

Source code in pyeuclid/formalization/relation.py
81
82
83
84
85
86
class Lt(Relation):
    def __init__(self, v1: Point, v2: Point):
        """Ordering helper used to canonicalize inference rule assignments."""
        super().__init__()
        self.v1 = v1
        self.v2 = v2

__init__(v1, v2)

Ordering helper used to canonicalize inference rule assignments.

Source code in pyeuclid/formalization/relation.py
82
83
84
85
86
def __init__(self, v1: Point, v2: Point):
    """Ordering helper used to canonicalize inference rule assignments."""
    super().__init__()
    self.v1 = v1
    self.v2 = v2

Midpoint

Bases: Relation

p1 is the midpoint of segment p2p3.

Source code in pyeuclid/formalization/relation.py
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
class Midpoint(Relation):
    """p1 is the midpoint of segment p2p3."""

    def __init__(self, p1: Point, p2: Point, p3: Point):
        """
        Args:
            p1 (Point): Candidate midpoint.
            p2 (Point): Segment endpoint.
            p3 (Point): Segment endpoint.
        """
        super().__init__()
        self.p1 = p1
        self.p2, self.p3 = sort_points(p2, p3)

    def definition(self):
        """Midpoint expressed via equal lengths, collinearity, and betweenness.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) - Length(self.p1, self.p3),
            Collinear(self.p1, self.p2, self.p3),
            Different(self.p2, self.p3),
            Between(self.p1, self.p2, self.p3),
        ]

__init__(p1, p2, p3)

Parameters:

Name Type Description Default
p1 Point

Candidate midpoint.

required
p2 Point

Segment endpoint.

required
p3 Point

Segment endpoint.

required
Source code in pyeuclid/formalization/relation.py
291
292
293
294
295
296
297
298
299
300
def __init__(self, p1: Point, p2: Point, p3: Point):
    """
    Args:
        p1 (Point): Candidate midpoint.
        p2 (Point): Segment endpoint.
        p3 (Point): Segment endpoint.
    """
    super().__init__()
    self.p1 = p1
    self.p2, self.p3 = sort_points(p2, p3)

definition()

Midpoint expressed via equal lengths, collinearity, and betweenness.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
302
303
304
305
306
307
308
309
310
311
312
313
def definition(self):
    """Midpoint expressed via equal lengths, collinearity, and betweenness.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) - Length(self.p1, self.p3),
        Collinear(self.p1, self.p2, self.p3),
        Different(self.p2, self.p3),
        Between(self.p1, self.p2, self.p3),
    ]

NotCollinear

Bases: Relation

Points p1,p2,p3 are not collinear.

Source code in pyeuclid/formalization/relation.py
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
class NotCollinear(Relation):
    """Points p1,p2,p3 are not collinear."""

    def __init__(self, p1, p2, p3):
        """
        Args:
            p1 (Point)
            p2 (Point)
            p3 (Point)
        """
        super().__init__()
        self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

    def definition(self):
        """Expand non-collinearity into primitive constraints."""
        return [
            Not(Collinear(self.p1, self.p2, self.p3)),
            Different(self.p1, self.p2, self.p3)
        ]

__init__(p1, p2, p3)

Source code in pyeuclid/formalization/relation.py
270
271
272
273
274
275
276
277
278
def __init__(self, p1, p2, p3):
    """
    Args:
        p1 (Point)
        p2 (Point)
        p3 (Point)
    """
    super().__init__()
    self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

definition()

Expand non-collinearity into primitive constraints.

Source code in pyeuclid/formalization/relation.py
280
281
282
283
284
285
def definition(self):
    """Expand non-collinearity into primitive constraints."""
    return [
        Not(Collinear(self.p1, self.p2, self.p3)),
        Different(self.p1, self.p2, self.p3)
    ]

OppositeSide

Bases: Relation

Points p1,p2 lie on opposite sides of the line (p3,p4).

Source code in pyeuclid/formalization/relation.py
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
class OppositeSide(Relation):
    """Points p1,p2 lie on opposite sides of the line (p3,p4)."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1 (Point): First query point.
            p2 (Point): Second query point.
            p3 (Point): Line endpoint 1.
            p4 (Point): Line endpoint 2.
        """
        super().__init__()
        self.p1, self.p2 = sort_points(p1, p2)
        self.p3, self.p4 = sort_points(p3, p4)

    def definition(self):
        """Logical expansion expressing opposite-side constraints.

        Returns:
            list[Relation]: Primitive relations defining opposite sides.
        """
        return [
            Not(Collinear(self.p1, self.p3, self.p4)),
            Not(Collinear(self.p2, self.p3, self.p4)),
            Not(SameSide(self.p1, self.p2, self.p3, self.p4)),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1 Point

First query point.

required
p2 Point

Second query point.

required
p3 Point

Line endpoint 1.

required
p4 Point

Line endpoint 2.

required
Source code in pyeuclid/formalization/relation.py
220
221
222
223
224
225
226
227
228
229
230
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1 (Point): First query point.
        p2 (Point): Second query point.
        p3 (Point): Line endpoint 1.
        p4 (Point): Line endpoint 2.
    """
    super().__init__()
    self.p1, self.p2 = sort_points(p1, p2)
    self.p3, self.p4 = sort_points(p3, p4)

definition()

Logical expansion expressing opposite-side constraints.

Returns:

Type Description

list[Relation]: Primitive relations defining opposite sides.

Source code in pyeuclid/formalization/relation.py
232
233
234
235
236
237
238
239
240
241
242
def definition(self):
    """Logical expansion expressing opposite-side constraints.

    Returns:
        list[Relation]: Primitive relations defining opposite sides.
    """
    return [
        Not(Collinear(self.p1, self.p3, self.p4)),
        Not(Collinear(self.p2, self.p3, self.p4)),
        Not(SameSide(self.p1, self.p2, self.p3, self.p4)),
    ]

Parallel

Bases: Relation

Segments (p1,p2) and (p3,p4) are parallel.

Source code in pyeuclid/formalization/relation.py
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
class Parallel(Relation):
    """Segments (p1,p2) and (p3,p4) are parallel."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1, p2 (Point): First segment endpoints.
            p3, p4 (Point): Second segment endpoints.
        """
        super().__init__()
        p1, p2 = sort_points(p1, p2)
        p3, p4 = sort_points(p3, p4)
        self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

    def permutations(self):
        """Enumerate symmetric endpoint permutations preserving segment groups.

        Returns:
            list[tuple[Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p1, self.p2, self.p4, self.p3),
            (self.p2, self.p1, self.p3, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
            (self.p3, self.p4, self.p1, self.p2),
            (self.p4, self.p3, self.p1, self.p2),
            (self.p3, self.p4, self.p2, self.p1),
            (self.p4, self.p3, self.p2, self.p1),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1, p2 Point

First segment endpoints.

required
p3, p4 Point

Second segment endpoints.

required
Source code in pyeuclid/formalization/relation.py
396
397
398
399
400
401
402
403
404
405
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1, p2 (Point): First segment endpoints.
        p3, p4 (Point): Second segment endpoints.
    """
    super().__init__()
    p1, p2 = sort_points(p1, p2)
    p3, p4 = sort_points(p3, p4)
    self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

permutations()

Enumerate symmetric endpoint permutations preserving segment groups.

Returns:

Type Description

list[tuple[Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
def permutations(self):
    """Enumerate symmetric endpoint permutations preserving segment groups.

    Returns:
        list[tuple[Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p1, self.p2, self.p4, self.p3),
        (self.p2, self.p1, self.p3, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
        (self.p3, self.p4, self.p1, self.p2),
        (self.p4, self.p3, self.p1, self.p2),
        (self.p3, self.p4, self.p2, self.p1),
        (self.p4, self.p3, self.p2, self.p1),
    ]

Pentagon

Bases: Relation

Points form a cyclically ordered pentagon.

Source code in pyeuclid/formalization/relation.py
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
class Pentagon(Relation):
    """Points form a cyclically ordered pentagon."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point):
        """
        Args:
            p1, p2, p3, p4, p5 (Point): Vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5 = sort_cyclic_points(p1, p2, p3, p4, p5)

    def permutations(self):
        """Enumerate cyclic and reversed vertex orderings.

        Returns:
            list[tuple[Point, Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4, self.p5),
            (self.p2, self.p3, self.p4, self.p5, self.p1),
            (self.p3, self.p4, self.p5, self.p1, self.p2),
            (self.p4, self.p5, self.p1, self.p2, self.p3),
            (self.p5, self.p1, self.p2, self.p3, self.p4),
            (self.p5, self.p4, self.p3, self.p2, self.p1),
            (self.p4, self.p3, self.p2, self.p1, self.p5),
            (self.p3, self.p2, self.p1, self.p5, self.p4),
            (self.p2, self.p1, self.p5, self.p4, self.p3),
            (self.p1, self.p5, self.p4, self.p3, self.p2),
        ]

__init__(p1, p2, p3, p4, p5)

Parameters:

Name Type Description Default
p1, p2, p3, p4, p5 Point

Vertices.

required
Source code in pyeuclid/formalization/relation.py
500
501
502
503
504
505
506
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point):
    """
    Args:
        p1, p2, p3, p4, p5 (Point): Vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5 = sort_cyclic_points(p1, p2, p3, p4, p5)

permutations()

Enumerate cyclic and reversed vertex orderings.

Returns:

Type Description

list[tuple[Point, Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
def permutations(self):
    """Enumerate cyclic and reversed vertex orderings.

    Returns:
        list[tuple[Point, Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4, self.p5),
        (self.p2, self.p3, self.p4, self.p5, self.p1),
        (self.p3, self.p4, self.p5, self.p1, self.p2),
        (self.p4, self.p5, self.p1, self.p2, self.p3),
        (self.p5, self.p1, self.p2, self.p3, self.p4),
        (self.p5, self.p4, self.p3, self.p2, self.p1),
        (self.p4, self.p3, self.p2, self.p1, self.p5),
        (self.p3, self.p2, self.p1, self.p5, self.p4),
        (self.p2, self.p1, self.p5, self.p4, self.p3),
        (self.p1, self.p5, self.p4, self.p3, self.p2),
    ]

Perpendicular

Bases: Relation

Segments (p1,p2) and (p3,p4) are perpendicular.

Source code in pyeuclid/formalization/relation.py
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
class Perpendicular(Relation):
    """Segments (p1,p2) and (p3,p4) are perpendicular."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1, p2 (Point): First segment endpoints.
            p3, p4 (Point): Second segment endpoints.
        """
        super().__init__()
        p1, p2 = sort_points(p1, p2)
        p3, p4 = sort_points(p3, p4)
        self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

    def permutations(self):
        """Enumerate symmetric endpoint permutations preserving segment groups.

        Returns:
            list[tuple[Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p1, self.p2, self.p4, self.p3),
            (self.p2, self.p1, self.p3, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
            (self.p3, self.p4, self.p1, self.p2),
            (self.p4, self.p3, self.p1, self.p2),
            (self.p3, self.p4, self.p2, self.p1),
            (self.p4, self.p3, self.p2, self.p1),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1, p2 Point

First segment endpoints.

required
p3, p4 Point

Second segment endpoints.

required
Source code in pyeuclid/formalization/relation.py
428
429
430
431
432
433
434
435
436
437
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1, p2 (Point): First segment endpoints.
        p3, p4 (Point): Second segment endpoints.
    """
    super().__init__()
    p1, p2 = sort_points(p1, p2)
    p3, p4 = sort_points(p3, p4)
    self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

permutations()

Enumerate symmetric endpoint permutations preserving segment groups.

Returns:

Type Description

list[tuple[Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
def permutations(self):
    """Enumerate symmetric endpoint permutations preserving segment groups.

    Returns:
        list[tuple[Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p1, self.p2, self.p4, self.p3),
        (self.p2, self.p1, self.p3, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
        (self.p3, self.p4, self.p1, self.p2),
        (self.p4, self.p3, self.p1, self.p2),
        (self.p3, self.p4, self.p2, self.p1),
        (self.p4, self.p3, self.p2, self.p1),
    ]

Point

Named geometric point used throughout the formalization.

Source code in pyeuclid/formalization/relation.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Point:
    """Named geometric point used throughout the formalization."""

    def __init__(self, name: str):
        """Create a point.

        Args:
            name (str): Identifier (underscores are not allowed).
        """
        self.name = name
        assert "_" not in name

    def __str__(self):
        return self.name

    def __eq__(self, other):
        return str(self) == str(other)

    def __repr__(self):
        return str(self)

    def __hash__(self):
        return hash(str(self))

__init__(name)

Create a point.

Parameters:

Name Type Description Default
name str

Identifier (underscores are not allowed).

required
Source code in pyeuclid/formalization/relation.py
16
17
18
19
20
21
22
23
def __init__(self, name: str):
    """Create a point.

    Args:
        name (str): Identifier (underscores are not allowed).
    """
    self.name = name
    assert "_" not in name

Quadrilateral

Bases: Relation

Points form a cyclically ordered quadrilateral.

Source code in pyeuclid/formalization/relation.py
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
class Quadrilateral(Relation):
    """Points form a cyclically ordered quadrilateral."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1, p2, p3, p4 (Point): Vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4 = sort_cyclic_points(p1, p2, p3, p4)

    def permutations(self):
        """Enumerate cyclic and reversed vertex orderings.

        Returns:
            list[tuple[Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p2, self.p3, self.p4, self.p1),
            (self.p3, self.p4, self.p1, self.p2),
            (self.p4, self.p1, self.p2, self.p3),
            (self.p4, self.p3, self.p2, self.p1),
            (self.p3, self.p2, self.p1, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
            (self.p1, self.p4, self.p3, self.p2),
        ]

    def definition(self):
        """Opposite sides must lie on opposite sides of diagonals.

        Returns:
            list[Relation]: Primitive opposite-side relations.
        """
        return [
            OppositeSide(self.p1, self.p3, self.p2, self.p4),
            OppositeSide(self.p2, self.p4, self.p1, self.p3),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1, p2, p3, p4 Point

Vertices.

required
Source code in pyeuclid/formalization/relation.py
460
461
462
463
464
465
466
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1, p2, p3, p4 (Point): Vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4 = sort_cyclic_points(p1, p2, p3, p4)

definition()

Opposite sides must lie on opposite sides of diagonals.

Returns:

Type Description

list[Relation]: Primitive opposite-side relations.

Source code in pyeuclid/formalization/relation.py
485
486
487
488
489
490
491
492
493
494
def definition(self):
    """Opposite sides must lie on opposite sides of diagonals.

    Returns:
        list[Relation]: Primitive opposite-side relations.
    """
    return [
        OppositeSide(self.p1, self.p3, self.p2, self.p4),
        OppositeSide(self.p2, self.p4, self.p1, self.p3),
    ]

permutations()

Enumerate cyclic and reversed vertex orderings.

Returns:

Type Description

list[tuple[Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
def permutations(self):
    """Enumerate cyclic and reversed vertex orderings.

    Returns:
        list[tuple[Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p2, self.p3, self.p4, self.p1),
        (self.p3, self.p4, self.p1, self.p2),
        (self.p4, self.p1, self.p2, self.p3),
        (self.p4, self.p3, self.p2, self.p1),
        (self.p3, self.p2, self.p1, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
        (self.p1, self.p4, self.p3, self.p2),
    ]

Relation

Base class for logical relations over points.

Source code in pyeuclid/formalization/relation.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class Relation:
    """Base class for logical relations over points."""

    def __init__(self):
        self.negated = False

    def negate(self):
        """Toggle negation flag in-place."""
        self.negated = not self.negated

    def get_points(self):
        """Return all point instances contained in the relation."""
        points = []
        for v in vars(self).values():
            if isinstance(v, Point):
                points.append(v)
            elif isinstance(v, list):
                for p in v:
                    if isinstance(p, Point):
                        points.append(p)
        return points

    def __str__(self):
        """Readable representation, prefixed with Not() when negated."""
        class_name = self.__class__.__name__
        points = self.get_points()
        args_name = ",".join([p.name for p in points])

        if not self.negated:
            return f"{class_name}({args_name})"
        else:
            return f"Not({class_name}({args_name}))"

    def __repr__(self):
        return str(self)

    def __eq__(self, other):
        return str(self) == str(other)

    def __hash__(self):
        return hash(str(self))

__str__()

Readable representation, prefixed with Not() when negated.

Source code in pyeuclid/formalization/relation.py
60
61
62
63
64
65
66
67
68
69
def __str__(self):
    """Readable representation, prefixed with Not() when negated."""
    class_name = self.__class__.__name__
    points = self.get_points()
    args_name = ",".join([p.name for p in points])

    if not self.negated:
        return f"{class_name}({args_name})"
    else:
        return f"Not({class_name}({args_name}))"

get_points()

Return all point instances contained in the relation.

Source code in pyeuclid/formalization/relation.py
48
49
50
51
52
53
54
55
56
57
58
def get_points(self):
    """Return all point instances contained in the relation."""
    points = []
    for v in vars(self).values():
        if isinstance(v, Point):
            points.append(v)
        elif isinstance(v, list):
            for p in v:
                if isinstance(p, Point):
                    points.append(p)
    return points

negate()

Toggle negation flag in-place.

Source code in pyeuclid/formalization/relation.py
44
45
46
def negate(self):
    """Toggle negation flag in-place."""
    self.negated = not self.negated

SameSide

Bases: Relation

Points p1,p2 lie on the same side of the line (p3,p4).

Source code in pyeuclid/formalization/relation.py
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
class SameSide(Relation):
    """Points p1,p2 lie on the same side of the line (p3,p4)."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1 (Point): First query point.
            p2 (Point): Second query point.
            p3 (Point): Line endpoint 1.
            p4 (Point): Line endpoint 2.
        """
        super().__init__()
        self.p1, self.p2 = sort_points(p1, p2)
        self.p3, self.p4 = sort_points(p3, p4)

    def permutations(self):
        """Enumerate symmetric orderings for same-side tests."""
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p1, self.p2, self.p4, self.p3),
            (self.p2, self.p1, self.p3, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1 Point

First query point.

required
p2 Point

Second query point.

required
p3 Point

Line endpoint 1.

required
p4 Point

Line endpoint 2.

required
Source code in pyeuclid/formalization/relation.py
195
196
197
198
199
200
201
202
203
204
205
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1 (Point): First query point.
        p2 (Point): Second query point.
        p3 (Point): Line endpoint 1.
        p4 (Point): Line endpoint 2.
    """
    super().__init__()
    self.p1, self.p2 = sort_points(p1, p2)
    self.p3, self.p4 = sort_points(p3, p4)

permutations()

Enumerate symmetric orderings for same-side tests.

Source code in pyeuclid/formalization/relation.py
207
208
209
210
211
212
213
214
def permutations(self):
    """Enumerate symmetric orderings for same-side tests."""
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p1, self.p2, self.p4, self.p3),
        (self.p2, self.p1, self.p3, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
    ]

Similar

Bases: Relation

Triangles (p1,p2,p3) and (p4,p5,p6) are similar.

Source code in pyeuclid/formalization/relation.py
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
class Similar(Relation):
    """Triangles (p1,p2,p3) and (p4,p5,p6) are similar."""

    def __init__(
        self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
    ):
        """
        Args:
            p1, p2, p3 (Point): First triangle vertices.
            p4, p5, p6 (Point): Second triangle vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

    def definition(self):
        """Similarity expressed via length ratios and non-collinearity.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) / Length(self.p4, self.p5)
            - Length(self.p2, self.p3) / Length(self.p5, self.p6),
            Length(self.p1, self.p2) / Length(self.p4, self.p5)
            - Length(self.p3, self.p1) / Length(self.p6, self.p4),
            NotCollinear(self.p1, self.p2, self.p3),
        ]

__init__(p1, p2, p3, p4, p5, p6)

Parameters:

Name Type Description Default
p1, p2, p3 Point

First triangle vertices.

required
p4, p5, p6 Point

Second triangle vertices.

required
Source code in pyeuclid/formalization/relation.py
347
348
349
350
351
352
353
354
355
356
def __init__(
    self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
):
    """
    Args:
        p1, p2, p3 (Point): First triangle vertices.
        p4, p5, p6 (Point): Second triangle vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

definition()

Similarity expressed via length ratios and non-collinearity.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
358
359
360
361
362
363
364
365
366
367
368
369
370
def definition(self):
    """Similarity expressed via length ratios and non-collinearity.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) / Length(self.p4, self.p5)
        - Length(self.p2, self.p3) / Length(self.p5, self.p6),
        Length(self.p1, self.p2) / Length(self.p4, self.p5)
        - Length(self.p3, self.p1) / Length(self.p6, self.p4),
        NotCollinear(self.p1, self.p2, self.p3),
    ]

Similar4P

Bases: Relation

Two quadrilaterals are similar.

Source code in pyeuclid/formalization/relation.py
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
class Similar4P(Relation):
    """Two quadrilaterals are similar."""

    def __init__(
        self,
        p1: Point,
        p2: Point,
        p3: Point,
        p4: Point,
        p5: Point,
        p6: Point,
        p7: Point,
        p8: Point,
    ):
        """
        Args:
            p1..p4 (Point): First quadrilateral.
            p5..p8 (Point): Second quadrilateral.
        """
        super().__init__()
        point_map_12 = {p1: p5, p2: p6, p3: p7, p4: p8}
        point_map_21 = {p5: p1, p6: p2, p7: p3, p8: p4}

        sorted_1 = sort_cyclic_points(p1, p2, p3, p4)
        sorted_2 = sort_cyclic_points(p5, p6, p7, p8)
        if compare_names(sorted_1, sorted_2) == 0:
            self.p1, self.p2, self.p3, self.p4 = sorted_1
            self.p5,self.p6,self.p7,self.p8 = point_map_12[self.p1], point_map_12[
                self.p2], point_map_12[self.p3], point_map_12[self.p4]
        else:
            self.p1, self.p2, self.p3, self.p4 = sorted_2
            self.p5, self.p6, self.p7, self.p8 = point_map_21[self.p1], point_map_21[
                self.p2], point_map_21[self.p3], point_map_21[self.p4]

    def definition(self):
        """Similarity expressed via side ratios and angle equalities.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) / Length(self.p5, self.p6)
            - Length(self.p2, self.p3) / Length(self.p6, self.p7),
            Length(self.p1, self.p2) / Length(self.p5, self.p6)
            - Length(self.p3, self.p4) / Length(self.p7, self.p8),
            Length(self.p1, self.p2) / Length(self.p5, self.p6)
            - Length(self.p4, self.p1) / Length(self.p8, self.p5),
            Angle(self.p1, self.p2, self.p3) - Angle(self.p5, self.p6, self.p7),
            Angle(self.p2, self.p3, self.p4) - Angle(self.p6, self.p7, self.p8),
            Angle(self.p3, self.p4, self.p1) - Angle(self.p7, self.p8, self.p5),
            Angle(self.p4, self.p1, self.p2) - Angle(self.p8, self.p5, self.p6),
        ]

__init__(p1, p2, p3, p4, p5, p6, p7, p8)

Parameters:

Name Type Description Default
p1..p4 Point

First quadrilateral.

required
p5..p8 Point

Second quadrilateral.

required
Source code in pyeuclid/formalization/relation.py
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
def __init__(
    self,
    p1: Point,
    p2: Point,
    p3: Point,
    p4: Point,
    p5: Point,
    p6: Point,
    p7: Point,
    p8: Point,
):
    """
    Args:
        p1..p4 (Point): First quadrilateral.
        p5..p8 (Point): Second quadrilateral.
    """
    super().__init__()
    point_map_12 = {p1: p5, p2: p6, p3: p7, p4: p8}
    point_map_21 = {p5: p1, p6: p2, p7: p3, p8: p4}

    sorted_1 = sort_cyclic_points(p1, p2, p3, p4)
    sorted_2 = sort_cyclic_points(p5, p6, p7, p8)
    if compare_names(sorted_1, sorted_2) == 0:
        self.p1, self.p2, self.p3, self.p4 = sorted_1
        self.p5,self.p6,self.p7,self.p8 = point_map_12[self.p1], point_map_12[
            self.p2], point_map_12[self.p3], point_map_12[self.p4]
    else:
        self.p1, self.p2, self.p3, self.p4 = sorted_2
        self.p5, self.p6, self.p7, self.p8 = point_map_21[self.p1], point_map_21[
            self.p2], point_map_21[self.p3], point_map_21[self.p4]

definition()

Similarity expressed via side ratios and angle equalities.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
def definition(self):
    """Similarity expressed via side ratios and angle equalities.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) / Length(self.p5, self.p6)
        - Length(self.p2, self.p3) / Length(self.p6, self.p7),
        Length(self.p1, self.p2) / Length(self.p5, self.p6)
        - Length(self.p3, self.p4) / Length(self.p7, self.p8),
        Length(self.p1, self.p2) / Length(self.p5, self.p6)
        - Length(self.p4, self.p1) / Length(self.p8, self.p5),
        Angle(self.p1, self.p2, self.p3) - Angle(self.p5, self.p6, self.p7),
        Angle(self.p2, self.p3, self.p4) - Angle(self.p6, self.p7, self.p8),
        Angle(self.p3, self.p4, self.p1) - Angle(self.p7, self.p8, self.p5),
        Angle(self.p4, self.p1, self.p2) - Angle(self.p8, self.p5, self.p6),
    ]

Similar5P

Bases: Relation

Two pentagons are similar.

Source code in pyeuclid/formalization/relation.py
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
class Similar5P(Relation):
    """Two pentagons are similar."""

    def __init__(
        self,
        p1: Point,
        p2: Point,
        p3: Point,
        p4: Point,
        p5: Point,
        p6: Point,
        p7: Point,
        p8: Point,
        p9: Point,
        p10: Point,
    ):
        """
        Args:
            p1..p5 (Point): First pentagon.
            p6..p10 (Point): Second pentagon.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5, self.p6, self.p7, self.p8, self.p9, self.p10 = p1, p2, p3, p4, p5, p6, p7, p8, p9, p10

    def definition(self):
        """Similarity expressed via consecutive side ratios and angle equalities.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) / Length(self.p6, self.p7)
            - Length(self.p2, self.p3) / Length(self.p7, self.p8),
            Length(self.p2, self.p3) / Length(self.p7, self.p8)
            - Length(self.p3, self.p4) / Length(self.p8, self.p9),
            Length(self.p3, self.p4) / Length(self.p8, self.p9)
            - Length(self.p4, self.p5) / Length(self.p9, self.p10),
            Length(self.p4, self.p5) / Length(self.p9, self.p10)
            - Length(self.p5, self.p1) / Length(self.p10, self.p6),
            Length(self.p5, self.p1) / Length(self.p10, self.p6)
            - Length(self.p1, self.p2) / Length(self.p6, self.p7),
            Angle(self.p1, self.p2, self.p3) - Angle(self.p6, self.p7, self.p8),
            Angle(self.p2, self.p3, self.p4) - Angle(self.p7, self.p8, self.p9),
            Angle(self.p3, self.p4, self.p5) - Angle(self.p8, self.p9, self.p10),
            Angle(self.p4, self.p5, self.p1) - Angle(self.p9, self.p10, self.p6),
            Angle(self.p5, self.p1, self.p2) - Angle(self.p10, self.p6, self.p7),
        ]

__init__(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10)

Parameters:

Name Type Description Default
p1..p5 Point

First pentagon.

required
p6..p10 Point

Second pentagon.

required
Source code in pyeuclid/formalization/relation.py
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
def __init__(
    self,
    p1: Point,
    p2: Point,
    p3: Point,
    p4: Point,
    p5: Point,
    p6: Point,
    p7: Point,
    p8: Point,
    p9: Point,
    p10: Point,
):
    """
    Args:
        p1..p5 (Point): First pentagon.
        p6..p10 (Point): Second pentagon.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5, self.p6, self.p7, self.p8, self.p9, self.p10 = p1, p2, p3, p4, p5, p6, p7, p8, p9, p10

definition()

Similarity expressed via consecutive side ratios and angle equalities.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
def definition(self):
    """Similarity expressed via consecutive side ratios and angle equalities.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) / Length(self.p6, self.p7)
        - Length(self.p2, self.p3) / Length(self.p7, self.p8),
        Length(self.p2, self.p3) / Length(self.p7, self.p8)
        - Length(self.p3, self.p4) / Length(self.p8, self.p9),
        Length(self.p3, self.p4) / Length(self.p8, self.p9)
        - Length(self.p4, self.p5) / Length(self.p9, self.p10),
        Length(self.p4, self.p5) / Length(self.p9, self.p10)
        - Length(self.p5, self.p1) / Length(self.p10, self.p6),
        Length(self.p5, self.p1) / Length(self.p10, self.p6)
        - Length(self.p1, self.p2) / Length(self.p6, self.p7),
        Angle(self.p1, self.p2, self.p3) - Angle(self.p6, self.p7, self.p8),
        Angle(self.p2, self.p3, self.p4) - Angle(self.p7, self.p8, self.p9),
        Angle(self.p3, self.p4, self.p5) - Angle(self.p8, self.p9, self.p10),
        Angle(self.p4, self.p5, self.p1) - Angle(self.p9, self.p10, self.p6),
        Angle(self.p5, self.p1, self.p2) - Angle(self.p10, self.p6, self.p7),
    ]

register

Decorator that registers an inference rule class into named rule sets.

Source code in pyeuclid/engine/inference_rule.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class register():
    """Decorator that registers an inference rule class into named rule sets."""

    def __init__(self, *annotations):
        self.annotations = annotations

    def __call__(self, cls):
        for item in self.annotations:
            if not item in inference_rule_sets:
                inference_rule_sets[item] = [cls]
            else:
                inference_rule_sets[item].append(cls)

        def expanded_condition(self):
            lst = expand_definition(self._condition())
            return lst

        def expanded_conclusion(self):
            lst = expand_definition(self._conclusion())
            result = []
            for item in lst:
                if isinstance(item, sympy.core.numbers.Zero):
                    continue
                result.append(item)
            return result
        cls._condition = cls.condition
        cls._conclusion = cls.conclusion
        cls.condition = expanded_condition
        cls.conclusion = expanded_conclusion
        return cls

Angle(p1, p2, p3)

Symbolic angle at p2.

Returns:

Type Description

sympy.Symbol: Non-negative angle symbol.

Source code in pyeuclid/formalization/relation.py
108
109
110
111
112
113
114
115
def Angle(p1: Point, p2: Point, p3: Point):
    """Symbolic angle at p2.

    Returns:
        sympy.Symbol: Non-negative angle symbol.
    """
    p1, p3 = sort_points(p1, p3)
    return Symbol(f"Angle_{p1}_{p2}_{p3}", non_negative=True)

Area(*ps)

Symbolic polygonal area over an ordered point cycle.

Returns:

Type Description

sympy.Symbol: Positive area symbol.

Source code in pyeuclid/formalization/relation.py
128
129
130
131
132
133
134
135
def Area(*ps: list[Point]):
    """Symbolic polygonal area over an ordered point cycle.

    Returns:
        sympy.Symbol: Positive area symbol.
    """
    ps = sort_cyclic_points(*ps)
    return Symbol("_".join(["Area"] + [str(item) for item in ps]), positive=True)

Length(p1, p2)

Symbolic length between two points.

Returns:

Type Description

sympy.Symbol: Positive length symbol.

Source code in pyeuclid/formalization/relation.py
118
119
120
121
122
123
124
125
def Length(p1: Point, p2: Point):
    """Symbolic length between two points.

    Returns:
        sympy.Symbol: Positive length symbol.
    """
    p1, p2 = sort_points(p1, p2)
    return Symbol(f"Length_{str(p1)}_{str(p2)}", positive=True)

Not(p)

Return a negated shallow copy of a relation.

Source code in pyeuclid/formalization/relation.py
101
102
103
104
105
def Not(p: Relation) -> Relation:
    """Return a negated shallow copy of a relation."""
    other = copy.copy(p)
    other.negate()
    return other

Variable(name)

Free symbolic variable placeholder.

Returns:

Type Description

sympy.Symbol: Dimensionless variable symbol.

Source code in pyeuclid/formalization/relation.py
138
139
140
141
142
143
144
def Variable(name: str):
    """Free symbolic variable placeholder.

    Returns:
        sympy.Symbol: Dimensionless variable symbol.
    """
    return Symbol(f"Variable_{name}")

Deductive database for geometric inference rules.

Matches registered inference rules against the current State, instantiates them with concrete points, and applies resulting conclusions. Uses a Z3 encoding to search admissible assignments while preserving canonical point orderings via Lt constraints.

Between

Bases: Relation

p1 lies between p2 and p3 (on the same line).

Source code in pyeuclid/formalization/relation.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
class Between(Relation):
    """p1 lies between p2 and p3 (on the same line)."""

    def __init__(self, p1: Point, p2: Point, p3: Point):
        """
        p1 is between p2 and p3.

        Args:
            p1 (Point): Middle point.
            p2 (Point): Endpoint 1.
            p3 (Point): Endpoint 2.
        """
        super().__init__()
        p2, p3 = sort_points(p2, p3)
        self.p1, self.p2, self.p3 = p1, p2, p3

    def permutations(self):
        """Enumerate equivalent point orderings for the between relation.

        Returns:
            list[tuple[Point, Point, Point]]: Permitted permutations.
        """
        return [(self.p1, self.p2, self.p3), (self.p1, self.p3, self.p2)]

__init__(p1, p2, p3)

p1 is between p2 and p3.

Parameters:

Name Type Description Default
p1 Point

Middle point.

required
p2 Point

Endpoint 1.

required
p3 Point

Endpoint 2.

required
Source code in pyeuclid/formalization/relation.py
170
171
172
173
174
175
176
177
178
179
180
181
def __init__(self, p1: Point, p2: Point, p3: Point):
    """
    p1 is between p2 and p3.

    Args:
        p1 (Point): Middle point.
        p2 (Point): Endpoint 1.
        p3 (Point): Endpoint 2.
    """
    super().__init__()
    p2, p3 = sort_points(p2, p3)
    self.p1, self.p2, self.p3 = p1, p2, p3

permutations()

Enumerate equivalent point orderings for the between relation.

Returns:

Type Description

list[tuple[Point, Point, Point]]: Permitted permutations.

Source code in pyeuclid/formalization/relation.py
183
184
185
186
187
188
189
def permutations(self):
    """Enumerate equivalent point orderings for the between relation.

    Returns:
        list[tuple[Point, Point, Point]]: Permitted permutations.
    """
    return [(self.p1, self.p2, self.p3), (self.p1, self.p3, self.p2)]

Circle

Numerical circle.

Source code in pyeuclid/formalization/numericals.py
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
class Circle:
    """Numerical circle."""

    def __init__(
        self,
        center: Optional[Point] = None,
        radius: Optional[float] = None,
        p1: Optional[Point] = None,
        p2: Optional[Point] = None,
        p3: Optional[Point] = None,
    ):
        if not center:
            l12 = perpendicular_bisector(p1, p2)
            l23 = perpendicular_bisector(p2, p3)
            center = line_line_intersection(l12, l23)

        if not radius:
            p = p1 or p2 or p3
            radius = center.distance(p)

        self.center = center
        self.radius = radius

    def intersect(self, obj: Union[Line, Circle]) -> tuple[Point, ...]:
        if isinstance(obj, Line):
            return obj.intersect(self)

        if isinstance(obj, Circle):
            return circle_circle_intersection(self, obj)

    def sample_within(self, points: list[Point], n: int = 5) -> list[Point]:
        """Sample a point within the boundary of points."""
        result = None
        best = -1.0
        for _ in range(n):
            ang = unif(0.0, 2.0) * np.pi
            x = self.center + Point(np.cos(ang), np.sin(ang)) * self.radius
            mind = min([x.distance(p) for p in points])
            if mind > best:
                best = mind
                result = x
        return [result]

sample_within(points, n=5)

Sample a point within the boundary of points.

Source code in pyeuclid/formalization/numericals.py
457
458
459
460
461
462
463
464
465
466
467
468
def sample_within(self, points: list[Point], n: int = 5) -> list[Point]:
    """Sample a point within the boundary of points."""
    result = None
    best = -1.0
    for _ in range(n):
        ang = unif(0.0, 2.0) * np.pi
        x = self.center + Point(np.cos(ang), np.sin(ang)) * self.radius
        mind = min([x.distance(p) for p in points])
        if mind > best:
            best = mind
            result = x
    return [result]

Collinear

Bases: Relation

Points p1,p2,p3 are collinear.

Source code in pyeuclid/formalization/relation.py
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
class Collinear(Relation):
    """Points p1,p2,p3 are collinear."""

    def __init__(self, p1, p2, p3):
        """
        Args:
            p1 (Point)
            p2 (Point)
            p3 (Point)
        """
        super().__init__()
        self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

    def permutations(self):
        """Enumerate collinearity argument permutations.

        Returns:
            itertools.permutations: All orderings of the three points.
        """
        return itertools.permutations([self.p1, self.p2, self.p3])

__init__(p1, p2, p3)

Source code in pyeuclid/formalization/relation.py
248
249
250
251
252
253
254
255
256
def __init__(self, p1, p2, p3):
    """
    Args:
        p1 (Point)
        p2 (Point)
        p3 (Point)
    """
    super().__init__()
    self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

permutations()

Enumerate collinearity argument permutations.

Returns:

Type Description

itertools.permutations: All orderings of the three points.

Source code in pyeuclid/formalization/relation.py
258
259
260
261
262
263
264
def permutations(self):
    """Enumerate collinearity argument permutations.

    Returns:
        itertools.permutations: All orderings of the three points.
    """
    return itertools.permutations([self.p1, self.p2, self.p3])

Concyclic

Bases: Relation

All given points lie on the same circle.

Source code in pyeuclid/formalization/relation.py
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
class Concyclic(Relation):
    """All given points lie on the same circle."""

    def __init__(self, *ps: Point):
        """
        Args:
            *ps (Point): Points to be tested for concyclicity.
        """
        super().__init__()
        self.ps = list(sort_points(*ps))

    def permutations(self):
        """Enumerate symmetric permutations of the point set.

        Returns:
            itertools.permutations: All orderings of the points.
        """
        return itertools.permutations(self.ps)

__init__(*ps)

Parameters:

Name Type Description Default
*ps Point

Points to be tested for concyclicity.

()
Source code in pyeuclid/formalization/relation.py
376
377
378
379
380
381
382
def __init__(self, *ps: Point):
    """
    Args:
        *ps (Point): Points to be tested for concyclicity.
    """
    super().__init__()
    self.ps = list(sort_points(*ps))

permutations()

Enumerate symmetric permutations of the point set.

Returns:

Type Description

itertools.permutations: All orderings of the points.

Source code in pyeuclid/formalization/relation.py
384
385
386
387
388
389
390
def permutations(self):
    """Enumerate symmetric permutations of the point set.

    Returns:
        itertools.permutations: All orderings of the points.
    """
    return itertools.permutations(self.ps)

Congruent

Bases: Relation

Triangles (p1,p2,p3) and (p4,p5,p6) are congruent.

Source code in pyeuclid/formalization/relation.py
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
class Congruent(Relation):
    """Triangles (p1,p2,p3) and (p4,p5,p6) are congruent."""

    def __init__(
        self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
    ):
        """
        Args:
            p1, p2, p3 (Point): First triangle vertices.
            p4, p5, p6 (Point): Second triangle vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

    def definition(self):
        """Congruence expressed as equal side lengths and non-collinearity.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) - Length(self.p4, self.p5),
            Length(self.p2, self.p3) - Length(self.p5, self.p6),
            Length(self.p1, self.p3) - Length(self.p4, self.p6),
            NotCollinear(self.p1, self.p2, self.p3),
        ]

__init__(p1, p2, p3, p4, p5, p6)

Parameters:

Name Type Description Default
p1, p2, p3 Point

First triangle vertices.

required
p4, p5, p6 Point

Second triangle vertices.

required
Source code in pyeuclid/formalization/relation.py
319
320
321
322
323
324
325
326
327
328
def __init__(
    self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
):
    """
    Args:
        p1, p2, p3 (Point): First triangle vertices.
        p4, p5, p6 (Point): Second triangle vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

definition()

Congruence expressed as equal side lengths and non-collinearity.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
330
331
332
333
334
335
336
337
338
339
340
341
def definition(self):
    """Congruence expressed as equal side lengths and non-collinearity.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) - Length(self.p4, self.p5),
        Length(self.p2, self.p3) - Length(self.p5, self.p6),
        Length(self.p1, self.p3) - Length(self.p4, self.p6),
        NotCollinear(self.p1, self.p2, self.p3),
    ]

ConstructionRule

Base class for geometric construction rules.

Source code in pyeuclid/formalization/construction_rule.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class ConstructionRule:
    """Base class for geometric construction rules."""

    def __init__(self):
        """Initialize an empty construction rule."""
        pass

    def arguments(self):
        """Return the input entities required for the construction."""
        return []

    def constructed_points(self):
        """Return the points constructed by this rule."""
        return []

    def conditions(self):
        """Return prerequisite relations for the construction to be valid."""
        return []

    def conclusions(self):
        """Return relations implied after applying the construction."""
        return []

    def __str__(self):
        class_name = self.__class__.__name__
        attributes = ",".join(str(value) for _, value in vars(self).items())
        return f"{class_name}({attributes})"

__init__()

Initialize an empty construction rule.

Source code in pyeuclid/formalization/construction_rule.py
12
13
14
def __init__(self):
    """Initialize an empty construction rule."""
    pass

arguments()

Return the input entities required for the construction.

Source code in pyeuclid/formalization/construction_rule.py
16
17
18
def arguments(self):
    """Return the input entities required for the construction."""
    return []

conclusions()

Return relations implied after applying the construction.

Source code in pyeuclid/formalization/construction_rule.py
28
29
30
def conclusions(self):
    """Return relations implied after applying the construction."""
    return []

conditions()

Return prerequisite relations for the construction to be valid.

Source code in pyeuclid/formalization/construction_rule.py
24
25
26
def conditions(self):
    """Return prerequisite relations for the construction to be valid."""
    return []

constructed_points()

Return the points constructed by this rule.

Source code in pyeuclid/formalization/construction_rule.py
20
21
22
def constructed_points(self):
    """Return the points constructed by this rule."""
    return []

DeductiveDatabase

Match and apply geometric inference rules against a state.

Source code in pyeuclid/engine/deductive_database.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
class DeductiveDatabase:
    """Match and apply geometric inference rules against a state."""

    def __init__(self, state, inner_theorems=inference_rule_sets["ex"], outer_theorems=inference_rule_sets["basic"]):
        """Initialize a deductive database.

        Args:
            state (State): Shared state being reasoned over.
            inner_theorems (Iterable[type[InferenceRule]]): Higher-priority rules applied exhaustively first.
            outer_theorems (Iterable[type[InferenceRule]]): Secondary rules applied after inner closure.
        """
        self.state = state
        self.inner_theorems = inner_theorems
        self.outer_theorems = outer_theorems
        self.closure = False

    def get_applicable_theorems(self, theorems):
        """Instantiate and return all applicable rules from the given set.

        Returns:
            list[InferenceRule]: Concrete rule instances whose conditions are satisfiable in the current state.
        """
        def search_assignments(theorem):
            if not theorem in self.state.solvers:
                self.state.solvers[theorem] = Solver()
            solver = self.state.solvers[theorem]
            solver.push()
            slots = theorem.__init__.__annotations__
            formal_entities = {}
            for key, attr_type in slots.items():
                if isinstance(attr_type, str):
                    assert attr_type == 'Point'
                else:
                    assert attr_type.__name__ == "Point"
                formal_entities[key] = Point(key)
            example = theorem(**formal_entities)
            formal_conditions = example.condition()

            nbits = math.ceil(math.log2(len(self.state.points)))
            point_encoding = {}
            point_decoding = {}
            formal_points = {}
            points = list(self.state.points)
            points.sort(key=lambda x: x.name)
            for i, point in enumerate(points):
                point_encoding[point.name] = BitVecVal(i, nbits)
                point_decoding[BitVecVal(i, nbits)] = point
            for name in formal_entities:
                formal_points[name] = BitVec(name, nbits)
                if len(self.state.points) < 2**nbits:
                    solver.add(ULT(formal_points[name], len(self.state.points)))

            def in_component(formal, component):
                clause = False
                if len(formal) == 2:
                    for item in component:
                        actual, _ = get_points_and_symbols(item)
                        p1 = And(formal_points[formal[0]] == point_encoding[actual[0]],
                                formal_points[formal[1]] == point_encoding[actual[1]])
                        p2 = And(formal_points[formal[0]] == point_encoding[actual[1]],
                                formal_points[formal[1]] == point_encoding[actual[0]])
                        clause = Or(clause, Or(p1, p2))
                elif len(formal) == 3:
                    for item in component:
                        actual, _ = get_points_and_symbols(item)
                        p1 = And(formal_points[formal[0]] == point_encoding[actual[0]], formal_points[formal[1]]
                                == point_encoding[actual[1]], formal_points[formal[2]] == point_encoding[actual[2]])
                        p2 = And(formal_points[formal[0]] == point_encoding[actual[2]], formal_points[formal[1]]
                                == point_encoding[actual[1]], formal_points[formal[2]] == point_encoding[actual[0]])
                        clause = Or(clause, Or(p1, p2))
                else:
                    assert False
                return clause

            for cond in formal_conditions:
                clause = False
                if isinstance(cond, Relation):
                    formal = cond.get_points()
                else:
                    formal, _ = get_points_and_symbols(cond)
                if isinstance(cond, Relation):
                    if isinstance(cond, Equal):
                        clause = formal_points[cond.v1.name] == formal_points[cond.v2.name]
                        if cond.negated:
                            clause = z3.Not(clause)
                    elif isinstance(cond, Lt):
                        clause = ULT(
                            formal_points[cond.v1.name], formal_points[cond.v2.name])
                    else:
                        assert type(cond) in (
                            Collinear, SameSide, Between, Perpendicular, Concyclic, Parallel)
                        if type(cond) == Between:
                            clauses = []
                            for rel in self.state.relations:
                                if type(rel) == type(cond):
                                    assert not rel.negated
                                    permutations = rel.permutations()
                                    for perm in permutations:
                                        assignment = True
                                        for i in range(len(formal)):
                                            assignment = And(
                                                formal_points[formal[i]] == point_encoding[perm[i]], assignment)
                                        clauses.append(assignment)
                            clause = Or(*clauses)
                            if cond.negated:  # we have all between relations, and never store negated between relations
                                clause = z3.Not(clause)
                        else:
                            for rel in self.state.relations:
                                if type(rel) == type(cond) and rel.negated == cond.negated:
                                    if hasattr(rel, "permutations"):
                                        permutations = rel.permutations()
                                    else:
                                        permutations = [
                                            re.pattern.findall(str(rel))]
                                    for perm in permutations:
                                        if not isinstance(perm[0], str):
                                            perm = [item.name for item in perm]
                                        partial_assignment = True
                                        for i in range(len(formal)):
                                            partial_assignment = And(
                                                formal_points[formal[i]] == point_encoding[perm[i]], partial_assignment)
                                        clause = Or(partial_assignment, clause)
                            if type(cond) == Collinear:
                                degenerate = Or(formal_points[formal[0]]==formal_points[formal[1]], formal_points[formal[1]]==formal_points[formal[2]], formal_points[formal[2]]==formal_points[formal[0]])
                                clause = Or(clause, degenerate)
                elif isinstance(cond, sympy.core.expr.Expr):
                    pattern_eqlength = re.compile(r"^-?Length\w+ [-\+] Length\w+$")
                    pattern_eqangle = re.compile(r"^-?Angle\w+ [-\+] Angle\w+$")
                    pattern_eqratio = re.compile(
                        r"^-?Length\w+/Length\w+ [\+-] Length\w+/Length\w+$")
                    pattern_angle_const = re.compile(
                        r"^-?Angle\w+ [-\+] [\w/\d]+$")
                    pattern_angle_sum = re.compile(
                        r"^-?Angle\w+ [-\+] Angle\w+ [-\+] [\w/\d]+$")
                    s = str(cond)
                    if pattern_eqlength.match(s):
                        points, _ = get_points_and_symbols(cond)
                        l, r = points[:2], points[2:]
                        for component in self.state.lengths.equivalence_classes().values():
                            clause = Or(clause, And(in_component(
                                l, component), in_component(r, component)))
                    elif pattern_eqangle.match(s):
                        points, _ = get_points_and_symbols(cond)
                        l, r = points[:3], points[3:]
                        for component in self.state.angles.equivalence_classes().values():
                            clause = Or(clause, And(in_component(
                                l, component), in_component(r, component)))
                    elif pattern_eqratio.match(s):
                        points, _ = get_points_and_symbols(cond)
                        a, b, c, d = points[:2], points[2:4], points[4:6], points[6:8]
                        for ratios in self.state.ratios.values():
                            l_clause = False
                            r_clause = False
                            for ratio in ratios:
                                _, symbols = get_points_and_symbols(ratio)
                                length1, length2 = symbols
                                length1, length2 = self.state.lengths.find(
                                    length1), self.state.lengths.find(length2)
                                component1, component2 = self.state.lengths.equivalence_classes(
                                )[length1], self.state.lengths.equivalence_classes()[length2]
                                l_clause = Or(l_clause, And(in_component(
                                    a, component1), in_component(b, component2)))
                                r_clause = Or(r_clause, And(in_component(
                                    c, component1), in_component(d, component2)))
                            clause = Or(clause, And(l_clause, r_clause))
                    elif pattern_angle_const.match(s):
                        points, _ = get_points_and_symbols(cond)
                        left = points[:3]
                        cnst = [arg for arg in cond.args if len(arg.free_symbols)==0][0]
                        cnst = abs(cnst)
                        for rep, component in self.state.angles.equivalence_classes().items():
                            if self.state.check_conditions(cnst - rep):
                                clause = in_component(left, component)
                                break
                    else:
                        assert pattern_angle_sum.match(s)
                        cnst = [arg for arg in cond.args if len(
                            arg.free_symbols) == 0][0]
                        cnst = abs(cnst)
                        points, _ = get_points_and_symbols(cond)
                        left, right = points[:3], points[3:]
                        for rep, angle_sums in self.state.angle_sums.items():
                            if self.state.check_conditions(cnst-rep):
                                for angle_sum in angle_sums:
                                    if isinstance(angle_sum, sympy.core.add.Add):
                                        angle1, angle2 = angle_sum.args
                                        # angle1 + angle2
                                    else:
                                        angle1 = list(angle_sum.free_symbols)[0]
                                        angle2 = angle1
                                        # 2 * angle_1
                                    angle1, angle2 = self.state.angles.find(angle1), self.state.angles.find(angle2)
                                    try:
                                        component1, component2 = self.state.angles.equivalence_classes(
                                        )[angle1], self.state.angles.equivalence_classes()[angle2]
                                    except:
                                        breakpoint()
                                        assert False
                                    clause = Or(clause, And(in_component(
                                        left, component1), in_component(right, component2)))
                                break
                solver.add(clause)
            solutions = []
            assignments = []
            while solver.check() == z3.sat:
                m = solver.model()
                dic = {str(i): point_decoding[m[i]] for i in m}
                concrete = theorem(**dic)
                concrete._depth = self.state.current_depth
                # if try complex, solutions in later iterations may be weaker than previous ones and unionfind because of abondoning complex equations, causing check condition failure
                if not self.state.try_complex and not self.state.check_conditions(concrete.condition()):
                    for condition in concrete.condition():
                        if not self.state.check_conditions(condition):
                            print(f"Failed condition: {condition}")
                            breakpoint()
                            self.state.check_conditions(condition)
                            assert False
                if not concrete.degenerate():
                    assignments.append(concrete)
                solution = False
                for i in m:
                    solution = Or((formal_points[str(i)] != m[i]), solution)
                solver.add(solution)
                solutions.append(solution)
            solver.pop()
            for item in solutions:
                solver.add(item)
            solver.push()
            assignments.sort(key=lambda x: str(x))
            return assignments

        applicable_theorems = []
        pbar = tqdm(theorems, disable=self.state.silent)
        for theorem in pbar:
            pbar.set_description(
                f"{theorem.__name__} #rels {len(self.state.relations)} # eqns {len(self.state.equations)}")
            concrete_theorems = search_assignments(theorem)
            applicable_theorems += concrete_theorems
        return applicable_theorems

    def apply(self, inferences):
        """Apply instantiated rules by adding their conclusions to the state.

        Args:
            inferences (Iterable[InferenceRule]): Instantiated rules to apply.

        Returns:
            None
        """
        last = None
        cnt = 0
        for item in inferences:
            tmp = type(item)
            if not tmp == last:
                if cnt > 3:
                    if not self.state.silent:
                        self.state.logger.info(f"...and {cnt-3} more.")
                cnt = 0
                last = tmp
            if cnt < 3:
                if not self.state.silent:
                    self.state.logger.info(str(item))
            cnt += 1
            conclusions = item.conclusion()
            for i, conclusion in enumerate(conclusions):
                if isinstance(conclusion, sympy.core.expr.Expr):
                    conclusion = Traced(conclusion)
                    conclusion.sources = [item]
                else:
                    conclusion.source = item
                conclusion.depth = self.state.current_depth
                item.depth = self.state.current_depth
                conclusions[i] = conclusion
            self.state.add_relations(conclusions)
        if cnt > 3:
            if not self.state.silent:
                self.state.logger.info(f"...and {cnt - 3} more.")

    def run(self):
        """Execute one deductive phase over inner then outer theorems.

        Updates `closure` when no further inferences are available.

        Returns:
            None
        """
        inner_closure = True
        while True:
            if self.state.complete() is not None:
                return
            inner_applicable = self.get_applicable_theorems(self.inner_theorems)
            self.apply(inner_applicable)
            if len(inner_applicable) == 0:
                break
            inner_closure = False

        if self.state.complete() is not None:
            return

        applicable_theorems = self.get_applicable_theorems(self.outer_theorems)
        self.apply(applicable_theorems)

        if len(applicable_theorems) == 0 and inner_closure:
            self.closure = True
            if not self.state.silent:
                self.state.logger.debug("Found Closure")
            return

__init__(state, inner_theorems=inference_rule_sets['ex'], outer_theorems=inference_rule_sets['basic'])

Initialize a deductive database.

Parameters:

Name Type Description Default
state State

Shared state being reasoned over.

required
inner_theorems Iterable[type[InferenceRule]]

Higher-priority rules applied exhaustively first.

inference_rule_sets['ex']
outer_theorems Iterable[type[InferenceRule]]

Secondary rules applied after inner closure.

inference_rule_sets['basic']
Source code in pyeuclid/engine/deductive_database.py
22
23
24
25
26
27
28
29
30
31
32
33
def __init__(self, state, inner_theorems=inference_rule_sets["ex"], outer_theorems=inference_rule_sets["basic"]):
    """Initialize a deductive database.

    Args:
        state (State): Shared state being reasoned over.
        inner_theorems (Iterable[type[InferenceRule]]): Higher-priority rules applied exhaustively first.
        outer_theorems (Iterable[type[InferenceRule]]): Secondary rules applied after inner closure.
    """
    self.state = state
    self.inner_theorems = inner_theorems
    self.outer_theorems = outer_theorems
    self.closure = False

apply(inferences)

Apply instantiated rules by adding their conclusions to the state.

Parameters:

Name Type Description Default
inferences Iterable[InferenceRule]

Instantiated rules to apply.

required

Returns:

Type Description

None

Source code in pyeuclid/engine/deductive_database.py
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
def apply(self, inferences):
    """Apply instantiated rules by adding their conclusions to the state.

    Args:
        inferences (Iterable[InferenceRule]): Instantiated rules to apply.

    Returns:
        None
    """
    last = None
    cnt = 0
    for item in inferences:
        tmp = type(item)
        if not tmp == last:
            if cnt > 3:
                if not self.state.silent:
                    self.state.logger.info(f"...and {cnt-3} more.")
            cnt = 0
            last = tmp
        if cnt < 3:
            if not self.state.silent:
                self.state.logger.info(str(item))
        cnt += 1
        conclusions = item.conclusion()
        for i, conclusion in enumerate(conclusions):
            if isinstance(conclusion, sympy.core.expr.Expr):
                conclusion = Traced(conclusion)
                conclusion.sources = [item]
            else:
                conclusion.source = item
            conclusion.depth = self.state.current_depth
            item.depth = self.state.current_depth
            conclusions[i] = conclusion
        self.state.add_relations(conclusions)
    if cnt > 3:
        if not self.state.silent:
            self.state.logger.info(f"...and {cnt - 3} more.")

get_applicable_theorems(theorems)

Instantiate and return all applicable rules from the given set.

Returns:

Type Description

list[InferenceRule]: Concrete rule instances whose conditions are satisfiable in the current state.

Source code in pyeuclid/engine/deductive_database.py
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
def get_applicable_theorems(self, theorems):
    """Instantiate and return all applicable rules from the given set.

    Returns:
        list[InferenceRule]: Concrete rule instances whose conditions are satisfiable in the current state.
    """
    def search_assignments(theorem):
        if not theorem in self.state.solvers:
            self.state.solvers[theorem] = Solver()
        solver = self.state.solvers[theorem]
        solver.push()
        slots = theorem.__init__.__annotations__
        formal_entities = {}
        for key, attr_type in slots.items():
            if isinstance(attr_type, str):
                assert attr_type == 'Point'
            else:
                assert attr_type.__name__ == "Point"
            formal_entities[key] = Point(key)
        example = theorem(**formal_entities)
        formal_conditions = example.condition()

        nbits = math.ceil(math.log2(len(self.state.points)))
        point_encoding = {}
        point_decoding = {}
        formal_points = {}
        points = list(self.state.points)
        points.sort(key=lambda x: x.name)
        for i, point in enumerate(points):
            point_encoding[point.name] = BitVecVal(i, nbits)
            point_decoding[BitVecVal(i, nbits)] = point
        for name in formal_entities:
            formal_points[name] = BitVec(name, nbits)
            if len(self.state.points) < 2**nbits:
                solver.add(ULT(formal_points[name], len(self.state.points)))

        def in_component(formal, component):
            clause = False
            if len(formal) == 2:
                for item in component:
                    actual, _ = get_points_and_symbols(item)
                    p1 = And(formal_points[formal[0]] == point_encoding[actual[0]],
                            formal_points[formal[1]] == point_encoding[actual[1]])
                    p2 = And(formal_points[formal[0]] == point_encoding[actual[1]],
                            formal_points[formal[1]] == point_encoding[actual[0]])
                    clause = Or(clause, Or(p1, p2))
            elif len(formal) == 3:
                for item in component:
                    actual, _ = get_points_and_symbols(item)
                    p1 = And(formal_points[formal[0]] == point_encoding[actual[0]], formal_points[formal[1]]
                            == point_encoding[actual[1]], formal_points[formal[2]] == point_encoding[actual[2]])
                    p2 = And(formal_points[formal[0]] == point_encoding[actual[2]], formal_points[formal[1]]
                            == point_encoding[actual[1]], formal_points[formal[2]] == point_encoding[actual[0]])
                    clause = Or(clause, Or(p1, p2))
            else:
                assert False
            return clause

        for cond in formal_conditions:
            clause = False
            if isinstance(cond, Relation):
                formal = cond.get_points()
            else:
                formal, _ = get_points_and_symbols(cond)
            if isinstance(cond, Relation):
                if isinstance(cond, Equal):
                    clause = formal_points[cond.v1.name] == formal_points[cond.v2.name]
                    if cond.negated:
                        clause = z3.Not(clause)
                elif isinstance(cond, Lt):
                    clause = ULT(
                        formal_points[cond.v1.name], formal_points[cond.v2.name])
                else:
                    assert type(cond) in (
                        Collinear, SameSide, Between, Perpendicular, Concyclic, Parallel)
                    if type(cond) == Between:
                        clauses = []
                        for rel in self.state.relations:
                            if type(rel) == type(cond):
                                assert not rel.negated
                                permutations = rel.permutations()
                                for perm in permutations:
                                    assignment = True
                                    for i in range(len(formal)):
                                        assignment = And(
                                            formal_points[formal[i]] == point_encoding[perm[i]], assignment)
                                    clauses.append(assignment)
                        clause = Or(*clauses)
                        if cond.negated:  # we have all between relations, and never store negated between relations
                            clause = z3.Not(clause)
                    else:
                        for rel in self.state.relations:
                            if type(rel) == type(cond) and rel.negated == cond.negated:
                                if hasattr(rel, "permutations"):
                                    permutations = rel.permutations()
                                else:
                                    permutations = [
                                        re.pattern.findall(str(rel))]
                                for perm in permutations:
                                    if not isinstance(perm[0], str):
                                        perm = [item.name for item in perm]
                                    partial_assignment = True
                                    for i in range(len(formal)):
                                        partial_assignment = And(
                                            formal_points[formal[i]] == point_encoding[perm[i]], partial_assignment)
                                    clause = Or(partial_assignment, clause)
                        if type(cond) == Collinear:
                            degenerate = Or(formal_points[formal[0]]==formal_points[formal[1]], formal_points[formal[1]]==formal_points[formal[2]], formal_points[formal[2]]==formal_points[formal[0]])
                            clause = Or(clause, degenerate)
            elif isinstance(cond, sympy.core.expr.Expr):
                pattern_eqlength = re.compile(r"^-?Length\w+ [-\+] Length\w+$")
                pattern_eqangle = re.compile(r"^-?Angle\w+ [-\+] Angle\w+$")
                pattern_eqratio = re.compile(
                    r"^-?Length\w+/Length\w+ [\+-] Length\w+/Length\w+$")
                pattern_angle_const = re.compile(
                    r"^-?Angle\w+ [-\+] [\w/\d]+$")
                pattern_angle_sum = re.compile(
                    r"^-?Angle\w+ [-\+] Angle\w+ [-\+] [\w/\d]+$")
                s = str(cond)
                if pattern_eqlength.match(s):
                    points, _ = get_points_and_symbols(cond)
                    l, r = points[:2], points[2:]
                    for component in self.state.lengths.equivalence_classes().values():
                        clause = Or(clause, And(in_component(
                            l, component), in_component(r, component)))
                elif pattern_eqangle.match(s):
                    points, _ = get_points_and_symbols(cond)
                    l, r = points[:3], points[3:]
                    for component in self.state.angles.equivalence_classes().values():
                        clause = Or(clause, And(in_component(
                            l, component), in_component(r, component)))
                elif pattern_eqratio.match(s):
                    points, _ = get_points_and_symbols(cond)
                    a, b, c, d = points[:2], points[2:4], points[4:6], points[6:8]
                    for ratios in self.state.ratios.values():
                        l_clause = False
                        r_clause = False
                        for ratio in ratios:
                            _, symbols = get_points_and_symbols(ratio)
                            length1, length2 = symbols
                            length1, length2 = self.state.lengths.find(
                                length1), self.state.lengths.find(length2)
                            component1, component2 = self.state.lengths.equivalence_classes(
                            )[length1], self.state.lengths.equivalence_classes()[length2]
                            l_clause = Or(l_clause, And(in_component(
                                a, component1), in_component(b, component2)))
                            r_clause = Or(r_clause, And(in_component(
                                c, component1), in_component(d, component2)))
                        clause = Or(clause, And(l_clause, r_clause))
                elif pattern_angle_const.match(s):
                    points, _ = get_points_and_symbols(cond)
                    left = points[:3]
                    cnst = [arg for arg in cond.args if len(arg.free_symbols)==0][0]
                    cnst = abs(cnst)
                    for rep, component in self.state.angles.equivalence_classes().items():
                        if self.state.check_conditions(cnst - rep):
                            clause = in_component(left, component)
                            break
                else:
                    assert pattern_angle_sum.match(s)
                    cnst = [arg for arg in cond.args if len(
                        arg.free_symbols) == 0][0]
                    cnst = abs(cnst)
                    points, _ = get_points_and_symbols(cond)
                    left, right = points[:3], points[3:]
                    for rep, angle_sums in self.state.angle_sums.items():
                        if self.state.check_conditions(cnst-rep):
                            for angle_sum in angle_sums:
                                if isinstance(angle_sum, sympy.core.add.Add):
                                    angle1, angle2 = angle_sum.args
                                    # angle1 + angle2
                                else:
                                    angle1 = list(angle_sum.free_symbols)[0]
                                    angle2 = angle1
                                    # 2 * angle_1
                                angle1, angle2 = self.state.angles.find(angle1), self.state.angles.find(angle2)
                                try:
                                    component1, component2 = self.state.angles.equivalence_classes(
                                    )[angle1], self.state.angles.equivalence_classes()[angle2]
                                except:
                                    breakpoint()
                                    assert False
                                clause = Or(clause, And(in_component(
                                    left, component1), in_component(right, component2)))
                            break
            solver.add(clause)
        solutions = []
        assignments = []
        while solver.check() == z3.sat:
            m = solver.model()
            dic = {str(i): point_decoding[m[i]] for i in m}
            concrete = theorem(**dic)
            concrete._depth = self.state.current_depth
            # if try complex, solutions in later iterations may be weaker than previous ones and unionfind because of abondoning complex equations, causing check condition failure
            if not self.state.try_complex and not self.state.check_conditions(concrete.condition()):
                for condition in concrete.condition():
                    if not self.state.check_conditions(condition):
                        print(f"Failed condition: {condition}")
                        breakpoint()
                        self.state.check_conditions(condition)
                        assert False
            if not concrete.degenerate():
                assignments.append(concrete)
            solution = False
            for i in m:
                solution = Or((formal_points[str(i)] != m[i]), solution)
            solver.add(solution)
            solutions.append(solution)
        solver.pop()
        for item in solutions:
            solver.add(item)
        solver.push()
        assignments.sort(key=lambda x: str(x))
        return assignments

    applicable_theorems = []
    pbar = tqdm(theorems, disable=self.state.silent)
    for theorem in pbar:
        pbar.set_description(
            f"{theorem.__name__} #rels {len(self.state.relations)} # eqns {len(self.state.equations)}")
        concrete_theorems = search_assignments(theorem)
        applicable_theorems += concrete_theorems
    return applicable_theorems

run()

Execute one deductive phase over inner then outer theorems.

Updates closure when no further inferences are available.

Returns:

Type Description

None

Source code in pyeuclid/engine/deductive_database.py
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
def run(self):
    """Execute one deductive phase over inner then outer theorems.

    Updates `closure` when no further inferences are available.

    Returns:
        None
    """
    inner_closure = True
    while True:
        if self.state.complete() is not None:
            return
        inner_applicable = self.get_applicable_theorems(self.inner_theorems)
        self.apply(inner_applicable)
        if len(inner_applicable) == 0:
            break
        inner_closure = False

    if self.state.complete() is not None:
        return

    applicable_theorems = self.get_applicable_theorems(self.outer_theorems)
    self.apply(applicable_theorems)

    if len(applicable_theorems) == 0 and inner_closure:
        self.closure = True
        if not self.state.silent:
            self.state.logger.debug("Found Closure")
        return

Diagram

Source code in pyeuclid/formalization/diagram.py
  22
  23
  24
  25
  26
  27
  28
  29
  30
  31
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
class Diagram:    
    def __new__(cls, constructions_list:list[list[ConstructionRule]]=None, save_path=None, cache_folder=os.path.join(ROOT_DIR, 'cache'), resample=False):
        """Load from cache if available, otherwise construct a new diagram instance."""
        if not resample and cache_folder is not None:
            if not os.path.exists(cache_folder):
                os.makedirs(cache_folder)

            if constructions_list is not None:
                file_name = f"{hash_constructions_list(constructions_list)}.pkl"
                file_path = os.path.join(cache_folder, file_name)
                try:
                    if os.path.exists(file_path):
                        with open(file_path, 'rb') as f:
                            instance = pickle.load(f)
                            instance.save_path = save_path
                            instance.save_diagram()
                            return instance
                except:
                    pass

        instance = super().__new__(cls)
        return instance

    def __init__(self, constructions_list:list[list[ConstructionRule]]=None, save_path=None, cache_folder=os.path.join(ROOT_DIR, 'cache'), resample=False):
        if hasattr(self, 'cache_folder'):
            return

        self.points = []
        self.segments = []
        self.circles = []

        self.name2point = {}
        self.point2name = {}

        self.fig, self.ax = None, None

        self.constructions_list = constructions_list
        self.save_path = save_path
        self.cache_folder = cache_folder

        if constructions_list is not None:                
            self.construct_diagram()

    def clear(self):
        """Reset all stored points, segments, circles, and name mappings."""
        self.points.clear()
        self.segments.clear()
        self.circles.clear()

        self.name2point.clear()
        self.point2name.clear()

    def show(self):
        """Render the diagram with matplotlib."""
        self.draw_diagram(show=True)

    def save_to_cache(self):
        """Persist the diagram to cache if caching is enabled."""
        if self.cache_folder is not None:
            file_name = f"{hash_constructions_list(self.constructions_list)}.pkl"
            file_path = os.path.join(self.cache_folder, file_name)
            with open(file_path, 'wb') as f:
                pickle.dump(self, f)

    def add_constructions(self, constructions):
        """Add a new batch of constructions, retrying if degeneracy occurs."""
        for _ in range(MAX_DIAGRAM_ATTEMPTS):
            try:
                self.construct(constructions)
                self.constructions_list.append(constructions)
                return
            except:
                continue

        print(f"Failed to add the constructions after {MAX_DIAGRAM_ATTEMPTS} attempts.")
        raise Exception()

    def construct_diagram(self):
        """Construct the full diagram from all construction batches, with retries."""
        for _ in range(MAX_DIAGRAM_ATTEMPTS):
            try:
                self.clear()
                for constructions in self.constructions_list:
                    self.construct(constructions)
                self.draw_diagram()
                self.save_to_cache()
                return
            except:
                continue

        print(f"Failed to construct a diagram after {MAX_DIAGRAM_ATTEMPTS} attempts.")
        raise Exception()

    def construct(self, constructions: list[ConstructionRule]):
        """Apply a single batch of construction rules to extend the diagram."""
        constructed_points = constructions[0].constructed_points()
        if any(construction.constructed_points() != constructed_points for construction in constructions[1:]):
            raise Exception()

        to_be_intersected = []
        for construction in constructions:
            # print(construction.__class__.__name__ + '('+','.join([str(name) for name in construction.arguments()])+')')
            # for c in construction.conditions:
            #     if not self.numerical_check(c):
            #         raise Exception()

            to_be_intersected += self.sketch(construction)

        new_points = self.reduce(to_be_intersected, self.points)

        if check_too_close(new_points, self.points):
            raise Exception()

        if check_too_far(new_points, self.points):
            raise Exception()

        self.points += new_points

        for p, np in zip(constructed_points, new_points):
            self.name2point[p.name] = np
            self.point2name[np] = p.name

        for construction in constructions:
            self.draw(new_points, construction)

    def numerical_check_goal(self, goal):
        """Check if the current diagram satisfies a goal relation/expression."""
        if isinstance(goal, tuple):
            for g in goal:
                if self.numerical_check(g):
                    return True, g
        else:
            if self.numerical_check(goal):
                return True, goal
        return False, goal

    def numerical_check(self, relation):
        """Numerically evaluate whether a relation/expression holds in the diagram."""
        if isinstance(relation, Relation):
            func = globals()['check_' + relation.__class__.__name__.lower()]
            args = [self.name2point[p.name] for p in relation.get_points()]
            return func(args)
        else:
            symbol_to_value = {}
            symbols, symbol_names = parse_expression(relation)

            for angle_symbol, angle_name in zip(symbols['Angle'], symbol_names['Angle']):
                angle_value = calculate_angle(*[self.name2point[n] for n in angle_name])
                symbol_to_value[angle_symbol] = angle_value

            for length_symbol, length_name in zip(symbols['Length'], symbol_names['Length']):
                length_value = calculate_length(*[self.name2point[n] for n in length_name])
                symbol_to_value[length_symbol] = length_value

            evaluated_expr = relation.subs(symbol_to_value)
            if close_enough(float(evaluated_expr.evalf()), 0):
                return True
            else:
                return False

    def sketch(self, construction):
        func = getattr(self, 'sketch_' + construction.__class__.__name__[10:])
        args = [arg if isinstance(arg, float) else self.name2point[arg.name] for arg in construction.arguments()]
        result = func(*args)
        if isinstance(result, list):
            return result
        else:
            return [result]

    def sketch_angle_bisector(self, *args: list[Point]) -> Ray:
        """Ray that bisects angle ABC."""
        a, b, c = args
        dist_ab = a.distance(b)
        dist_bc = b.distance(c)
        x = b + (c - b) * (dist_ab / dist_bc)
        m = (a + x) * 0.5
        return Ray(b, m)

    def sketch_angle_mirror(self, *args: list[Point]) -> Ray:
        """Mirror of ray BA across BC."""
        a, b, c = args
        ab = a - b
        cb = c - b

        dist_ab = a.distance(b)
        ang_ab = np.arctan2(ab.y / dist_ab, ab.x / dist_ab)
        dist_cb = c.distance(b)
        ang_bc = np.arctan2(cb.y / dist_cb, cb.x / dist_cb)

        ang_bx = 2 * ang_bc - ang_ab
        x = b + Point(np.cos(ang_bx), np.sin(ang_bx))
        return Ray(b, x)

    def sketch_circle(self, *args: list[Point]) -> Point:
        """Center of circle through three points."""
        a, b, c = args
        l1 = perpendicular_bisector(a, b)
        l2 = perpendicular_bisector(b, c)
        x = line_line_intersection(l1, l2)
        return x

    def sketch_circumcenter(self, *args: list[Point]) -> Point:
        """Circumcenter of triangle ABC."""
        a, b, c = args
        l1 = perpendicular_bisector(a, b)
        l2 = perpendicular_bisector(b, c)
        x = line_line_intersection(l1, l2)
        return x

    def sketch_eq_quadrangle(self, *args: list[Point]) -> list[Point]:
        """Randomly sample a quadrilateral with opposite sides equal."""
        a = Point(0.0, 0.0)
        b = Point(1.0, 0.0)

        length = np.random.uniform(0.5, 2.0)
        ang = np.random.uniform(np.pi / 3, np.pi * 2 / 3)
        d = head_from(a, ang, length)

        ang = ang_of(b, d)
        ang = np.random.uniform(ang / 10, ang / 9)
        c = head_from(b, ang, length)
        a, b, c, d = random_rfss(a, b, c, d)
        return [a, b, c, d]

    def sketch_eq_trapezoid(self, *args: list[Point]) -> list[Point]:
        """Randomly sample an isosceles trapezoid."""
        a = Point(0.0, 0.0)
        b = Point(1.0, 0.0)
        l = unif(0.5, 2.0)

        height = unif(0.5, 2.0)
        c = Point(0.5 + l / 2.0, height)
        d = Point(0.5 - l / 2.0, height)

        a, b, c, d = random_rfss(a, b, c, d)
        return [a, b, c, d]

    def sketch_eq_triangle(self, *args: list[Point]) -> list[Circle]:
        """Circles defining an equilateral triangle on BC."""
        b, c = args
        return [Circle(center=b, radius=b.distance(c)), Circle(center=c, radius=b.distance(c))]

    def sketch_eqangle2(self, *args: list[Point]) -> Point:
        """Point X such that angle ABX equals angle XCB."""
        a, b, c = args

        ba = b.distance(a)
        bc = b.distance(c)
        l = ba * ba / bc

        if unif(0.0, 1.0) < 0.5:
            be = min(l, bc)
            be = unif(be * 0.1, be * 0.9)
        else:
            be = max(l, bc)
            be = unif(be * 1.1, be * 1.5)

        e = b + (c - b) * (be / bc)
        y = b + (a - b) * (be / l)
        return line_line_intersection(Line(c, y), Line(a, e))

    def sketch_eqdia_quadrangle(self, *args) -> list[Point]:
        """Quadrilateral with equal diagonals."""
        m = unif(0.3, 0.7)
        n = unif(0.3, 0.7)
        a = Point(-m, 0.0)
        c = Point(1 - m, 0.0)
        b = Point(0.0, -n)
        d = Point(0.0, 1 - n)

        ang = unif(-0.25 * np.pi, 0.25 * np.pi)
        sin, cos = np.sin(ang), np.cos(ang)
        b = b.rotate(sin, cos)
        d = d.rotate(sin, cos)
        a, b, c, d = random_rfss(a, b, c, d)
        return [a, b, c, d]

    def sketch_eqdistance(self, *args) -> Circle:
        """Circle centered at A with radius BC."""
        a, b, c = args
        return Circle(center=a, radius=b.distance(c))

    def sketch_eqdistance2(self, *args) -> Circle:
        """Circle centered at A with radius alpha*BC."""
        a, b, c, alpha = args
        return Circle(center=a, radius=alpha*b.distance(c))

    def sketch_eqdistance3(self, *args) -> Circle:
        """Circle centered at A with fixed radius alpha."""
        a, alpha = args
        return Circle(center=a, radius=alpha)

    def sketch_foot(self, *args) -> Point:
        """Foot of perpendicular from A to line BC."""
        a, b, c = args
        line_bc = Line(b, c)
        tline = a.perpendicular_line(line_bc)
        return line_line_intersection(tline, line_bc)

    def sketch_free(self, *args) -> Point:
        """Free point uniformly sampled in a box."""
        return Point(unif(-1, 1), unif(-1, 1))

    def sketch_incenter(self, *args) -> Point:
        """Incenter of triangle ABC."""
        a, b, c = args
        l1 = self.sketch_angle_bisector(a, b, c)
        l2 = self.sketch_angle_bisector(b, c, a)
        return line_line_intersection(l1, l2)

    def sketch_incenter2(self, *args) -> list[Point]:
        """Incenter plus touch points on each side."""
        a, b, c = args
        i = self.sketch_incenter(a, b, c)
        x = i.foot(Line(b, c))
        y = i.foot(Line(c, a))
        z = i.foot(Line(a, b))
        return [x, y, z, i]

    def sketch_excenter(self, *args) -> Point:
        """Excenter opposite B in triangle ABC."""
        a, b, c = args
        l1 = self.sketch_angle_bisector(b, a, c)
        l2 = self.sketch_angle_bisector(a, b, c).perpendicular_line(b)
        return line_line_intersection(l1, l2)

    def sketch_excenter2(self, *args) -> list[Point]:
        """Excenter plus touch points on extended sides."""
        a, b, c = args
        i = self.sketch_excenter(a, b, c)
        x = i.foot(Line(b, c))
        y = i.foot(Line(c, a))
        z = i.foot(Line(a, b))
        return [x, y, z, i]

    def sketch_centroid(self, *args) -> list[Point]:
        """Mid-segment points and centroid of triangle ABC."""
        a, b, c = args
        x = (b + c) * 0.5
        y = (c + a) * 0.5
        z = (a + b) * 0.5
        i = line_line_intersection(Line(a, x), Line(b, y))
        return [x, y, z, i]

    def sketch_intersection_cc(self, *args) -> list[Circle]:
        """Two circles centered at O and W through A."""
        o, w, a = args
        return [Circle(center=o, radius=o.distance(a)), Circle(center=w, radius=w.distance(a))]

    def sketch_intersection_lc(self, *args) -> list:
        """Line and circle defined by A,O,B for intersection."""
        a, o, b = args
        return [Line(b, a), Circle(center=o, radius=o.distance(b))]

    def sketch_intersection_ll(self, *args) -> Point:
        """Intersection of lines AB and CD."""
        a, b, c, d = args
        l1 = Line(a, b)
        l2 = Line(c, d)
        return line_line_intersection(l1, l2)

    def sketch_intersection_lp(self, *args) -> Point:
        a, b, c, m, n = args
        l1 = Line(a,b)
        l2 = self.sketch_on_pline(c, m, n)
        return line_line_intersection(l1, l2)

    def sketch_intersection_lt(self, *args) -> Point:
        a, b, c, d, e = args
        l1 = Line(a, b)
        l2 = self.sketch_on_tline(c, d, e)
        return line_line_intersection(l1, l2)

    def sketch_intersection_pp(self, *args) -> Point:
        a, b, c, d, e, f = args
        l1 = self.sketch_on_pline(a, b, c)
        l2 = self.sketch_on_pline(d, e, f)
        return line_line_intersection(l1, l2)

    def sketch_intersection_tt(self, *args) -> Point:
        a, b, c, d, e, f = args
        l1 = self.sketch_on_tline(a, b, c)
        l2 = self.sketch_on_tline(d, e, f)
        return line_line_intersection(l1, l2)

    def sketch_iso_triangle(self, *args) -> list[Point]:
        base = unif(0.5, 1.5)
        height = unif(0.5, 1.5)

        b = Point(-base / 2, 0.0)
        c = Point(base / 2, 0.0)
        a = Point(0.0, height)
        a, b, c = random_rfss(a, b, c)
        return [a, b, c]

    def sketch_lc_tangent(self, *args) -> Line:
        a, o = args
        return self.sketch_on_tline(a, a, o)

    def sketch_midpoint(self, *args) -> Point:
        a, b = args
        return (a + b) * 0.5

    def sketch_mirror(self, *args) -> Point:
        a, b = args
        return b * 2 - a

    def sketch_nsquare(self, *args) -> Point:
        a, b = args
        ang = -np.pi / 2
        return a + (b - a).rotate(np.sin(ang), np.cos(ang))

    def sketch_on_aline(self, *args) -> Line:
        e, d, c, b, a = args
        ab = a - b
        cb = c - b
        de = d - e

        dab = a.distance(b)
        ang_ab = np.arctan2(ab.y / dab, ab.x / dab)

        dcb = c.distance(b)
        ang_bc = np.arctan2(cb.y / dcb, cb.x / dcb)

        dde = d.distance(e)
        ang_de = np.arctan2(de.y / dde, de.x / dde)

        ang_ex = ang_de + ang_bc - ang_ab
        x = e + Point(np.cos(ang_ex), np.sin(ang_ex))
        return Ray(e, x)

    def sketch_on_bline(self, *args) -> Line:
        a, b = args
        m = (a + b) * 0.5
        return m.perpendicular_line(Line(a, b))

    def sketch_on_circle(self, *args) -> Circle:
        o, a = args
        return Circle(o, o.distance(a))

    def sketch_on_line(self, *args) -> Line:
        a, b = args
        return Line(a, b)

    def sketch_on_pline(self, *args) -> Line:
        a, b, c = args
        return a.parallel_line(Line(b, c))

    def sketch_on_tline(self, *args) -> Line:
        a, b, c = args
        return a.perpendicular_line(Line(b, c))

    def sketch_orthocenter(self, *args) -> Point:
        a, b, c = args
        l1 = self.sketch_on_tline(a, b, c)
        l2 = self.sketch_on_tline(b, c, a)
        return line_line_intersection(l1, l2)

    def sketch_parallelogram(self, *args) -> Point:
        a, b, c = args
        l1 = self.sketch_on_pline(a, b, c)
        l2 = self.sketch_on_pline(c, a, b)
        return line_line_intersection(l1, l2)

    def sketch_pentagon(self, *args) -> list[Point]:
        points = [Point(1.0, 0.0)]
        ang = 0.0

        for i in range(4):
            ang += (2 * np.pi - ang) / (5 - i) * unif(0.5, 1.5)
            point = Point(np.cos(ang), np.sin(ang))
            points.append(point)

        a, b, c, d, e = points  # pylint: disable=unbalanced-tuple-unpacking
        a, b, c, d, e = random_rfss(a, b, c, d, e)
        return [a, b, c, d, e]

    def sketch_psquare(self, *args) -> Point:
        a, b = args
        ang = np.pi / 2
        return a + (b - a).rotate(np.sin(ang), np.cos(ang))

    def sketch_quadrangle(self, *args) -> list[Point]:
        a = Point(0.0, 0.0)
        b = Point(1.0, 0.0)

        length = np.random.uniform(0.5, 2.0)
        ang = np.random.uniform(np.pi / 3, np.pi * 2 / 3)
        d = head_from(a, ang, length)

        ang = ang_of(b, d)
        ang = np.random.uniform(ang / 10, ang / 9)
        c = head_from(b, ang, length)
        a, b, c, d = random_rfss(a, b, c, d)
        return [a, b, c, d]

    def sketch_r_trapezoid(self, *args) -> list[Point]:
        """Right trapezoid with AB horizontal and AD vertical."""
        a = Point(0.0, 1.0)
        d = Point(0.0, 0.0)
        b = Point(unif(0.5, 1.5), 1.0)
        c = Point(unif(0.5, 1.5), 0.0)
        a, b, c, d = random_rfss(a, b, c, d)
        return [a, b, c, d]

    def sketch_r_triangle(self, *args) -> list[Point]:
        """Random right triangle with legs on axes."""
        a = Point(0.0, 0.0)
        b = Point(0.0, unif(0.5, 2.0))
        c = Point(unif(0.5, 2.0), 0.0)
        a, b, c = random_rfss(a, b, c)
        return [a, b, c]

    def sketch_rectangle(self, *args) -> list[Point]:
        """Axis-aligned rectangle with random width/height."""
        a = Point(0.0, 0.0)
        b = Point(0.0, 1.0)
        l = unif(0.5, 2.0)
        c = Point(l, 1.0)
        d = Point(l, 0.0)
        a, b, c, d = random_rfss(a, b, c, d)
        return [a, b, c, d]

    def sketch_reflect(self, *args) -> Point:
        """Reflect point A across line BC."""
        a, b, c = args
        m = a.foot(Line(b, c))
        return m * 2 - a

    def sketch_risos(self, *args) -> list[Point]:
        """Right isosceles triangle."""
        a = Point(0.0, 0.0)
        b = Point(0.0, 1.0)
        c = Point(1.0, 0.0)
        a, b, c = random_rfss(a, b, c)
        return [a, b, c]

    def sketch_s_angle(self, *args) -> Ray:
        """Ray at point B making angle alpha with BA."""
        a, b, alpha = args
        ang = alpha / 180 * np.pi
        x = b + (a - b).rotatea(ang)
        return Ray(b, x)

    def sketch_segment(self, *args) -> list[Point]:
        """Random segment endpoints in [-1,1] box."""
        a = Point(unif(-1, 1), unif(-1, 1))
        b = Point(unif(-1, 1), unif(-1, 1))
        return [a, b]

    def sketch_shift(self, *args) -> Point:
        """Translate C by vector BA."""
        c, b, a = args
        return c + (b - a)

    def sketch_square(self, *args) -> list[Point]:
        """Square constructed on segment AB."""
        a, b = args
        c = b + (a - b).rotatea(-np.pi / 2)
        d = a + (b - a).rotatea(np.pi / 2)
        return [c, d]

    def sketch_isquare(self, *args) -> list[Point]:
        """Axis-aligned unit square, randomly re-ordered."""
        a = Point(0.0, 0.0)
        b = Point(1.0, 0.0)
        c = Point(1.0, 1.0)
        d = Point(0.0, 1.0)
        a, b, c, d = random_rfss(a, b, c, d)
        return [a, b, c, d]

    def sketch_trapezoid(self, *args) -> list[Point]:
        """Random trapezoid with AB // CD."""
        d = Point(0.0, 0.0)
        c = Point(1.0, 0.0)

        base = unif(0.5, 2.0)
        height = unif(0.5, 2.0)
        a = Point(unif(0.2, 0.5), height)
        b = Point(a.x + base, height)
        a, b, c, d = random_rfss(a, b, c, d)
        return [a, b, c, d]

    def sketch_triangle(self, *args) -> list[Point]:
        """Random triangle."""
        a = Point(0.0, 0.0)
        b = Point(1.0, 0.0)
        ac = unif(0.5, 2.0)
        ang = unif(0.2, 0.8) * np.pi
        c = head_from(a, ang, ac)
        return [a, b, c]

    def sketch_triangle12(self, *args) -> list[Point]:
        """Triangle with side-length ratios near 1:2."""
        b = Point(0.0, 0.0)
        c = Point(unif(1.5, 2.5), 0.0)
        a, _ = circle_circle_intersection(Circle(b, 1.0), Circle(c, 2.0))
        a, b, c = random_rfss(a, b, c)
        return [a, b, c]

    def sketch_2l1c(self, *args) -> list[Point]:
        """Intersections of perpendiculars from P to AC/BC with circle centered at P."""
        a, b, c, p = args
        bc, ac = Line(b, c), Line(a, c)
        circle = Circle(p, p.distance(a))

        d, d_ = line_circle_intersection(p.perpendicular_line(bc), circle)
        if bc.diff_side(d_, a):
            d = d_

        e, e_ = line_circle_intersection(p.perpendicular_line(ac), circle)
        if ac.diff_side(e_, b):
            e = e_

        df = d.perpendicular_line(Line(p, d))
        ef = e.perpendicular_line(Line(p, e))
        f = line_line_intersection(df, ef)

        g, g_ = line_circle_intersection(Line(c, f), circle)
        if bc.same_side(g_, a):
            g = g_

        b_ = c + (b - c) / b.distance(c)
        a_ = c + (a - c) / a.distance(c)
        m = (a_ + b_) * 0.5
        x = line_line_intersection(Line(c, m), Line(p, g))
        return [x.foot(ac), x.foot(bc), g, x]

    def sketch_e5128(self, *args) -> list[Point]:
        """Problem-specific construction e5128."""
        a, b, c, d = args
        g = (a + b) * 0.5
        de = Line(d, g)

        e, f = line_circle_intersection(de, Circle(c, c.distance(b)))

        if e.distance(d) < f.distance(d):
            e = f
        return [e, g]

    def sketch_3peq(self, *args) -> list[Point]:
        """Three-point equidistance construction."""
        a, b, c = args
        ab, bc, ca = Line(a, b), Line(b, c), Line(c, a)

        z = b + (c - b) * np.random.uniform(-0.5, 1.5)

        z_ = z * 2 - c
        l = z_.parallel_line(ca)
        x = line_line_intersection(l, ab)
        y = z * 2 - x
        return [x, y, z]

    def sketch_trisect(self, *args) -> list[Point]:
        """Trisect angle ABC."""
        a, b, c = args
        ang1 = ang_of(b, a)
        ang2 = ang_of(b, c)

        swap = 0
        if ang1 > ang2:
            ang1, ang2 = ang2, ang1
            swap += 1

        if ang2 - ang1 > np.pi:
            ang1, ang2 = ang2, ang1 + 2 * np.pi
            swap += 1

        angx = ang1 + (ang2 - ang1) / 3
        angy = ang2 - (ang2 - ang1) / 3

        x = b + Point(np.cos(angx), np.sin(angx))
        y = b + Point(np.cos(angy), np.sin(angy))

        ac = Line(a, c)
        x = line_line_intersection(Line(b, x), ac)
        y = line_line_intersection(Line(b, y), ac)

        if swap == 1:
            return [y, x]
        return [x, y]

    def sketch_trisegment(self, *args) -> list[Point]:
        """Trisect segment AB."""
        a, b = args
        x, y = a + (b - a) * (1.0 / 3), a + (b - a) * (2.0 / 3)
        return [x, y]

    def sketch_on_dia(self, *args) -> Circle:
        """Circle with diameter AB."""
        a, b = args
        o = (a + b) * 0.5
        return Circle(o, o.distance(a))

    def sketch_ieq_triangle(self, *args) -> list[Point]:
        a = Point(0.0, 0.0)
        b = Point(1.0, 0.0)

        c, _ = Circle(a, a.distance(b)).intersect(Circle(b, b.distance(a)))
        return [a, b, c]

    def sketch_on_opline(self, *args) -> Ray:
        a, b = args
        return Ray(a, a + a - b)

    def sketch_cc_tangent(self, *args) -> list[Point]:
        o, a, w, b = args
        ra, rb = o.distance(a), w.distance(b)

        ow = Line(o, w)
        if close_enough(ra, rb):
            oo = ow.perpendicular_line(o)
            oa = Circle(o, ra)
            x, z = line_circle_intersection(oo, oa)
            y = x + w - o
            t = z + w - o
            return [x, y, z, t]

    def sketch_cc_tangent0(self, *args) -> Ray:
        o, a, w, b = args
        return self.sketch_cc_tangent(o, a, w, b)[:2]

    def sketch_eqangle3(self, *args) -> list[Point]:
        a, b, d, e, f = args
        de = d.distance(e)
        ef = e.distance(f)
        ab = b.distance(a)
        ang_ax = ang_of(a, b) + ang_between(e, d, f)
        x = head_from(a, ang_ax, length=de / ef * ab)   
        o = self.sketch_circle(a, b, x)
        return Circle(o, o.distance(a))

    def sketch_tangent(self, *args) -> list[Point]:
        a, o, b = args
        dia = self.sketch_dia([a, o])
        return list(circle_circle_intersection(Circle(o, o.distance(b)), dia))

    def sketch_on_circum(self, *args) -> Circle:
        a, b, c = args
        o = self.sketch_circle(a, b, c)
        return Circle(o, o.distance(a))

    def sketch_sameside(self, *args) -> HalfPlane:
        a, b, c = args
        return HalfPlane(a, b, c)

    def sketch_opposingsides(self, *args) -> HalfPlane:
        a, b, c = args
        return HalfPlane(a, b, c, opposingsides=True)

    def reduce(self, objs, existing_points) -> list[Point]:
        """Reduce intersecting objects into sampled intersection points.

        Filters half-planes, handles point-only cases, samples within half-planes,
        or intersects pairs of essential geometric objects.
        """
        essential_objs = [i for i in objs if not isinstance(i, HalfPlane)]
        halfplane_objs = [i for i in objs if isinstance(i, HalfPlane)]

        if all(isinstance(o, Point) for o in objs):
            return objs

        elif all(isinstance(o, HalfPlane) for o in objs):
            if len(objs) == 1:
                return objs[0].sample_within_halfplanes(existing_points,[])
            else:
                return objs[0].sample_within_halfplanes(existing_points,objs[1:])

        elif len(essential_objs) == 1:
            if not halfplane_objs:
                return objs[0].sample_within(existing_points)
            else:
                return objs[0].sample_within_halfplanes(existing_points,halfplane_objs)

        elif len(essential_objs) == 2:
            a, b = essential_objs
            result = a.intersect(b)

            if isinstance(result, Point):
                if halfplane_objs and not all(i.contains(result) for i in halfplane_objs):
                    raise Exception()
                return [result]

            a, b = result

            if halfplane_objs:
                a_correct_side = all(i.contains(a) for i in halfplane_objs)
                b_correct_side = all(i.contains(b) for i in halfplane_objs)

                if a_correct_side and not b_correct_side:
                    return [a]
                elif b_correct_side and not a_correct_side:
                    return [b]
                elif not a_correct_side and not b_correct_side:
                    raise Exception()

            a_close = any([a.close(x) for x in existing_points])
            b_close = any([b.close(x) for x in existing_points])

            if a_close and not b_close:
                return [b]

            elif b_close and not a_close:
                return [a]
            else:
                return [np.random.choice([a, b])]

    def draw(self, new_points, construction):
        func = getattr(self, 'draw_' + construction.__class__.__name__[10:])
        args = [arg if isinstance(arg, float) else self.name2point[arg.name] for arg in construction.arguments()]
        func(*new_points, *args)

    def draw_angle_bisector(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(b, x))

    def draw_angle_mirror(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(b, x))

    def draw_circle(self, *args):
        x, a, b, c = args
        # self.segments.append(Segment(a, x))
        # self.segments.append(Segment(b, x))
        # self.segments.append(Segment(c, x))
        self.circles.append(Circle(x, x.distance(a)))

    def draw_circumcenter(self, *args):
        x, a, b, c = args
        # self.segments.append(Segment(a, x))
        # self.segments.append(Segment(b, x))
        # self.segments.append(Segment(c, x))
        self.circles.append(Circle(x, x.distance(a)))

    def draw_eq_quadrangle(self, *args):
        a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, a))

    def draw_eq_trapezoid(self, *args):
        a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, a))

    def draw_eq_triangle(self, *args):
        x, b, c = args
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, x))
        self.segments.append(Segment(x, b))

    def draw_eqangle2(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(b, a))
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(c, b))
        self.segments.append(Segment(b, b))

    def draw_eqdia_quadrangle(self, *args):
        a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, a))
        self.segments.append(Segment(b, d))
        self.segments.append(Segment(a, c))

    def draw_eqdistance(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(b, c))

    def draw_eqdistance2(self, *args):
        x, a, b, c, alpha = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(b, c))

    def draw_eqdistance2(self, *args):
        x, a, alpha = args
        self.segments.append(Segment(x, a))

    def draw_foot(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(x, b))
        self.segments.append(Segment(x, c))
        self.segments.append(Segment(b, c))

    def draw_free(self, *args):
        x = args

    def draw_incenter(self, *args):
        i, a, b, c = args
        x = i.foot(Line(b, c))
        y = i.foot(Line(c, a))
        z = i.foot(Line(a, b))
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))
        self.circles.append(Circle(p1=x, p2=y, p3=z))

    def draw_incenter2(self, *args):
        x, y, z, i, a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))
        self.circles.append(Circle(p1=x, p2=y, p3=z))

    def draw_excenter(self, *args):
        i, a, b, c = args
        x = i.foot(Line(b, c))
        y = i.foot(Line(c, a))
        z = i.foot(Line(a, b))
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))
        self.circles.append(Circle(p1=x, p2=y, p3=z))

    def draw_excenter2(self, *args):
        x, y, z, i, a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))
        self.circles.append(Circle(p1=x, p2=y, p3=z))

    def draw_centroid(self, *args):
        x, y, z, i, a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, y))
        self.segments.append(Segment(c, z))

    def draw_intersection_cc(self, *args):
        x, o, w, a = args
        self.segments.append(Segment(o, a))
        self.segments.append(Segment(o, x))
        self.segments.append(Segment(w, a))
        self.segments.append(Segment(w, x))
        self.circles.append(Circle(o, o.distance(a)))
        self.circles.append(Circle(w, w.distance(a)))

    def draw_intersection_lc(self, *args):
        x, a, o, b = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(x, b))
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(o, b))
        self.segments.append(Segment(o, x))
        self.circles.append(Circle(o, o.distance(b)))

    def draw_intersection_ll(self, *args):
        x, a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, x))
        self.segments.append(Segment(c, x))
        self.segments.append(Segment(d, x))

    def draw_intersection_lp(self, *args):
        x, a, b, c, m, n = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(m, n))
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, x))
        self.segments.append(Segment(c, x))

    def draw_intersection_lp(self, *args):
        x, a, b, c, m, n = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(m, n))
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, x))
        self.segments.append(Segment(c, x))

    def draw_intersection_lt(self, *args):
        x, a, b, c, d, e = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(d, e))
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, x))
        self.segments.append(Segment(c, x))

    def draw_intersection_pp(self, *args):
        x, a, b, c, d, e, f = args
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(d, x))
        self.segments.append(Segment(e, f))

    def draw_intersection_tt(self, *args):
        x, a, b, c, d, e, f = args
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(d, x))
        self.segments.append(Segment(e, f))

    def draw_iso_triangle(self, *args):
        a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))

    def draw_lc_tangent(self, *args):
        x, a, o = args
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(a, o))
        self.circles.append(Circle(o, o.distance(a)))

    def draw_midpoint(self, *args):
        x, a, b = args
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, x))

    def draw_mirror(self, *args):
        x, a, b = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, x))

    def draw_nsquare(self, *args):
        x, a, b = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, x))

    def draw_on_aline(self, *args):
        x, a, b, c, d, e = args
        self.segments.append(Segment(e, d))
        self.segments.append(Segment(d, c))
        self.segments.append(Segment(b, a))
        self.segments.append(Segment(a, x))

    def draw_on_bline(self, *args):
        x, a, b = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, x))

    def draw_on_circle(self, *args):
        x, o, a = args
        self.segments.append(Segment(o, a))
        self.segments.append(Segment(o, x))
        self.circles.append(Circle(o, o.distance(x)))

    def draw_on_line(self, *args):
        x, a, b = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(x, b))
        self.segments.append(Segment(a, b))

    def draw_on_pline(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(b, c))

    def draw_on_tline(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(b, c))

    def draw_orthocenter(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))

    def draw_parallelogram(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, x))

    def draw_parallelogram(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, x))

    def draw_pentagon(self, *args):
        a, b, c, d, e = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, e))
        self.segments.append(Segment(e, a))

    def draw_psquare(self, *args):
        x, a, b = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(a, b))

    def draw_quadrangle(self, *args):
        a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, a))

    def draw_r_trapezoid(self, *args):
        a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, a))

    def draw_r_triangle(self, *args):
        a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))

    def draw_rectangle(self, *args):
        a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, a))

    def draw_reflect(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(b, c))

    def draw_risos(self, *args):
        a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))

    def draw_s_angle(self, *args):
        x, a, b, alpha = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, x))

    def draw_segment(self, *args):
        a, b = args
        self.segments.append(Segment(a, b))

    def draw_s_segment(self, *args):
        a, b, alpha = args
        self.segments.append(Segment(a, b))

    def draw_shift(self, *args):
        x, b, c, d = args
        self.segments.append(Segment(x, b))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(x, c))
        self.segments.append(Segment(b, d))

    def draw_square(self, *args):
        x, y, a, b = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, x))
        self.segments.append(Segment(x, y))
        self.segments.append(Segment(y, a))

    def draw_isquare(self, *args):
        a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, a))

    def draw_trapezoid(self, *args):
        a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, a))

    def draw_triangle(self, *args):
        a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))

    def draw_triangle12(self, *args):
        a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))

    def draw_2l1c(self, *args):
        x, y, z, i, a, b, c, o = args
        self.segments.append(Segment(a, c))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(a, o))
        self.segments.append(Segment(b, o))

        self.segments.append(Segment(i, x))
        self.segments.append(Segment(i, y))
        self.segments.append(Segment(i, z))

        self.segments.append(Segment(c, x))
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(c, y))
        self.segments.append(Segment(b, y))
        self.segments.append(Segment(o, z))

        self.circles.append(Circle(i, i.distance(x)))
        self.circles.append(Circle(o, o.distance(a)))

    def draw_e5128(self, *args):
        x, y, a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, a))

        self.segments.append(Segment(a, x))
        self.segments.append(Segment(x, y))
        self.segments.append(Segment(c, x))

    def draw_3peq(self, *args):
        x, y, z, a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))

        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, x))

        self.segments.append(Segment(a, y))
        self.segments.append(Segment(c, y))

        self.segments.append(Segment(c, z))
        self.segments.append(Segment(b, z))

        self.segments.append(Segment(x, y))
        self.segments.append(Segment(y, z))
        self.segments.append(Segment(z, x))

    def draw_trisect(self, *args):
        x, y, a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))

        self.segments.append(Segment(b, x))
        self.segments.append(Segment(b, y))

        self.segments.append(Segment(a, x))
        self.segments.append(Segment(x, y))
        self.segments.append(Segment(y, c))

    def draw_trisegment(self, *args):
        x, y, a, b = args
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(x, y))
        self.segments.append(Segment(y, b))

    def draw_on_dia(self, *args):
        x, a, b = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(x, b))

    def draw_ieq_triangle(self, *args):
        a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))

    def draw_on_opline(self, *args):
        x, a, b = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(x, b))

    def draw_cc_tangent0(self, *args):
        x, y, o, a, w, b = args
        self.segments.append(Segment(o, a))
        self.segments.append(Segment(o, x))

        self.segments.append(Segment(w, b))
        self.segments.append(Segment(w, y))

        self.segments.append(Segment(x, y))

        self.circles.append(Circle(o, o.distance(a)))
        self.circles.append(Circle(w, w.distance(b)))

    def draw_cc_tangent(self, *args):
        x, y, z, i, o, a, w, b = args
        self.segments.append(Segment(o, a))
        self.segments.append(Segment(o, x))
        self.segments.append(Segment(o, z))

        self.segments.append(Segment(w, b))
        self.segments.append(Segment(w, y))
        self.segments.append(Segment(w, i))

        self.segments.append(Segment(x, y))
        self.segments.append(Segment(z, i))

        self.circles.append(Circle(o, o.distance(a)))
        self.circles.append(Circle(w, w.distance(b)))

    def draw_eqangle3(self, *args):
        x, a, b, d, e, f = args
        self.segments.append(Segment(f, d))
        self.segments.append(Segment(d, e))

        self.segments.append(Segment(a, x))
        self.segments.append(Segment(x, b))

    def draw_tangent(self, *args):
        x, y, a, o, b = args
        self.segments.append(Segment(o, b))
        self.segments.append(Segment(o, x))
        self.segments.append(Segment(o, y))
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(a, y))

        self.circles.append(Circle(o, o.distance(b)))

    def draw_on_circum(self, *args):
        x, a, b, c = args
        self.circles.append(Circle(p1=a, p2=b, p3=c))

    def draw_sameside(self, *args):
        x, a, b, c = args

    def draw_opposingsides(self, *args):
        x, a, b, c = args

    def draw_diagram(self, show=False):
        """Draw the current diagram; optionally display the matplotlib figure."""
        imsize = 512 / 100
        self.fig, self.ax = plt.subplots(figsize=(imsize, imsize), dpi=300)
        self.ax.set_facecolor((1.0, 1.0, 1.0))

        for segment in self.segments:
            p1, p2 = segment.p1, segment.p2
            lx, ly = (p1.x, p2.x), (p1.y, p2.y)
            self.ax.plot(lx, ly, color='black', lw=1.2, alpha=0.8, ls='-')

        for circle in self.circles:
            self.ax.add_patch(
                plt.Circle(
                    (circle.center.x, circle.center.y),
                    circle.radius,
                    color='red',
                    alpha=0.8,
                    fill=False,
                    lw=1.2,
                    ls='-'
                )
            )

        for p in self.points:
            self.ax.scatter(p.x, p.y, color='black', s=15)
            self.ax.annotate(self.point2name[p], (p.x+0.015, p.y+0.015), color='black', fontsize=8)

        self.ax.set_aspect('equal')
        self.ax.set_axis_off()
        self.fig.subplots_adjust(left=0.05, right=0.95, top=0.95, bottom=0.05, wspace=0, hspace=0)
        xmin = min([p.x for p in self.points])
        xmax = max([p.x for p in self.points])
        ymin = min([p.y for p in self.points])
        ymax = max([p.y for p in self.points])
        x_margin = (xmax - xmin) * 0.1
        y_margin = (ymax - ymin) * 0.1

        self.ax.margins(x_margin, y_margin)

        self.save_diagram()

        if show:
            plt.show()

        plt.close(self.fig)

    def save_diagram(self):
        if self.save_path is not None:
            parent_dir = os.path.dirname(self.save_path)
            if parent_dir and not os.path.exists(parent_dir):
                os.makedirs(parent_dir)
            self.fig.savefig(self.save_path)

__new__(constructions_list=None, save_path=None, cache_folder=os.path.join(ROOT_DIR, 'cache'), resample=False)

Load from cache if available, otherwise construct a new diagram instance.

Source code in pyeuclid/formalization/diagram.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def __new__(cls, constructions_list:list[list[ConstructionRule]]=None, save_path=None, cache_folder=os.path.join(ROOT_DIR, 'cache'), resample=False):
    """Load from cache if available, otherwise construct a new diagram instance."""
    if not resample and cache_folder is not None:
        if not os.path.exists(cache_folder):
            os.makedirs(cache_folder)

        if constructions_list is not None:
            file_name = f"{hash_constructions_list(constructions_list)}.pkl"
            file_path = os.path.join(cache_folder, file_name)
            try:
                if os.path.exists(file_path):
                    with open(file_path, 'rb') as f:
                        instance = pickle.load(f)
                        instance.save_path = save_path
                        instance.save_diagram()
                        return instance
            except:
                pass

    instance = super().__new__(cls)
    return instance

add_constructions(constructions)

Add a new batch of constructions, retrying if degeneracy occurs.

Source code in pyeuclid/formalization/diagram.py
86
87
88
89
90
91
92
93
94
95
96
97
def add_constructions(self, constructions):
    """Add a new batch of constructions, retrying if degeneracy occurs."""
    for _ in range(MAX_DIAGRAM_ATTEMPTS):
        try:
            self.construct(constructions)
            self.constructions_list.append(constructions)
            return
        except:
            continue

    print(f"Failed to add the constructions after {MAX_DIAGRAM_ATTEMPTS} attempts.")
    raise Exception()

clear()

Reset all stored points, segments, circles, and name mappings.

Source code in pyeuclid/formalization/diagram.py
65
66
67
68
69
70
71
72
def clear(self):
    """Reset all stored points, segments, circles, and name mappings."""
    self.points.clear()
    self.segments.clear()
    self.circles.clear()

    self.name2point.clear()
    self.point2name.clear()

construct(constructions)

Apply a single batch of construction rules to extend the diagram.

Source code in pyeuclid/formalization/diagram.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
def construct(self, constructions: list[ConstructionRule]):
    """Apply a single batch of construction rules to extend the diagram."""
    constructed_points = constructions[0].constructed_points()
    if any(construction.constructed_points() != constructed_points for construction in constructions[1:]):
        raise Exception()

    to_be_intersected = []
    for construction in constructions:
        # print(construction.__class__.__name__ + '('+','.join([str(name) for name in construction.arguments()])+')')
        # for c in construction.conditions:
        #     if not self.numerical_check(c):
        #         raise Exception()

        to_be_intersected += self.sketch(construction)

    new_points = self.reduce(to_be_intersected, self.points)

    if check_too_close(new_points, self.points):
        raise Exception()

    if check_too_far(new_points, self.points):
        raise Exception()

    self.points += new_points

    for p, np in zip(constructed_points, new_points):
        self.name2point[p.name] = np
        self.point2name[np] = p.name

    for construction in constructions:
        self.draw(new_points, construction)

construct_diagram()

Construct the full diagram from all construction batches, with retries.

Source code in pyeuclid/formalization/diagram.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def construct_diagram(self):
    """Construct the full diagram from all construction batches, with retries."""
    for _ in range(MAX_DIAGRAM_ATTEMPTS):
        try:
            self.clear()
            for constructions in self.constructions_list:
                self.construct(constructions)
            self.draw_diagram()
            self.save_to_cache()
            return
        except:
            continue

    print(f"Failed to construct a diagram after {MAX_DIAGRAM_ATTEMPTS} attempts.")
    raise Exception()

draw_diagram(show=False)

Draw the current diagram; optionally display the matplotlib figure.

Source code in pyeuclid/formalization/diagram.py
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
def draw_diagram(self, show=False):
    """Draw the current diagram; optionally display the matplotlib figure."""
    imsize = 512 / 100
    self.fig, self.ax = plt.subplots(figsize=(imsize, imsize), dpi=300)
    self.ax.set_facecolor((1.0, 1.0, 1.0))

    for segment in self.segments:
        p1, p2 = segment.p1, segment.p2
        lx, ly = (p1.x, p2.x), (p1.y, p2.y)
        self.ax.plot(lx, ly, color='black', lw=1.2, alpha=0.8, ls='-')

    for circle in self.circles:
        self.ax.add_patch(
            plt.Circle(
                (circle.center.x, circle.center.y),
                circle.radius,
                color='red',
                alpha=0.8,
                fill=False,
                lw=1.2,
                ls='-'
            )
        )

    for p in self.points:
        self.ax.scatter(p.x, p.y, color='black', s=15)
        self.ax.annotate(self.point2name[p], (p.x+0.015, p.y+0.015), color='black', fontsize=8)

    self.ax.set_aspect('equal')
    self.ax.set_axis_off()
    self.fig.subplots_adjust(left=0.05, right=0.95, top=0.95, bottom=0.05, wspace=0, hspace=0)
    xmin = min([p.x for p in self.points])
    xmax = max([p.x for p in self.points])
    ymin = min([p.y for p in self.points])
    ymax = max([p.y for p in self.points])
    x_margin = (xmax - xmin) * 0.1
    y_margin = (ymax - ymin) * 0.1

    self.ax.margins(x_margin, y_margin)

    self.save_diagram()

    if show:
        plt.show()

    plt.close(self.fig)

numerical_check(relation)

Numerically evaluate whether a relation/expression holds in the diagram.

Source code in pyeuclid/formalization/diagram.py
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
def numerical_check(self, relation):
    """Numerically evaluate whether a relation/expression holds in the diagram."""
    if isinstance(relation, Relation):
        func = globals()['check_' + relation.__class__.__name__.lower()]
        args = [self.name2point[p.name] for p in relation.get_points()]
        return func(args)
    else:
        symbol_to_value = {}
        symbols, symbol_names = parse_expression(relation)

        for angle_symbol, angle_name in zip(symbols['Angle'], symbol_names['Angle']):
            angle_value = calculate_angle(*[self.name2point[n] for n in angle_name])
            symbol_to_value[angle_symbol] = angle_value

        for length_symbol, length_name in zip(symbols['Length'], symbol_names['Length']):
            length_value = calculate_length(*[self.name2point[n] for n in length_name])
            symbol_to_value[length_symbol] = length_value

        evaluated_expr = relation.subs(symbol_to_value)
        if close_enough(float(evaluated_expr.evalf()), 0):
            return True
        else:
            return False

numerical_check_goal(goal)

Check if the current diagram satisfies a goal relation/expression.

Source code in pyeuclid/formalization/diagram.py
147
148
149
150
151
152
153
154
155
156
def numerical_check_goal(self, goal):
    """Check if the current diagram satisfies a goal relation/expression."""
    if isinstance(goal, tuple):
        for g in goal:
            if self.numerical_check(g):
                return True, g
    else:
        if self.numerical_check(goal):
            return True, goal
    return False, goal

reduce(objs, existing_points)

Reduce intersecting objects into sampled intersection points.

Filters half-planes, handles point-only cases, samples within half-planes, or intersects pairs of essential geometric objects.

Source code in pyeuclid/formalization/diagram.py
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
def reduce(self, objs, existing_points) -> list[Point]:
    """Reduce intersecting objects into sampled intersection points.

    Filters half-planes, handles point-only cases, samples within half-planes,
    or intersects pairs of essential geometric objects.
    """
    essential_objs = [i for i in objs if not isinstance(i, HalfPlane)]
    halfplane_objs = [i for i in objs if isinstance(i, HalfPlane)]

    if all(isinstance(o, Point) for o in objs):
        return objs

    elif all(isinstance(o, HalfPlane) for o in objs):
        if len(objs) == 1:
            return objs[0].sample_within_halfplanes(existing_points,[])
        else:
            return objs[0].sample_within_halfplanes(existing_points,objs[1:])

    elif len(essential_objs) == 1:
        if not halfplane_objs:
            return objs[0].sample_within(existing_points)
        else:
            return objs[0].sample_within_halfplanes(existing_points,halfplane_objs)

    elif len(essential_objs) == 2:
        a, b = essential_objs
        result = a.intersect(b)

        if isinstance(result, Point):
            if halfplane_objs and not all(i.contains(result) for i in halfplane_objs):
                raise Exception()
            return [result]

        a, b = result

        if halfplane_objs:
            a_correct_side = all(i.contains(a) for i in halfplane_objs)
            b_correct_side = all(i.contains(b) for i in halfplane_objs)

            if a_correct_side and not b_correct_side:
                return [a]
            elif b_correct_side and not a_correct_side:
                return [b]
            elif not a_correct_side and not b_correct_side:
                raise Exception()

        a_close = any([a.close(x) for x in existing_points])
        b_close = any([b.close(x) for x in existing_points])

        if a_close and not b_close:
            return [b]

        elif b_close and not a_close:
            return [a]
        else:
            return [np.random.choice([a, b])]

save_to_cache()

Persist the diagram to cache if caching is enabled.

Source code in pyeuclid/formalization/diagram.py
78
79
80
81
82
83
84
def save_to_cache(self):
    """Persist the diagram to cache if caching is enabled."""
    if self.cache_folder is not None:
        file_name = f"{hash_constructions_list(self.constructions_list)}.pkl"
        file_path = os.path.join(self.cache_folder, file_name)
        with open(file_path, 'wb') as f:
            pickle.dump(self, f)

show()

Render the diagram with matplotlib.

Source code in pyeuclid/formalization/diagram.py
74
75
76
def show(self):
    """Render the diagram with matplotlib."""
    self.draw_diagram(show=True)

sketch_2l1c(*args)

Intersections of perpendiculars from P to AC/BC with circle centered at P.

Source code in pyeuclid/formalization/diagram.py
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
def sketch_2l1c(self, *args) -> list[Point]:
    """Intersections of perpendiculars from P to AC/BC with circle centered at P."""
    a, b, c, p = args
    bc, ac = Line(b, c), Line(a, c)
    circle = Circle(p, p.distance(a))

    d, d_ = line_circle_intersection(p.perpendicular_line(bc), circle)
    if bc.diff_side(d_, a):
        d = d_

    e, e_ = line_circle_intersection(p.perpendicular_line(ac), circle)
    if ac.diff_side(e_, b):
        e = e_

    df = d.perpendicular_line(Line(p, d))
    ef = e.perpendicular_line(Line(p, e))
    f = line_line_intersection(df, ef)

    g, g_ = line_circle_intersection(Line(c, f), circle)
    if bc.same_side(g_, a):
        g = g_

    b_ = c + (b - c) / b.distance(c)
    a_ = c + (a - c) / a.distance(c)
    m = (a_ + b_) * 0.5
    x = line_line_intersection(Line(c, m), Line(p, g))
    return [x.foot(ac), x.foot(bc), g, x]

sketch_3peq(*args)

Three-point equidistance construction.

Source code in pyeuclid/formalization/diagram.py
662
663
664
665
666
667
668
669
670
671
672
673
def sketch_3peq(self, *args) -> list[Point]:
    """Three-point equidistance construction."""
    a, b, c = args
    ab, bc, ca = Line(a, b), Line(b, c), Line(c, a)

    z = b + (c - b) * np.random.uniform(-0.5, 1.5)

    z_ = z * 2 - c
    l = z_.parallel_line(ca)
    x = line_line_intersection(l, ab)
    y = z * 2 - x
    return [x, y, z]

sketch_angle_bisector(*args)

Ray that bisects angle ABC.

Source code in pyeuclid/formalization/diagram.py
191
192
193
194
195
196
197
198
def sketch_angle_bisector(self, *args: list[Point]) -> Ray:
    """Ray that bisects angle ABC."""
    a, b, c = args
    dist_ab = a.distance(b)
    dist_bc = b.distance(c)
    x = b + (c - b) * (dist_ab / dist_bc)
    m = (a + x) * 0.5
    return Ray(b, m)

sketch_angle_mirror(*args)

Mirror of ray BA across BC.

Source code in pyeuclid/formalization/diagram.py
200
201
202
203
204
205
206
207
208
209
210
211
212
213
def sketch_angle_mirror(self, *args: list[Point]) -> Ray:
    """Mirror of ray BA across BC."""
    a, b, c = args
    ab = a - b
    cb = c - b

    dist_ab = a.distance(b)
    ang_ab = np.arctan2(ab.y / dist_ab, ab.x / dist_ab)
    dist_cb = c.distance(b)
    ang_bc = np.arctan2(cb.y / dist_cb, cb.x / dist_cb)

    ang_bx = 2 * ang_bc - ang_ab
    x = b + Point(np.cos(ang_bx), np.sin(ang_bx))
    return Ray(b, x)

sketch_centroid(*args)

Mid-segment points and centroid of triangle ABC.

Source code in pyeuclid/formalization/diagram.py
357
358
359
360
361
362
363
364
def sketch_centroid(self, *args) -> list[Point]:
    """Mid-segment points and centroid of triangle ABC."""
    a, b, c = args
    x = (b + c) * 0.5
    y = (c + a) * 0.5
    z = (a + b) * 0.5
    i = line_line_intersection(Line(a, x), Line(b, y))
    return [x, y, z, i]

sketch_circle(*args)

Center of circle through three points.

Source code in pyeuclid/formalization/diagram.py
215
216
217
218
219
220
221
def sketch_circle(self, *args: list[Point]) -> Point:
    """Center of circle through three points."""
    a, b, c = args
    l1 = perpendicular_bisector(a, b)
    l2 = perpendicular_bisector(b, c)
    x = line_line_intersection(l1, l2)
    return x

sketch_circumcenter(*args)

Circumcenter of triangle ABC.

Source code in pyeuclid/formalization/diagram.py
223
224
225
226
227
228
229
def sketch_circumcenter(self, *args: list[Point]) -> Point:
    """Circumcenter of triangle ABC."""
    a, b, c = args
    l1 = perpendicular_bisector(a, b)
    l2 = perpendicular_bisector(b, c)
    x = line_line_intersection(l1, l2)
    return x

sketch_e5128(*args)

Problem-specific construction e5128.

Source code in pyeuclid/formalization/diagram.py
650
651
652
653
654
655
656
657
658
659
660
def sketch_e5128(self, *args) -> list[Point]:
    """Problem-specific construction e5128."""
    a, b, c, d = args
    g = (a + b) * 0.5
    de = Line(d, g)

    e, f = line_circle_intersection(de, Circle(c, c.distance(b)))

    if e.distance(d) < f.distance(d):
        e = f
    return [e, g]

sketch_eq_quadrangle(*args)

Randomly sample a quadrilateral with opposite sides equal.

Source code in pyeuclid/formalization/diagram.py
231
232
233
234
235
236
237
238
239
240
241
242
243
244
def sketch_eq_quadrangle(self, *args: list[Point]) -> list[Point]:
    """Randomly sample a quadrilateral with opposite sides equal."""
    a = Point(0.0, 0.0)
    b = Point(1.0, 0.0)

    length = np.random.uniform(0.5, 2.0)
    ang = np.random.uniform(np.pi / 3, np.pi * 2 / 3)
    d = head_from(a, ang, length)

    ang = ang_of(b, d)
    ang = np.random.uniform(ang / 10, ang / 9)
    c = head_from(b, ang, length)
    a, b, c, d = random_rfss(a, b, c, d)
    return [a, b, c, d]

sketch_eq_trapezoid(*args)

Randomly sample an isosceles trapezoid.

Source code in pyeuclid/formalization/diagram.py
246
247
248
249
250
251
252
253
254
255
256
257
def sketch_eq_trapezoid(self, *args: list[Point]) -> list[Point]:
    """Randomly sample an isosceles trapezoid."""
    a = Point(0.0, 0.0)
    b = Point(1.0, 0.0)
    l = unif(0.5, 2.0)

    height = unif(0.5, 2.0)
    c = Point(0.5 + l / 2.0, height)
    d = Point(0.5 - l / 2.0, height)

    a, b, c, d = random_rfss(a, b, c, d)
    return [a, b, c, d]

sketch_eq_triangle(*args)

Circles defining an equilateral triangle on BC.

Source code in pyeuclid/formalization/diagram.py
259
260
261
262
def sketch_eq_triangle(self, *args: list[Point]) -> list[Circle]:
    """Circles defining an equilateral triangle on BC."""
    b, c = args
    return [Circle(center=b, radius=b.distance(c)), Circle(center=c, radius=b.distance(c))]

sketch_eqangle2(*args)

Point X such that angle ABX equals angle XCB.

Source code in pyeuclid/formalization/diagram.py
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
def sketch_eqangle2(self, *args: list[Point]) -> Point:
    """Point X such that angle ABX equals angle XCB."""
    a, b, c = args

    ba = b.distance(a)
    bc = b.distance(c)
    l = ba * ba / bc

    if unif(0.0, 1.0) < 0.5:
        be = min(l, bc)
        be = unif(be * 0.1, be * 0.9)
    else:
        be = max(l, bc)
        be = unif(be * 1.1, be * 1.5)

    e = b + (c - b) * (be / bc)
    y = b + (a - b) * (be / l)
    return line_line_intersection(Line(c, y), Line(a, e))

sketch_eqdia_quadrangle(*args)

Quadrilateral with equal diagonals.

Source code in pyeuclid/formalization/diagram.py
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
def sketch_eqdia_quadrangle(self, *args) -> list[Point]:
    """Quadrilateral with equal diagonals."""
    m = unif(0.3, 0.7)
    n = unif(0.3, 0.7)
    a = Point(-m, 0.0)
    c = Point(1 - m, 0.0)
    b = Point(0.0, -n)
    d = Point(0.0, 1 - n)

    ang = unif(-0.25 * np.pi, 0.25 * np.pi)
    sin, cos = np.sin(ang), np.cos(ang)
    b = b.rotate(sin, cos)
    d = d.rotate(sin, cos)
    a, b, c, d = random_rfss(a, b, c, d)
    return [a, b, c, d]

sketch_eqdistance(*args)

Circle centered at A with radius BC.

Source code in pyeuclid/formalization/diagram.py
299
300
301
302
def sketch_eqdistance(self, *args) -> Circle:
    """Circle centered at A with radius BC."""
    a, b, c = args
    return Circle(center=a, radius=b.distance(c))

sketch_eqdistance2(*args)

Circle centered at A with radius alpha*BC.

Source code in pyeuclid/formalization/diagram.py
304
305
306
307
def sketch_eqdistance2(self, *args) -> Circle:
    """Circle centered at A with radius alpha*BC."""
    a, b, c, alpha = args
    return Circle(center=a, radius=alpha*b.distance(c))

sketch_eqdistance3(*args)

Circle centered at A with fixed radius alpha.

Source code in pyeuclid/formalization/diagram.py
309
310
311
312
def sketch_eqdistance3(self, *args) -> Circle:
    """Circle centered at A with fixed radius alpha."""
    a, alpha = args
    return Circle(center=a, radius=alpha)

sketch_excenter(*args)

Excenter opposite B in triangle ABC.

Source code in pyeuclid/formalization/diagram.py
341
342
343
344
345
346
def sketch_excenter(self, *args) -> Point:
    """Excenter opposite B in triangle ABC."""
    a, b, c = args
    l1 = self.sketch_angle_bisector(b, a, c)
    l2 = self.sketch_angle_bisector(a, b, c).perpendicular_line(b)
    return line_line_intersection(l1, l2)

sketch_excenter2(*args)

Excenter plus touch points on extended sides.

Source code in pyeuclid/formalization/diagram.py
348
349
350
351
352
353
354
355
def sketch_excenter2(self, *args) -> list[Point]:
    """Excenter plus touch points on extended sides."""
    a, b, c = args
    i = self.sketch_excenter(a, b, c)
    x = i.foot(Line(b, c))
    y = i.foot(Line(c, a))
    z = i.foot(Line(a, b))
    return [x, y, z, i]

sketch_foot(*args)

Foot of perpendicular from A to line BC.

Source code in pyeuclid/formalization/diagram.py
314
315
316
317
318
319
def sketch_foot(self, *args) -> Point:
    """Foot of perpendicular from A to line BC."""
    a, b, c = args
    line_bc = Line(b, c)
    tline = a.perpendicular_line(line_bc)
    return line_line_intersection(tline, line_bc)

sketch_free(*args)

Free point uniformly sampled in a box.

Source code in pyeuclid/formalization/diagram.py
321
322
323
def sketch_free(self, *args) -> Point:
    """Free point uniformly sampled in a box."""
    return Point(unif(-1, 1), unif(-1, 1))

sketch_incenter(*args)

Incenter of triangle ABC.

Source code in pyeuclid/formalization/diagram.py
325
326
327
328
329
330
def sketch_incenter(self, *args) -> Point:
    """Incenter of triangle ABC."""
    a, b, c = args
    l1 = self.sketch_angle_bisector(a, b, c)
    l2 = self.sketch_angle_bisector(b, c, a)
    return line_line_intersection(l1, l2)

sketch_incenter2(*args)

Incenter plus touch points on each side.

Source code in pyeuclid/formalization/diagram.py
332
333
334
335
336
337
338
339
def sketch_incenter2(self, *args) -> list[Point]:
    """Incenter plus touch points on each side."""
    a, b, c = args
    i = self.sketch_incenter(a, b, c)
    x = i.foot(Line(b, c))
    y = i.foot(Line(c, a))
    z = i.foot(Line(a, b))
    return [x, y, z, i]

sketch_intersection_cc(*args)

Two circles centered at O and W through A.

Source code in pyeuclid/formalization/diagram.py
366
367
368
369
def sketch_intersection_cc(self, *args) -> list[Circle]:
    """Two circles centered at O and W through A."""
    o, w, a = args
    return [Circle(center=o, radius=o.distance(a)), Circle(center=w, radius=w.distance(a))]

sketch_intersection_lc(*args)

Line and circle defined by A,O,B for intersection.

Source code in pyeuclid/formalization/diagram.py
371
372
373
374
def sketch_intersection_lc(self, *args) -> list:
    """Line and circle defined by A,O,B for intersection."""
    a, o, b = args
    return [Line(b, a), Circle(center=o, radius=o.distance(b))]

sketch_intersection_ll(*args)

Intersection of lines AB and CD.

Source code in pyeuclid/formalization/diagram.py
376
377
378
379
380
381
def sketch_intersection_ll(self, *args) -> Point:
    """Intersection of lines AB and CD."""
    a, b, c, d = args
    l1 = Line(a, b)
    l2 = Line(c, d)
    return line_line_intersection(l1, l2)

sketch_isquare(*args)

Axis-aligned unit square, randomly re-ordered.

Source code in pyeuclid/formalization/diagram.py
584
585
586
587
588
589
590
591
def sketch_isquare(self, *args) -> list[Point]:
    """Axis-aligned unit square, randomly re-ordered."""
    a = Point(0.0, 0.0)
    b = Point(1.0, 0.0)
    c = Point(1.0, 1.0)
    d = Point(0.0, 1.0)
    a, b, c, d = random_rfss(a, b, c, d)
    return [a, b, c, d]

sketch_on_dia(*args)

Circle with diameter AB.

Source code in pyeuclid/formalization/diagram.py
710
711
712
713
714
def sketch_on_dia(self, *args) -> Circle:
    """Circle with diameter AB."""
    a, b = args
    o = (a + b) * 0.5
    return Circle(o, o.distance(a))

sketch_r_trapezoid(*args)

Right trapezoid with AB horizontal and AD vertical.

Source code in pyeuclid/formalization/diagram.py
518
519
520
521
522
523
524
525
def sketch_r_trapezoid(self, *args) -> list[Point]:
    """Right trapezoid with AB horizontal and AD vertical."""
    a = Point(0.0, 1.0)
    d = Point(0.0, 0.0)
    b = Point(unif(0.5, 1.5), 1.0)
    c = Point(unif(0.5, 1.5), 0.0)
    a, b, c, d = random_rfss(a, b, c, d)
    return [a, b, c, d]

sketch_r_triangle(*args)

Random right triangle with legs on axes.

Source code in pyeuclid/formalization/diagram.py
527
528
529
530
531
532
533
def sketch_r_triangle(self, *args) -> list[Point]:
    """Random right triangle with legs on axes."""
    a = Point(0.0, 0.0)
    b = Point(0.0, unif(0.5, 2.0))
    c = Point(unif(0.5, 2.0), 0.0)
    a, b, c = random_rfss(a, b, c)
    return [a, b, c]

sketch_rectangle(*args)

Axis-aligned rectangle with random width/height.

Source code in pyeuclid/formalization/diagram.py
535
536
537
538
539
540
541
542
543
def sketch_rectangle(self, *args) -> list[Point]:
    """Axis-aligned rectangle with random width/height."""
    a = Point(0.0, 0.0)
    b = Point(0.0, 1.0)
    l = unif(0.5, 2.0)
    c = Point(l, 1.0)
    d = Point(l, 0.0)
    a, b, c, d = random_rfss(a, b, c, d)
    return [a, b, c, d]

sketch_reflect(*args)

Reflect point A across line BC.

Source code in pyeuclid/formalization/diagram.py
545
546
547
548
549
def sketch_reflect(self, *args) -> Point:
    """Reflect point A across line BC."""
    a, b, c = args
    m = a.foot(Line(b, c))
    return m * 2 - a

sketch_risos(*args)

Right isosceles triangle.

Source code in pyeuclid/formalization/diagram.py
551
552
553
554
555
556
557
def sketch_risos(self, *args) -> list[Point]:
    """Right isosceles triangle."""
    a = Point(0.0, 0.0)
    b = Point(0.0, 1.0)
    c = Point(1.0, 0.0)
    a, b, c = random_rfss(a, b, c)
    return [a, b, c]

sketch_s_angle(*args)

Ray at point B making angle alpha with BA.

Source code in pyeuclid/formalization/diagram.py
559
560
561
562
563
564
def sketch_s_angle(self, *args) -> Ray:
    """Ray at point B making angle alpha with BA."""
    a, b, alpha = args
    ang = alpha / 180 * np.pi
    x = b + (a - b).rotatea(ang)
    return Ray(b, x)

sketch_segment(*args)

Random segment endpoints in [-1,1] box.

Source code in pyeuclid/formalization/diagram.py
566
567
568
569
570
def sketch_segment(self, *args) -> list[Point]:
    """Random segment endpoints in [-1,1] box."""
    a = Point(unif(-1, 1), unif(-1, 1))
    b = Point(unif(-1, 1), unif(-1, 1))
    return [a, b]

sketch_shift(*args)

Translate C by vector BA.

Source code in pyeuclid/formalization/diagram.py
572
573
574
575
def sketch_shift(self, *args) -> Point:
    """Translate C by vector BA."""
    c, b, a = args
    return c + (b - a)

sketch_square(*args)

Square constructed on segment AB.

Source code in pyeuclid/formalization/diagram.py
577
578
579
580
581
582
def sketch_square(self, *args) -> list[Point]:
    """Square constructed on segment AB."""
    a, b = args
    c = b + (a - b).rotatea(-np.pi / 2)
    d = a + (b - a).rotatea(np.pi / 2)
    return [c, d]

sketch_trapezoid(*args)

Random trapezoid with AB // CD.

Source code in pyeuclid/formalization/diagram.py
593
594
595
596
597
598
599
600
601
602
603
def sketch_trapezoid(self, *args) -> list[Point]:
    """Random trapezoid with AB // CD."""
    d = Point(0.0, 0.0)
    c = Point(1.0, 0.0)

    base = unif(0.5, 2.0)
    height = unif(0.5, 2.0)
    a = Point(unif(0.2, 0.5), height)
    b = Point(a.x + base, height)
    a, b, c, d = random_rfss(a, b, c, d)
    return [a, b, c, d]

sketch_triangle(*args)

Random triangle.

Source code in pyeuclid/formalization/diagram.py
605
606
607
608
609
610
611
612
def sketch_triangle(self, *args) -> list[Point]:
    """Random triangle."""
    a = Point(0.0, 0.0)
    b = Point(1.0, 0.0)
    ac = unif(0.5, 2.0)
    ang = unif(0.2, 0.8) * np.pi
    c = head_from(a, ang, ac)
    return [a, b, c]

sketch_triangle12(*args)

Triangle with side-length ratios near 1:2.

Source code in pyeuclid/formalization/diagram.py
614
615
616
617
618
619
620
def sketch_triangle12(self, *args) -> list[Point]:
    """Triangle with side-length ratios near 1:2."""
    b = Point(0.0, 0.0)
    c = Point(unif(1.5, 2.5), 0.0)
    a, _ = circle_circle_intersection(Circle(b, 1.0), Circle(c, 2.0))
    a, b, c = random_rfss(a, b, c)
    return [a, b, c]

sketch_trisect(*args)

Trisect angle ABC.

Source code in pyeuclid/formalization/diagram.py
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
def sketch_trisect(self, *args) -> list[Point]:
    """Trisect angle ABC."""
    a, b, c = args
    ang1 = ang_of(b, a)
    ang2 = ang_of(b, c)

    swap = 0
    if ang1 > ang2:
        ang1, ang2 = ang2, ang1
        swap += 1

    if ang2 - ang1 > np.pi:
        ang1, ang2 = ang2, ang1 + 2 * np.pi
        swap += 1

    angx = ang1 + (ang2 - ang1) / 3
    angy = ang2 - (ang2 - ang1) / 3

    x = b + Point(np.cos(angx), np.sin(angx))
    y = b + Point(np.cos(angy), np.sin(angy))

    ac = Line(a, c)
    x = line_line_intersection(Line(b, x), ac)
    y = line_line_intersection(Line(b, y), ac)

    if swap == 1:
        return [y, x]
    return [x, y]

sketch_trisegment(*args)

Trisect segment AB.

Source code in pyeuclid/formalization/diagram.py
704
705
706
707
708
def sketch_trisegment(self, *args) -> list[Point]:
    """Trisect segment AB."""
    a, b = args
    x, y = a + (b - a) * (1.0 / 3), a + (b - a) * (2.0 / 3)
    return [x, y]

Different

Bases: Relation

All provided points must be pairwise distinct.

Source code in pyeuclid/formalization/relation.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
class Different(Relation):
    """All provided points must be pairwise distinct."""

    def __init__(self, *ps: list[Point]):
        """
        Args:
            *ps (Point): Points that must be distinct.
        """
        super().__init__()
        self.ps = ps

    def definition(self):
        """Expand to pairwise inequality relations.

        Returns:
            list[Relation]: Negated equalities for every point pair.
        """
        return [Not(Equal(self.ps[i], self.ps[j])) for i in range(len(self.ps)) for j in range(i + 1, len(self.ps))]

__init__(*ps)

Parameters:

Name Type Description Default
*ps Point

Points that must be distinct.

()
Source code in pyeuclid/formalization/relation.py
150
151
152
153
154
155
156
def __init__(self, *ps: list[Point]):
    """
    Args:
        *ps (Point): Points that must be distinct.
    """
    super().__init__()
    self.ps = ps

definition()

Expand to pairwise inequality relations.

Returns:

Type Description

list[Relation]: Negated equalities for every point pair.

Source code in pyeuclid/formalization/relation.py
158
159
160
161
162
163
164
def definition(self):
    """Expand to pairwise inequality relations.

    Returns:
        list[Relation]: Negated equalities for every point pair.
    """
    return [Not(Equal(self.ps[i], self.ps[j])) for i in range(len(self.ps)) for j in range(i + 1, len(self.ps))]

Equal

Bases: Relation

Point equality relation.

Source code in pyeuclid/formalization/relation.py
89
90
91
92
93
94
95
96
97
98
class Equal(Relation):
    """Point equality relation."""

    def __init__(self, v1: Point, v2: Point):
        super().__init__()
        self.v1, self.v2 = sort_points(v1, v2)

    def permutations(self):
        """Enumerate equivalent orderings of the two points."""
        return [(self.v1, self.v2), (self.v2, self.v1)]

permutations()

Enumerate equivalent orderings of the two points.

Source code in pyeuclid/formalization/relation.py
96
97
98
def permutations(self):
    """Enumerate equivalent orderings of the two points."""
    return [(self.v1, self.v2), (self.v2, self.v1)]

HalfPlane

Numerical HalfPlane.

Source code in pyeuclid/formalization/numericals.py
471
472
473
474
475
476
477
478
479
class HalfPlane:
    """Numerical HalfPlane."""

    def __init__(self, a: Point, b: Point, c: Point, opposingsides=False):
        self.line = Line(b, c)
        assert abs(self.line(a)) > ATOM
        self.sign = self.line.sign(a)
        if opposingsides:
            self.sign = -self.sign

InferenceRule

Base class for all geometric inference rules.

Subclasses implement condition() and conclusion(), each returning relations/equations. The register decorator wraps these to expand definitions and filter zero expressions before the deductive database uses them.

Source code in pyeuclid/engine/inference_rule.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
class InferenceRule:
    """Base class for all geometric inference rules.

    Subclasses implement `condition()` and `conclusion()`, each returning
    relations/equations. The `register` decorator wraps these to expand
    definitions and filter zero expressions before the deductive database uses
    them.
    """

    def __init__(self):
        pass

    def condition(self):
        """Return premises (relations/equations) required to trigger the rule."""

    def conclusion(self):
        """Return relations/equations that are added when the rule fires."""

    def get_entities_in_condition(self):
        entities = set()
        for i in self.condition()[2]:
            entities = entities.union(set(i.get_entities()))
        return entities

    def degenerate(self):
        return False

    def get_entities_in_conclusion(self):
        entities = set()
        for i in self.conclusion()[2]:
            entities = entities.union(set(i.get_entities()))
        return entities

    def __str__(self):
        class_name = self.__class__.__name__
        content = []
        for key, value in vars(self).items():
            if key.startswith("_") or key == "depth":
                continue
            if not isinstance(value, Iterable):
                content.append(str(value))
            else:
                content.append(','.join(str(i) for i in value))
        attributes = ','.join(content)
        return f"{class_name}({attributes})"

    def __repr__(self):
        return str(self)

    def __eq__(self, other):
        return str(self) == str(other)

    def __hash__(self):
        return hash(str(self))

conclusion()

Return relations/equations that are added when the rule fires.

Source code in pyeuclid/engine/inference_rule.py
60
61
def conclusion(self):
    """Return relations/equations that are added when the rule fires."""

condition()

Return premises (relations/equations) required to trigger the rule.

Source code in pyeuclid/engine/inference_rule.py
57
58
def condition(self):
    """Return premises (relations/equations) required to trigger the rule."""

Line

Numerical line.

Source code in pyeuclid/formalization/numericals.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
class Line:
    """Numerical line."""
    def __init__(self,
          p1: Point = None,
          p2: Point = None,
          coefficients: tuple[int, int, int] = None
    ):        
        a, b, c = coefficients or (
            p1.y - p2.y,
            p2.x - p1.x,
            p1.x * p2.y - p2.x * p1.y,
        )

        if a < 0.0 or a == 0.0 and b > 0.0:
            a, b, c = -a, -b, -c

        self.coefficients = a, b, c

    def same(self, other: Line) -> bool:
        a, b, c = self.coefficients
        x, y, z = other.coefficients
        return close_enough(a * y, b * x) and close_enough(b * z, c * y)

    def parallel_line(self, p: Point) -> Line:
        a, b, _ = self.coefficients
        return Line(coefficients=(a, b, -a * p.x - b * p.y))

    def perpendicular_line(self, p: Point) -> Line:
        a, b, _ = self.coefficients
        return Line(p, p + Point(a, b))

    def intersect(self, obj):
        if isinstance(obj, Line):
            return line_line_intersection(self, obj)

        if isinstance(obj, Circle):
            return line_circle_intersection(self, obj)

    def distance(self, p: Point) -> float:
        a, b, c = self.coefficients
        return abs(self(p.x, p.y)) / math.sqrt(a * a + b * b)

    def __call__(self, x: Point, y: Point = None) -> float:
        if isinstance(x, Point) and y is None:
            return self(x.x, x.y)
        a, b, c = self.coefficients
        return x * a + y * b + c

    def is_parallel(self, other: Line) -> bool:
        a, b, _ = self.coefficients
        x, y, _ = other.coefficients
        return abs(a * y - b * x) < ATOM

    def is_perp(self, other: Line) -> bool:
        a, b, _ = self.coefficients
        x, y, _ = other.coefficients
        return abs(a * x + b * y) < ATOM

    def diff_side(self, p1: Point, p2: Point) -> Optional[bool]:
        d1 = self(p1.x, p1.y)
        d2 = self(p2.x, p2.y)
        if abs(d1) < ATOM or abs(d2) < ATOM:
            return None
        return d1 * d2 < 0

    def same_side(self, p1: Point, p2: Point) -> Optional[bool]:
        d1 = self(p1.x, p1.y)
        d2 = self(p2.x, p2.y)
        if abs(d1) < ATOM or abs(d2) < ATOM:
            return None
        return d1 * d2 > 0

    def sign(self, point: Point) -> int:
        s = self(point.x, point.y)
        if s > 0:
            return 1
        elif s < 0:
            return -1
        return 0

    def sample_within(self, points: list[Point], n: int = 5) -> list[Point]:
        """Sample a point within the boundary of points."""
        center = sum(points, Point(0.0, 0.0)) * (1.0 / len(points))
        radius = max([p.distance(center) for p in points])
        if close_enough(center.distance(self), radius):
            center = center.foot(self)
        a, b = line_circle_intersection(self, Circle(center.foot(self), radius))
        result = None
        best = -1.0
        for _ in range(n):
            rand = unif(0.0, 1.0)
            x = a + (b - a) * rand
            mind = min([x.distance(p) for p in points])
            if mind > best:
                best = mind
                result = x
        return [result]

    def sample_within_halfplanes(self, points: list[Point], halfplanes: list[HalfPlane], n: int = 5) -> list[Point]:
        """Sample points on the line within the intersection of half-plane constraints and near existing points."""
        # Parameterize the line: L(t) = P0 + t * d
        # P0 is a point on the line, d is the direction vector
        # # Get the direction vector (dx, dy) of the line
        a, b, c = self.coefficients
        if abs(a) > ATOM and abs(b) > ATOM:
            # General case: direction vector perpendicular to normal vector (a, b)
            d = Point(-b, a)
        elif abs(a) > ATOM:
            # Vertical line 
            d = Point(0, 1)
        elif abs(b) > ATOM:
            # Horizontal line
            d = Point(1, 0)
        else:
            raise ValueError("Invalid line with zero coefficients")

        # Find a point P0 on the line

        if abs(a) > ATOM:
            x0 = (-c - b * 0) / a  # Set y = 0
            y0 = 0
        elif abs(b) > ATOM:
            x0 = 0
            y0 = (-c - a * 0) / b  # Set x = 0
        else:
            raise ValueError("Invalid line with zero coefficients")

        P0 = Point(x0, y0)

        # Project existing points onto the line to get an initial interval
        t_points = []
        for p in points:
            # Vector from P0 to p
            vec = p - P0
            # Project vec onto d
            t = (vec.x * d.x + vec.y * d.y) / (d.x ** 2 + d.y ** 2)
            t_points.append(t)
        if not t_points:
            raise ValueError("No existing points provided for sampling")

        # Determine the interval based on existing points
        t_points.sort()
        t_center = sum(t_points) / len(t_points)
        t_radius = max(abs(t - t_center) for t in t_points)

        # Define an initial interval around the existing points
        t_init_min = t_center - t_radius
        t_init_max = t_center + t_radius

        # Initialize the interval as [t_init_min, t_init_max]
        t_min = t_init_min
        t_max = t_init_max

        # Process half-plane constraints
        for hp in halfplanes:
            # For each half-plane, compute K and H0
            a_h, b_h, c_h = hp.line.coefficients
            sign_h = hp.sign  # +1 or -1
            # Compute K = a_h * dx + b_h * dy
            K = a_h * d.x + b_h * d.y
            # Compute H0 = a_h * x0 + b_h * y0 + c_h
            H0 = a_h * P0.x + b_h * P0.y + c_h
            # The half-plane inequality is sign_h * (K * t + H0) >= 0
            S = sign_h
            if abs(K) < ATOM:
                # K is zero
                if S * H0 >= 0:
                    # The entire line satisfies the constraint
                    continue
                else:
                    # The line is entirely outside the half-plane
                    return []
            else:
                t0 = -H0 / K
                if K * S > 0:
                    # Inequality is t >= t0
                    t_min = max(t_min, t0)
                else:
                    # Inequality is t <= t0
                    t_max = min(t_max, t0)
        # After processing all half-planes, check if the interval is valid
        if t_min > t_max:
            # Empty interval
            return []
        else:
            # The intersection is [t_min, t_max]
            # Sample n points within this interval
            result = None
            best = -1.0
            for _ in range(n):
                t = unif(t_min, t_max)
                p = Point(P0.x + t * d.x, P0.y + t * d.y)
                # Calculate the minimum distance to existing points
                mind = min(p.distance(q) for q in points)
                if mind > best:
                    best = mind
                    result = p
            if result is None:
                raise ValueError("Cannot find a suitable point within the constraints")
            return [result]

sample_within(points, n=5)

Sample a point within the boundary of points.

Source code in pyeuclid/formalization/numericals.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
def sample_within(self, points: list[Point], n: int = 5) -> list[Point]:
    """Sample a point within the boundary of points."""
    center = sum(points, Point(0.0, 0.0)) * (1.0 / len(points))
    radius = max([p.distance(center) for p in points])
    if close_enough(center.distance(self), radius):
        center = center.foot(self)
    a, b = line_circle_intersection(self, Circle(center.foot(self), radius))
    result = None
    best = -1.0
    for _ in range(n):
        rand = unif(0.0, 1.0)
        x = a + (b - a) * rand
        mind = min([x.distance(p) for p in points])
        if mind > best:
            best = mind
            result = x
    return [result]

sample_within_halfplanes(points, halfplanes, n=5)

Sample points on the line within the intersection of half-plane constraints and near existing points.

Source code in pyeuclid/formalization/numericals.py
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
def sample_within_halfplanes(self, points: list[Point], halfplanes: list[HalfPlane], n: int = 5) -> list[Point]:
    """Sample points on the line within the intersection of half-plane constraints and near existing points."""
    # Parameterize the line: L(t) = P0 + t * d
    # P0 is a point on the line, d is the direction vector
    # # Get the direction vector (dx, dy) of the line
    a, b, c = self.coefficients
    if abs(a) > ATOM and abs(b) > ATOM:
        # General case: direction vector perpendicular to normal vector (a, b)
        d = Point(-b, a)
    elif abs(a) > ATOM:
        # Vertical line 
        d = Point(0, 1)
    elif abs(b) > ATOM:
        # Horizontal line
        d = Point(1, 0)
    else:
        raise ValueError("Invalid line with zero coefficients")

    # Find a point P0 on the line

    if abs(a) > ATOM:
        x0 = (-c - b * 0) / a  # Set y = 0
        y0 = 0
    elif abs(b) > ATOM:
        x0 = 0
        y0 = (-c - a * 0) / b  # Set x = 0
    else:
        raise ValueError("Invalid line with zero coefficients")

    P0 = Point(x0, y0)

    # Project existing points onto the line to get an initial interval
    t_points = []
    for p in points:
        # Vector from P0 to p
        vec = p - P0
        # Project vec onto d
        t = (vec.x * d.x + vec.y * d.y) / (d.x ** 2 + d.y ** 2)
        t_points.append(t)
    if not t_points:
        raise ValueError("No existing points provided for sampling")

    # Determine the interval based on existing points
    t_points.sort()
    t_center = sum(t_points) / len(t_points)
    t_radius = max(abs(t - t_center) for t in t_points)

    # Define an initial interval around the existing points
    t_init_min = t_center - t_radius
    t_init_max = t_center + t_radius

    # Initialize the interval as [t_init_min, t_init_max]
    t_min = t_init_min
    t_max = t_init_max

    # Process half-plane constraints
    for hp in halfplanes:
        # For each half-plane, compute K and H0
        a_h, b_h, c_h = hp.line.coefficients
        sign_h = hp.sign  # +1 or -1
        # Compute K = a_h * dx + b_h * dy
        K = a_h * d.x + b_h * d.y
        # Compute H0 = a_h * x0 + b_h * y0 + c_h
        H0 = a_h * P0.x + b_h * P0.y + c_h
        # The half-plane inequality is sign_h * (K * t + H0) >= 0
        S = sign_h
        if abs(K) < ATOM:
            # K is zero
            if S * H0 >= 0:
                # The entire line satisfies the constraint
                continue
            else:
                # The line is entirely outside the half-plane
                return []
        else:
            t0 = -H0 / K
            if K * S > 0:
                # Inequality is t >= t0
                t_min = max(t_min, t0)
            else:
                # Inequality is t <= t0
                t_max = min(t_max, t0)
    # After processing all half-planes, check if the interval is valid
    if t_min > t_max:
        # Empty interval
        return []
    else:
        # The intersection is [t_min, t_max]
        # Sample n points within this interval
        result = None
        best = -1.0
        for _ in range(n):
            t = unif(t_min, t_max)
            p = Point(P0.x + t * d.x, P0.y + t * d.y)
            # Calculate the minimum distance to existing points
            mind = min(p.distance(q) for q in points)
            if mind > best:
                best = mind
                result = p
        if result is None:
            raise ValueError("Cannot find a suitable point within the constraints")
        return [result]

Lt

Bases: Relation

Source code in pyeuclid/formalization/relation.py
81
82
83
84
85
86
class Lt(Relation):
    def __init__(self, v1: Point, v2: Point):
        """Ordering helper used to canonicalize inference rule assignments."""
        super().__init__()
        self.v1 = v1
        self.v2 = v2

__init__(v1, v2)

Ordering helper used to canonicalize inference rule assignments.

Source code in pyeuclid/formalization/relation.py
82
83
84
85
86
def __init__(self, v1: Point, v2: Point):
    """Ordering helper used to canonicalize inference rule assignments."""
    super().__init__()
    self.v1 = v1
    self.v2 = v2

Midpoint

Bases: Relation

p1 is the midpoint of segment p2p3.

Source code in pyeuclid/formalization/relation.py
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
class Midpoint(Relation):
    """p1 is the midpoint of segment p2p3."""

    def __init__(self, p1: Point, p2: Point, p3: Point):
        """
        Args:
            p1 (Point): Candidate midpoint.
            p2 (Point): Segment endpoint.
            p3 (Point): Segment endpoint.
        """
        super().__init__()
        self.p1 = p1
        self.p2, self.p3 = sort_points(p2, p3)

    def definition(self):
        """Midpoint expressed via equal lengths, collinearity, and betweenness.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) - Length(self.p1, self.p3),
            Collinear(self.p1, self.p2, self.p3),
            Different(self.p2, self.p3),
            Between(self.p1, self.p2, self.p3),
        ]

__init__(p1, p2, p3)

Parameters:

Name Type Description Default
p1 Point

Candidate midpoint.

required
p2 Point

Segment endpoint.

required
p3 Point

Segment endpoint.

required
Source code in pyeuclid/formalization/relation.py
291
292
293
294
295
296
297
298
299
300
def __init__(self, p1: Point, p2: Point, p3: Point):
    """
    Args:
        p1 (Point): Candidate midpoint.
        p2 (Point): Segment endpoint.
        p3 (Point): Segment endpoint.
    """
    super().__init__()
    self.p1 = p1
    self.p2, self.p3 = sort_points(p2, p3)

definition()

Midpoint expressed via equal lengths, collinearity, and betweenness.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
302
303
304
305
306
307
308
309
310
311
312
313
def definition(self):
    """Midpoint expressed via equal lengths, collinearity, and betweenness.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) - Length(self.p1, self.p3),
        Collinear(self.p1, self.p2, self.p3),
        Different(self.p2, self.p3),
        Between(self.p1, self.p2, self.p3),
    ]

NotCollinear

Bases: Relation

Points p1,p2,p3 are not collinear.

Source code in pyeuclid/formalization/relation.py
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
class NotCollinear(Relation):
    """Points p1,p2,p3 are not collinear."""

    def __init__(self, p1, p2, p3):
        """
        Args:
            p1 (Point)
            p2 (Point)
            p3 (Point)
        """
        super().__init__()
        self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

    def definition(self):
        """Expand non-collinearity into primitive constraints."""
        return [
            Not(Collinear(self.p1, self.p2, self.p3)),
            Different(self.p1, self.p2, self.p3)
        ]

__init__(p1, p2, p3)

Source code in pyeuclid/formalization/relation.py
270
271
272
273
274
275
276
277
278
def __init__(self, p1, p2, p3):
    """
    Args:
        p1 (Point)
        p2 (Point)
        p3 (Point)
    """
    super().__init__()
    self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

definition()

Expand non-collinearity into primitive constraints.

Source code in pyeuclid/formalization/relation.py
280
281
282
283
284
285
def definition(self):
    """Expand non-collinearity into primitive constraints."""
    return [
        Not(Collinear(self.p1, self.p2, self.p3)),
        Different(self.p1, self.p2, self.p3)
    ]

OppositeSide

Bases: Relation

Points p1,p2 lie on opposite sides of the line (p3,p4).

Source code in pyeuclid/formalization/relation.py
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
class OppositeSide(Relation):
    """Points p1,p2 lie on opposite sides of the line (p3,p4)."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1 (Point): First query point.
            p2 (Point): Second query point.
            p3 (Point): Line endpoint 1.
            p4 (Point): Line endpoint 2.
        """
        super().__init__()
        self.p1, self.p2 = sort_points(p1, p2)
        self.p3, self.p4 = sort_points(p3, p4)

    def definition(self):
        """Logical expansion expressing opposite-side constraints.

        Returns:
            list[Relation]: Primitive relations defining opposite sides.
        """
        return [
            Not(Collinear(self.p1, self.p3, self.p4)),
            Not(Collinear(self.p2, self.p3, self.p4)),
            Not(SameSide(self.p1, self.p2, self.p3, self.p4)),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1 Point

First query point.

required
p2 Point

Second query point.

required
p3 Point

Line endpoint 1.

required
p4 Point

Line endpoint 2.

required
Source code in pyeuclid/formalization/relation.py
220
221
222
223
224
225
226
227
228
229
230
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1 (Point): First query point.
        p2 (Point): Second query point.
        p3 (Point): Line endpoint 1.
        p4 (Point): Line endpoint 2.
    """
    super().__init__()
    self.p1, self.p2 = sort_points(p1, p2)
    self.p3, self.p4 = sort_points(p3, p4)

definition()

Logical expansion expressing opposite-side constraints.

Returns:

Type Description

list[Relation]: Primitive relations defining opposite sides.

Source code in pyeuclid/formalization/relation.py
232
233
234
235
236
237
238
239
240
241
242
def definition(self):
    """Logical expansion expressing opposite-side constraints.

    Returns:
        list[Relation]: Primitive relations defining opposite sides.
    """
    return [
        Not(Collinear(self.p1, self.p3, self.p4)),
        Not(Collinear(self.p2, self.p3, self.p4)),
        Not(SameSide(self.p1, self.p2, self.p3, self.p4)),
    ]

Parallel

Bases: Relation

Segments (p1,p2) and (p3,p4) are parallel.

Source code in pyeuclid/formalization/relation.py
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
class Parallel(Relation):
    """Segments (p1,p2) and (p3,p4) are parallel."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1, p2 (Point): First segment endpoints.
            p3, p4 (Point): Second segment endpoints.
        """
        super().__init__()
        p1, p2 = sort_points(p1, p2)
        p3, p4 = sort_points(p3, p4)
        self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

    def permutations(self):
        """Enumerate symmetric endpoint permutations preserving segment groups.

        Returns:
            list[tuple[Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p1, self.p2, self.p4, self.p3),
            (self.p2, self.p1, self.p3, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
            (self.p3, self.p4, self.p1, self.p2),
            (self.p4, self.p3, self.p1, self.p2),
            (self.p3, self.p4, self.p2, self.p1),
            (self.p4, self.p3, self.p2, self.p1),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1, p2 Point

First segment endpoints.

required
p3, p4 Point

Second segment endpoints.

required
Source code in pyeuclid/formalization/relation.py
396
397
398
399
400
401
402
403
404
405
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1, p2 (Point): First segment endpoints.
        p3, p4 (Point): Second segment endpoints.
    """
    super().__init__()
    p1, p2 = sort_points(p1, p2)
    p3, p4 = sort_points(p3, p4)
    self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

permutations()

Enumerate symmetric endpoint permutations preserving segment groups.

Returns:

Type Description

list[tuple[Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
def permutations(self):
    """Enumerate symmetric endpoint permutations preserving segment groups.

    Returns:
        list[tuple[Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p1, self.p2, self.p4, self.p3),
        (self.p2, self.p1, self.p3, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
        (self.p3, self.p4, self.p1, self.p2),
        (self.p4, self.p3, self.p1, self.p2),
        (self.p3, self.p4, self.p2, self.p1),
        (self.p4, self.p3, self.p2, self.p1),
    ]

Pentagon

Bases: Relation

Points form a cyclically ordered pentagon.

Source code in pyeuclid/formalization/relation.py
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
class Pentagon(Relation):
    """Points form a cyclically ordered pentagon."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point):
        """
        Args:
            p1, p2, p3, p4, p5 (Point): Vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5 = sort_cyclic_points(p1, p2, p3, p4, p5)

    def permutations(self):
        """Enumerate cyclic and reversed vertex orderings.

        Returns:
            list[tuple[Point, Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4, self.p5),
            (self.p2, self.p3, self.p4, self.p5, self.p1),
            (self.p3, self.p4, self.p5, self.p1, self.p2),
            (self.p4, self.p5, self.p1, self.p2, self.p3),
            (self.p5, self.p1, self.p2, self.p3, self.p4),
            (self.p5, self.p4, self.p3, self.p2, self.p1),
            (self.p4, self.p3, self.p2, self.p1, self.p5),
            (self.p3, self.p2, self.p1, self.p5, self.p4),
            (self.p2, self.p1, self.p5, self.p4, self.p3),
            (self.p1, self.p5, self.p4, self.p3, self.p2),
        ]

__init__(p1, p2, p3, p4, p5)

Parameters:

Name Type Description Default
p1, p2, p3, p4, p5 Point

Vertices.

required
Source code in pyeuclid/formalization/relation.py
500
501
502
503
504
505
506
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point):
    """
    Args:
        p1, p2, p3, p4, p5 (Point): Vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5 = sort_cyclic_points(p1, p2, p3, p4, p5)

permutations()

Enumerate cyclic and reversed vertex orderings.

Returns:

Type Description

list[tuple[Point, Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
def permutations(self):
    """Enumerate cyclic and reversed vertex orderings.

    Returns:
        list[tuple[Point, Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4, self.p5),
        (self.p2, self.p3, self.p4, self.p5, self.p1),
        (self.p3, self.p4, self.p5, self.p1, self.p2),
        (self.p4, self.p5, self.p1, self.p2, self.p3),
        (self.p5, self.p1, self.p2, self.p3, self.p4),
        (self.p5, self.p4, self.p3, self.p2, self.p1),
        (self.p4, self.p3, self.p2, self.p1, self.p5),
        (self.p3, self.p2, self.p1, self.p5, self.p4),
        (self.p2, self.p1, self.p5, self.p4, self.p3),
        (self.p1, self.p5, self.p4, self.p3, self.p2),
    ]

Perpendicular

Bases: Relation

Segments (p1,p2) and (p3,p4) are perpendicular.

Source code in pyeuclid/formalization/relation.py
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
class Perpendicular(Relation):
    """Segments (p1,p2) and (p3,p4) are perpendicular."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1, p2 (Point): First segment endpoints.
            p3, p4 (Point): Second segment endpoints.
        """
        super().__init__()
        p1, p2 = sort_points(p1, p2)
        p3, p4 = sort_points(p3, p4)
        self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

    def permutations(self):
        """Enumerate symmetric endpoint permutations preserving segment groups.

        Returns:
            list[tuple[Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p1, self.p2, self.p4, self.p3),
            (self.p2, self.p1, self.p3, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
            (self.p3, self.p4, self.p1, self.p2),
            (self.p4, self.p3, self.p1, self.p2),
            (self.p3, self.p4, self.p2, self.p1),
            (self.p4, self.p3, self.p2, self.p1),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1, p2 Point

First segment endpoints.

required
p3, p4 Point

Second segment endpoints.

required
Source code in pyeuclid/formalization/relation.py
428
429
430
431
432
433
434
435
436
437
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1, p2 (Point): First segment endpoints.
        p3, p4 (Point): Second segment endpoints.
    """
    super().__init__()
    p1, p2 = sort_points(p1, p2)
    p3, p4 = sort_points(p3, p4)
    self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

permutations()

Enumerate symmetric endpoint permutations preserving segment groups.

Returns:

Type Description

list[tuple[Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
def permutations(self):
    """Enumerate symmetric endpoint permutations preserving segment groups.

    Returns:
        list[tuple[Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p1, self.p2, self.p4, self.p3),
        (self.p2, self.p1, self.p3, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
        (self.p3, self.p4, self.p1, self.p2),
        (self.p4, self.p3, self.p1, self.p2),
        (self.p3, self.p4, self.p2, self.p1),
        (self.p4, self.p3, self.p2, self.p1),
    ]

Point

Named geometric point used throughout the formalization.

Source code in pyeuclid/formalization/relation.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Point:
    """Named geometric point used throughout the formalization."""

    def __init__(self, name: str):
        """Create a point.

        Args:
            name (str): Identifier (underscores are not allowed).
        """
        self.name = name
        assert "_" not in name

    def __str__(self):
        return self.name

    def __eq__(self, other):
        return str(self) == str(other)

    def __repr__(self):
        return str(self)

    def __hash__(self):
        return hash(str(self))

__init__(name)

Create a point.

Parameters:

Name Type Description Default
name str

Identifier (underscores are not allowed).

required
Source code in pyeuclid/formalization/relation.py
16
17
18
19
20
21
22
23
def __init__(self, name: str):
    """Create a point.

    Args:
        name (str): Identifier (underscores are not allowed).
    """
    self.name = name
    assert "_" not in name

Quadrilateral

Bases: Relation

Points form a cyclically ordered quadrilateral.

Source code in pyeuclid/formalization/relation.py
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
class Quadrilateral(Relation):
    """Points form a cyclically ordered quadrilateral."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1, p2, p3, p4 (Point): Vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4 = sort_cyclic_points(p1, p2, p3, p4)

    def permutations(self):
        """Enumerate cyclic and reversed vertex orderings.

        Returns:
            list[tuple[Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p2, self.p3, self.p4, self.p1),
            (self.p3, self.p4, self.p1, self.p2),
            (self.p4, self.p1, self.p2, self.p3),
            (self.p4, self.p3, self.p2, self.p1),
            (self.p3, self.p2, self.p1, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
            (self.p1, self.p4, self.p3, self.p2),
        ]

    def definition(self):
        """Opposite sides must lie on opposite sides of diagonals.

        Returns:
            list[Relation]: Primitive opposite-side relations.
        """
        return [
            OppositeSide(self.p1, self.p3, self.p2, self.p4),
            OppositeSide(self.p2, self.p4, self.p1, self.p3),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1, p2, p3, p4 Point

Vertices.

required
Source code in pyeuclid/formalization/relation.py
460
461
462
463
464
465
466
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1, p2, p3, p4 (Point): Vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4 = sort_cyclic_points(p1, p2, p3, p4)

definition()

Opposite sides must lie on opposite sides of diagonals.

Returns:

Type Description

list[Relation]: Primitive opposite-side relations.

Source code in pyeuclid/formalization/relation.py
485
486
487
488
489
490
491
492
493
494
def definition(self):
    """Opposite sides must lie on opposite sides of diagonals.

    Returns:
        list[Relation]: Primitive opposite-side relations.
    """
    return [
        OppositeSide(self.p1, self.p3, self.p2, self.p4),
        OppositeSide(self.p2, self.p4, self.p1, self.p3),
    ]

permutations()

Enumerate cyclic and reversed vertex orderings.

Returns:

Type Description

list[tuple[Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
def permutations(self):
    """Enumerate cyclic and reversed vertex orderings.

    Returns:
        list[tuple[Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p2, self.p3, self.p4, self.p1),
        (self.p3, self.p4, self.p1, self.p2),
        (self.p4, self.p1, self.p2, self.p3),
        (self.p4, self.p3, self.p2, self.p1),
        (self.p3, self.p2, self.p1, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
        (self.p1, self.p4, self.p3, self.p2),
    ]

Ray

Bases: Line

Numerical ray.

Source code in pyeuclid/formalization/numericals.py
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
class Ray(Line):
    """Numerical ray."""

    def __init__(self, tail: Point, head: Point):
        self.line = Line(tail, head)
        self.coefficients = self.line.coefficients
        self.tail = tail
        self.head = head

    def intersect(self, obj) -> Point:
        if isinstance(obj, (Ray, Line)):
            return line_line_intersection(self.line, obj)

        a, b = line_circle_intersection(self.line, obj)

        if a.close(self.tail):
            return b
        if b.close(self.tail):
            return a

        v = self.head - self.tail
        va = a - self.tail
        vb = b - self.tail

        if v.dot(va) > 0:
            return a
        if v.dot(vb) > 0:
            return b

        raise Exception()

    def sample_within_halfplanes(self, points: list[Point], halfplanes: list[HalfPlane], n: int = 5) -> list[Point]:
        """Sample points on the half-line within the intersection of half-plane constraints and near existing points."""

        # Parameterize the half-line: L(t) = tail + t * d, t >= 0
        d = self.head - self.tail
        d_norm_sq = d.x ** 2 + d.y ** 2
        if d_norm_sq < ATOM:
            raise ValueError("Invalid HalfLine with zero length")

        # Project existing points onto the half-line to get an initial interval
        t_points = []
        for p in points:
            # Vector from tail to p
            vec = p - self.tail
            # Project vec onto d
            t = (vec.x * d.x + vec.y * d.y) / d_norm_sq
            if t >= 0:
                t_points.append(t)
        if not t_points:
            # If no existing points project onto the half-line, define a default interval
            t_init_min = 0
            t_init_max = 1  # For example, length 1 along the half-line
        else:
            # Determine the interval based on existing points
            t_points.sort()
            t_center = sum(t_points) / len(t_points)
            t_radius = max(abs(t - t_center) for t in t_points)
            # Define an initial interval around the existing points
            t_init_min = max(0, t_center - t_radius)
            t_init_max = t_center + t_radius

        # Initialize the interval as [t_init_min, t_init_max]
        t_min = t_init_min
        t_max = t_init_max

        # Process half-plane constraints
        for hp in halfplanes:
            a_h, b_h, c_h = hp.line.coefficients
            sign_h = hp.sign  # +1 or -1

            # Compute K = a_h * dx + b_h * dy
            K = a_h * d.x + b_h * d.y

            # Compute H0 = a_h * tail.x + b_h * tail.y + c_h
            H0 = a_h * self.tail.x + b_h * self.tail.y + c_h

            # The half-plane inequality is sign_h * (K * t + H0) >= 0
            S = sign_h

            if abs(K) < ATOM:
                # K is zero
                if S * H0 >= 0:
                    # The entire half-line satisfies the constraint
                    continue
                else:
                    # The half-line is entirely outside the half-plane
                    return []
            else:
                t0 = -H0 / K
                if K * S > 0:
                    # Inequality is t >= t0
                    if t0 >= 0:
                        t_min = max(t_min, t0)
                    else:
                        t_min = t_min  # t_min remains as is (t >= 0)
                else:
                    # Inequality is t <= t0
                    t_max = min(t_max, t0)
                    if t_max < 0:
                        # Entire interval is before the tail (t < 0), no valid t
                        return []

        # After processing all half-planes, check if the interval is valid
        if t_min > t_max:
            # Empty interval
            return []
        else:
            # The intersection is [t_min, t_max]
            # Ensure t_min >= 0
            t_min = max(t_min, 0)
            if t_min > t_max:
                # No valid t
                return []
            # Sample n points within this interval
            result = None
            best = -1.0
            for _ in range(n):
                t = unif(t_min, t_max)
                p = Point(self.tail.x + t * d.x, self.tail.y + t * d.y)
                # Calculate the minimum distance to existing points
                mind = min(p.distance(q) for q in points)
                if mind > best:
                    best = mind
                    result = p
            if result is None:
                raise ValueError("Cannot find a suitable point within the constraints")
            return [result]

sample_within_halfplanes(points, halfplanes, n=5)

Sample points on the half-line within the intersection of half-plane constraints and near existing points.

Source code in pyeuclid/formalization/numericals.py
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
def sample_within_halfplanes(self, points: list[Point], halfplanes: list[HalfPlane], n: int = 5) -> list[Point]:
    """Sample points on the half-line within the intersection of half-plane constraints and near existing points."""

    # Parameterize the half-line: L(t) = tail + t * d, t >= 0
    d = self.head - self.tail
    d_norm_sq = d.x ** 2 + d.y ** 2
    if d_norm_sq < ATOM:
        raise ValueError("Invalid HalfLine with zero length")

    # Project existing points onto the half-line to get an initial interval
    t_points = []
    for p in points:
        # Vector from tail to p
        vec = p - self.tail
        # Project vec onto d
        t = (vec.x * d.x + vec.y * d.y) / d_norm_sq
        if t >= 0:
            t_points.append(t)
    if not t_points:
        # If no existing points project onto the half-line, define a default interval
        t_init_min = 0
        t_init_max = 1  # For example, length 1 along the half-line
    else:
        # Determine the interval based on existing points
        t_points.sort()
        t_center = sum(t_points) / len(t_points)
        t_radius = max(abs(t - t_center) for t in t_points)
        # Define an initial interval around the existing points
        t_init_min = max(0, t_center - t_radius)
        t_init_max = t_center + t_radius

    # Initialize the interval as [t_init_min, t_init_max]
    t_min = t_init_min
    t_max = t_init_max

    # Process half-plane constraints
    for hp in halfplanes:
        a_h, b_h, c_h = hp.line.coefficients
        sign_h = hp.sign  # +1 or -1

        # Compute K = a_h * dx + b_h * dy
        K = a_h * d.x + b_h * d.y

        # Compute H0 = a_h * tail.x + b_h * tail.y + c_h
        H0 = a_h * self.tail.x + b_h * self.tail.y + c_h

        # The half-plane inequality is sign_h * (K * t + H0) >= 0
        S = sign_h

        if abs(K) < ATOM:
            # K is zero
            if S * H0 >= 0:
                # The entire half-line satisfies the constraint
                continue
            else:
                # The half-line is entirely outside the half-plane
                return []
        else:
            t0 = -H0 / K
            if K * S > 0:
                # Inequality is t >= t0
                if t0 >= 0:
                    t_min = max(t_min, t0)
                else:
                    t_min = t_min  # t_min remains as is (t >= 0)
            else:
                # Inequality is t <= t0
                t_max = min(t_max, t0)
                if t_max < 0:
                    # Entire interval is before the tail (t < 0), no valid t
                    return []

    # After processing all half-planes, check if the interval is valid
    if t_min > t_max:
        # Empty interval
        return []
    else:
        # The intersection is [t_min, t_max]
        # Ensure t_min >= 0
        t_min = max(t_min, 0)
        if t_min > t_max:
            # No valid t
            return []
        # Sample n points within this interval
        result = None
        best = -1.0
        for _ in range(n):
            t = unif(t_min, t_max)
            p = Point(self.tail.x + t * d.x, self.tail.y + t * d.y)
            # Calculate the minimum distance to existing points
            mind = min(p.distance(q) for q in points)
            if mind > best:
                best = mind
                result = p
        if result is None:
            raise ValueError("Cannot find a suitable point within the constraints")
        return [result]

Relation

Base class for logical relations over points.

Source code in pyeuclid/formalization/relation.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class Relation:
    """Base class for logical relations over points."""

    def __init__(self):
        self.negated = False

    def negate(self):
        """Toggle negation flag in-place."""
        self.negated = not self.negated

    def get_points(self):
        """Return all point instances contained in the relation."""
        points = []
        for v in vars(self).values():
            if isinstance(v, Point):
                points.append(v)
            elif isinstance(v, list):
                for p in v:
                    if isinstance(p, Point):
                        points.append(p)
        return points

    def __str__(self):
        """Readable representation, prefixed with Not() when negated."""
        class_name = self.__class__.__name__
        points = self.get_points()
        args_name = ",".join([p.name for p in points])

        if not self.negated:
            return f"{class_name}({args_name})"
        else:
            return f"Not({class_name}({args_name}))"

    def __repr__(self):
        return str(self)

    def __eq__(self, other):
        return str(self) == str(other)

    def __hash__(self):
        return hash(str(self))

__str__()

Readable representation, prefixed with Not() when negated.

Source code in pyeuclid/formalization/relation.py
60
61
62
63
64
65
66
67
68
69
def __str__(self):
    """Readable representation, prefixed with Not() when negated."""
    class_name = self.__class__.__name__
    points = self.get_points()
    args_name = ",".join([p.name for p in points])

    if not self.negated:
        return f"{class_name}({args_name})"
    else:
        return f"Not({class_name}({args_name}))"

get_points()

Return all point instances contained in the relation.

Source code in pyeuclid/formalization/relation.py
48
49
50
51
52
53
54
55
56
57
58
def get_points(self):
    """Return all point instances contained in the relation."""
    points = []
    for v in vars(self).values():
        if isinstance(v, Point):
            points.append(v)
        elif isinstance(v, list):
            for p in v:
                if isinstance(p, Point):
                    points.append(p)
    return points

negate()

Toggle negation flag in-place.

Source code in pyeuclid/formalization/relation.py
44
45
46
def negate(self):
    """Toggle negation flag in-place."""
    self.negated = not self.negated

SameSide

Bases: Relation

Points p1,p2 lie on the same side of the line (p3,p4).

Source code in pyeuclid/formalization/relation.py
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
class SameSide(Relation):
    """Points p1,p2 lie on the same side of the line (p3,p4)."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1 (Point): First query point.
            p2 (Point): Second query point.
            p3 (Point): Line endpoint 1.
            p4 (Point): Line endpoint 2.
        """
        super().__init__()
        self.p1, self.p2 = sort_points(p1, p2)
        self.p3, self.p4 = sort_points(p3, p4)

    def permutations(self):
        """Enumerate symmetric orderings for same-side tests."""
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p1, self.p2, self.p4, self.p3),
            (self.p2, self.p1, self.p3, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1 Point

First query point.

required
p2 Point

Second query point.

required
p3 Point

Line endpoint 1.

required
p4 Point

Line endpoint 2.

required
Source code in pyeuclid/formalization/relation.py
195
196
197
198
199
200
201
202
203
204
205
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1 (Point): First query point.
        p2 (Point): Second query point.
        p3 (Point): Line endpoint 1.
        p4 (Point): Line endpoint 2.
    """
    super().__init__()
    self.p1, self.p2 = sort_points(p1, p2)
    self.p3, self.p4 = sort_points(p3, p4)

permutations()

Enumerate symmetric orderings for same-side tests.

Source code in pyeuclid/formalization/relation.py
207
208
209
210
211
212
213
214
def permutations(self):
    """Enumerate symmetric orderings for same-side tests."""
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p1, self.p2, self.p4, self.p3),
        (self.p2, self.p1, self.p3, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
    ]

Similar

Bases: Relation

Triangles (p1,p2,p3) and (p4,p5,p6) are similar.

Source code in pyeuclid/formalization/relation.py
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
class Similar(Relation):
    """Triangles (p1,p2,p3) and (p4,p5,p6) are similar."""

    def __init__(
        self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
    ):
        """
        Args:
            p1, p2, p3 (Point): First triangle vertices.
            p4, p5, p6 (Point): Second triangle vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

    def definition(self):
        """Similarity expressed via length ratios and non-collinearity.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) / Length(self.p4, self.p5)
            - Length(self.p2, self.p3) / Length(self.p5, self.p6),
            Length(self.p1, self.p2) / Length(self.p4, self.p5)
            - Length(self.p3, self.p1) / Length(self.p6, self.p4),
            NotCollinear(self.p1, self.p2, self.p3),
        ]

__init__(p1, p2, p3, p4, p5, p6)

Parameters:

Name Type Description Default
p1, p2, p3 Point

First triangle vertices.

required
p4, p5, p6 Point

Second triangle vertices.

required
Source code in pyeuclid/formalization/relation.py
347
348
349
350
351
352
353
354
355
356
def __init__(
    self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
):
    """
    Args:
        p1, p2, p3 (Point): First triangle vertices.
        p4, p5, p6 (Point): Second triangle vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

definition()

Similarity expressed via length ratios and non-collinearity.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
358
359
360
361
362
363
364
365
366
367
368
369
370
def definition(self):
    """Similarity expressed via length ratios and non-collinearity.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) / Length(self.p4, self.p5)
        - Length(self.p2, self.p3) / Length(self.p5, self.p6),
        Length(self.p1, self.p2) / Length(self.p4, self.p5)
        - Length(self.p3, self.p1) / Length(self.p6, self.p4),
        NotCollinear(self.p1, self.p2, self.p3),
    ]

Similar4P

Bases: Relation

Two quadrilaterals are similar.

Source code in pyeuclid/formalization/relation.py
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
class Similar4P(Relation):
    """Two quadrilaterals are similar."""

    def __init__(
        self,
        p1: Point,
        p2: Point,
        p3: Point,
        p4: Point,
        p5: Point,
        p6: Point,
        p7: Point,
        p8: Point,
    ):
        """
        Args:
            p1..p4 (Point): First quadrilateral.
            p5..p8 (Point): Second quadrilateral.
        """
        super().__init__()
        point_map_12 = {p1: p5, p2: p6, p3: p7, p4: p8}
        point_map_21 = {p5: p1, p6: p2, p7: p3, p8: p4}

        sorted_1 = sort_cyclic_points(p1, p2, p3, p4)
        sorted_2 = sort_cyclic_points(p5, p6, p7, p8)
        if compare_names(sorted_1, sorted_2) == 0:
            self.p1, self.p2, self.p3, self.p4 = sorted_1
            self.p5,self.p6,self.p7,self.p8 = point_map_12[self.p1], point_map_12[
                self.p2], point_map_12[self.p3], point_map_12[self.p4]
        else:
            self.p1, self.p2, self.p3, self.p4 = sorted_2
            self.p5, self.p6, self.p7, self.p8 = point_map_21[self.p1], point_map_21[
                self.p2], point_map_21[self.p3], point_map_21[self.p4]

    def definition(self):
        """Similarity expressed via side ratios and angle equalities.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) / Length(self.p5, self.p6)
            - Length(self.p2, self.p3) / Length(self.p6, self.p7),
            Length(self.p1, self.p2) / Length(self.p5, self.p6)
            - Length(self.p3, self.p4) / Length(self.p7, self.p8),
            Length(self.p1, self.p2) / Length(self.p5, self.p6)
            - Length(self.p4, self.p1) / Length(self.p8, self.p5),
            Angle(self.p1, self.p2, self.p3) - Angle(self.p5, self.p6, self.p7),
            Angle(self.p2, self.p3, self.p4) - Angle(self.p6, self.p7, self.p8),
            Angle(self.p3, self.p4, self.p1) - Angle(self.p7, self.p8, self.p5),
            Angle(self.p4, self.p1, self.p2) - Angle(self.p8, self.p5, self.p6),
        ]

__init__(p1, p2, p3, p4, p5, p6, p7, p8)

Parameters:

Name Type Description Default
p1..p4 Point

First quadrilateral.

required
p5..p8 Point

Second quadrilateral.

required
Source code in pyeuclid/formalization/relation.py
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
def __init__(
    self,
    p1: Point,
    p2: Point,
    p3: Point,
    p4: Point,
    p5: Point,
    p6: Point,
    p7: Point,
    p8: Point,
):
    """
    Args:
        p1..p4 (Point): First quadrilateral.
        p5..p8 (Point): Second quadrilateral.
    """
    super().__init__()
    point_map_12 = {p1: p5, p2: p6, p3: p7, p4: p8}
    point_map_21 = {p5: p1, p6: p2, p7: p3, p8: p4}

    sorted_1 = sort_cyclic_points(p1, p2, p3, p4)
    sorted_2 = sort_cyclic_points(p5, p6, p7, p8)
    if compare_names(sorted_1, sorted_2) == 0:
        self.p1, self.p2, self.p3, self.p4 = sorted_1
        self.p5,self.p6,self.p7,self.p8 = point_map_12[self.p1], point_map_12[
            self.p2], point_map_12[self.p3], point_map_12[self.p4]
    else:
        self.p1, self.p2, self.p3, self.p4 = sorted_2
        self.p5, self.p6, self.p7, self.p8 = point_map_21[self.p1], point_map_21[
            self.p2], point_map_21[self.p3], point_map_21[self.p4]

definition()

Similarity expressed via side ratios and angle equalities.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
def definition(self):
    """Similarity expressed via side ratios and angle equalities.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) / Length(self.p5, self.p6)
        - Length(self.p2, self.p3) / Length(self.p6, self.p7),
        Length(self.p1, self.p2) / Length(self.p5, self.p6)
        - Length(self.p3, self.p4) / Length(self.p7, self.p8),
        Length(self.p1, self.p2) / Length(self.p5, self.p6)
        - Length(self.p4, self.p1) / Length(self.p8, self.p5),
        Angle(self.p1, self.p2, self.p3) - Angle(self.p5, self.p6, self.p7),
        Angle(self.p2, self.p3, self.p4) - Angle(self.p6, self.p7, self.p8),
        Angle(self.p3, self.p4, self.p1) - Angle(self.p7, self.p8, self.p5),
        Angle(self.p4, self.p1, self.p2) - Angle(self.p8, self.p5, self.p6),
    ]

Similar5P

Bases: Relation

Two pentagons are similar.

Source code in pyeuclid/formalization/relation.py
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
class Similar5P(Relation):
    """Two pentagons are similar."""

    def __init__(
        self,
        p1: Point,
        p2: Point,
        p3: Point,
        p4: Point,
        p5: Point,
        p6: Point,
        p7: Point,
        p8: Point,
        p9: Point,
        p10: Point,
    ):
        """
        Args:
            p1..p5 (Point): First pentagon.
            p6..p10 (Point): Second pentagon.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5, self.p6, self.p7, self.p8, self.p9, self.p10 = p1, p2, p3, p4, p5, p6, p7, p8, p9, p10

    def definition(self):
        """Similarity expressed via consecutive side ratios and angle equalities.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) / Length(self.p6, self.p7)
            - Length(self.p2, self.p3) / Length(self.p7, self.p8),
            Length(self.p2, self.p3) / Length(self.p7, self.p8)
            - Length(self.p3, self.p4) / Length(self.p8, self.p9),
            Length(self.p3, self.p4) / Length(self.p8, self.p9)
            - Length(self.p4, self.p5) / Length(self.p9, self.p10),
            Length(self.p4, self.p5) / Length(self.p9, self.p10)
            - Length(self.p5, self.p1) / Length(self.p10, self.p6),
            Length(self.p5, self.p1) / Length(self.p10, self.p6)
            - Length(self.p1, self.p2) / Length(self.p6, self.p7),
            Angle(self.p1, self.p2, self.p3) - Angle(self.p6, self.p7, self.p8),
            Angle(self.p2, self.p3, self.p4) - Angle(self.p7, self.p8, self.p9),
            Angle(self.p3, self.p4, self.p5) - Angle(self.p8, self.p9, self.p10),
            Angle(self.p4, self.p5, self.p1) - Angle(self.p9, self.p10, self.p6),
            Angle(self.p5, self.p1, self.p2) - Angle(self.p10, self.p6, self.p7),
        ]

__init__(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10)

Parameters:

Name Type Description Default
p1..p5 Point

First pentagon.

required
p6..p10 Point

Second pentagon.

required
Source code in pyeuclid/formalization/relation.py
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
def __init__(
    self,
    p1: Point,
    p2: Point,
    p3: Point,
    p4: Point,
    p5: Point,
    p6: Point,
    p7: Point,
    p8: Point,
    p9: Point,
    p10: Point,
):
    """
    Args:
        p1..p5 (Point): First pentagon.
        p6..p10 (Point): Second pentagon.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5, self.p6, self.p7, self.p8, self.p9, self.p10 = p1, p2, p3, p4, p5, p6, p7, p8, p9, p10

definition()

Similarity expressed via consecutive side ratios and angle equalities.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
def definition(self):
    """Similarity expressed via consecutive side ratios and angle equalities.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) / Length(self.p6, self.p7)
        - Length(self.p2, self.p3) / Length(self.p7, self.p8),
        Length(self.p2, self.p3) / Length(self.p7, self.p8)
        - Length(self.p3, self.p4) / Length(self.p8, self.p9),
        Length(self.p3, self.p4) / Length(self.p8, self.p9)
        - Length(self.p4, self.p5) / Length(self.p9, self.p10),
        Length(self.p4, self.p5) / Length(self.p9, self.p10)
        - Length(self.p5, self.p1) / Length(self.p10, self.p6),
        Length(self.p5, self.p1) / Length(self.p10, self.p6)
        - Length(self.p1, self.p2) / Length(self.p6, self.p7),
        Angle(self.p1, self.p2, self.p3) - Angle(self.p6, self.p7, self.p8),
        Angle(self.p2, self.p3, self.p4) - Angle(self.p7, self.p8, self.p9),
        Angle(self.p3, self.p4, self.p5) - Angle(self.p8, self.p9, self.p10),
        Angle(self.p4, self.p5, self.p1) - Angle(self.p9, self.p10, self.p6),
        Angle(self.p5, self.p1, self.p2) - Angle(self.p10, self.p6, self.p7),
    ]

State

Mutable state holding points, relations, equations, and goal/solution status.

Source code in pyeuclid/formalization/state.py
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
class State:
    """Mutable state holding points, relations, equations, and goal/solution status."""

    def __init__(self):
        self.goal = None
        self.diagram = None
        self.points = set()
        self.relations = set()
        self.equations = []
        self.lengths = UnionFind()
        self.angles = UnionFind()
        self.var_types = {}
        self.ratios = {}
        self.angle_sums = {}

        self.current_depth = 0
        self.solutions = []
        self.solvers = {}
        self.try_complex = False
        self.silent = False
        self.logger = logging.getLogger(__name__)
        self.set_logger(logging.DEBUG)

    def load_problem(self, conditions=None, goal=None, diagram=None):        
        """Seed the state with initial conditions, goal, and optional diagram.

        Adds relations/equations, infers variable categories, sets the goal, and
        records an optional diagram instance.

        Args:
            conditions (Iterable | None): Relations/equations to seed the state.
            goal (Relation | sympy.Expr | None): Target to satisfy.
            diagram (Diagram | None): Optional diagram object.

        Returns:
            None
        """
        if conditions:
            self.add_relations(conditions)
            old_size = 0
            self.categorize_variable()
            size = len(self.var_types)
            while(size > old_size):
                self.categorize_variable()
                old_size = size
                size = len(self.var_types)
        if goal:
            self.goal = goal
        if diagram:
            self.diagram = diagram

    def set_logger(self, level):
        """Configure the state logger; rank-aware for MPI runs.

        Args:
            level (int): Logging level (e.g., logging.INFO).

        Returns:
            None
        """
        self.logger.setLevel(level)
        rank = os.environ.get("OMPI_COMM_WORLD_RANK", None)
        if not len(self.logger.handlers):
            handler = logging.StreamHandler(sys.stdout)
            if rank is None:
                formatter = logging.Formatter(
                    '%(levelname)s - %(message)s')  # %(asctime)s - %(name)s -
            else:
                formatter = logging.Formatter(
                    rank+' %(levelname)s - %(message)s')
            handler.setFormatter(formatter)
            self.logger.addHandler(handler)

    def add_relations(self, relations):
        """Add one or more relations/equations, expanding definitions as needed.

        Args:
            relations: Relation or sympy expression or iterable of them. Composite
                relations with a `definition()` are expanded before insertion.

        Returns:
            None
        """
        if not isinstance(relations, (tuple, list, set)):
            relations = [relations]
        for item in relations:
            if hasattr(item, "definition") and not item.negated:
                self.add_relations(item.definition())
            else:
                if isinstance(item, Relation):
                    self.add_relation(item)
                else:
                    self.add_equation(item)

    def add_relation(self, relation):
        """Insert a relation, ensuring its points are tracked."""
        if relation in self.relations:
            return
        points = relation.get_points()
        for p in points:
            self.add_point(p)
        self.relations.add(relation)

    def add_point(self, p):
        """Track a new point and initialize length union-find edges to existing points.

        Args:
            p (Point): Point to register.

        Returns:
            None
        """
        if not p in self.points:
            for point in self.points:
                self.lengths.add(Length(point, p))
            self.points.add(p)

    def add_equation(self, equation):
        """Insert an equation, tracing its depth and registering involved symbols.

        Args:
            equation (sympy.Expr): Equation to add.

        Returns:
            None
        """
        # allow redundant equations for neat proofs
        equation = Traced(equation, depth=self.current_depth)
        for item in self.equations:
            if equation.expr - item.expr == 0:
                return
        points, quantities = get_points_and_symbols(equation)
        for p in points:
            self.add_point(p)
        unionfind = None
        for quantity in quantities:
            if "Angle" in str(quantity):
                unionfind = self.angles
                unionfind.add(quantity)
            elif "Length" in str(quantity):
                unionfind = self.lengths
                unionfind.add(quantity)
        self.equations.append(equation)

    def categorize_variable(self):
        """Infer variable types (Angle/Length) from existing equations.

        Returns:
            None
        """
        angle_linear, length_linear, length_ratio, others = classify_equations(self.equations, self.var_types)
        for eq in self.equations:
            if "Variable" not in str(eq):
                continue
            _, entities = get_points_and_symbols(eq)
            label = None
            if eq in angle_linear and ("Angle" in str(eq) or "pi" in str(eq)):
                label = "Angle"
            elif eq in length_linear and "Length" in str(eq):
                label = "Length"
            elif eq in length_ratio and "Length" in str(eq):
                label = "Length"
            else:
                continue
            for entity in entities:
                if label is not None:
                    if entity in self.var_types:
                        if self.var_types[entity] is None: # dimensionless variable
                            continue
                        elif self.var_types[entity] != label:
                            self.var_types[entity] = None
                    else:
                        self.var_types[entity] = label

    def load_problem_from_text(self, text, diagram_path=None, resample=False):
        """Parse a textual benchmark instance and populate state+diagram.

        Builds a diagram, verifies numerical consistency with the goal, and
        populates points/relations deduced from construction rules and sampling.

        Args:
            text (str): Problem description string.
            diagram_path (str | None): Optional path for saving diagram.
            resample (bool): Force resampling even if cache exists.

        Returns:
            None
        Raises:
            Exception: If a consistent diagram cannot be generated in allotted attempts.
        """
        constructions_list = get_constructions_list_from_text(text)
        goal = get_goal_from_text(text)

        diagram = Diagram(constructions_list, diagram_path, resample=resample)
        satisfied, satisfied_goal = diagram.numerical_check_goal(goal)

        for _ in range(MAX_DIAGRAM_ATTEMPTS):
            if satisfied:
                break
            diagram = Diagram(constructions_list, diagram_path, resample=True)
            satisfied, satisfied_goal = diagram.numerical_check_goal(goal)

        if not satisfied:
            raise Exception(f"Failed to satisfy goal after {MAX_DIAGRAM_ATTEMPTS} attempts.")

        self.diagram = diagram
        self.goal = satisfied_goal
        # self.diagram.show()

        for constructions in constructions_list:
            for construction in constructions:
                for p in construction.constructed_points():
                    self.add_point(p)

                relations = construction.conclusions()
                if isinstance(relations, tuple):
                    if self.diagram.numerical_check(relations[0]):
                        assert not self.diagram.numerical_check(relations[1])
                        self.add_relations(relations[0])
                    else:
                        assert self.diagram.numerical_check(relations[1])
                        self.add_relations(relations[1])
                else:
                    self.add_relations(relations)

        for perm in permutations(self.points, 3):
            between_relation = Between(*perm)
            if self.diagram.numerical_check(between_relation):
                self.add_relations(between_relation)

            notcollinear_relation = NotCollinear(*perm)
            if self.diagram.numerical_check(notcollinear_relation):
                self.add_relations(notcollinear_relation)

        for perm in permutations(self.points, 4):
            sameside_relation = SameSide(*perm)
            if self.diagram.numerical_check(sameside_relation):
                self.add_relations(sameside_relation)

            oppositeside_relation = OppositeSide(*perm)
            if self.diagram.numerical_check(oppositeside_relation):
                self.add_relations(oppositeside_relation)

    def complete(self):
        """Return solved status: True/expr if goal satisfied, else None.

        Returns:
            bool | sympy.Expr | None: True or numeric expression if solved; otherwise None.
        """
        if isinstance(self.goal, Relation):
            if self.check_conditions(self.goal):
                return True
            else:
                return None
        else:
            assert isinstance(self.goal, sympy.core.expr.Expr)
            solution = self.simplify_equation(self.goal)
            if len(solution.free_symbols) == 0:
                return solution
            return None

    def simplify_equation(self, expr, depth=None):
        """Substitute solved variables into an expression.

        Args:
            expr (sympy.Expr): Expression to simplify.
            depth (int | None): Solution depth to use; defaults to latest.

        Returns:
            sympy.Expr: Simplified expression.
        """
        if depth is None:
            depth = len(self.solutions) - 1
        solved_vars = self.solutions[depth]
        expr = getattr(expr, "expr", expr)
        for symbol in expr.free_symbols:
            if symbol in solved_vars:
                value = solved_vars[symbol].expr
                expr = expr.subs(symbol, value)
        return expr

    def check_conditions(self, conditions):
        """Verify that a set of relations/equations holds in the current state.

        Expands relation definitions, checks presence in `relations`, and
        simplifies equations via solved variables.

        Args:
            conditions (Iterable | Relation | sympy.Expr): Conditions to verify.

        Returns:
            bool: True if all conditions hold; False otherwise.
        """
        if not type(conditions) in (list, tuple, set):
            conditions = [conditions]
        conditional_relations, conditional_equations = set(), []
        i = 0
        while i < len(conditions):
            item = conditions[i]
            if isinstance(item, Equal):
                if not ((item.v1 == item.v2) ^ item.negated):
                    return False
            elif hasattr(item, "definition") and not item.negated:
                unrolled = item.definition()
                if not (isinstance(unrolled, tuple) or isinstance(unrolled, list)):
                    unrolled = unrolled,
                conditions += unrolled
            # auxillary predicate for canonical ordering of inference rule params, does not used for checking
            elif isinstance(item, Lt):
                pass
            elif isinstance(item, Between):
                if item.negated:
                    if Not(item) in self.relations:
                        return False
                else:
                    if not item in self.relations:
                        return False
                    if item.p1 == item.p2 or item.p2 == item.p3 or item.p3 == item.p1:
                        return False
            elif isinstance(item, Relation):
                if isinstance(item, Collinear) and (item.p1 == item.p2 or item.p2 == item.p3 or item.p3 == item.p1):
                    pass
                elif not item in self.relations:
                    return False
            else:
                conditional_equations.append(self.simplify_equation(item))
            i += 1
        equation_satisfied = check_equalities(conditional_equations)
        return equation_satisfied

add_equation(equation)

Insert an equation, tracing its depth and registering involved symbols.

Parameters:

Name Type Description Default
equation Expr

Equation to add.

required

Returns:

Type Description

None

Source code in pyeuclid/formalization/state.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
def add_equation(self, equation):
    """Insert an equation, tracing its depth and registering involved symbols.

    Args:
        equation (sympy.Expr): Equation to add.

    Returns:
        None
    """
    # allow redundant equations for neat proofs
    equation = Traced(equation, depth=self.current_depth)
    for item in self.equations:
        if equation.expr - item.expr == 0:
            return
    points, quantities = get_points_and_symbols(equation)
    for p in points:
        self.add_point(p)
    unionfind = None
    for quantity in quantities:
        if "Angle" in str(quantity):
            unionfind = self.angles
            unionfind.add(quantity)
        elif "Length" in str(quantity):
            unionfind = self.lengths
            unionfind.add(quantity)
    self.equations.append(equation)

add_point(p)

Track a new point and initialize length union-find edges to existing points.

Parameters:

Name Type Description Default
p Point

Point to register.

required

Returns:

Type Description

None

Source code in pyeuclid/formalization/state.py
118
119
120
121
122
123
124
125
126
127
128
129
130
def add_point(self, p):
    """Track a new point and initialize length union-find edges to existing points.

    Args:
        p (Point): Point to register.

    Returns:
        None
    """
    if not p in self.points:
        for point in self.points:
            self.lengths.add(Length(point, p))
        self.points.add(p)

add_relation(relation)

Insert a relation, ensuring its points are tracked.

Source code in pyeuclid/formalization/state.py
109
110
111
112
113
114
115
116
def add_relation(self, relation):
    """Insert a relation, ensuring its points are tracked."""
    if relation in self.relations:
        return
    points = relation.get_points()
    for p in points:
        self.add_point(p)
    self.relations.add(relation)

add_relations(relations)

Add one or more relations/equations, expanding definitions as needed.

Parameters:

Name Type Description Default
relations

Relation or sympy expression or iterable of them. Composite relations with a definition() are expanded before insertion.

required

Returns:

Type Description

None

Source code in pyeuclid/formalization/state.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
def add_relations(self, relations):
    """Add one or more relations/equations, expanding definitions as needed.

    Args:
        relations: Relation or sympy expression or iterable of them. Composite
            relations with a `definition()` are expanded before insertion.

    Returns:
        None
    """
    if not isinstance(relations, (tuple, list, set)):
        relations = [relations]
    for item in relations:
        if hasattr(item, "definition") and not item.negated:
            self.add_relations(item.definition())
        else:
            if isinstance(item, Relation):
                self.add_relation(item)
            else:
                self.add_equation(item)

categorize_variable()

Infer variable types (Angle/Length) from existing equations.

Returns:

Type Description

None

Source code in pyeuclid/formalization/state.py
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
def categorize_variable(self):
    """Infer variable types (Angle/Length) from existing equations.

    Returns:
        None
    """
    angle_linear, length_linear, length_ratio, others = classify_equations(self.equations, self.var_types)
    for eq in self.equations:
        if "Variable" not in str(eq):
            continue
        _, entities = get_points_and_symbols(eq)
        label = None
        if eq in angle_linear and ("Angle" in str(eq) or "pi" in str(eq)):
            label = "Angle"
        elif eq in length_linear and "Length" in str(eq):
            label = "Length"
        elif eq in length_ratio and "Length" in str(eq):
            label = "Length"
        else:
            continue
        for entity in entities:
            if label is not None:
                if entity in self.var_types:
                    if self.var_types[entity] is None: # dimensionless variable
                        continue
                    elif self.var_types[entity] != label:
                        self.var_types[entity] = None
                else:
                    self.var_types[entity] = label

check_conditions(conditions)

Verify that a set of relations/equations holds in the current state.

Expands relation definitions, checks presence in relations, and simplifies equations via solved variables.

Parameters:

Name Type Description Default
conditions Iterable | Relation | Expr

Conditions to verify.

required

Returns:

Name Type Description
bool

True if all conditions hold; False otherwise.

Source code in pyeuclid/formalization/state.py
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
def check_conditions(self, conditions):
    """Verify that a set of relations/equations holds in the current state.

    Expands relation definitions, checks presence in `relations`, and
    simplifies equations via solved variables.

    Args:
        conditions (Iterable | Relation | sympy.Expr): Conditions to verify.

    Returns:
        bool: True if all conditions hold; False otherwise.
    """
    if not type(conditions) in (list, tuple, set):
        conditions = [conditions]
    conditional_relations, conditional_equations = set(), []
    i = 0
    while i < len(conditions):
        item = conditions[i]
        if isinstance(item, Equal):
            if not ((item.v1 == item.v2) ^ item.negated):
                return False
        elif hasattr(item, "definition") and not item.negated:
            unrolled = item.definition()
            if not (isinstance(unrolled, tuple) or isinstance(unrolled, list)):
                unrolled = unrolled,
            conditions += unrolled
        # auxillary predicate for canonical ordering of inference rule params, does not used for checking
        elif isinstance(item, Lt):
            pass
        elif isinstance(item, Between):
            if item.negated:
                if Not(item) in self.relations:
                    return False
            else:
                if not item in self.relations:
                    return False
                if item.p1 == item.p2 or item.p2 == item.p3 or item.p3 == item.p1:
                    return False
        elif isinstance(item, Relation):
            if isinstance(item, Collinear) and (item.p1 == item.p2 or item.p2 == item.p3 or item.p3 == item.p1):
                pass
            elif not item in self.relations:
                return False
        else:
            conditional_equations.append(self.simplify_equation(item))
        i += 1
    equation_satisfied = check_equalities(conditional_equations)
    return equation_satisfied

complete()

Return solved status: True/expr if goal satisfied, else None.

Returns:

Type Description

bool | sympy.Expr | None: True or numeric expression if solved; otherwise None.

Source code in pyeuclid/formalization/state.py
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
def complete(self):
    """Return solved status: True/expr if goal satisfied, else None.

    Returns:
        bool | sympy.Expr | None: True or numeric expression if solved; otherwise None.
    """
    if isinstance(self.goal, Relation):
        if self.check_conditions(self.goal):
            return True
        else:
            return None
    else:
        assert isinstance(self.goal, sympy.core.expr.Expr)
        solution = self.simplify_equation(self.goal)
        if len(solution.free_symbols) == 0:
            return solution
        return None

load_problem(conditions=None, goal=None, diagram=None)

Seed the state with initial conditions, goal, and optional diagram.

Adds relations/equations, infers variable categories, sets the goal, and records an optional diagram instance.

Parameters:

Name Type Description Default
conditions Iterable | None

Relations/equations to seed the state.

None
goal Relation | Expr | None

Target to satisfy.

None
diagram Diagram | None

Optional diagram object.

None

Returns:

Type Description

None

Source code in pyeuclid/formalization/state.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
def load_problem(self, conditions=None, goal=None, diagram=None):        
    """Seed the state with initial conditions, goal, and optional diagram.

    Adds relations/equations, infers variable categories, sets the goal, and
    records an optional diagram instance.

    Args:
        conditions (Iterable | None): Relations/equations to seed the state.
        goal (Relation | sympy.Expr | None): Target to satisfy.
        diagram (Diagram | None): Optional diagram object.

    Returns:
        None
    """
    if conditions:
        self.add_relations(conditions)
        old_size = 0
        self.categorize_variable()
        size = len(self.var_types)
        while(size > old_size):
            self.categorize_variable()
            old_size = size
            size = len(self.var_types)
    if goal:
        self.goal = goal
    if diagram:
        self.diagram = diagram

load_problem_from_text(text, diagram_path=None, resample=False)

Parse a textual benchmark instance and populate state+diagram.

Builds a diagram, verifies numerical consistency with the goal, and populates points/relations deduced from construction rules and sampling.

Parameters:

Name Type Description Default
text str

Problem description string.

required
diagram_path str | None

Optional path for saving diagram.

None
resample bool

Force resampling even if cache exists.

False

Returns:

Type Description

None

Raises: Exception: If a consistent diagram cannot be generated in allotted attempts.

Source code in pyeuclid/formalization/state.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
def load_problem_from_text(self, text, diagram_path=None, resample=False):
    """Parse a textual benchmark instance and populate state+diagram.

    Builds a diagram, verifies numerical consistency with the goal, and
    populates points/relations deduced from construction rules and sampling.

    Args:
        text (str): Problem description string.
        diagram_path (str | None): Optional path for saving diagram.
        resample (bool): Force resampling even if cache exists.

    Returns:
        None
    Raises:
        Exception: If a consistent diagram cannot be generated in allotted attempts.
    """
    constructions_list = get_constructions_list_from_text(text)
    goal = get_goal_from_text(text)

    diagram = Diagram(constructions_list, diagram_path, resample=resample)
    satisfied, satisfied_goal = diagram.numerical_check_goal(goal)

    for _ in range(MAX_DIAGRAM_ATTEMPTS):
        if satisfied:
            break
        diagram = Diagram(constructions_list, diagram_path, resample=True)
        satisfied, satisfied_goal = diagram.numerical_check_goal(goal)

    if not satisfied:
        raise Exception(f"Failed to satisfy goal after {MAX_DIAGRAM_ATTEMPTS} attempts.")

    self.diagram = diagram
    self.goal = satisfied_goal
    # self.diagram.show()

    for constructions in constructions_list:
        for construction in constructions:
            for p in construction.constructed_points():
                self.add_point(p)

            relations = construction.conclusions()
            if isinstance(relations, tuple):
                if self.diagram.numerical_check(relations[0]):
                    assert not self.diagram.numerical_check(relations[1])
                    self.add_relations(relations[0])
                else:
                    assert self.diagram.numerical_check(relations[1])
                    self.add_relations(relations[1])
            else:
                self.add_relations(relations)

    for perm in permutations(self.points, 3):
        between_relation = Between(*perm)
        if self.diagram.numerical_check(between_relation):
            self.add_relations(between_relation)

        notcollinear_relation = NotCollinear(*perm)
        if self.diagram.numerical_check(notcollinear_relation):
            self.add_relations(notcollinear_relation)

    for perm in permutations(self.points, 4):
        sameside_relation = SameSide(*perm)
        if self.diagram.numerical_check(sameside_relation):
            self.add_relations(sameside_relation)

        oppositeside_relation = OppositeSide(*perm)
        if self.diagram.numerical_check(oppositeside_relation):
            self.add_relations(oppositeside_relation)

set_logger(level)

Configure the state logger; rank-aware for MPI runs.

Parameters:

Name Type Description Default
level int

Logging level (e.g., logging.INFO).

required

Returns:

Type Description

None

Source code in pyeuclid/formalization/state.py
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
def set_logger(self, level):
    """Configure the state logger; rank-aware for MPI runs.

    Args:
        level (int): Logging level (e.g., logging.INFO).

    Returns:
        None
    """
    self.logger.setLevel(level)
    rank = os.environ.get("OMPI_COMM_WORLD_RANK", None)
    if not len(self.logger.handlers):
        handler = logging.StreamHandler(sys.stdout)
        if rank is None:
            formatter = logging.Formatter(
                '%(levelname)s - %(message)s')  # %(asctime)s - %(name)s -
        else:
            formatter = logging.Formatter(
                rank+' %(levelname)s - %(message)s')
        handler.setFormatter(formatter)
        self.logger.addHandler(handler)

simplify_equation(expr, depth=None)

Substitute solved variables into an expression.

Parameters:

Name Type Description Default
expr Expr

Expression to simplify.

required
depth int | None

Solution depth to use; defaults to latest.

None

Returns:

Type Description

sympy.Expr: Simplified expression.

Source code in pyeuclid/formalization/state.py
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
def simplify_equation(self, expr, depth=None):
    """Substitute solved variables into an expression.

    Args:
        expr (sympy.Expr): Expression to simplify.
        depth (int | None): Solution depth to use; defaults to latest.

    Returns:
        sympy.Expr: Simplified expression.
    """
    if depth is None:
        depth = len(self.solutions) - 1
    solved_vars = self.solutions[depth]
    expr = getattr(expr, "expr", expr)
    for symbol in expr.free_symbols:
        if symbol in solved_vars:
            value = solved_vars[symbol].expr
            expr = expr.subs(symbol, value)
    return expr

construct_angle_bisector

Bases: ConstructionRule

Construct the bisector point X of angle ABC.

Source code in pyeuclid/formalization/construction_rule.py
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
@register("AG")
class construct_angle_bisector(ConstructionRule):
    """Construct the bisector point X of angle ABC."""
    def __init__(self, x, a, b, c):
        self.x, self.a, self.b, self.c = x, a, b, c

    def arguments(self):
        return [self.a, self.b, self.c]

    def constructed_points(self):
        return [self.x]

    def conditions(self):
        return [NotCollinear(self.a, self.b, self.c)]

    def conclusions(self):
        return [Angle(self.a, self.b, self.x) - Angle(self.x, self.b, self.c)]

construct_angle_mirror

Bases: ConstructionRule

Construct point X as the mirror of BA across BC.

Source code in pyeuclid/formalization/construction_rule.py
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
@register("AG")
class construct_angle_mirror(ConstructionRule):
    """Construct point X as the mirror of BA across BC."""
    def __init__(self, x, a, b, c):
        self.x, self.a, self.b, self.c = x, a, b, c

    def arguments(self):
        return [self.a, self.b, self.c]

    def constructed_points(self):
        return [self.x]

    def conditions(self):
        return [NotCollinear(self.a, self.b, self.c)]

    def conclusions(self):
        return [Angle(self.a, self.b, self.c) - Angle(self.c, self.b, self.x)]

construct_circle

Bases: ConstructionRule

Construct circle center X equidistant from A, B, C.

Source code in pyeuclid/formalization/construction_rule.py
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
@register("AG")
class construct_circle(ConstructionRule):
    """Construct circle center X equidistant from A, B, C."""
    def __init__(self, x, a, b, c):
        self.x, self.a, self.b, self.c = x, a, b, c

    def arguments(self):
        return [self.a, self.b, self.c]

    def constructed_points(self):
        return [self.x]

    def conditions(self):
        return [NotCollinear(self.a, self.b, self.c)]

    def conclusions(self):
        return [
            Length(self.x, self.a) - Length(self.x, self.b),
            Length(self.x, self.b) - Length(self.x, self.c),
        ]

construct_circumcenter

Bases: ConstructionRule

Construct circumcenter X of triangle ABC.

Source code in pyeuclid/formalization/construction_rule.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
@register("AG")
class construct_circumcenter(ConstructionRule):
    """Construct circumcenter X of triangle ABC."""
    def __init__(self, x, a, b, c):
        self.x, self.a, self.b, self.c = x, a, b, c

    def arguments(self):
        return [self.a, self.b, self.c]

    def constructed_points(self):
        return [self.x]

    def conditions(self):
        return [NotCollinear(self.a, self.b, self.c)]

    def conclusions(self):
        return [
            Length(self.x, self.a) - Length(self.x, self.b),
            Length(self.x, self.b) - Length(self.x, self.c),
        ]

construct_eq_quadrangle

Bases: ConstructionRule

Construct quadrilateral ABCD with equal diagonals.

Source code in pyeuclid/formalization/construction_rule.py
141
142
143
144
145
146
147
148
149
150
151
152
153
@register("AG")
class construct_eq_quadrangle(ConstructionRule):
    """Construct quadrilateral ABCD with equal diagonals."""
    def __init__(self, a, b, c, d):
        self.a, self.b, self.c, self.d = a, b, c, d

    def constructed_points(self):
        return [self.a, self.b, self.c, self.d]

    def conclusions(self):
        return [
            Length(self.a, self.d) - Length(self.b, self.c)
        ]

construct_eq_trapezoid

Bases: ConstructionRule

Construct isosceles trapezoid ABCD (AB ∥ CD).

Source code in pyeuclid/formalization/construction_rule.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
@register("AG")
class construct_eq_trapezoid(ConstructionRule):
    """Construct isosceles trapezoid ABCD (AB ∥ CD)."""
    def __init__(self, a, b, c, d):
        self.a, self.b, self.c, self.d = a, b, c, d

    def constructed_points(self):
        return [self.a, self.b, self.c, self.d]

    def conclusions(self):
        return [
            Length(self.a, self.d) - Length(self.b, self.c),
            Parallel(self.a, self.b, self.c, self.d),
            Angle(self.d, self.a, self.b) - Angle(self.a, self.b, self.c),
            Angle(self.b, self.c, self.d) - Angle(self.c, self.d, self.a),
        ]

construct_eq_triangle

Bases: ConstructionRule

Construct equilateral triangle with vertex X and base BC.

Source code in pyeuclid/formalization/construction_rule.py
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
@register("AG")
class construct_eq_triangle(ConstructionRule):
    """Construct equilateral triangle with vertex X and base BC."""
    def __init__(self, x, b, c):
        self.x, self.b, self.c = x, b, c

    def arguments(self):
        return [self.b, self.c]

    def constructed_points(self):
        return [self.x]

    def conditions(self):
        return [Different(self.b, self.c)]

    def conclusions(self):
        return [
            Length(self.x, self.b) - Length(self.b, self.c),
            Length(self.b, self.c) - Length(self.c, self.x),
            Angle(self.x, self.b, self.c) - Angle(self.b, self.c, self.x),
            Angle(self.c, self.x, self.b) - Angle(self.x, self.b, self.c),
        ]

construct_eqangle2

Bases: ConstructionRule

Construct X so that angle ABX equals angle XCB.

Source code in pyeuclid/formalization/construction_rule.py
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
@register("AG")
class construct_eqangle2(ConstructionRule):
    """Construct X so that angle ABX equals angle XCB."""
    def __init__(self, x, a, b, c):
        self.x, self.a, self.b, self.c = x, a, b, c

    def arguments(self):
        return [self.a, self.b, self.c]

    def constructed_points(self):
        return [self.x]

    def conditions(self):
        return [NotCollinear(self.a, self.b, self.c)]

    def conclusions(self):
        return [Angle(self.b, self.a, self.x) - Angle(self.x, self.c, self.b)]

register

Decorator that registers an inference rule class into named rule sets.

Source code in pyeuclid/engine/inference_rule.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class register():
    """Decorator that registers an inference rule class into named rule sets."""

    def __init__(self, *annotations):
        self.annotations = annotations

    def __call__(self, cls):
        for item in self.annotations:
            if not item in inference_rule_sets:
                inference_rule_sets[item] = [cls]
            else:
                inference_rule_sets[item].append(cls)

        def expanded_condition(self):
            lst = expand_definition(self._condition())
            return lst

        def expanded_conclusion(self):
            lst = expand_definition(self._conclusion())
            result = []
            for item in lst:
                if isinstance(item, sympy.core.numbers.Zero):
                    continue
                result.append(item)
            return result
        cls._condition = cls.condition
        cls._conclusion = cls.conclusion
        cls.condition = expanded_condition
        cls.conclusion = expanded_conclusion
        return cls

Angle(p1, p2, p3)

Symbolic angle at p2.

Returns:

Type Description

sympy.Symbol: Non-negative angle symbol.

Source code in pyeuclid/formalization/relation.py
108
109
110
111
112
113
114
115
def Angle(p1: Point, p2: Point, p3: Point):
    """Symbolic angle at p2.

    Returns:
        sympy.Symbol: Non-negative angle symbol.
    """
    p1, p3 = sort_points(p1, p3)
    return Symbol(f"Angle_{p1}_{p2}_{p3}", non_negative=True)

Area(*ps)

Symbolic polygonal area over an ordered point cycle.

Returns:

Type Description

sympy.Symbol: Positive area symbol.

Source code in pyeuclid/formalization/relation.py
128
129
130
131
132
133
134
135
def Area(*ps: list[Point]):
    """Symbolic polygonal area over an ordered point cycle.

    Returns:
        sympy.Symbol: Positive area symbol.
    """
    ps = sort_cyclic_points(*ps)
    return Symbol("_".join(["Area"] + [str(item) for item in ps]), positive=True)

Length(p1, p2)

Symbolic length between two points.

Returns:

Type Description

sympy.Symbol: Positive length symbol.

Source code in pyeuclid/formalization/relation.py
118
119
120
121
122
123
124
125
def Length(p1: Point, p2: Point):
    """Symbolic length between two points.

    Returns:
        sympy.Symbol: Positive length symbol.
    """
    p1, p2 = sort_points(p1, p2)
    return Symbol(f"Length_{str(p1)}_{str(p2)}", positive=True)

Not(p)

Return a negated shallow copy of a relation.

Source code in pyeuclid/formalization/relation.py
101
102
103
104
105
def Not(p: Relation) -> Relation:
    """Return a negated shallow copy of a relation."""
    other = copy.copy(p)
    other.negate()
    return other

Variable(name)

Free symbolic variable placeholder.

Returns:

Type Description

sympy.Symbol: Dimensionless variable symbol.

Source code in pyeuclid/formalization/relation.py
138
139
140
141
142
143
144
def Variable(name: str):
    """Free symbolic variable placeholder.

    Returns:
        sympy.Symbol: Dimensionless variable symbol.
    """
    return Symbol(f"Variable_{name}")

circle_circle_intersection(c1, c2)

Returns a pair of Points as intersections of c1 and c2.

Source code in pyeuclid/formalization/numericals.py
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
def circle_circle_intersection(c1: Circle, c2: Circle) -> tuple[Point, Point]:
    """Returns a pair of Points as intersections of c1 and c2."""
    # circle 1: (x0, y0), radius r0
    # circle 2: (x1, y1), radius r1
    x0, y0, r0 = c1.center.x, c1.center.y, c1.radius
    x1, y1, r1 = c2.center.x, c2.center.y, c2.radius

    d = math.sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2)
    if d == 0:
        raise Exception()

    a = (r0**2 - r1**2 + d**2) / (2 * d)
    h = r0**2 - a**2
    if h < 0:
        raise Exception()
    h = np.sqrt(h)
    x2 = x0 + a * (x1 - x0) / d
    y2 = y0 + a * (y1 - y0) / d
    x3 = x2 + h * (y1 - y0) / d
    y3 = y2 - h * (x1 - x0) / d
    x4 = x2 - h * (y1 - y0) / d
    y4 = y2 + h * (x1 - x0) / d

    return Point(x3, y3), Point(x4, y4)

get_constructions_list_from_text(text)

Parse the constructions section of a text instance into rule objects.

Parameters:

Name Type Description Default
text str

Full benchmark line containing constructions and goal separated by ' ? '.

required

Returns:

Type Description

list[list[ConstructionRule]]: Nested list of construction batches.

Source code in pyeuclid/formalization/translation.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def get_constructions_list_from_text(text):
    """Parse the constructions section of a text instance into rule objects.

    Args:
        text (str): Full benchmark line containing constructions and goal separated by ' ? '.

    Returns:
        list[list[ConstructionRule]]: Nested list of construction batches.
    """
    parts = text.split(' ? ')
    constructions_text_list = parts[0].split('; ')
    constructions_list = []

    for constructions_text in constructions_text_list:
        constructions_text = constructions_text.split(' = ')[1]
        construction_text_list = constructions_text.split(', ')
        constructions = []
        for construction_text in construction_text_list:
            construction_text = construction_text.split(' ')
            rule_name = construction_text[0]
            arg_names = [name.replace('_', '') for name in construction_text[1:]]
            rule = globals()['construct_'+rule_name]
            args = [float(arg_name) if is_float(arg_name) else Point(arg_name) for arg_name in arg_names]
            construction = rule(*args)
            constructions.append(construction)
        constructions_list.append(constructions)

    return constructions_list

get_goal_from_text(text)

Parse the goal portion of a text instance into a Relation or expression.

Parameters:

Name Type Description Default
text str

Full benchmark line containing constructions and goal.

required

Returns:

Type Description

Relation | sympy.Expr | tuple[Relation, Relation] | None: Parsed goal or None.

Source code in pyeuclid/formalization/translation.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
def get_goal_from_text(text):
    """Parse the goal portion of a text instance into a Relation or expression.

    Args:
        text (str): Full benchmark line containing constructions and goal.

    Returns:
        Relation | sympy.Expr | tuple[Relation, Relation] | None: Parsed goal or None.
    """
    parts = text.split(' ? ')
    goal_text = parts[1] if len(parts) > 1 else None
    goal = None
    if goal_text:
        goal_text = goal_text.split(' ')
        goal_name = goal_text[0]
        arg_names = [name.replace('_', '') for name in goal_text[1:]]
        args = [Point(arg_name) for arg_name in arg_names]
        if goal_name == 'cong':
            goal = Length(*args[:2]) - Length(*args[2:])
        elif goal_name == 'cyclic':
            goal = Concyclic(*args)
        elif goal_name == 'coll':
            goal = Collinear(*args)
        elif goal_name == 'perp':
            goal = Perpendicular(*args)
        elif goal_name == 'para':
            goal = Parallel(*args)
        elif goal_name == 'eqratio':
            goal = Length(*args[:2])/Length(*args[2:4]) - Length(*args[4:6])/Length(*args[6:8])
        elif goal_name == 'eqangle':
            def extract_angle(points):
                count = Counter(points)
                repeating = next(p for p, c in count.items() if c == 2)
                singles = [p for p, c in count.items() if c == 1]
                return singles[0], repeating, singles[1]
            angle1 = Angle(*extract_angle(args[:4]))
            angle2 = Angle(*extract_angle(args[4:]))
            # The goal may involve either equal angles or supplementary angles
            goal = (angle1 - angle2, angle1 + angle2 - pi)
        elif goal_name == 'midp':
            goal = Midpoint(*args)
        elif goal_name == 'simtri':
            goal = Similar(*args)
        elif goal_name == 'contri':
            goal = Congruent(*args)

    return goal

line_circle_intersection(line, circle)

Returns a pair of points as intersections of line and circle.

Source code in pyeuclid/formalization/numericals.py
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
def line_circle_intersection(line: Line, circle: Circle) -> tuple[Point, Point]:
    """Returns a pair of points as intersections of line and circle."""
    a, b, c = line.coefficients
    r = float(circle.radius)
    center = circle.center
    p, q = center.x, center.y

    if b == 0:
        x = -c / a
        x_p = x - p
        x_p2 = x_p * x_p
        y = solve_quad(1, -2 * q, q * q + x_p2 - r * r)
        if y is None:
            raise Exception()
        y1, y2 = y
        return (Point(x, y1), Point(x, y2))

    if a == 0:
        y = -c / b
        y_q = y - q
        y_q2 = y_q * y_q
        x = solve_quad(1, -2 * p, p * p + y_q2 - r * r)
        if x is None:
            raise Exception()
        x1, x2 = x
        return (Point(x1, y), Point(x2, y))

    c_ap = c + a * p
    a2 = a * a
    y = solve_quad(
        a2 + b * b, 2 * (b * c_ap - a2 * q), c_ap * c_ap + a2 * (q * q - r * r)
    )
    if y is None:
        raise Exception()
    y1, y2 = y

    return Point(-(b * y1 + c) / a, y1), Point(-(b * y2 + c) / a, y2)

parse_texts_from_file(file_name)

Load every other line from a benchmark file as a problem description.

Parameters:

Name Type Description Default
file_name str

Path to benchmark text file.

required

Returns:

Type Description

list[str]: Problem description strings.

Source code in pyeuclid/formalization/translation.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
def parse_texts_from_file(file_name):
    """Load every other line from a benchmark file as a problem description.

    Args:
        file_name (str): Path to benchmark text file.

    Returns:
        list[str]: Problem description strings.
    """
    with open(file_name, "r") as f:
        lines = f.readlines()

    texts = [lines[i].strip() for i in range(1, len(lines), 2)]
    return texts

random_rfss(*points)

Random rotate-flip-scale-shift a point cloud.

Source code in pyeuclid/formalization/numericals.py
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
def random_rfss(*points: list[Point]) -> list[Point]:
    """Random rotate-flip-scale-shift a point cloud."""
    # center point cloud.
    average = sum(points, Point(0.0, 0.0)) * (1.0 / len(points))
    points = [p - average for p in points]

    # rotate
    ang = unif(0.0, 2 * np.pi)
    sin, cos = np.sin(ang), np.cos(ang)
    # scale and shift
    scale = unif(0.5, 2.0)
    shift = Point(unif(-1, 1), unif(-1, 1))
    points = [p.rotate(sin, cos) * scale + shift for p in points]

    # randomly flip
    if np.random.rand() < 0.5:
        points = [p.flip() for p in points]

    return points

solve_quad(a, b, c)

Solve a x^2 + bx + c = 0.

Source code in pyeuclid/formalization/numericals.py
513
514
515
516
517
518
519
520
521
def solve_quad(a: float, b: float, c: float) -> tuple[float, float]:
    """Solve a x^2 + bx + c = 0."""
    a = 2 * a
    d = b * b - 2 * a * c
    if d < 0:
        return None  # the caller should expect this result.

    y = math.sqrt(d)
    return (-b - y) / a, (-b + y) / a

Algebraic solver for geometric equations.

Manages sympy-based elimination, simplification, and solution extraction for angle/length variables tracked in the shared State. Computes equivalence classes for ratios and angle sums to aid later inference.

AlgebraicSystem

Symbolic equation processor for geometric variables.

Source code in pyeuclid/engine/algebraic_system.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
class AlgebraicSystem:
    """Symbolic equation processor for geometric variables."""

    def __init__(self, state):
        """Create an algebraic system bound to a `State`.

        Args:
            state (State): Shared state containing equations and solved variables.
        """
        self.state = state

    def process_equation(self, eqn, check=False):
        """Simplify an equation by dropping small factors and factoring.

        Args:
            eqn (sympy.Expr): Expression to simplify.
            check (bool): If True, raise on degenerate factors instead of returning 0.

        Returns:
            sympy.Expr: Simplified expression (possibly 0).
        """
        if isinstance(eqn, sympy.core.add.Add):
            add_args = []
            for item in eqn.args:
                if isinstance(item, sympy.core.mul.Mul) and is_small(item.args[0]):
                    continue
                add_args.append(item)
            eqn = sympy.core.add.Add(*add_args)
        if is_small(eqn):
            return sympy.sympify(0)
        eqn, denominator = eqn.as_numer_denom()
        factors = None
        try:
            with Timeout(0.1) as tt:
                factors = factor_list(eqn)
        except:
            pass
        if factors is None:
            return eqn
        if is_small(factors[0]):
            return sympy.sympify(0)
        factors = factors[1]  # removes constant coefficient
        if any([is_small(item[0]) for item in factors]):
            return sympy.sympify(0)
        factors = [item[0] for item in factors if not item[0].is_positive]
        if len(factors) == 0:
            if check:
                assert False
            else:
                return sympy.sympify(0)
        eqn = factors[0]
        for item in factors[1:]:
            eqn = eqn*item

        return eqn

    def process_solutions(self, var, eqn, solutions, var_types):
        """Filter and sanitize candidate solutions for a single variable.

        Args:
            var (sympy.Symbol): Variable being solved.
            eqn (sympy.Expr): Original equation.
            solutions (list): Candidate solutions from sympy.
            var_types (dict): Mapping of symbols to semantic type ("Angle"/"Length"/None).

        Returns:
            sympy.Expr | None: A single viable solution or None if ambiguous/invalid.
        """
        symbols = eqn.free_symbols
        solutions = [item for item in solutions if len(item.free_symbols) == len(
            symbols) - 1]  # remove degenerate solutions
        if len(symbols) == 1:
            solutions = [sympy.re(sol.simplify())
                        for sol in solutions if abs(sympy.im(sol)) < 1e-3]
            try:
                if str(var).startswith("Angle"):
                    solutions = {j for j in solutions if j >= 0 and j <= math.pi+eps}
                    # Prioitize non-zero and non-flat angle
                    if len(solutions) > 1:
                        solutions = {j for j in solutions if j != 0 and j != sympy.pi}
                elif var_types.get(var, None) == "Angle":
                    solutions = {j for j in solutions if j >=
                                0 and j <= 180+eps/math.pi*180}
                    # Prioitize non-zero and non-flat angle
                    if len(solutions) > 1:
                        solutions = {j for j in solutions if j != 0 and j != 180}
                if len(solutions) > 1:
                    solutions = [item for item in solutions if item >= 0]
                if len(solutions) > 1:
                    solutions = [item for item in solutions if item > 0]
            except:
                if str(var).startswith("Angle"):
                    solutions = {j for j in solutions if float(j) >= 0 and float(j) <= math.pi+eps}
                    # Prioitize non-zero and non-flat angle
                    if len(solutions) > 1:
                        solutions = {j for j in solutions if float(j) != 0 and float(j) != sympy.pi}
                elif var_types.get(var, None) == "Angle":
                    solutions = {j for j in solutions if j >=
                                0 and j <= 180+eps/math.pi*180}
                    # Prioitize non-zero and non-flat angle
                    if len(solutions) > 1:
                        solutions = {j for j in solutions if float(j) != 0 and float(j) != 180}
                if len(solutions) > 1:
                    solutions = [item for item in solutions if float(item) >= 0]
                if len(solutions) > 1:
                    solutions = [item for item in solutions if float(item) > 0]

        if len(solutions) == 1:
            return solutions.pop()
        return None

    def elim(self, equations, var_types):        
        """Triangularize equations to solve single-variable expressions where possible.

        Args:
            equations (list[Traced]): Equations to solve.
            var_types (dict): Mapping of symbols to semantic type ("Angle"/"Length"/None).

        Returns:
            tuple[list[sympy.Symbol], dict]: Free variables list and solved expressions map.
        """
        free_vars = []
        raw_equations = equations
        equations = [item.expr for item in equations]
        for eqn in equations:
            free_vars += eqn.free_symbols
        free_vars = set(free_vars)
        free_vars = list(free_vars)
        free_vars.sort(key=lambda x: x.name)
        exprs = {}
        # Triangulate
        for i, eqn in enumerate(equations):
            eqn = self.process_equation(eqn, check=True)
            if eqn == 0:
                raw_equations[i].redundant = True
                continue
            symbols = list(eqn.free_symbols)
            symbols.sort(key=lambda x: str(x))
            expr = None
            for var in symbols:
                solutions = None
                expr = None
                solutions = sympy.solve(eqn, var)
                expr = self.process_solutions(var, eqn, solutions, var_types)
                if expr is None:
                    continue
                else:
                    break
            if expr is None:
                continue
            if expr == 0 and "length" in str(var).lower():
                breakpoint()
                assert False
            if expr == 0 and "radius" in str(var).lower():
                breakpoint()
                assert False
            if not var in exprs:
                exprs[var] = expr
            elif check_equalities(expr-exprs[var]):  # redundant equation
                equations[i] = sympy.sympify(0)
                raw_equations[i].redundant = True
                continue
            else:
                breakpoint()  # contradiction
                assert False
            if var in free_vars:
                free_vars.remove(var)
            eqns = [(idx+i+1, item) for idx,
                    item in enumerate(equations[i+1:]) if var in item.free_symbols]
            for idx, item in eqns:
                if var in getattr(equations[idx], "free_symbols", []):
                    equations[idx] = item.subs(var, exprs[var])

        # Diagonalize
        for i, (key, value) in enumerate(exprs.items()):
            for j, key1 in enumerate(exprs.keys()):
                if j == i:
                    break
                if key in getattr(exprs[key1], "free_symbols", []):
                    old = exprs[key1]
                    exprs[key1] = exprs[key1].subs(key, value)
                    if str(exprs[key1]) == "0" and "Length" in str(key1):
                        breakpoint()
                        assert False
                        pass
        exprs = {key: value for key, value in exprs.items()}
        return free_vars, exprs


    def solve_equation(self):
        """Solve current equations, updating the state's solution stack and unions.

        Returns:
            None
        """
        if len(self.state.solutions) > self.state.current_depth: # have solved for this depth
            return
        raw_equations = [item for item in self.state.equations if not item.redundant]
        try_complex = self.state.try_complex
        var_types = self.state.var_types
        solved_vars = {}
        angle_linear, length_linear, length_ratio, others = classify_equations(raw_equations, var_types)
        for eqs, source in (angle_linear, "angle_linear"),  (length_ratio, "length_ratio"):
            free, solved = self.elim(eqs, var_types)
            for key, value in solved.items():
                value = Traced(value, depth=self.state.current_depth, sources=[source])
                value.symbol = key
                solved_vars[key] = value
        used = []
        progress = True
        exact_exhausted = False
        # prioritize on equations that contain only one variable to solve for exact values
        # then try to solve equations that are not much too complicated
        while progress:
            progress = False
            for i, eqn in enumerate(length_linear+others):
                if i in used or eqn.redundant:
                    continue
                symbols = eqn.free_symbols
                raw_eqn = eqn
                for symbol in symbols:
                    if symbol in solved_vars:
                        eqn = eqn.subs(symbol, solved_vars[symbol])
                symbols = eqn.free_symbols
                expr = self.process_equation(eqn.expr)
                tmp = str(expr)
                complexity = tmp.count("sin") + tmp.count("cos") + \
                    tmp.count("tan") + tmp.count("**")
                if try_complex and exact_exhausted:
                    if len(symbols) > 1 and complexity > 1:
                        continue
                else:
                    if len(symbols) > 1:
                        continue
                if len(symbols) == 0:
                    eqn.redundant = True
                    continue
                for symbol in symbols:
                    solutions = None
                    solution = None
                    pattern = re.compile(r"(cos|sin)\(\d+\*" + str(symbol) + r"\)")
                    # sympy cannot handle solutions with +k*pi/n correctly, only one solution is returned
                    if pattern.search(tmp):
                        continue
                    with Timeout(0.1) as tt:
                        solutions = sympy.solve(expr, symbol, domain=sympy.S.Reals)
                        # timeout when solving sin(AngleD_C_E)/20 - sin(AngleD_C_E + pi/3)/12
                        # stack overflow infinite recursion when computing the real part of sqrt(2)*cos(x)/28 - cos(x + pi/4)/7
                    if solutions is None:
                        # solving can fail on complicated equations
                        continue
                    solution = self.process_solutions(symbol, expr, solutions, var_types)
                    if solution is None:
                        continue
                    break
                if not solution is None:
                    used.append(i)
                    progress = True
                    solution = Traced(solution, sources=eqn.sources, depth=self.state.current_depth)
                    solution.symbol = symbol
                    solved_vars[symbol] = solution
                    for key, value in solved_vars.items():
                        if symbol in value.free_symbols:
                            original = solved_vars[key]
                            solved_vars[key] = value.subs(symbol, solution)
                            if solved_vars[key] == 0 and 'length' in str(key).lower():
                                breakpoint()
                                assert False
                else:
                    if not self.state.silent:
                        self.state.logger.debug(f"abondended complex equation {eqn, raw_eqn}")
            if not progress and try_complex and not exact_exhausted:
                progress = True
                exact_exhausted = True
        self.state.solutions.append(solved_vars)
        # extract equivalence relations and store in union find
        dic = {}
        eqns = []
        for key, value in solved_vars.items():
            if not "Angle" in str(key) and not "Length" in str(key):
                continue
            value = value.expr
            if value in dic:
                eqns.append((dic[value], key))
            elif isinstance(value, sympy.core.symbol.Symbol) and ("Angle" in str(value) or "Length" in str(value)):
                eqns.append((key, value))
            else:
                dic[value] = key
        for eqn in eqns:
            # Remove the assertion or handle the case when unionfind is None
            unionfind = None
            if "Length" in str(eqn):
                unionfind = self.state.lengths
            if "Angle" in str(eqn):
                unionfind = self.state.angles
            if unionfind is not None:
                l, r = eqn
                unionfind.union(l, r)


    def compute_ratio_and_angle_sum(self):
        def merge(dic):
            merged = {}
            keys = list(dic.keys())
            for i in range(len(keys)):
                if keys[i] is None:
                    continue
                merged[keys[i]] = dic[keys[i]]
                for j in range(i+1, len(keys)):
                    if keys[j] is None:
                        continue
                    if is_small(keys[i]-keys[j]):
                        merged[keys[i]] += dic[keys[j]]
                        keys[j] = None
            return merged
        dic = {}
        tmp = self.state.lengths.equivalence_classes()
        for x in tmp:
            for y in tmp:
                expr = self.state.simplify_equation(x/y)
                if not expr in dic:
                    dic[expr] = [sympy.core.mul.Mul(x, 1/y, evaluate=False)]
                else:
                    dic[expr].append(sympy.core.mul.Mul(
                        x, 1/y, evaluate=False))
        self.state.ratios = dic # merge(dic)
        dic = {}
        tmp = self.state.angles.equivalence_classes()
        for x in tmp:
            for y in tmp:
                expr = self.state.simplify_equation(x+y)
                if not expr in dic:
                    dic[expr] = [x+y]
                else:
                    dic[expr].append(x+y)
        self.state.angle_sums = dic #merge(dic)

    def run(self):
        """Full algebraic pass: solve equations then compute ratio/angle equivalences.

        Returns:
            None
        """
        self.solve_equation()
        self.compute_ratio_and_angle_sum()

__init__(state)

Create an algebraic system bound to a State.

Parameters:

Name Type Description Default
state State

Shared state containing equations and solved variables.

required
Source code in pyeuclid/engine/algebraic_system.py
22
23
24
25
26
27
28
def __init__(self, state):
    """Create an algebraic system bound to a `State`.

    Args:
        state (State): Shared state containing equations and solved variables.
    """
    self.state = state

elim(equations, var_types)

Triangularize equations to solve single-variable expressions where possible.

Parameters:

Name Type Description Default
equations list[Traced]

Equations to solve.

required
var_types dict

Mapping of symbols to semantic type ("Angle"/"Length"/None).

required

Returns:

Type Description

tuple[list[sympy.Symbol], dict]: Free variables list and solved expressions map.

Source code in pyeuclid/engine/algebraic_system.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
def elim(self, equations, var_types):        
    """Triangularize equations to solve single-variable expressions where possible.

    Args:
        equations (list[Traced]): Equations to solve.
        var_types (dict): Mapping of symbols to semantic type ("Angle"/"Length"/None).

    Returns:
        tuple[list[sympy.Symbol], dict]: Free variables list and solved expressions map.
    """
    free_vars = []
    raw_equations = equations
    equations = [item.expr for item in equations]
    for eqn in equations:
        free_vars += eqn.free_symbols
    free_vars = set(free_vars)
    free_vars = list(free_vars)
    free_vars.sort(key=lambda x: x.name)
    exprs = {}
    # Triangulate
    for i, eqn in enumerate(equations):
        eqn = self.process_equation(eqn, check=True)
        if eqn == 0:
            raw_equations[i].redundant = True
            continue
        symbols = list(eqn.free_symbols)
        symbols.sort(key=lambda x: str(x))
        expr = None
        for var in symbols:
            solutions = None
            expr = None
            solutions = sympy.solve(eqn, var)
            expr = self.process_solutions(var, eqn, solutions, var_types)
            if expr is None:
                continue
            else:
                break
        if expr is None:
            continue
        if expr == 0 and "length" in str(var).lower():
            breakpoint()
            assert False
        if expr == 0 and "radius" in str(var).lower():
            breakpoint()
            assert False
        if not var in exprs:
            exprs[var] = expr
        elif check_equalities(expr-exprs[var]):  # redundant equation
            equations[i] = sympy.sympify(0)
            raw_equations[i].redundant = True
            continue
        else:
            breakpoint()  # contradiction
            assert False
        if var in free_vars:
            free_vars.remove(var)
        eqns = [(idx+i+1, item) for idx,
                item in enumerate(equations[i+1:]) if var in item.free_symbols]
        for idx, item in eqns:
            if var in getattr(equations[idx], "free_symbols", []):
                equations[idx] = item.subs(var, exprs[var])

    # Diagonalize
    for i, (key, value) in enumerate(exprs.items()):
        for j, key1 in enumerate(exprs.keys()):
            if j == i:
                break
            if key in getattr(exprs[key1], "free_symbols", []):
                old = exprs[key1]
                exprs[key1] = exprs[key1].subs(key, value)
                if str(exprs[key1]) == "0" and "Length" in str(key1):
                    breakpoint()
                    assert False
                    pass
    exprs = {key: value for key, value in exprs.items()}
    return free_vars, exprs

process_equation(eqn, check=False)

Simplify an equation by dropping small factors and factoring.

Parameters:

Name Type Description Default
eqn Expr

Expression to simplify.

required
check bool

If True, raise on degenerate factors instead of returning 0.

False

Returns:

Type Description

sympy.Expr: Simplified expression (possibly 0).

Source code in pyeuclid/engine/algebraic_system.py
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def process_equation(self, eqn, check=False):
    """Simplify an equation by dropping small factors and factoring.

    Args:
        eqn (sympy.Expr): Expression to simplify.
        check (bool): If True, raise on degenerate factors instead of returning 0.

    Returns:
        sympy.Expr: Simplified expression (possibly 0).
    """
    if isinstance(eqn, sympy.core.add.Add):
        add_args = []
        for item in eqn.args:
            if isinstance(item, sympy.core.mul.Mul) and is_small(item.args[0]):
                continue
            add_args.append(item)
        eqn = sympy.core.add.Add(*add_args)
    if is_small(eqn):
        return sympy.sympify(0)
    eqn, denominator = eqn.as_numer_denom()
    factors = None
    try:
        with Timeout(0.1) as tt:
            factors = factor_list(eqn)
    except:
        pass
    if factors is None:
        return eqn
    if is_small(factors[0]):
        return sympy.sympify(0)
    factors = factors[1]  # removes constant coefficient
    if any([is_small(item[0]) for item in factors]):
        return sympy.sympify(0)
    factors = [item[0] for item in factors if not item[0].is_positive]
    if len(factors) == 0:
        if check:
            assert False
        else:
            return sympy.sympify(0)
    eqn = factors[0]
    for item in factors[1:]:
        eqn = eqn*item

    return eqn

process_solutions(var, eqn, solutions, var_types)

Filter and sanitize candidate solutions for a single variable.

Parameters:

Name Type Description Default
var Symbol

Variable being solved.

required
eqn Expr

Original equation.

required
solutions list

Candidate solutions from sympy.

required
var_types dict

Mapping of symbols to semantic type ("Angle"/"Length"/None).

required

Returns:

Type Description

sympy.Expr | None: A single viable solution or None if ambiguous/invalid.

Source code in pyeuclid/engine/algebraic_system.py
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
def process_solutions(self, var, eqn, solutions, var_types):
    """Filter and sanitize candidate solutions for a single variable.

    Args:
        var (sympy.Symbol): Variable being solved.
        eqn (sympy.Expr): Original equation.
        solutions (list): Candidate solutions from sympy.
        var_types (dict): Mapping of symbols to semantic type ("Angle"/"Length"/None).

    Returns:
        sympy.Expr | None: A single viable solution or None if ambiguous/invalid.
    """
    symbols = eqn.free_symbols
    solutions = [item for item in solutions if len(item.free_symbols) == len(
        symbols) - 1]  # remove degenerate solutions
    if len(symbols) == 1:
        solutions = [sympy.re(sol.simplify())
                    for sol in solutions if abs(sympy.im(sol)) < 1e-3]
        try:
            if str(var).startswith("Angle"):
                solutions = {j for j in solutions if j >= 0 and j <= math.pi+eps}
                # Prioitize non-zero and non-flat angle
                if len(solutions) > 1:
                    solutions = {j for j in solutions if j != 0 and j != sympy.pi}
            elif var_types.get(var, None) == "Angle":
                solutions = {j for j in solutions if j >=
                            0 and j <= 180+eps/math.pi*180}
                # Prioitize non-zero and non-flat angle
                if len(solutions) > 1:
                    solutions = {j for j in solutions if j != 0 and j != 180}
            if len(solutions) > 1:
                solutions = [item for item in solutions if item >= 0]
            if len(solutions) > 1:
                solutions = [item for item in solutions if item > 0]
        except:
            if str(var).startswith("Angle"):
                solutions = {j for j in solutions if float(j) >= 0 and float(j) <= math.pi+eps}
                # Prioitize non-zero and non-flat angle
                if len(solutions) > 1:
                    solutions = {j for j in solutions if float(j) != 0 and float(j) != sympy.pi}
            elif var_types.get(var, None) == "Angle":
                solutions = {j for j in solutions if j >=
                            0 and j <= 180+eps/math.pi*180}
                # Prioitize non-zero and non-flat angle
                if len(solutions) > 1:
                    solutions = {j for j in solutions if float(j) != 0 and float(j) != 180}
            if len(solutions) > 1:
                solutions = [item for item in solutions if float(item) >= 0]
            if len(solutions) > 1:
                solutions = [item for item in solutions if float(item) > 0]

    if len(solutions) == 1:
        return solutions.pop()
    return None

run()

Full algebraic pass: solve equations then compute ratio/angle equivalences.

Returns:

Type Description

None

Source code in pyeuclid/engine/algebraic_system.py
356
357
358
359
360
361
362
363
def run(self):
    """Full algebraic pass: solve equations then compute ratio/angle equivalences.

    Returns:
        None
    """
    self.solve_equation()
    self.compute_ratio_and_angle_sum()

solve_equation()

Solve current equations, updating the state's solution stack and unions.

Returns:

Type Description

None

Source code in pyeuclid/engine/algebraic_system.py
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
def solve_equation(self):
    """Solve current equations, updating the state's solution stack and unions.

    Returns:
        None
    """
    if len(self.state.solutions) > self.state.current_depth: # have solved for this depth
        return
    raw_equations = [item for item in self.state.equations if not item.redundant]
    try_complex = self.state.try_complex
    var_types = self.state.var_types
    solved_vars = {}
    angle_linear, length_linear, length_ratio, others = classify_equations(raw_equations, var_types)
    for eqs, source in (angle_linear, "angle_linear"),  (length_ratio, "length_ratio"):
        free, solved = self.elim(eqs, var_types)
        for key, value in solved.items():
            value = Traced(value, depth=self.state.current_depth, sources=[source])
            value.symbol = key
            solved_vars[key] = value
    used = []
    progress = True
    exact_exhausted = False
    # prioritize on equations that contain only one variable to solve for exact values
    # then try to solve equations that are not much too complicated
    while progress:
        progress = False
        for i, eqn in enumerate(length_linear+others):
            if i in used or eqn.redundant:
                continue
            symbols = eqn.free_symbols
            raw_eqn = eqn
            for symbol in symbols:
                if symbol in solved_vars:
                    eqn = eqn.subs(symbol, solved_vars[symbol])
            symbols = eqn.free_symbols
            expr = self.process_equation(eqn.expr)
            tmp = str(expr)
            complexity = tmp.count("sin") + tmp.count("cos") + \
                tmp.count("tan") + tmp.count("**")
            if try_complex and exact_exhausted:
                if len(symbols) > 1 and complexity > 1:
                    continue
            else:
                if len(symbols) > 1:
                    continue
            if len(symbols) == 0:
                eqn.redundant = True
                continue
            for symbol in symbols:
                solutions = None
                solution = None
                pattern = re.compile(r"(cos|sin)\(\d+\*" + str(symbol) + r"\)")
                # sympy cannot handle solutions with +k*pi/n correctly, only one solution is returned
                if pattern.search(tmp):
                    continue
                with Timeout(0.1) as tt:
                    solutions = sympy.solve(expr, symbol, domain=sympy.S.Reals)
                    # timeout when solving sin(AngleD_C_E)/20 - sin(AngleD_C_E + pi/3)/12
                    # stack overflow infinite recursion when computing the real part of sqrt(2)*cos(x)/28 - cos(x + pi/4)/7
                if solutions is None:
                    # solving can fail on complicated equations
                    continue
                solution = self.process_solutions(symbol, expr, solutions, var_types)
                if solution is None:
                    continue
                break
            if not solution is None:
                used.append(i)
                progress = True
                solution = Traced(solution, sources=eqn.sources, depth=self.state.current_depth)
                solution.symbol = symbol
                solved_vars[symbol] = solution
                for key, value in solved_vars.items():
                    if symbol in value.free_symbols:
                        original = solved_vars[key]
                        solved_vars[key] = value.subs(symbol, solution)
                        if solved_vars[key] == 0 and 'length' in str(key).lower():
                            breakpoint()
                            assert False
            else:
                if not self.state.silent:
                    self.state.logger.debug(f"abondended complex equation {eqn, raw_eqn}")
        if not progress and try_complex and not exact_exhausted:
            progress = True
            exact_exhausted = True
    self.state.solutions.append(solved_vars)
    # extract equivalence relations and store in union find
    dic = {}
    eqns = []
    for key, value in solved_vars.items():
        if not "Angle" in str(key) and not "Length" in str(key):
            continue
        value = value.expr
        if value in dic:
            eqns.append((dic[value], key))
        elif isinstance(value, sympy.core.symbol.Symbol) and ("Angle" in str(value) or "Length" in str(value)):
            eqns.append((key, value))
        else:
            dic[value] = key
    for eqn in eqns:
        # Remove the assertion or handle the case when unionfind is None
        unionfind = None
        if "Length" in str(eqn):
            unionfind = self.state.lengths
        if "Angle" in str(eqn):
            unionfind = self.state.angles
        if unionfind is not None:
            l, r = eqn
            unionfind.union(l, r)

Between

Bases: Relation

p1 lies between p2 and p3 (on the same line).

Source code in pyeuclid/formalization/relation.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
class Between(Relation):
    """p1 lies between p2 and p3 (on the same line)."""

    def __init__(self, p1: Point, p2: Point, p3: Point):
        """
        p1 is between p2 and p3.

        Args:
            p1 (Point): Middle point.
            p2 (Point): Endpoint 1.
            p3 (Point): Endpoint 2.
        """
        super().__init__()
        p2, p3 = sort_points(p2, p3)
        self.p1, self.p2, self.p3 = p1, p2, p3

    def permutations(self):
        """Enumerate equivalent point orderings for the between relation.

        Returns:
            list[tuple[Point, Point, Point]]: Permitted permutations.
        """
        return [(self.p1, self.p2, self.p3), (self.p1, self.p3, self.p2)]

__init__(p1, p2, p3)

p1 is between p2 and p3.

Parameters:

Name Type Description Default
p1 Point

Middle point.

required
p2 Point

Endpoint 1.

required
p3 Point

Endpoint 2.

required
Source code in pyeuclid/formalization/relation.py
170
171
172
173
174
175
176
177
178
179
180
181
def __init__(self, p1: Point, p2: Point, p3: Point):
    """
    p1 is between p2 and p3.

    Args:
        p1 (Point): Middle point.
        p2 (Point): Endpoint 1.
        p3 (Point): Endpoint 2.
    """
    super().__init__()
    p2, p3 = sort_points(p2, p3)
    self.p1, self.p2, self.p3 = p1, p2, p3

permutations()

Enumerate equivalent point orderings for the between relation.

Returns:

Type Description

list[tuple[Point, Point, Point]]: Permitted permutations.

Source code in pyeuclid/formalization/relation.py
183
184
185
186
187
188
189
def permutations(self):
    """Enumerate equivalent point orderings for the between relation.

    Returns:
        list[tuple[Point, Point, Point]]: Permitted permutations.
    """
    return [(self.p1, self.p2, self.p3), (self.p1, self.p3, self.p2)]

Collinear

Bases: Relation

Points p1,p2,p3 are collinear.

Source code in pyeuclid/formalization/relation.py
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
class Collinear(Relation):
    """Points p1,p2,p3 are collinear."""

    def __init__(self, p1, p2, p3):
        """
        Args:
            p1 (Point)
            p2 (Point)
            p3 (Point)
        """
        super().__init__()
        self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

    def permutations(self):
        """Enumerate collinearity argument permutations.

        Returns:
            itertools.permutations: All orderings of the three points.
        """
        return itertools.permutations([self.p1, self.p2, self.p3])

__init__(p1, p2, p3)

Source code in pyeuclid/formalization/relation.py
248
249
250
251
252
253
254
255
256
def __init__(self, p1, p2, p3):
    """
    Args:
        p1 (Point)
        p2 (Point)
        p3 (Point)
    """
    super().__init__()
    self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

permutations()

Enumerate collinearity argument permutations.

Returns:

Type Description

itertools.permutations: All orderings of the three points.

Source code in pyeuclid/formalization/relation.py
258
259
260
261
262
263
264
def permutations(self):
    """Enumerate collinearity argument permutations.

    Returns:
        itertools.permutations: All orderings of the three points.
    """
    return itertools.permutations([self.p1, self.p2, self.p3])

Concyclic

Bases: Relation

All given points lie on the same circle.

Source code in pyeuclid/formalization/relation.py
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
class Concyclic(Relation):
    """All given points lie on the same circle."""

    def __init__(self, *ps: Point):
        """
        Args:
            *ps (Point): Points to be tested for concyclicity.
        """
        super().__init__()
        self.ps = list(sort_points(*ps))

    def permutations(self):
        """Enumerate symmetric permutations of the point set.

        Returns:
            itertools.permutations: All orderings of the points.
        """
        return itertools.permutations(self.ps)

__init__(*ps)

Parameters:

Name Type Description Default
*ps Point

Points to be tested for concyclicity.

()
Source code in pyeuclid/formalization/relation.py
376
377
378
379
380
381
382
def __init__(self, *ps: Point):
    """
    Args:
        *ps (Point): Points to be tested for concyclicity.
    """
    super().__init__()
    self.ps = list(sort_points(*ps))

permutations()

Enumerate symmetric permutations of the point set.

Returns:

Type Description

itertools.permutations: All orderings of the points.

Source code in pyeuclid/formalization/relation.py
384
385
386
387
388
389
390
def permutations(self):
    """Enumerate symmetric permutations of the point set.

    Returns:
        itertools.permutations: All orderings of the points.
    """
    return itertools.permutations(self.ps)

Congruent

Bases: Relation

Triangles (p1,p2,p3) and (p4,p5,p6) are congruent.

Source code in pyeuclid/formalization/relation.py
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
class Congruent(Relation):
    """Triangles (p1,p2,p3) and (p4,p5,p6) are congruent."""

    def __init__(
        self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
    ):
        """
        Args:
            p1, p2, p3 (Point): First triangle vertices.
            p4, p5, p6 (Point): Second triangle vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

    def definition(self):
        """Congruence expressed as equal side lengths and non-collinearity.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) - Length(self.p4, self.p5),
            Length(self.p2, self.p3) - Length(self.p5, self.p6),
            Length(self.p1, self.p3) - Length(self.p4, self.p6),
            NotCollinear(self.p1, self.p2, self.p3),
        ]

__init__(p1, p2, p3, p4, p5, p6)

Parameters:

Name Type Description Default
p1, p2, p3 Point

First triangle vertices.

required
p4, p5, p6 Point

Second triangle vertices.

required
Source code in pyeuclid/formalization/relation.py
319
320
321
322
323
324
325
326
327
328
def __init__(
    self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
):
    """
    Args:
        p1, p2, p3 (Point): First triangle vertices.
        p4, p5, p6 (Point): Second triangle vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

definition()

Congruence expressed as equal side lengths and non-collinearity.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
330
331
332
333
334
335
336
337
338
339
340
341
def definition(self):
    """Congruence expressed as equal side lengths and non-collinearity.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) - Length(self.p4, self.p5),
        Length(self.p2, self.p3) - Length(self.p5, self.p6),
        Length(self.p1, self.p3) - Length(self.p4, self.p6),
        NotCollinear(self.p1, self.p2, self.p3),
    ]

Different

Bases: Relation

All provided points must be pairwise distinct.

Source code in pyeuclid/formalization/relation.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
class Different(Relation):
    """All provided points must be pairwise distinct."""

    def __init__(self, *ps: list[Point]):
        """
        Args:
            *ps (Point): Points that must be distinct.
        """
        super().__init__()
        self.ps = ps

    def definition(self):
        """Expand to pairwise inequality relations.

        Returns:
            list[Relation]: Negated equalities for every point pair.
        """
        return [Not(Equal(self.ps[i], self.ps[j])) for i in range(len(self.ps)) for j in range(i + 1, len(self.ps))]

__init__(*ps)

Parameters:

Name Type Description Default
*ps Point

Points that must be distinct.

()
Source code in pyeuclid/formalization/relation.py
150
151
152
153
154
155
156
def __init__(self, *ps: list[Point]):
    """
    Args:
        *ps (Point): Points that must be distinct.
    """
    super().__init__()
    self.ps = ps

definition()

Expand to pairwise inequality relations.

Returns:

Type Description

list[Relation]: Negated equalities for every point pair.

Source code in pyeuclid/formalization/relation.py
158
159
160
161
162
163
164
def definition(self):
    """Expand to pairwise inequality relations.

    Returns:
        list[Relation]: Negated equalities for every point pair.
    """
    return [Not(Equal(self.ps[i], self.ps[j])) for i in range(len(self.ps)) for j in range(i + 1, len(self.ps))]

Equal

Bases: Relation

Point equality relation.

Source code in pyeuclid/formalization/relation.py
89
90
91
92
93
94
95
96
97
98
class Equal(Relation):
    """Point equality relation."""

    def __init__(self, v1: Point, v2: Point):
        super().__init__()
        self.v1, self.v2 = sort_points(v1, v2)

    def permutations(self):
        """Enumerate equivalent orderings of the two points."""
        return [(self.v1, self.v2), (self.v2, self.v1)]

permutations()

Enumerate equivalent orderings of the two points.

Source code in pyeuclid/formalization/relation.py
96
97
98
def permutations(self):
    """Enumerate equivalent orderings of the two points."""
    return [(self.v1, self.v2), (self.v2, self.v1)]

InferenceRule

Base class for all geometric inference rules.

Subclasses implement condition() and conclusion(), each returning relations/equations. The register decorator wraps these to expand definitions and filter zero expressions before the deductive database uses them.

Source code in pyeuclid/engine/inference_rule.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
class InferenceRule:
    """Base class for all geometric inference rules.

    Subclasses implement `condition()` and `conclusion()`, each returning
    relations/equations. The `register` decorator wraps these to expand
    definitions and filter zero expressions before the deductive database uses
    them.
    """

    def __init__(self):
        pass

    def condition(self):
        """Return premises (relations/equations) required to trigger the rule."""

    def conclusion(self):
        """Return relations/equations that are added when the rule fires."""

    def get_entities_in_condition(self):
        entities = set()
        for i in self.condition()[2]:
            entities = entities.union(set(i.get_entities()))
        return entities

    def degenerate(self):
        return False

    def get_entities_in_conclusion(self):
        entities = set()
        for i in self.conclusion()[2]:
            entities = entities.union(set(i.get_entities()))
        return entities

    def __str__(self):
        class_name = self.__class__.__name__
        content = []
        for key, value in vars(self).items():
            if key.startswith("_") or key == "depth":
                continue
            if not isinstance(value, Iterable):
                content.append(str(value))
            else:
                content.append(','.join(str(i) for i in value))
        attributes = ','.join(content)
        return f"{class_name}({attributes})"

    def __repr__(self):
        return str(self)

    def __eq__(self, other):
        return str(self) == str(other)

    def __hash__(self):
        return hash(str(self))

conclusion()

Return relations/equations that are added when the rule fires.

Source code in pyeuclid/engine/inference_rule.py
60
61
def conclusion(self):
    """Return relations/equations that are added when the rule fires."""

condition()

Return premises (relations/equations) required to trigger the rule.

Source code in pyeuclid/engine/inference_rule.py
57
58
def condition(self):
    """Return premises (relations/equations) required to trigger the rule."""

Lt

Bases: Relation

Source code in pyeuclid/formalization/relation.py
81
82
83
84
85
86
class Lt(Relation):
    def __init__(self, v1: Point, v2: Point):
        """Ordering helper used to canonicalize inference rule assignments."""
        super().__init__()
        self.v1 = v1
        self.v2 = v2

__init__(v1, v2)

Ordering helper used to canonicalize inference rule assignments.

Source code in pyeuclid/formalization/relation.py
82
83
84
85
86
def __init__(self, v1: Point, v2: Point):
    """Ordering helper used to canonicalize inference rule assignments."""
    super().__init__()
    self.v1 = v1
    self.v2 = v2

Midpoint

Bases: Relation

p1 is the midpoint of segment p2p3.

Source code in pyeuclid/formalization/relation.py
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
class Midpoint(Relation):
    """p1 is the midpoint of segment p2p3."""

    def __init__(self, p1: Point, p2: Point, p3: Point):
        """
        Args:
            p1 (Point): Candidate midpoint.
            p2 (Point): Segment endpoint.
            p3 (Point): Segment endpoint.
        """
        super().__init__()
        self.p1 = p1
        self.p2, self.p3 = sort_points(p2, p3)

    def definition(self):
        """Midpoint expressed via equal lengths, collinearity, and betweenness.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) - Length(self.p1, self.p3),
            Collinear(self.p1, self.p2, self.p3),
            Different(self.p2, self.p3),
            Between(self.p1, self.p2, self.p3),
        ]

__init__(p1, p2, p3)

Parameters:

Name Type Description Default
p1 Point

Candidate midpoint.

required
p2 Point

Segment endpoint.

required
p3 Point

Segment endpoint.

required
Source code in pyeuclid/formalization/relation.py
291
292
293
294
295
296
297
298
299
300
def __init__(self, p1: Point, p2: Point, p3: Point):
    """
    Args:
        p1 (Point): Candidate midpoint.
        p2 (Point): Segment endpoint.
        p3 (Point): Segment endpoint.
    """
    super().__init__()
    self.p1 = p1
    self.p2, self.p3 = sort_points(p2, p3)

definition()

Midpoint expressed via equal lengths, collinearity, and betweenness.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
302
303
304
305
306
307
308
309
310
311
312
313
def definition(self):
    """Midpoint expressed via equal lengths, collinearity, and betweenness.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) - Length(self.p1, self.p3),
        Collinear(self.p1, self.p2, self.p3),
        Different(self.p2, self.p3),
        Between(self.p1, self.p2, self.p3),
    ]

NotCollinear

Bases: Relation

Points p1,p2,p3 are not collinear.

Source code in pyeuclid/formalization/relation.py
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
class NotCollinear(Relation):
    """Points p1,p2,p3 are not collinear."""

    def __init__(self, p1, p2, p3):
        """
        Args:
            p1 (Point)
            p2 (Point)
            p3 (Point)
        """
        super().__init__()
        self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

    def definition(self):
        """Expand non-collinearity into primitive constraints."""
        return [
            Not(Collinear(self.p1, self.p2, self.p3)),
            Different(self.p1, self.p2, self.p3)
        ]

__init__(p1, p2, p3)

Source code in pyeuclid/formalization/relation.py
270
271
272
273
274
275
276
277
278
def __init__(self, p1, p2, p3):
    """
    Args:
        p1 (Point)
        p2 (Point)
        p3 (Point)
    """
    super().__init__()
    self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

definition()

Expand non-collinearity into primitive constraints.

Source code in pyeuclid/formalization/relation.py
280
281
282
283
284
285
def definition(self):
    """Expand non-collinearity into primitive constraints."""
    return [
        Not(Collinear(self.p1, self.p2, self.p3)),
        Different(self.p1, self.p2, self.p3)
    ]

OppositeSide

Bases: Relation

Points p1,p2 lie on opposite sides of the line (p3,p4).

Source code in pyeuclid/formalization/relation.py
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
class OppositeSide(Relation):
    """Points p1,p2 lie on opposite sides of the line (p3,p4)."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1 (Point): First query point.
            p2 (Point): Second query point.
            p3 (Point): Line endpoint 1.
            p4 (Point): Line endpoint 2.
        """
        super().__init__()
        self.p1, self.p2 = sort_points(p1, p2)
        self.p3, self.p4 = sort_points(p3, p4)

    def definition(self):
        """Logical expansion expressing opposite-side constraints.

        Returns:
            list[Relation]: Primitive relations defining opposite sides.
        """
        return [
            Not(Collinear(self.p1, self.p3, self.p4)),
            Not(Collinear(self.p2, self.p3, self.p4)),
            Not(SameSide(self.p1, self.p2, self.p3, self.p4)),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1 Point

First query point.

required
p2 Point

Second query point.

required
p3 Point

Line endpoint 1.

required
p4 Point

Line endpoint 2.

required
Source code in pyeuclid/formalization/relation.py
220
221
222
223
224
225
226
227
228
229
230
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1 (Point): First query point.
        p2 (Point): Second query point.
        p3 (Point): Line endpoint 1.
        p4 (Point): Line endpoint 2.
    """
    super().__init__()
    self.p1, self.p2 = sort_points(p1, p2)
    self.p3, self.p4 = sort_points(p3, p4)

definition()

Logical expansion expressing opposite-side constraints.

Returns:

Type Description

list[Relation]: Primitive relations defining opposite sides.

Source code in pyeuclid/formalization/relation.py
232
233
234
235
236
237
238
239
240
241
242
def definition(self):
    """Logical expansion expressing opposite-side constraints.

    Returns:
        list[Relation]: Primitive relations defining opposite sides.
    """
    return [
        Not(Collinear(self.p1, self.p3, self.p4)),
        Not(Collinear(self.p2, self.p3, self.p4)),
        Not(SameSide(self.p1, self.p2, self.p3, self.p4)),
    ]

Parallel

Bases: Relation

Segments (p1,p2) and (p3,p4) are parallel.

Source code in pyeuclid/formalization/relation.py
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
class Parallel(Relation):
    """Segments (p1,p2) and (p3,p4) are parallel."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1, p2 (Point): First segment endpoints.
            p3, p4 (Point): Second segment endpoints.
        """
        super().__init__()
        p1, p2 = sort_points(p1, p2)
        p3, p4 = sort_points(p3, p4)
        self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

    def permutations(self):
        """Enumerate symmetric endpoint permutations preserving segment groups.

        Returns:
            list[tuple[Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p1, self.p2, self.p4, self.p3),
            (self.p2, self.p1, self.p3, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
            (self.p3, self.p4, self.p1, self.p2),
            (self.p4, self.p3, self.p1, self.p2),
            (self.p3, self.p4, self.p2, self.p1),
            (self.p4, self.p3, self.p2, self.p1),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1, p2 Point

First segment endpoints.

required
p3, p4 Point

Second segment endpoints.

required
Source code in pyeuclid/formalization/relation.py
396
397
398
399
400
401
402
403
404
405
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1, p2 (Point): First segment endpoints.
        p3, p4 (Point): Second segment endpoints.
    """
    super().__init__()
    p1, p2 = sort_points(p1, p2)
    p3, p4 = sort_points(p3, p4)
    self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

permutations()

Enumerate symmetric endpoint permutations preserving segment groups.

Returns:

Type Description

list[tuple[Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
def permutations(self):
    """Enumerate symmetric endpoint permutations preserving segment groups.

    Returns:
        list[tuple[Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p1, self.p2, self.p4, self.p3),
        (self.p2, self.p1, self.p3, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
        (self.p3, self.p4, self.p1, self.p2),
        (self.p4, self.p3, self.p1, self.p2),
        (self.p3, self.p4, self.p2, self.p1),
        (self.p4, self.p3, self.p2, self.p1),
    ]

Pentagon

Bases: Relation

Points form a cyclically ordered pentagon.

Source code in pyeuclid/formalization/relation.py
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
class Pentagon(Relation):
    """Points form a cyclically ordered pentagon."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point):
        """
        Args:
            p1, p2, p3, p4, p5 (Point): Vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5 = sort_cyclic_points(p1, p2, p3, p4, p5)

    def permutations(self):
        """Enumerate cyclic and reversed vertex orderings.

        Returns:
            list[tuple[Point, Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4, self.p5),
            (self.p2, self.p3, self.p4, self.p5, self.p1),
            (self.p3, self.p4, self.p5, self.p1, self.p2),
            (self.p4, self.p5, self.p1, self.p2, self.p3),
            (self.p5, self.p1, self.p2, self.p3, self.p4),
            (self.p5, self.p4, self.p3, self.p2, self.p1),
            (self.p4, self.p3, self.p2, self.p1, self.p5),
            (self.p3, self.p2, self.p1, self.p5, self.p4),
            (self.p2, self.p1, self.p5, self.p4, self.p3),
            (self.p1, self.p5, self.p4, self.p3, self.p2),
        ]

__init__(p1, p2, p3, p4, p5)

Parameters:

Name Type Description Default
p1, p2, p3, p4, p5 Point

Vertices.

required
Source code in pyeuclid/formalization/relation.py
500
501
502
503
504
505
506
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point):
    """
    Args:
        p1, p2, p3, p4, p5 (Point): Vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5 = sort_cyclic_points(p1, p2, p3, p4, p5)

permutations()

Enumerate cyclic and reversed vertex orderings.

Returns:

Type Description

list[tuple[Point, Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
def permutations(self):
    """Enumerate cyclic and reversed vertex orderings.

    Returns:
        list[tuple[Point, Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4, self.p5),
        (self.p2, self.p3, self.p4, self.p5, self.p1),
        (self.p3, self.p4, self.p5, self.p1, self.p2),
        (self.p4, self.p5, self.p1, self.p2, self.p3),
        (self.p5, self.p1, self.p2, self.p3, self.p4),
        (self.p5, self.p4, self.p3, self.p2, self.p1),
        (self.p4, self.p3, self.p2, self.p1, self.p5),
        (self.p3, self.p2, self.p1, self.p5, self.p4),
        (self.p2, self.p1, self.p5, self.p4, self.p3),
        (self.p1, self.p5, self.p4, self.p3, self.p2),
    ]

Perpendicular

Bases: Relation

Segments (p1,p2) and (p3,p4) are perpendicular.

Source code in pyeuclid/formalization/relation.py
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
class Perpendicular(Relation):
    """Segments (p1,p2) and (p3,p4) are perpendicular."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1, p2 (Point): First segment endpoints.
            p3, p4 (Point): Second segment endpoints.
        """
        super().__init__()
        p1, p2 = sort_points(p1, p2)
        p3, p4 = sort_points(p3, p4)
        self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

    def permutations(self):
        """Enumerate symmetric endpoint permutations preserving segment groups.

        Returns:
            list[tuple[Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p1, self.p2, self.p4, self.p3),
            (self.p2, self.p1, self.p3, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
            (self.p3, self.p4, self.p1, self.p2),
            (self.p4, self.p3, self.p1, self.p2),
            (self.p3, self.p4, self.p2, self.p1),
            (self.p4, self.p3, self.p2, self.p1),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1, p2 Point

First segment endpoints.

required
p3, p4 Point

Second segment endpoints.

required
Source code in pyeuclid/formalization/relation.py
428
429
430
431
432
433
434
435
436
437
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1, p2 (Point): First segment endpoints.
        p3, p4 (Point): Second segment endpoints.
    """
    super().__init__()
    p1, p2 = sort_points(p1, p2)
    p3, p4 = sort_points(p3, p4)
    self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

permutations()

Enumerate symmetric endpoint permutations preserving segment groups.

Returns:

Type Description

list[tuple[Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
def permutations(self):
    """Enumerate symmetric endpoint permutations preserving segment groups.

    Returns:
        list[tuple[Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p1, self.p2, self.p4, self.p3),
        (self.p2, self.p1, self.p3, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
        (self.p3, self.p4, self.p1, self.p2),
        (self.p4, self.p3, self.p1, self.p2),
        (self.p3, self.p4, self.p2, self.p1),
        (self.p4, self.p3, self.p2, self.p1),
    ]

Point

Named geometric point used throughout the formalization.

Source code in pyeuclid/formalization/relation.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Point:
    """Named geometric point used throughout the formalization."""

    def __init__(self, name: str):
        """Create a point.

        Args:
            name (str): Identifier (underscores are not allowed).
        """
        self.name = name
        assert "_" not in name

    def __str__(self):
        return self.name

    def __eq__(self, other):
        return str(self) == str(other)

    def __repr__(self):
        return str(self)

    def __hash__(self):
        return hash(str(self))

__init__(name)

Create a point.

Parameters:

Name Type Description Default
name str

Identifier (underscores are not allowed).

required
Source code in pyeuclid/formalization/relation.py
16
17
18
19
20
21
22
23
def __init__(self, name: str):
    """Create a point.

    Args:
        name (str): Identifier (underscores are not allowed).
    """
    self.name = name
    assert "_" not in name

Quadrilateral

Bases: Relation

Points form a cyclically ordered quadrilateral.

Source code in pyeuclid/formalization/relation.py
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
class Quadrilateral(Relation):
    """Points form a cyclically ordered quadrilateral."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1, p2, p3, p4 (Point): Vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4 = sort_cyclic_points(p1, p2, p3, p4)

    def permutations(self):
        """Enumerate cyclic and reversed vertex orderings.

        Returns:
            list[tuple[Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p2, self.p3, self.p4, self.p1),
            (self.p3, self.p4, self.p1, self.p2),
            (self.p4, self.p1, self.p2, self.p3),
            (self.p4, self.p3, self.p2, self.p1),
            (self.p3, self.p2, self.p1, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
            (self.p1, self.p4, self.p3, self.p2),
        ]

    def definition(self):
        """Opposite sides must lie on opposite sides of diagonals.

        Returns:
            list[Relation]: Primitive opposite-side relations.
        """
        return [
            OppositeSide(self.p1, self.p3, self.p2, self.p4),
            OppositeSide(self.p2, self.p4, self.p1, self.p3),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1, p2, p3, p4 Point

Vertices.

required
Source code in pyeuclid/formalization/relation.py
460
461
462
463
464
465
466
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1, p2, p3, p4 (Point): Vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4 = sort_cyclic_points(p1, p2, p3, p4)

definition()

Opposite sides must lie on opposite sides of diagonals.

Returns:

Type Description

list[Relation]: Primitive opposite-side relations.

Source code in pyeuclid/formalization/relation.py
485
486
487
488
489
490
491
492
493
494
def definition(self):
    """Opposite sides must lie on opposite sides of diagonals.

    Returns:
        list[Relation]: Primitive opposite-side relations.
    """
    return [
        OppositeSide(self.p1, self.p3, self.p2, self.p4),
        OppositeSide(self.p2, self.p4, self.p1, self.p3),
    ]

permutations()

Enumerate cyclic and reversed vertex orderings.

Returns:

Type Description

list[tuple[Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
def permutations(self):
    """Enumerate cyclic and reversed vertex orderings.

    Returns:
        list[tuple[Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p2, self.p3, self.p4, self.p1),
        (self.p3, self.p4, self.p1, self.p2),
        (self.p4, self.p1, self.p2, self.p3),
        (self.p4, self.p3, self.p2, self.p1),
        (self.p3, self.p2, self.p1, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
        (self.p1, self.p4, self.p3, self.p2),
    ]

Relation

Base class for logical relations over points.

Source code in pyeuclid/formalization/relation.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class Relation:
    """Base class for logical relations over points."""

    def __init__(self):
        self.negated = False

    def negate(self):
        """Toggle negation flag in-place."""
        self.negated = not self.negated

    def get_points(self):
        """Return all point instances contained in the relation."""
        points = []
        for v in vars(self).values():
            if isinstance(v, Point):
                points.append(v)
            elif isinstance(v, list):
                for p in v:
                    if isinstance(p, Point):
                        points.append(p)
        return points

    def __str__(self):
        """Readable representation, prefixed with Not() when negated."""
        class_name = self.__class__.__name__
        points = self.get_points()
        args_name = ",".join([p.name for p in points])

        if not self.negated:
            return f"{class_name}({args_name})"
        else:
            return f"Not({class_name}({args_name}))"

    def __repr__(self):
        return str(self)

    def __eq__(self, other):
        return str(self) == str(other)

    def __hash__(self):
        return hash(str(self))

__str__()

Readable representation, prefixed with Not() when negated.

Source code in pyeuclid/formalization/relation.py
60
61
62
63
64
65
66
67
68
69
def __str__(self):
    """Readable representation, prefixed with Not() when negated."""
    class_name = self.__class__.__name__
    points = self.get_points()
    args_name = ",".join([p.name for p in points])

    if not self.negated:
        return f"{class_name}({args_name})"
    else:
        return f"Not({class_name}({args_name}))"

get_points()

Return all point instances contained in the relation.

Source code in pyeuclid/formalization/relation.py
48
49
50
51
52
53
54
55
56
57
58
def get_points(self):
    """Return all point instances contained in the relation."""
    points = []
    for v in vars(self).values():
        if isinstance(v, Point):
            points.append(v)
        elif isinstance(v, list):
            for p in v:
                if isinstance(p, Point):
                    points.append(p)
    return points

negate()

Toggle negation flag in-place.

Source code in pyeuclid/formalization/relation.py
44
45
46
def negate(self):
    """Toggle negation flag in-place."""
    self.negated = not self.negated

SameSide

Bases: Relation

Points p1,p2 lie on the same side of the line (p3,p4).

Source code in pyeuclid/formalization/relation.py
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
class SameSide(Relation):
    """Points p1,p2 lie on the same side of the line (p3,p4)."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1 (Point): First query point.
            p2 (Point): Second query point.
            p3 (Point): Line endpoint 1.
            p4 (Point): Line endpoint 2.
        """
        super().__init__()
        self.p1, self.p2 = sort_points(p1, p2)
        self.p3, self.p4 = sort_points(p3, p4)

    def permutations(self):
        """Enumerate symmetric orderings for same-side tests."""
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p1, self.p2, self.p4, self.p3),
            (self.p2, self.p1, self.p3, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1 Point

First query point.

required
p2 Point

Second query point.

required
p3 Point

Line endpoint 1.

required
p4 Point

Line endpoint 2.

required
Source code in pyeuclid/formalization/relation.py
195
196
197
198
199
200
201
202
203
204
205
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1 (Point): First query point.
        p2 (Point): Second query point.
        p3 (Point): Line endpoint 1.
        p4 (Point): Line endpoint 2.
    """
    super().__init__()
    self.p1, self.p2 = sort_points(p1, p2)
    self.p3, self.p4 = sort_points(p3, p4)

permutations()

Enumerate symmetric orderings for same-side tests.

Source code in pyeuclid/formalization/relation.py
207
208
209
210
211
212
213
214
def permutations(self):
    """Enumerate symmetric orderings for same-side tests."""
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p1, self.p2, self.p4, self.p3),
        (self.p2, self.p1, self.p3, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
    ]

Similar

Bases: Relation

Triangles (p1,p2,p3) and (p4,p5,p6) are similar.

Source code in pyeuclid/formalization/relation.py
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
class Similar(Relation):
    """Triangles (p1,p2,p3) and (p4,p5,p6) are similar."""

    def __init__(
        self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
    ):
        """
        Args:
            p1, p2, p3 (Point): First triangle vertices.
            p4, p5, p6 (Point): Second triangle vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

    def definition(self):
        """Similarity expressed via length ratios and non-collinearity.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) / Length(self.p4, self.p5)
            - Length(self.p2, self.p3) / Length(self.p5, self.p6),
            Length(self.p1, self.p2) / Length(self.p4, self.p5)
            - Length(self.p3, self.p1) / Length(self.p6, self.p4),
            NotCollinear(self.p1, self.p2, self.p3),
        ]

__init__(p1, p2, p3, p4, p5, p6)

Parameters:

Name Type Description Default
p1, p2, p3 Point

First triangle vertices.

required
p4, p5, p6 Point

Second triangle vertices.

required
Source code in pyeuclid/formalization/relation.py
347
348
349
350
351
352
353
354
355
356
def __init__(
    self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
):
    """
    Args:
        p1, p2, p3 (Point): First triangle vertices.
        p4, p5, p6 (Point): Second triangle vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

definition()

Similarity expressed via length ratios and non-collinearity.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
358
359
360
361
362
363
364
365
366
367
368
369
370
def definition(self):
    """Similarity expressed via length ratios and non-collinearity.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) / Length(self.p4, self.p5)
        - Length(self.p2, self.p3) / Length(self.p5, self.p6),
        Length(self.p1, self.p2) / Length(self.p4, self.p5)
        - Length(self.p3, self.p1) / Length(self.p6, self.p4),
        NotCollinear(self.p1, self.p2, self.p3),
    ]

Similar4P

Bases: Relation

Two quadrilaterals are similar.

Source code in pyeuclid/formalization/relation.py
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
class Similar4P(Relation):
    """Two quadrilaterals are similar."""

    def __init__(
        self,
        p1: Point,
        p2: Point,
        p3: Point,
        p4: Point,
        p5: Point,
        p6: Point,
        p7: Point,
        p8: Point,
    ):
        """
        Args:
            p1..p4 (Point): First quadrilateral.
            p5..p8 (Point): Second quadrilateral.
        """
        super().__init__()
        point_map_12 = {p1: p5, p2: p6, p3: p7, p4: p8}
        point_map_21 = {p5: p1, p6: p2, p7: p3, p8: p4}

        sorted_1 = sort_cyclic_points(p1, p2, p3, p4)
        sorted_2 = sort_cyclic_points(p5, p6, p7, p8)
        if compare_names(sorted_1, sorted_2) == 0:
            self.p1, self.p2, self.p3, self.p4 = sorted_1
            self.p5,self.p6,self.p7,self.p8 = point_map_12[self.p1], point_map_12[
                self.p2], point_map_12[self.p3], point_map_12[self.p4]
        else:
            self.p1, self.p2, self.p3, self.p4 = sorted_2
            self.p5, self.p6, self.p7, self.p8 = point_map_21[self.p1], point_map_21[
                self.p2], point_map_21[self.p3], point_map_21[self.p4]

    def definition(self):
        """Similarity expressed via side ratios and angle equalities.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) / Length(self.p5, self.p6)
            - Length(self.p2, self.p3) / Length(self.p6, self.p7),
            Length(self.p1, self.p2) / Length(self.p5, self.p6)
            - Length(self.p3, self.p4) / Length(self.p7, self.p8),
            Length(self.p1, self.p2) / Length(self.p5, self.p6)
            - Length(self.p4, self.p1) / Length(self.p8, self.p5),
            Angle(self.p1, self.p2, self.p3) - Angle(self.p5, self.p6, self.p7),
            Angle(self.p2, self.p3, self.p4) - Angle(self.p6, self.p7, self.p8),
            Angle(self.p3, self.p4, self.p1) - Angle(self.p7, self.p8, self.p5),
            Angle(self.p4, self.p1, self.p2) - Angle(self.p8, self.p5, self.p6),
        ]

__init__(p1, p2, p3, p4, p5, p6, p7, p8)

Parameters:

Name Type Description Default
p1..p4 Point

First quadrilateral.

required
p5..p8 Point

Second quadrilateral.

required
Source code in pyeuclid/formalization/relation.py
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
def __init__(
    self,
    p1: Point,
    p2: Point,
    p3: Point,
    p4: Point,
    p5: Point,
    p6: Point,
    p7: Point,
    p8: Point,
):
    """
    Args:
        p1..p4 (Point): First quadrilateral.
        p5..p8 (Point): Second quadrilateral.
    """
    super().__init__()
    point_map_12 = {p1: p5, p2: p6, p3: p7, p4: p8}
    point_map_21 = {p5: p1, p6: p2, p7: p3, p8: p4}

    sorted_1 = sort_cyclic_points(p1, p2, p3, p4)
    sorted_2 = sort_cyclic_points(p5, p6, p7, p8)
    if compare_names(sorted_1, sorted_2) == 0:
        self.p1, self.p2, self.p3, self.p4 = sorted_1
        self.p5,self.p6,self.p7,self.p8 = point_map_12[self.p1], point_map_12[
            self.p2], point_map_12[self.p3], point_map_12[self.p4]
    else:
        self.p1, self.p2, self.p3, self.p4 = sorted_2
        self.p5, self.p6, self.p7, self.p8 = point_map_21[self.p1], point_map_21[
            self.p2], point_map_21[self.p3], point_map_21[self.p4]

definition()

Similarity expressed via side ratios and angle equalities.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
def definition(self):
    """Similarity expressed via side ratios and angle equalities.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) / Length(self.p5, self.p6)
        - Length(self.p2, self.p3) / Length(self.p6, self.p7),
        Length(self.p1, self.p2) / Length(self.p5, self.p6)
        - Length(self.p3, self.p4) / Length(self.p7, self.p8),
        Length(self.p1, self.p2) / Length(self.p5, self.p6)
        - Length(self.p4, self.p1) / Length(self.p8, self.p5),
        Angle(self.p1, self.p2, self.p3) - Angle(self.p5, self.p6, self.p7),
        Angle(self.p2, self.p3, self.p4) - Angle(self.p6, self.p7, self.p8),
        Angle(self.p3, self.p4, self.p1) - Angle(self.p7, self.p8, self.p5),
        Angle(self.p4, self.p1, self.p2) - Angle(self.p8, self.p5, self.p6),
    ]

Similar5P

Bases: Relation

Two pentagons are similar.

Source code in pyeuclid/formalization/relation.py
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
class Similar5P(Relation):
    """Two pentagons are similar."""

    def __init__(
        self,
        p1: Point,
        p2: Point,
        p3: Point,
        p4: Point,
        p5: Point,
        p6: Point,
        p7: Point,
        p8: Point,
        p9: Point,
        p10: Point,
    ):
        """
        Args:
            p1..p5 (Point): First pentagon.
            p6..p10 (Point): Second pentagon.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5, self.p6, self.p7, self.p8, self.p9, self.p10 = p1, p2, p3, p4, p5, p6, p7, p8, p9, p10

    def definition(self):
        """Similarity expressed via consecutive side ratios and angle equalities.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) / Length(self.p6, self.p7)
            - Length(self.p2, self.p3) / Length(self.p7, self.p8),
            Length(self.p2, self.p3) / Length(self.p7, self.p8)
            - Length(self.p3, self.p4) / Length(self.p8, self.p9),
            Length(self.p3, self.p4) / Length(self.p8, self.p9)
            - Length(self.p4, self.p5) / Length(self.p9, self.p10),
            Length(self.p4, self.p5) / Length(self.p9, self.p10)
            - Length(self.p5, self.p1) / Length(self.p10, self.p6),
            Length(self.p5, self.p1) / Length(self.p10, self.p6)
            - Length(self.p1, self.p2) / Length(self.p6, self.p7),
            Angle(self.p1, self.p2, self.p3) - Angle(self.p6, self.p7, self.p8),
            Angle(self.p2, self.p3, self.p4) - Angle(self.p7, self.p8, self.p9),
            Angle(self.p3, self.p4, self.p5) - Angle(self.p8, self.p9, self.p10),
            Angle(self.p4, self.p5, self.p1) - Angle(self.p9, self.p10, self.p6),
            Angle(self.p5, self.p1, self.p2) - Angle(self.p10, self.p6, self.p7),
        ]

__init__(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10)

Parameters:

Name Type Description Default
p1..p5 Point

First pentagon.

required
p6..p10 Point

Second pentagon.

required
Source code in pyeuclid/formalization/relation.py
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
def __init__(
    self,
    p1: Point,
    p2: Point,
    p3: Point,
    p4: Point,
    p5: Point,
    p6: Point,
    p7: Point,
    p8: Point,
    p9: Point,
    p10: Point,
):
    """
    Args:
        p1..p5 (Point): First pentagon.
        p6..p10 (Point): Second pentagon.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5, self.p6, self.p7, self.p8, self.p9, self.p10 = p1, p2, p3, p4, p5, p6, p7, p8, p9, p10

definition()

Similarity expressed via consecutive side ratios and angle equalities.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
def definition(self):
    """Similarity expressed via consecutive side ratios and angle equalities.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) / Length(self.p6, self.p7)
        - Length(self.p2, self.p3) / Length(self.p7, self.p8),
        Length(self.p2, self.p3) / Length(self.p7, self.p8)
        - Length(self.p3, self.p4) / Length(self.p8, self.p9),
        Length(self.p3, self.p4) / Length(self.p8, self.p9)
        - Length(self.p4, self.p5) / Length(self.p9, self.p10),
        Length(self.p4, self.p5) / Length(self.p9, self.p10)
        - Length(self.p5, self.p1) / Length(self.p10, self.p6),
        Length(self.p5, self.p1) / Length(self.p10, self.p6)
        - Length(self.p1, self.p2) / Length(self.p6, self.p7),
        Angle(self.p1, self.p2, self.p3) - Angle(self.p6, self.p7, self.p8),
        Angle(self.p2, self.p3, self.p4) - Angle(self.p7, self.p8, self.p9),
        Angle(self.p3, self.p4, self.p5) - Angle(self.p8, self.p9, self.p10),
        Angle(self.p4, self.p5, self.p1) - Angle(self.p9, self.p10, self.p6),
        Angle(self.p5, self.p1, self.p2) - Angle(self.p10, self.p6, self.p7),
    ]

register

Decorator that registers an inference rule class into named rule sets.

Source code in pyeuclid/engine/inference_rule.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class register():
    """Decorator that registers an inference rule class into named rule sets."""

    def __init__(self, *annotations):
        self.annotations = annotations

    def __call__(self, cls):
        for item in self.annotations:
            if not item in inference_rule_sets:
                inference_rule_sets[item] = [cls]
            else:
                inference_rule_sets[item].append(cls)

        def expanded_condition(self):
            lst = expand_definition(self._condition())
            return lst

        def expanded_conclusion(self):
            lst = expand_definition(self._conclusion())
            result = []
            for item in lst:
                if isinstance(item, sympy.core.numbers.Zero):
                    continue
                result.append(item)
            return result
        cls._condition = cls.condition
        cls._conclusion = cls.conclusion
        cls.condition = expanded_condition
        cls.conclusion = expanded_conclusion
        return cls

Angle(p1, p2, p3)

Symbolic angle at p2.

Returns:

Type Description

sympy.Symbol: Non-negative angle symbol.

Source code in pyeuclid/formalization/relation.py
108
109
110
111
112
113
114
115
def Angle(p1: Point, p2: Point, p3: Point):
    """Symbolic angle at p2.

    Returns:
        sympy.Symbol: Non-negative angle symbol.
    """
    p1, p3 = sort_points(p1, p3)
    return Symbol(f"Angle_{p1}_{p2}_{p3}", non_negative=True)

Area(*ps)

Symbolic polygonal area over an ordered point cycle.

Returns:

Type Description

sympy.Symbol: Positive area symbol.

Source code in pyeuclid/formalization/relation.py
128
129
130
131
132
133
134
135
def Area(*ps: list[Point]):
    """Symbolic polygonal area over an ordered point cycle.

    Returns:
        sympy.Symbol: Positive area symbol.
    """
    ps = sort_cyclic_points(*ps)
    return Symbol("_".join(["Area"] + [str(item) for item in ps]), positive=True)

Length(p1, p2)

Symbolic length between two points.

Returns:

Type Description

sympy.Symbol: Positive length symbol.

Source code in pyeuclid/formalization/relation.py
118
119
120
121
122
123
124
125
def Length(p1: Point, p2: Point):
    """Symbolic length between two points.

    Returns:
        sympy.Symbol: Positive length symbol.
    """
    p1, p2 = sort_points(p1, p2)
    return Symbol(f"Length_{str(p1)}_{str(p2)}", positive=True)

Not(p)

Return a negated shallow copy of a relation.

Source code in pyeuclid/formalization/relation.py
101
102
103
104
105
def Not(p: Relation) -> Relation:
    """Return a negated shallow copy of a relation."""
    other = copy.copy(p)
    other.negate()
    return other

Variable(name)

Free symbolic variable placeholder.

Returns:

Type Description

sympy.Symbol: Dimensionless variable symbol.

Source code in pyeuclid/formalization/relation.py
138
139
140
141
142
143
144
def Variable(name: str):
    """Free symbolic variable placeholder.

    Returns:
        sympy.Symbol: Dimensionless variable symbol.
    """
    return Symbol(f"Variable_{name}")

State container for geometric problems and intermediate deductions.

Between

Bases: Relation

p1 lies between p2 and p3 (on the same line).

Source code in pyeuclid/formalization/relation.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
class Between(Relation):
    """p1 lies between p2 and p3 (on the same line)."""

    def __init__(self, p1: Point, p2: Point, p3: Point):
        """
        p1 is between p2 and p3.

        Args:
            p1 (Point): Middle point.
            p2 (Point): Endpoint 1.
            p3 (Point): Endpoint 2.
        """
        super().__init__()
        p2, p3 = sort_points(p2, p3)
        self.p1, self.p2, self.p3 = p1, p2, p3

    def permutations(self):
        """Enumerate equivalent point orderings for the between relation.

        Returns:
            list[tuple[Point, Point, Point]]: Permitted permutations.
        """
        return [(self.p1, self.p2, self.p3), (self.p1, self.p3, self.p2)]

__init__(p1, p2, p3)

p1 is between p2 and p3.

Parameters:

Name Type Description Default
p1 Point

Middle point.

required
p2 Point

Endpoint 1.

required
p3 Point

Endpoint 2.

required
Source code in pyeuclid/formalization/relation.py
170
171
172
173
174
175
176
177
178
179
180
181
def __init__(self, p1: Point, p2: Point, p3: Point):
    """
    p1 is between p2 and p3.

    Args:
        p1 (Point): Middle point.
        p2 (Point): Endpoint 1.
        p3 (Point): Endpoint 2.
    """
    super().__init__()
    p2, p3 = sort_points(p2, p3)
    self.p1, self.p2, self.p3 = p1, p2, p3

permutations()

Enumerate equivalent point orderings for the between relation.

Returns:

Type Description

list[tuple[Point, Point, Point]]: Permitted permutations.

Source code in pyeuclid/formalization/relation.py
183
184
185
186
187
188
189
def permutations(self):
    """Enumerate equivalent point orderings for the between relation.

    Returns:
        list[tuple[Point, Point, Point]]: Permitted permutations.
    """
    return [(self.p1, self.p2, self.p3), (self.p1, self.p3, self.p2)]

Circle

Numerical circle.

Source code in pyeuclid/formalization/numericals.py
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
class Circle:
    """Numerical circle."""

    def __init__(
        self,
        center: Optional[Point] = None,
        radius: Optional[float] = None,
        p1: Optional[Point] = None,
        p2: Optional[Point] = None,
        p3: Optional[Point] = None,
    ):
        if not center:
            l12 = perpendicular_bisector(p1, p2)
            l23 = perpendicular_bisector(p2, p3)
            center = line_line_intersection(l12, l23)

        if not radius:
            p = p1 or p2 or p3
            radius = center.distance(p)

        self.center = center
        self.radius = radius

    def intersect(self, obj: Union[Line, Circle]) -> tuple[Point, ...]:
        if isinstance(obj, Line):
            return obj.intersect(self)

        if isinstance(obj, Circle):
            return circle_circle_intersection(self, obj)

    def sample_within(self, points: list[Point], n: int = 5) -> list[Point]:
        """Sample a point within the boundary of points."""
        result = None
        best = -1.0
        for _ in range(n):
            ang = unif(0.0, 2.0) * np.pi
            x = self.center + Point(np.cos(ang), np.sin(ang)) * self.radius
            mind = min([x.distance(p) for p in points])
            if mind > best:
                best = mind
                result = x
        return [result]

sample_within(points, n=5)

Sample a point within the boundary of points.

Source code in pyeuclid/formalization/numericals.py
457
458
459
460
461
462
463
464
465
466
467
468
def sample_within(self, points: list[Point], n: int = 5) -> list[Point]:
    """Sample a point within the boundary of points."""
    result = None
    best = -1.0
    for _ in range(n):
        ang = unif(0.0, 2.0) * np.pi
        x = self.center + Point(np.cos(ang), np.sin(ang)) * self.radius
        mind = min([x.distance(p) for p in points])
        if mind > best:
            best = mind
            result = x
    return [result]

Collinear

Bases: Relation

Points p1,p2,p3 are collinear.

Source code in pyeuclid/formalization/relation.py
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
class Collinear(Relation):
    """Points p1,p2,p3 are collinear."""

    def __init__(self, p1, p2, p3):
        """
        Args:
            p1 (Point)
            p2 (Point)
            p3 (Point)
        """
        super().__init__()
        self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

    def permutations(self):
        """Enumerate collinearity argument permutations.

        Returns:
            itertools.permutations: All orderings of the three points.
        """
        return itertools.permutations([self.p1, self.p2, self.p3])

__init__(p1, p2, p3)

Source code in pyeuclid/formalization/relation.py
248
249
250
251
252
253
254
255
256
def __init__(self, p1, p2, p3):
    """
    Args:
        p1 (Point)
        p2 (Point)
        p3 (Point)
    """
    super().__init__()
    self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

permutations()

Enumerate collinearity argument permutations.

Returns:

Type Description

itertools.permutations: All orderings of the three points.

Source code in pyeuclid/formalization/relation.py
258
259
260
261
262
263
264
def permutations(self):
    """Enumerate collinearity argument permutations.

    Returns:
        itertools.permutations: All orderings of the three points.
    """
    return itertools.permutations([self.p1, self.p2, self.p3])

Concyclic

Bases: Relation

All given points lie on the same circle.

Source code in pyeuclid/formalization/relation.py
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
class Concyclic(Relation):
    """All given points lie on the same circle."""

    def __init__(self, *ps: Point):
        """
        Args:
            *ps (Point): Points to be tested for concyclicity.
        """
        super().__init__()
        self.ps = list(sort_points(*ps))

    def permutations(self):
        """Enumerate symmetric permutations of the point set.

        Returns:
            itertools.permutations: All orderings of the points.
        """
        return itertools.permutations(self.ps)

__init__(*ps)

Parameters:

Name Type Description Default
*ps Point

Points to be tested for concyclicity.

()
Source code in pyeuclid/formalization/relation.py
376
377
378
379
380
381
382
def __init__(self, *ps: Point):
    """
    Args:
        *ps (Point): Points to be tested for concyclicity.
    """
    super().__init__()
    self.ps = list(sort_points(*ps))

permutations()

Enumerate symmetric permutations of the point set.

Returns:

Type Description

itertools.permutations: All orderings of the points.

Source code in pyeuclid/formalization/relation.py
384
385
386
387
388
389
390
def permutations(self):
    """Enumerate symmetric permutations of the point set.

    Returns:
        itertools.permutations: All orderings of the points.
    """
    return itertools.permutations(self.ps)

Congruent

Bases: Relation

Triangles (p1,p2,p3) and (p4,p5,p6) are congruent.

Source code in pyeuclid/formalization/relation.py
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
class Congruent(Relation):
    """Triangles (p1,p2,p3) and (p4,p5,p6) are congruent."""

    def __init__(
        self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
    ):
        """
        Args:
            p1, p2, p3 (Point): First triangle vertices.
            p4, p5, p6 (Point): Second triangle vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

    def definition(self):
        """Congruence expressed as equal side lengths and non-collinearity.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) - Length(self.p4, self.p5),
            Length(self.p2, self.p3) - Length(self.p5, self.p6),
            Length(self.p1, self.p3) - Length(self.p4, self.p6),
            NotCollinear(self.p1, self.p2, self.p3),
        ]

__init__(p1, p2, p3, p4, p5, p6)

Parameters:

Name Type Description Default
p1, p2, p3 Point

First triangle vertices.

required
p4, p5, p6 Point

Second triangle vertices.

required
Source code in pyeuclid/formalization/relation.py
319
320
321
322
323
324
325
326
327
328
def __init__(
    self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
):
    """
    Args:
        p1, p2, p3 (Point): First triangle vertices.
        p4, p5, p6 (Point): Second triangle vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

definition()

Congruence expressed as equal side lengths and non-collinearity.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
330
331
332
333
334
335
336
337
338
339
340
341
def definition(self):
    """Congruence expressed as equal side lengths and non-collinearity.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) - Length(self.p4, self.p5),
        Length(self.p2, self.p3) - Length(self.p5, self.p6),
        Length(self.p1, self.p3) - Length(self.p4, self.p6),
        NotCollinear(self.p1, self.p2, self.p3),
    ]

ConstructionRule

Base class for geometric construction rules.

Source code in pyeuclid/formalization/construction_rule.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class ConstructionRule:
    """Base class for geometric construction rules."""

    def __init__(self):
        """Initialize an empty construction rule."""
        pass

    def arguments(self):
        """Return the input entities required for the construction."""
        return []

    def constructed_points(self):
        """Return the points constructed by this rule."""
        return []

    def conditions(self):
        """Return prerequisite relations for the construction to be valid."""
        return []

    def conclusions(self):
        """Return relations implied after applying the construction."""
        return []

    def __str__(self):
        class_name = self.__class__.__name__
        attributes = ",".join(str(value) for _, value in vars(self).items())
        return f"{class_name}({attributes})"

__init__()

Initialize an empty construction rule.

Source code in pyeuclid/formalization/construction_rule.py
12
13
14
def __init__(self):
    """Initialize an empty construction rule."""
    pass

arguments()

Return the input entities required for the construction.

Source code in pyeuclid/formalization/construction_rule.py
16
17
18
def arguments(self):
    """Return the input entities required for the construction."""
    return []

conclusions()

Return relations implied after applying the construction.

Source code in pyeuclid/formalization/construction_rule.py
28
29
30
def conclusions(self):
    """Return relations implied after applying the construction."""
    return []

conditions()

Return prerequisite relations for the construction to be valid.

Source code in pyeuclid/formalization/construction_rule.py
24
25
26
def conditions(self):
    """Return prerequisite relations for the construction to be valid."""
    return []

constructed_points()

Return the points constructed by this rule.

Source code in pyeuclid/formalization/construction_rule.py
20
21
22
def constructed_points(self):
    """Return the points constructed by this rule."""
    return []

Diagram

Source code in pyeuclid/formalization/diagram.py
  22
  23
  24
  25
  26
  27
  28
  29
  30
  31
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
class Diagram:    
    def __new__(cls, constructions_list:list[list[ConstructionRule]]=None, save_path=None, cache_folder=os.path.join(ROOT_DIR, 'cache'), resample=False):
        """Load from cache if available, otherwise construct a new diagram instance."""
        if not resample and cache_folder is not None:
            if not os.path.exists(cache_folder):
                os.makedirs(cache_folder)

            if constructions_list is not None:
                file_name = f"{hash_constructions_list(constructions_list)}.pkl"
                file_path = os.path.join(cache_folder, file_name)
                try:
                    if os.path.exists(file_path):
                        with open(file_path, 'rb') as f:
                            instance = pickle.load(f)
                            instance.save_path = save_path
                            instance.save_diagram()
                            return instance
                except:
                    pass

        instance = super().__new__(cls)
        return instance

    def __init__(self, constructions_list:list[list[ConstructionRule]]=None, save_path=None, cache_folder=os.path.join(ROOT_DIR, 'cache'), resample=False):
        if hasattr(self, 'cache_folder'):
            return

        self.points = []
        self.segments = []
        self.circles = []

        self.name2point = {}
        self.point2name = {}

        self.fig, self.ax = None, None

        self.constructions_list = constructions_list
        self.save_path = save_path
        self.cache_folder = cache_folder

        if constructions_list is not None:                
            self.construct_diagram()

    def clear(self):
        """Reset all stored points, segments, circles, and name mappings."""
        self.points.clear()
        self.segments.clear()
        self.circles.clear()

        self.name2point.clear()
        self.point2name.clear()

    def show(self):
        """Render the diagram with matplotlib."""
        self.draw_diagram(show=True)

    def save_to_cache(self):
        """Persist the diagram to cache if caching is enabled."""
        if self.cache_folder is not None:
            file_name = f"{hash_constructions_list(self.constructions_list)}.pkl"
            file_path = os.path.join(self.cache_folder, file_name)
            with open(file_path, 'wb') as f:
                pickle.dump(self, f)

    def add_constructions(self, constructions):
        """Add a new batch of constructions, retrying if degeneracy occurs."""
        for _ in range(MAX_DIAGRAM_ATTEMPTS):
            try:
                self.construct(constructions)
                self.constructions_list.append(constructions)
                return
            except:
                continue

        print(f"Failed to add the constructions after {MAX_DIAGRAM_ATTEMPTS} attempts.")
        raise Exception()

    def construct_diagram(self):
        """Construct the full diagram from all construction batches, with retries."""
        for _ in range(MAX_DIAGRAM_ATTEMPTS):
            try:
                self.clear()
                for constructions in self.constructions_list:
                    self.construct(constructions)
                self.draw_diagram()
                self.save_to_cache()
                return
            except:
                continue

        print(f"Failed to construct a diagram after {MAX_DIAGRAM_ATTEMPTS} attempts.")
        raise Exception()

    def construct(self, constructions: list[ConstructionRule]):
        """Apply a single batch of construction rules to extend the diagram."""
        constructed_points = constructions[0].constructed_points()
        if any(construction.constructed_points() != constructed_points for construction in constructions[1:]):
            raise Exception()

        to_be_intersected = []
        for construction in constructions:
            # print(construction.__class__.__name__ + '('+','.join([str(name) for name in construction.arguments()])+')')
            # for c in construction.conditions:
            #     if not self.numerical_check(c):
            #         raise Exception()

            to_be_intersected += self.sketch(construction)

        new_points = self.reduce(to_be_intersected, self.points)

        if check_too_close(new_points, self.points):
            raise Exception()

        if check_too_far(new_points, self.points):
            raise Exception()

        self.points += new_points

        for p, np in zip(constructed_points, new_points):
            self.name2point[p.name] = np
            self.point2name[np] = p.name

        for construction in constructions:
            self.draw(new_points, construction)

    def numerical_check_goal(self, goal):
        """Check if the current diagram satisfies a goal relation/expression."""
        if isinstance(goal, tuple):
            for g in goal:
                if self.numerical_check(g):
                    return True, g
        else:
            if self.numerical_check(goal):
                return True, goal
        return False, goal

    def numerical_check(self, relation):
        """Numerically evaluate whether a relation/expression holds in the diagram."""
        if isinstance(relation, Relation):
            func = globals()['check_' + relation.__class__.__name__.lower()]
            args = [self.name2point[p.name] for p in relation.get_points()]
            return func(args)
        else:
            symbol_to_value = {}
            symbols, symbol_names = parse_expression(relation)

            for angle_symbol, angle_name in zip(symbols['Angle'], symbol_names['Angle']):
                angle_value = calculate_angle(*[self.name2point[n] for n in angle_name])
                symbol_to_value[angle_symbol] = angle_value

            for length_symbol, length_name in zip(symbols['Length'], symbol_names['Length']):
                length_value = calculate_length(*[self.name2point[n] for n in length_name])
                symbol_to_value[length_symbol] = length_value

            evaluated_expr = relation.subs(symbol_to_value)
            if close_enough(float(evaluated_expr.evalf()), 0):
                return True
            else:
                return False

    def sketch(self, construction):
        func = getattr(self, 'sketch_' + construction.__class__.__name__[10:])
        args = [arg if isinstance(arg, float) else self.name2point[arg.name] for arg in construction.arguments()]
        result = func(*args)
        if isinstance(result, list):
            return result
        else:
            return [result]

    def sketch_angle_bisector(self, *args: list[Point]) -> Ray:
        """Ray that bisects angle ABC."""
        a, b, c = args
        dist_ab = a.distance(b)
        dist_bc = b.distance(c)
        x = b + (c - b) * (dist_ab / dist_bc)
        m = (a + x) * 0.5
        return Ray(b, m)

    def sketch_angle_mirror(self, *args: list[Point]) -> Ray:
        """Mirror of ray BA across BC."""
        a, b, c = args
        ab = a - b
        cb = c - b

        dist_ab = a.distance(b)
        ang_ab = np.arctan2(ab.y / dist_ab, ab.x / dist_ab)
        dist_cb = c.distance(b)
        ang_bc = np.arctan2(cb.y / dist_cb, cb.x / dist_cb)

        ang_bx = 2 * ang_bc - ang_ab
        x = b + Point(np.cos(ang_bx), np.sin(ang_bx))
        return Ray(b, x)

    def sketch_circle(self, *args: list[Point]) -> Point:
        """Center of circle through three points."""
        a, b, c = args
        l1 = perpendicular_bisector(a, b)
        l2 = perpendicular_bisector(b, c)
        x = line_line_intersection(l1, l2)
        return x

    def sketch_circumcenter(self, *args: list[Point]) -> Point:
        """Circumcenter of triangle ABC."""
        a, b, c = args
        l1 = perpendicular_bisector(a, b)
        l2 = perpendicular_bisector(b, c)
        x = line_line_intersection(l1, l2)
        return x

    def sketch_eq_quadrangle(self, *args: list[Point]) -> list[Point]:
        """Randomly sample a quadrilateral with opposite sides equal."""
        a = Point(0.0, 0.0)
        b = Point(1.0, 0.0)

        length = np.random.uniform(0.5, 2.0)
        ang = np.random.uniform(np.pi / 3, np.pi * 2 / 3)
        d = head_from(a, ang, length)

        ang = ang_of(b, d)
        ang = np.random.uniform(ang / 10, ang / 9)
        c = head_from(b, ang, length)
        a, b, c, d = random_rfss(a, b, c, d)
        return [a, b, c, d]

    def sketch_eq_trapezoid(self, *args: list[Point]) -> list[Point]:
        """Randomly sample an isosceles trapezoid."""
        a = Point(0.0, 0.0)
        b = Point(1.0, 0.0)
        l = unif(0.5, 2.0)

        height = unif(0.5, 2.0)
        c = Point(0.5 + l / 2.0, height)
        d = Point(0.5 - l / 2.0, height)

        a, b, c, d = random_rfss(a, b, c, d)
        return [a, b, c, d]

    def sketch_eq_triangle(self, *args: list[Point]) -> list[Circle]:
        """Circles defining an equilateral triangle on BC."""
        b, c = args
        return [Circle(center=b, radius=b.distance(c)), Circle(center=c, radius=b.distance(c))]

    def sketch_eqangle2(self, *args: list[Point]) -> Point:
        """Point X such that angle ABX equals angle XCB."""
        a, b, c = args

        ba = b.distance(a)
        bc = b.distance(c)
        l = ba * ba / bc

        if unif(0.0, 1.0) < 0.5:
            be = min(l, bc)
            be = unif(be * 0.1, be * 0.9)
        else:
            be = max(l, bc)
            be = unif(be * 1.1, be * 1.5)

        e = b + (c - b) * (be / bc)
        y = b + (a - b) * (be / l)
        return line_line_intersection(Line(c, y), Line(a, e))

    def sketch_eqdia_quadrangle(self, *args) -> list[Point]:
        """Quadrilateral with equal diagonals."""
        m = unif(0.3, 0.7)
        n = unif(0.3, 0.7)
        a = Point(-m, 0.0)
        c = Point(1 - m, 0.0)
        b = Point(0.0, -n)
        d = Point(0.0, 1 - n)

        ang = unif(-0.25 * np.pi, 0.25 * np.pi)
        sin, cos = np.sin(ang), np.cos(ang)
        b = b.rotate(sin, cos)
        d = d.rotate(sin, cos)
        a, b, c, d = random_rfss(a, b, c, d)
        return [a, b, c, d]

    def sketch_eqdistance(self, *args) -> Circle:
        """Circle centered at A with radius BC."""
        a, b, c = args
        return Circle(center=a, radius=b.distance(c))

    def sketch_eqdistance2(self, *args) -> Circle:
        """Circle centered at A with radius alpha*BC."""
        a, b, c, alpha = args
        return Circle(center=a, radius=alpha*b.distance(c))

    def sketch_eqdistance3(self, *args) -> Circle:
        """Circle centered at A with fixed radius alpha."""
        a, alpha = args
        return Circle(center=a, radius=alpha)

    def sketch_foot(self, *args) -> Point:
        """Foot of perpendicular from A to line BC."""
        a, b, c = args
        line_bc = Line(b, c)
        tline = a.perpendicular_line(line_bc)
        return line_line_intersection(tline, line_bc)

    def sketch_free(self, *args) -> Point:
        """Free point uniformly sampled in a box."""
        return Point(unif(-1, 1), unif(-1, 1))

    def sketch_incenter(self, *args) -> Point:
        """Incenter of triangle ABC."""
        a, b, c = args
        l1 = self.sketch_angle_bisector(a, b, c)
        l2 = self.sketch_angle_bisector(b, c, a)
        return line_line_intersection(l1, l2)

    def sketch_incenter2(self, *args) -> list[Point]:
        """Incenter plus touch points on each side."""
        a, b, c = args
        i = self.sketch_incenter(a, b, c)
        x = i.foot(Line(b, c))
        y = i.foot(Line(c, a))
        z = i.foot(Line(a, b))
        return [x, y, z, i]

    def sketch_excenter(self, *args) -> Point:
        """Excenter opposite B in triangle ABC."""
        a, b, c = args
        l1 = self.sketch_angle_bisector(b, a, c)
        l2 = self.sketch_angle_bisector(a, b, c).perpendicular_line(b)
        return line_line_intersection(l1, l2)

    def sketch_excenter2(self, *args) -> list[Point]:
        """Excenter plus touch points on extended sides."""
        a, b, c = args
        i = self.sketch_excenter(a, b, c)
        x = i.foot(Line(b, c))
        y = i.foot(Line(c, a))
        z = i.foot(Line(a, b))
        return [x, y, z, i]

    def sketch_centroid(self, *args) -> list[Point]:
        """Mid-segment points and centroid of triangle ABC."""
        a, b, c = args
        x = (b + c) * 0.5
        y = (c + a) * 0.5
        z = (a + b) * 0.5
        i = line_line_intersection(Line(a, x), Line(b, y))
        return [x, y, z, i]

    def sketch_intersection_cc(self, *args) -> list[Circle]:
        """Two circles centered at O and W through A."""
        o, w, a = args
        return [Circle(center=o, radius=o.distance(a)), Circle(center=w, radius=w.distance(a))]

    def sketch_intersection_lc(self, *args) -> list:
        """Line and circle defined by A,O,B for intersection."""
        a, o, b = args
        return [Line(b, a), Circle(center=o, radius=o.distance(b))]

    def sketch_intersection_ll(self, *args) -> Point:
        """Intersection of lines AB and CD."""
        a, b, c, d = args
        l1 = Line(a, b)
        l2 = Line(c, d)
        return line_line_intersection(l1, l2)

    def sketch_intersection_lp(self, *args) -> Point:
        a, b, c, m, n = args
        l1 = Line(a,b)
        l2 = self.sketch_on_pline(c, m, n)
        return line_line_intersection(l1, l2)

    def sketch_intersection_lt(self, *args) -> Point:
        a, b, c, d, e = args
        l1 = Line(a, b)
        l2 = self.sketch_on_tline(c, d, e)
        return line_line_intersection(l1, l2)

    def sketch_intersection_pp(self, *args) -> Point:
        a, b, c, d, e, f = args
        l1 = self.sketch_on_pline(a, b, c)
        l2 = self.sketch_on_pline(d, e, f)
        return line_line_intersection(l1, l2)

    def sketch_intersection_tt(self, *args) -> Point:
        a, b, c, d, e, f = args
        l1 = self.sketch_on_tline(a, b, c)
        l2 = self.sketch_on_tline(d, e, f)
        return line_line_intersection(l1, l2)

    def sketch_iso_triangle(self, *args) -> list[Point]:
        base = unif(0.5, 1.5)
        height = unif(0.5, 1.5)

        b = Point(-base / 2, 0.0)
        c = Point(base / 2, 0.0)
        a = Point(0.0, height)
        a, b, c = random_rfss(a, b, c)
        return [a, b, c]

    def sketch_lc_tangent(self, *args) -> Line:
        a, o = args
        return self.sketch_on_tline(a, a, o)

    def sketch_midpoint(self, *args) -> Point:
        a, b = args
        return (a + b) * 0.5

    def sketch_mirror(self, *args) -> Point:
        a, b = args
        return b * 2 - a

    def sketch_nsquare(self, *args) -> Point:
        a, b = args
        ang = -np.pi / 2
        return a + (b - a).rotate(np.sin(ang), np.cos(ang))

    def sketch_on_aline(self, *args) -> Line:
        e, d, c, b, a = args
        ab = a - b
        cb = c - b
        de = d - e

        dab = a.distance(b)
        ang_ab = np.arctan2(ab.y / dab, ab.x / dab)

        dcb = c.distance(b)
        ang_bc = np.arctan2(cb.y / dcb, cb.x / dcb)

        dde = d.distance(e)
        ang_de = np.arctan2(de.y / dde, de.x / dde)

        ang_ex = ang_de + ang_bc - ang_ab
        x = e + Point(np.cos(ang_ex), np.sin(ang_ex))
        return Ray(e, x)

    def sketch_on_bline(self, *args) -> Line:
        a, b = args
        m = (a + b) * 0.5
        return m.perpendicular_line(Line(a, b))

    def sketch_on_circle(self, *args) -> Circle:
        o, a = args
        return Circle(o, o.distance(a))

    def sketch_on_line(self, *args) -> Line:
        a, b = args
        return Line(a, b)

    def sketch_on_pline(self, *args) -> Line:
        a, b, c = args
        return a.parallel_line(Line(b, c))

    def sketch_on_tline(self, *args) -> Line:
        a, b, c = args
        return a.perpendicular_line(Line(b, c))

    def sketch_orthocenter(self, *args) -> Point:
        a, b, c = args
        l1 = self.sketch_on_tline(a, b, c)
        l2 = self.sketch_on_tline(b, c, a)
        return line_line_intersection(l1, l2)

    def sketch_parallelogram(self, *args) -> Point:
        a, b, c = args
        l1 = self.sketch_on_pline(a, b, c)
        l2 = self.sketch_on_pline(c, a, b)
        return line_line_intersection(l1, l2)

    def sketch_pentagon(self, *args) -> list[Point]:
        points = [Point(1.0, 0.0)]
        ang = 0.0

        for i in range(4):
            ang += (2 * np.pi - ang) / (5 - i) * unif(0.5, 1.5)
            point = Point(np.cos(ang), np.sin(ang))
            points.append(point)

        a, b, c, d, e = points  # pylint: disable=unbalanced-tuple-unpacking
        a, b, c, d, e = random_rfss(a, b, c, d, e)
        return [a, b, c, d, e]

    def sketch_psquare(self, *args) -> Point:
        a, b = args
        ang = np.pi / 2
        return a + (b - a).rotate(np.sin(ang), np.cos(ang))

    def sketch_quadrangle(self, *args) -> list[Point]:
        a = Point(0.0, 0.0)
        b = Point(1.0, 0.0)

        length = np.random.uniform(0.5, 2.0)
        ang = np.random.uniform(np.pi / 3, np.pi * 2 / 3)
        d = head_from(a, ang, length)

        ang = ang_of(b, d)
        ang = np.random.uniform(ang / 10, ang / 9)
        c = head_from(b, ang, length)
        a, b, c, d = random_rfss(a, b, c, d)
        return [a, b, c, d]

    def sketch_r_trapezoid(self, *args) -> list[Point]:
        """Right trapezoid with AB horizontal and AD vertical."""
        a = Point(0.0, 1.0)
        d = Point(0.0, 0.0)
        b = Point(unif(0.5, 1.5), 1.0)
        c = Point(unif(0.5, 1.5), 0.0)
        a, b, c, d = random_rfss(a, b, c, d)
        return [a, b, c, d]

    def sketch_r_triangle(self, *args) -> list[Point]:
        """Random right triangle with legs on axes."""
        a = Point(0.0, 0.0)
        b = Point(0.0, unif(0.5, 2.0))
        c = Point(unif(0.5, 2.0), 0.0)
        a, b, c = random_rfss(a, b, c)
        return [a, b, c]

    def sketch_rectangle(self, *args) -> list[Point]:
        """Axis-aligned rectangle with random width/height."""
        a = Point(0.0, 0.0)
        b = Point(0.0, 1.0)
        l = unif(0.5, 2.0)
        c = Point(l, 1.0)
        d = Point(l, 0.0)
        a, b, c, d = random_rfss(a, b, c, d)
        return [a, b, c, d]

    def sketch_reflect(self, *args) -> Point:
        """Reflect point A across line BC."""
        a, b, c = args
        m = a.foot(Line(b, c))
        return m * 2 - a

    def sketch_risos(self, *args) -> list[Point]:
        """Right isosceles triangle."""
        a = Point(0.0, 0.0)
        b = Point(0.0, 1.0)
        c = Point(1.0, 0.0)
        a, b, c = random_rfss(a, b, c)
        return [a, b, c]

    def sketch_s_angle(self, *args) -> Ray:
        """Ray at point B making angle alpha with BA."""
        a, b, alpha = args
        ang = alpha / 180 * np.pi
        x = b + (a - b).rotatea(ang)
        return Ray(b, x)

    def sketch_segment(self, *args) -> list[Point]:
        """Random segment endpoints in [-1,1] box."""
        a = Point(unif(-1, 1), unif(-1, 1))
        b = Point(unif(-1, 1), unif(-1, 1))
        return [a, b]

    def sketch_shift(self, *args) -> Point:
        """Translate C by vector BA."""
        c, b, a = args
        return c + (b - a)

    def sketch_square(self, *args) -> list[Point]:
        """Square constructed on segment AB."""
        a, b = args
        c = b + (a - b).rotatea(-np.pi / 2)
        d = a + (b - a).rotatea(np.pi / 2)
        return [c, d]

    def sketch_isquare(self, *args) -> list[Point]:
        """Axis-aligned unit square, randomly re-ordered."""
        a = Point(0.0, 0.0)
        b = Point(1.0, 0.0)
        c = Point(1.0, 1.0)
        d = Point(0.0, 1.0)
        a, b, c, d = random_rfss(a, b, c, d)
        return [a, b, c, d]

    def sketch_trapezoid(self, *args) -> list[Point]:
        """Random trapezoid with AB // CD."""
        d = Point(0.0, 0.0)
        c = Point(1.0, 0.0)

        base = unif(0.5, 2.0)
        height = unif(0.5, 2.0)
        a = Point(unif(0.2, 0.5), height)
        b = Point(a.x + base, height)
        a, b, c, d = random_rfss(a, b, c, d)
        return [a, b, c, d]

    def sketch_triangle(self, *args) -> list[Point]:
        """Random triangle."""
        a = Point(0.0, 0.0)
        b = Point(1.0, 0.0)
        ac = unif(0.5, 2.0)
        ang = unif(0.2, 0.8) * np.pi
        c = head_from(a, ang, ac)
        return [a, b, c]

    def sketch_triangle12(self, *args) -> list[Point]:
        """Triangle with side-length ratios near 1:2."""
        b = Point(0.0, 0.0)
        c = Point(unif(1.5, 2.5), 0.0)
        a, _ = circle_circle_intersection(Circle(b, 1.0), Circle(c, 2.0))
        a, b, c = random_rfss(a, b, c)
        return [a, b, c]

    def sketch_2l1c(self, *args) -> list[Point]:
        """Intersections of perpendiculars from P to AC/BC with circle centered at P."""
        a, b, c, p = args
        bc, ac = Line(b, c), Line(a, c)
        circle = Circle(p, p.distance(a))

        d, d_ = line_circle_intersection(p.perpendicular_line(bc), circle)
        if bc.diff_side(d_, a):
            d = d_

        e, e_ = line_circle_intersection(p.perpendicular_line(ac), circle)
        if ac.diff_side(e_, b):
            e = e_

        df = d.perpendicular_line(Line(p, d))
        ef = e.perpendicular_line(Line(p, e))
        f = line_line_intersection(df, ef)

        g, g_ = line_circle_intersection(Line(c, f), circle)
        if bc.same_side(g_, a):
            g = g_

        b_ = c + (b - c) / b.distance(c)
        a_ = c + (a - c) / a.distance(c)
        m = (a_ + b_) * 0.5
        x = line_line_intersection(Line(c, m), Line(p, g))
        return [x.foot(ac), x.foot(bc), g, x]

    def sketch_e5128(self, *args) -> list[Point]:
        """Problem-specific construction e5128."""
        a, b, c, d = args
        g = (a + b) * 0.5
        de = Line(d, g)

        e, f = line_circle_intersection(de, Circle(c, c.distance(b)))

        if e.distance(d) < f.distance(d):
            e = f
        return [e, g]

    def sketch_3peq(self, *args) -> list[Point]:
        """Three-point equidistance construction."""
        a, b, c = args
        ab, bc, ca = Line(a, b), Line(b, c), Line(c, a)

        z = b + (c - b) * np.random.uniform(-0.5, 1.5)

        z_ = z * 2 - c
        l = z_.parallel_line(ca)
        x = line_line_intersection(l, ab)
        y = z * 2 - x
        return [x, y, z]

    def sketch_trisect(self, *args) -> list[Point]:
        """Trisect angle ABC."""
        a, b, c = args
        ang1 = ang_of(b, a)
        ang2 = ang_of(b, c)

        swap = 0
        if ang1 > ang2:
            ang1, ang2 = ang2, ang1
            swap += 1

        if ang2 - ang1 > np.pi:
            ang1, ang2 = ang2, ang1 + 2 * np.pi
            swap += 1

        angx = ang1 + (ang2 - ang1) / 3
        angy = ang2 - (ang2 - ang1) / 3

        x = b + Point(np.cos(angx), np.sin(angx))
        y = b + Point(np.cos(angy), np.sin(angy))

        ac = Line(a, c)
        x = line_line_intersection(Line(b, x), ac)
        y = line_line_intersection(Line(b, y), ac)

        if swap == 1:
            return [y, x]
        return [x, y]

    def sketch_trisegment(self, *args) -> list[Point]:
        """Trisect segment AB."""
        a, b = args
        x, y = a + (b - a) * (1.0 / 3), a + (b - a) * (2.0 / 3)
        return [x, y]

    def sketch_on_dia(self, *args) -> Circle:
        """Circle with diameter AB."""
        a, b = args
        o = (a + b) * 0.5
        return Circle(o, o.distance(a))

    def sketch_ieq_triangle(self, *args) -> list[Point]:
        a = Point(0.0, 0.0)
        b = Point(1.0, 0.0)

        c, _ = Circle(a, a.distance(b)).intersect(Circle(b, b.distance(a)))
        return [a, b, c]

    def sketch_on_opline(self, *args) -> Ray:
        a, b = args
        return Ray(a, a + a - b)

    def sketch_cc_tangent(self, *args) -> list[Point]:
        o, a, w, b = args
        ra, rb = o.distance(a), w.distance(b)

        ow = Line(o, w)
        if close_enough(ra, rb):
            oo = ow.perpendicular_line(o)
            oa = Circle(o, ra)
            x, z = line_circle_intersection(oo, oa)
            y = x + w - o
            t = z + w - o
            return [x, y, z, t]

    def sketch_cc_tangent0(self, *args) -> Ray:
        o, a, w, b = args
        return self.sketch_cc_tangent(o, a, w, b)[:2]

    def sketch_eqangle3(self, *args) -> list[Point]:
        a, b, d, e, f = args
        de = d.distance(e)
        ef = e.distance(f)
        ab = b.distance(a)
        ang_ax = ang_of(a, b) + ang_between(e, d, f)
        x = head_from(a, ang_ax, length=de / ef * ab)   
        o = self.sketch_circle(a, b, x)
        return Circle(o, o.distance(a))

    def sketch_tangent(self, *args) -> list[Point]:
        a, o, b = args
        dia = self.sketch_dia([a, o])
        return list(circle_circle_intersection(Circle(o, o.distance(b)), dia))

    def sketch_on_circum(self, *args) -> Circle:
        a, b, c = args
        o = self.sketch_circle(a, b, c)
        return Circle(o, o.distance(a))

    def sketch_sameside(self, *args) -> HalfPlane:
        a, b, c = args
        return HalfPlane(a, b, c)

    def sketch_opposingsides(self, *args) -> HalfPlane:
        a, b, c = args
        return HalfPlane(a, b, c, opposingsides=True)

    def reduce(self, objs, existing_points) -> list[Point]:
        """Reduce intersecting objects into sampled intersection points.

        Filters half-planes, handles point-only cases, samples within half-planes,
        or intersects pairs of essential geometric objects.
        """
        essential_objs = [i for i in objs if not isinstance(i, HalfPlane)]
        halfplane_objs = [i for i in objs if isinstance(i, HalfPlane)]

        if all(isinstance(o, Point) for o in objs):
            return objs

        elif all(isinstance(o, HalfPlane) for o in objs):
            if len(objs) == 1:
                return objs[0].sample_within_halfplanes(existing_points,[])
            else:
                return objs[0].sample_within_halfplanes(existing_points,objs[1:])

        elif len(essential_objs) == 1:
            if not halfplane_objs:
                return objs[0].sample_within(existing_points)
            else:
                return objs[0].sample_within_halfplanes(existing_points,halfplane_objs)

        elif len(essential_objs) == 2:
            a, b = essential_objs
            result = a.intersect(b)

            if isinstance(result, Point):
                if halfplane_objs and not all(i.contains(result) for i in halfplane_objs):
                    raise Exception()
                return [result]

            a, b = result

            if halfplane_objs:
                a_correct_side = all(i.contains(a) for i in halfplane_objs)
                b_correct_side = all(i.contains(b) for i in halfplane_objs)

                if a_correct_side and not b_correct_side:
                    return [a]
                elif b_correct_side and not a_correct_side:
                    return [b]
                elif not a_correct_side and not b_correct_side:
                    raise Exception()

            a_close = any([a.close(x) for x in existing_points])
            b_close = any([b.close(x) for x in existing_points])

            if a_close and not b_close:
                return [b]

            elif b_close and not a_close:
                return [a]
            else:
                return [np.random.choice([a, b])]

    def draw(self, new_points, construction):
        func = getattr(self, 'draw_' + construction.__class__.__name__[10:])
        args = [arg if isinstance(arg, float) else self.name2point[arg.name] for arg in construction.arguments()]
        func(*new_points, *args)

    def draw_angle_bisector(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(b, x))

    def draw_angle_mirror(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(b, x))

    def draw_circle(self, *args):
        x, a, b, c = args
        # self.segments.append(Segment(a, x))
        # self.segments.append(Segment(b, x))
        # self.segments.append(Segment(c, x))
        self.circles.append(Circle(x, x.distance(a)))

    def draw_circumcenter(self, *args):
        x, a, b, c = args
        # self.segments.append(Segment(a, x))
        # self.segments.append(Segment(b, x))
        # self.segments.append(Segment(c, x))
        self.circles.append(Circle(x, x.distance(a)))

    def draw_eq_quadrangle(self, *args):
        a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, a))

    def draw_eq_trapezoid(self, *args):
        a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, a))

    def draw_eq_triangle(self, *args):
        x, b, c = args
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, x))
        self.segments.append(Segment(x, b))

    def draw_eqangle2(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(b, a))
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(c, b))
        self.segments.append(Segment(b, b))

    def draw_eqdia_quadrangle(self, *args):
        a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, a))
        self.segments.append(Segment(b, d))
        self.segments.append(Segment(a, c))

    def draw_eqdistance(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(b, c))

    def draw_eqdistance2(self, *args):
        x, a, b, c, alpha = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(b, c))

    def draw_eqdistance2(self, *args):
        x, a, alpha = args
        self.segments.append(Segment(x, a))

    def draw_foot(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(x, b))
        self.segments.append(Segment(x, c))
        self.segments.append(Segment(b, c))

    def draw_free(self, *args):
        x = args

    def draw_incenter(self, *args):
        i, a, b, c = args
        x = i.foot(Line(b, c))
        y = i.foot(Line(c, a))
        z = i.foot(Line(a, b))
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))
        self.circles.append(Circle(p1=x, p2=y, p3=z))

    def draw_incenter2(self, *args):
        x, y, z, i, a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))
        self.circles.append(Circle(p1=x, p2=y, p3=z))

    def draw_excenter(self, *args):
        i, a, b, c = args
        x = i.foot(Line(b, c))
        y = i.foot(Line(c, a))
        z = i.foot(Line(a, b))
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))
        self.circles.append(Circle(p1=x, p2=y, p3=z))

    def draw_excenter2(self, *args):
        x, y, z, i, a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))
        self.circles.append(Circle(p1=x, p2=y, p3=z))

    def draw_centroid(self, *args):
        x, y, z, i, a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, y))
        self.segments.append(Segment(c, z))

    def draw_intersection_cc(self, *args):
        x, o, w, a = args
        self.segments.append(Segment(o, a))
        self.segments.append(Segment(o, x))
        self.segments.append(Segment(w, a))
        self.segments.append(Segment(w, x))
        self.circles.append(Circle(o, o.distance(a)))
        self.circles.append(Circle(w, w.distance(a)))

    def draw_intersection_lc(self, *args):
        x, a, o, b = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(x, b))
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(o, b))
        self.segments.append(Segment(o, x))
        self.circles.append(Circle(o, o.distance(b)))

    def draw_intersection_ll(self, *args):
        x, a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, x))
        self.segments.append(Segment(c, x))
        self.segments.append(Segment(d, x))

    def draw_intersection_lp(self, *args):
        x, a, b, c, m, n = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(m, n))
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, x))
        self.segments.append(Segment(c, x))

    def draw_intersection_lp(self, *args):
        x, a, b, c, m, n = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(m, n))
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, x))
        self.segments.append(Segment(c, x))

    def draw_intersection_lt(self, *args):
        x, a, b, c, d, e = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(d, e))
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, x))
        self.segments.append(Segment(c, x))

    def draw_intersection_pp(self, *args):
        x, a, b, c, d, e, f = args
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(d, x))
        self.segments.append(Segment(e, f))

    def draw_intersection_tt(self, *args):
        x, a, b, c, d, e, f = args
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(d, x))
        self.segments.append(Segment(e, f))

    def draw_iso_triangle(self, *args):
        a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))

    def draw_lc_tangent(self, *args):
        x, a, o = args
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(a, o))
        self.circles.append(Circle(o, o.distance(a)))

    def draw_midpoint(self, *args):
        x, a, b = args
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, x))

    def draw_mirror(self, *args):
        x, a, b = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, x))

    def draw_nsquare(self, *args):
        x, a, b = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, x))

    def draw_on_aline(self, *args):
        x, a, b, c, d, e = args
        self.segments.append(Segment(e, d))
        self.segments.append(Segment(d, c))
        self.segments.append(Segment(b, a))
        self.segments.append(Segment(a, x))

    def draw_on_bline(self, *args):
        x, a, b = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, x))

    def draw_on_circle(self, *args):
        x, o, a = args
        self.segments.append(Segment(o, a))
        self.segments.append(Segment(o, x))
        self.circles.append(Circle(o, o.distance(x)))

    def draw_on_line(self, *args):
        x, a, b = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(x, b))
        self.segments.append(Segment(a, b))

    def draw_on_pline(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(b, c))

    def draw_on_tline(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(b, c))

    def draw_orthocenter(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))

    def draw_parallelogram(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, x))

    def draw_parallelogram(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, x))

    def draw_pentagon(self, *args):
        a, b, c, d, e = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, e))
        self.segments.append(Segment(e, a))

    def draw_psquare(self, *args):
        x, a, b = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(a, b))

    def draw_quadrangle(self, *args):
        a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, a))

    def draw_r_trapezoid(self, *args):
        a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, a))

    def draw_r_triangle(self, *args):
        a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))

    def draw_rectangle(self, *args):
        a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, a))

    def draw_reflect(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(b, c))

    def draw_risos(self, *args):
        a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))

    def draw_s_angle(self, *args):
        x, a, b, alpha = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, x))

    def draw_segment(self, *args):
        a, b = args
        self.segments.append(Segment(a, b))

    def draw_s_segment(self, *args):
        a, b, alpha = args
        self.segments.append(Segment(a, b))

    def draw_shift(self, *args):
        x, b, c, d = args
        self.segments.append(Segment(x, b))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(x, c))
        self.segments.append(Segment(b, d))

    def draw_square(self, *args):
        x, y, a, b = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, x))
        self.segments.append(Segment(x, y))
        self.segments.append(Segment(y, a))

    def draw_isquare(self, *args):
        a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, a))

    def draw_trapezoid(self, *args):
        a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, a))

    def draw_triangle(self, *args):
        a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))

    def draw_triangle12(self, *args):
        a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))

    def draw_2l1c(self, *args):
        x, y, z, i, a, b, c, o = args
        self.segments.append(Segment(a, c))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(a, o))
        self.segments.append(Segment(b, o))

        self.segments.append(Segment(i, x))
        self.segments.append(Segment(i, y))
        self.segments.append(Segment(i, z))

        self.segments.append(Segment(c, x))
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(c, y))
        self.segments.append(Segment(b, y))
        self.segments.append(Segment(o, z))

        self.circles.append(Circle(i, i.distance(x)))
        self.circles.append(Circle(o, o.distance(a)))

    def draw_e5128(self, *args):
        x, y, a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, a))

        self.segments.append(Segment(a, x))
        self.segments.append(Segment(x, y))
        self.segments.append(Segment(c, x))

    def draw_3peq(self, *args):
        x, y, z, a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))

        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, x))

        self.segments.append(Segment(a, y))
        self.segments.append(Segment(c, y))

        self.segments.append(Segment(c, z))
        self.segments.append(Segment(b, z))

        self.segments.append(Segment(x, y))
        self.segments.append(Segment(y, z))
        self.segments.append(Segment(z, x))

    def draw_trisect(self, *args):
        x, y, a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))

        self.segments.append(Segment(b, x))
        self.segments.append(Segment(b, y))

        self.segments.append(Segment(a, x))
        self.segments.append(Segment(x, y))
        self.segments.append(Segment(y, c))

    def draw_trisegment(self, *args):
        x, y, a, b = args
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(x, y))
        self.segments.append(Segment(y, b))

    def draw_on_dia(self, *args):
        x, a, b = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(x, b))

    def draw_ieq_triangle(self, *args):
        a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))

    def draw_on_opline(self, *args):
        x, a, b = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(x, b))

    def draw_cc_tangent0(self, *args):
        x, y, o, a, w, b = args
        self.segments.append(Segment(o, a))
        self.segments.append(Segment(o, x))

        self.segments.append(Segment(w, b))
        self.segments.append(Segment(w, y))

        self.segments.append(Segment(x, y))

        self.circles.append(Circle(o, o.distance(a)))
        self.circles.append(Circle(w, w.distance(b)))

    def draw_cc_tangent(self, *args):
        x, y, z, i, o, a, w, b = args
        self.segments.append(Segment(o, a))
        self.segments.append(Segment(o, x))
        self.segments.append(Segment(o, z))

        self.segments.append(Segment(w, b))
        self.segments.append(Segment(w, y))
        self.segments.append(Segment(w, i))

        self.segments.append(Segment(x, y))
        self.segments.append(Segment(z, i))

        self.circles.append(Circle(o, o.distance(a)))
        self.circles.append(Circle(w, w.distance(b)))

    def draw_eqangle3(self, *args):
        x, a, b, d, e, f = args
        self.segments.append(Segment(f, d))
        self.segments.append(Segment(d, e))

        self.segments.append(Segment(a, x))
        self.segments.append(Segment(x, b))

    def draw_tangent(self, *args):
        x, y, a, o, b = args
        self.segments.append(Segment(o, b))
        self.segments.append(Segment(o, x))
        self.segments.append(Segment(o, y))
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(a, y))

        self.circles.append(Circle(o, o.distance(b)))

    def draw_on_circum(self, *args):
        x, a, b, c = args
        self.circles.append(Circle(p1=a, p2=b, p3=c))

    def draw_sameside(self, *args):
        x, a, b, c = args

    def draw_opposingsides(self, *args):
        x, a, b, c = args

    def draw_diagram(self, show=False):
        """Draw the current diagram; optionally display the matplotlib figure."""
        imsize = 512 / 100
        self.fig, self.ax = plt.subplots(figsize=(imsize, imsize), dpi=300)
        self.ax.set_facecolor((1.0, 1.0, 1.0))

        for segment in self.segments:
            p1, p2 = segment.p1, segment.p2
            lx, ly = (p1.x, p2.x), (p1.y, p2.y)
            self.ax.plot(lx, ly, color='black', lw=1.2, alpha=0.8, ls='-')

        for circle in self.circles:
            self.ax.add_patch(
                plt.Circle(
                    (circle.center.x, circle.center.y),
                    circle.radius,
                    color='red',
                    alpha=0.8,
                    fill=False,
                    lw=1.2,
                    ls='-'
                )
            )

        for p in self.points:
            self.ax.scatter(p.x, p.y, color='black', s=15)
            self.ax.annotate(self.point2name[p], (p.x+0.015, p.y+0.015), color='black', fontsize=8)

        self.ax.set_aspect('equal')
        self.ax.set_axis_off()
        self.fig.subplots_adjust(left=0.05, right=0.95, top=0.95, bottom=0.05, wspace=0, hspace=0)
        xmin = min([p.x for p in self.points])
        xmax = max([p.x for p in self.points])
        ymin = min([p.y for p in self.points])
        ymax = max([p.y for p in self.points])
        x_margin = (xmax - xmin) * 0.1
        y_margin = (ymax - ymin) * 0.1

        self.ax.margins(x_margin, y_margin)

        self.save_diagram()

        if show:
            plt.show()

        plt.close(self.fig)

    def save_diagram(self):
        if self.save_path is not None:
            parent_dir = os.path.dirname(self.save_path)
            if parent_dir and not os.path.exists(parent_dir):
                os.makedirs(parent_dir)
            self.fig.savefig(self.save_path)

__new__(constructions_list=None, save_path=None, cache_folder=os.path.join(ROOT_DIR, 'cache'), resample=False)

Load from cache if available, otherwise construct a new diagram instance.

Source code in pyeuclid/formalization/diagram.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def __new__(cls, constructions_list:list[list[ConstructionRule]]=None, save_path=None, cache_folder=os.path.join(ROOT_DIR, 'cache'), resample=False):
    """Load from cache if available, otherwise construct a new diagram instance."""
    if not resample and cache_folder is not None:
        if not os.path.exists(cache_folder):
            os.makedirs(cache_folder)

        if constructions_list is not None:
            file_name = f"{hash_constructions_list(constructions_list)}.pkl"
            file_path = os.path.join(cache_folder, file_name)
            try:
                if os.path.exists(file_path):
                    with open(file_path, 'rb') as f:
                        instance = pickle.load(f)
                        instance.save_path = save_path
                        instance.save_diagram()
                        return instance
            except:
                pass

    instance = super().__new__(cls)
    return instance

add_constructions(constructions)

Add a new batch of constructions, retrying if degeneracy occurs.

Source code in pyeuclid/formalization/diagram.py
86
87
88
89
90
91
92
93
94
95
96
97
def add_constructions(self, constructions):
    """Add a new batch of constructions, retrying if degeneracy occurs."""
    for _ in range(MAX_DIAGRAM_ATTEMPTS):
        try:
            self.construct(constructions)
            self.constructions_list.append(constructions)
            return
        except:
            continue

    print(f"Failed to add the constructions after {MAX_DIAGRAM_ATTEMPTS} attempts.")
    raise Exception()

clear()

Reset all stored points, segments, circles, and name mappings.

Source code in pyeuclid/formalization/diagram.py
65
66
67
68
69
70
71
72
def clear(self):
    """Reset all stored points, segments, circles, and name mappings."""
    self.points.clear()
    self.segments.clear()
    self.circles.clear()

    self.name2point.clear()
    self.point2name.clear()

construct(constructions)

Apply a single batch of construction rules to extend the diagram.

Source code in pyeuclid/formalization/diagram.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
def construct(self, constructions: list[ConstructionRule]):
    """Apply a single batch of construction rules to extend the diagram."""
    constructed_points = constructions[0].constructed_points()
    if any(construction.constructed_points() != constructed_points for construction in constructions[1:]):
        raise Exception()

    to_be_intersected = []
    for construction in constructions:
        # print(construction.__class__.__name__ + '('+','.join([str(name) for name in construction.arguments()])+')')
        # for c in construction.conditions:
        #     if not self.numerical_check(c):
        #         raise Exception()

        to_be_intersected += self.sketch(construction)

    new_points = self.reduce(to_be_intersected, self.points)

    if check_too_close(new_points, self.points):
        raise Exception()

    if check_too_far(new_points, self.points):
        raise Exception()

    self.points += new_points

    for p, np in zip(constructed_points, new_points):
        self.name2point[p.name] = np
        self.point2name[np] = p.name

    for construction in constructions:
        self.draw(new_points, construction)

construct_diagram()

Construct the full diagram from all construction batches, with retries.

Source code in pyeuclid/formalization/diagram.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def construct_diagram(self):
    """Construct the full diagram from all construction batches, with retries."""
    for _ in range(MAX_DIAGRAM_ATTEMPTS):
        try:
            self.clear()
            for constructions in self.constructions_list:
                self.construct(constructions)
            self.draw_diagram()
            self.save_to_cache()
            return
        except:
            continue

    print(f"Failed to construct a diagram after {MAX_DIAGRAM_ATTEMPTS} attempts.")
    raise Exception()

draw_diagram(show=False)

Draw the current diagram; optionally display the matplotlib figure.

Source code in pyeuclid/formalization/diagram.py
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
def draw_diagram(self, show=False):
    """Draw the current diagram; optionally display the matplotlib figure."""
    imsize = 512 / 100
    self.fig, self.ax = plt.subplots(figsize=(imsize, imsize), dpi=300)
    self.ax.set_facecolor((1.0, 1.0, 1.0))

    for segment in self.segments:
        p1, p2 = segment.p1, segment.p2
        lx, ly = (p1.x, p2.x), (p1.y, p2.y)
        self.ax.plot(lx, ly, color='black', lw=1.2, alpha=0.8, ls='-')

    for circle in self.circles:
        self.ax.add_patch(
            plt.Circle(
                (circle.center.x, circle.center.y),
                circle.radius,
                color='red',
                alpha=0.8,
                fill=False,
                lw=1.2,
                ls='-'
            )
        )

    for p in self.points:
        self.ax.scatter(p.x, p.y, color='black', s=15)
        self.ax.annotate(self.point2name[p], (p.x+0.015, p.y+0.015), color='black', fontsize=8)

    self.ax.set_aspect('equal')
    self.ax.set_axis_off()
    self.fig.subplots_adjust(left=0.05, right=0.95, top=0.95, bottom=0.05, wspace=0, hspace=0)
    xmin = min([p.x for p in self.points])
    xmax = max([p.x for p in self.points])
    ymin = min([p.y for p in self.points])
    ymax = max([p.y for p in self.points])
    x_margin = (xmax - xmin) * 0.1
    y_margin = (ymax - ymin) * 0.1

    self.ax.margins(x_margin, y_margin)

    self.save_diagram()

    if show:
        plt.show()

    plt.close(self.fig)

numerical_check(relation)

Numerically evaluate whether a relation/expression holds in the diagram.

Source code in pyeuclid/formalization/diagram.py
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
def numerical_check(self, relation):
    """Numerically evaluate whether a relation/expression holds in the diagram."""
    if isinstance(relation, Relation):
        func = globals()['check_' + relation.__class__.__name__.lower()]
        args = [self.name2point[p.name] for p in relation.get_points()]
        return func(args)
    else:
        symbol_to_value = {}
        symbols, symbol_names = parse_expression(relation)

        for angle_symbol, angle_name in zip(symbols['Angle'], symbol_names['Angle']):
            angle_value = calculate_angle(*[self.name2point[n] for n in angle_name])
            symbol_to_value[angle_symbol] = angle_value

        for length_symbol, length_name in zip(symbols['Length'], symbol_names['Length']):
            length_value = calculate_length(*[self.name2point[n] for n in length_name])
            symbol_to_value[length_symbol] = length_value

        evaluated_expr = relation.subs(symbol_to_value)
        if close_enough(float(evaluated_expr.evalf()), 0):
            return True
        else:
            return False

numerical_check_goal(goal)

Check if the current diagram satisfies a goal relation/expression.

Source code in pyeuclid/formalization/diagram.py
147
148
149
150
151
152
153
154
155
156
def numerical_check_goal(self, goal):
    """Check if the current diagram satisfies a goal relation/expression."""
    if isinstance(goal, tuple):
        for g in goal:
            if self.numerical_check(g):
                return True, g
    else:
        if self.numerical_check(goal):
            return True, goal
    return False, goal

reduce(objs, existing_points)

Reduce intersecting objects into sampled intersection points.

Filters half-planes, handles point-only cases, samples within half-planes, or intersects pairs of essential geometric objects.

Source code in pyeuclid/formalization/diagram.py
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
def reduce(self, objs, existing_points) -> list[Point]:
    """Reduce intersecting objects into sampled intersection points.

    Filters half-planes, handles point-only cases, samples within half-planes,
    or intersects pairs of essential geometric objects.
    """
    essential_objs = [i for i in objs if not isinstance(i, HalfPlane)]
    halfplane_objs = [i for i in objs if isinstance(i, HalfPlane)]

    if all(isinstance(o, Point) for o in objs):
        return objs

    elif all(isinstance(o, HalfPlane) for o in objs):
        if len(objs) == 1:
            return objs[0].sample_within_halfplanes(existing_points,[])
        else:
            return objs[0].sample_within_halfplanes(existing_points,objs[1:])

    elif len(essential_objs) == 1:
        if not halfplane_objs:
            return objs[0].sample_within(existing_points)
        else:
            return objs[0].sample_within_halfplanes(existing_points,halfplane_objs)

    elif len(essential_objs) == 2:
        a, b = essential_objs
        result = a.intersect(b)

        if isinstance(result, Point):
            if halfplane_objs and not all(i.contains(result) for i in halfplane_objs):
                raise Exception()
            return [result]

        a, b = result

        if halfplane_objs:
            a_correct_side = all(i.contains(a) for i in halfplane_objs)
            b_correct_side = all(i.contains(b) for i in halfplane_objs)

            if a_correct_side and not b_correct_side:
                return [a]
            elif b_correct_side and not a_correct_side:
                return [b]
            elif not a_correct_side and not b_correct_side:
                raise Exception()

        a_close = any([a.close(x) for x in existing_points])
        b_close = any([b.close(x) for x in existing_points])

        if a_close and not b_close:
            return [b]

        elif b_close and not a_close:
            return [a]
        else:
            return [np.random.choice([a, b])]

save_to_cache()

Persist the diagram to cache if caching is enabled.

Source code in pyeuclid/formalization/diagram.py
78
79
80
81
82
83
84
def save_to_cache(self):
    """Persist the diagram to cache if caching is enabled."""
    if self.cache_folder is not None:
        file_name = f"{hash_constructions_list(self.constructions_list)}.pkl"
        file_path = os.path.join(self.cache_folder, file_name)
        with open(file_path, 'wb') as f:
            pickle.dump(self, f)

show()

Render the diagram with matplotlib.

Source code in pyeuclid/formalization/diagram.py
74
75
76
def show(self):
    """Render the diagram with matplotlib."""
    self.draw_diagram(show=True)

sketch_2l1c(*args)

Intersections of perpendiculars from P to AC/BC with circle centered at P.

Source code in pyeuclid/formalization/diagram.py
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
def sketch_2l1c(self, *args) -> list[Point]:
    """Intersections of perpendiculars from P to AC/BC with circle centered at P."""
    a, b, c, p = args
    bc, ac = Line(b, c), Line(a, c)
    circle = Circle(p, p.distance(a))

    d, d_ = line_circle_intersection(p.perpendicular_line(bc), circle)
    if bc.diff_side(d_, a):
        d = d_

    e, e_ = line_circle_intersection(p.perpendicular_line(ac), circle)
    if ac.diff_side(e_, b):
        e = e_

    df = d.perpendicular_line(Line(p, d))
    ef = e.perpendicular_line(Line(p, e))
    f = line_line_intersection(df, ef)

    g, g_ = line_circle_intersection(Line(c, f), circle)
    if bc.same_side(g_, a):
        g = g_

    b_ = c + (b - c) / b.distance(c)
    a_ = c + (a - c) / a.distance(c)
    m = (a_ + b_) * 0.5
    x = line_line_intersection(Line(c, m), Line(p, g))
    return [x.foot(ac), x.foot(bc), g, x]

sketch_3peq(*args)

Three-point equidistance construction.

Source code in pyeuclid/formalization/diagram.py
662
663
664
665
666
667
668
669
670
671
672
673
def sketch_3peq(self, *args) -> list[Point]:
    """Three-point equidistance construction."""
    a, b, c = args
    ab, bc, ca = Line(a, b), Line(b, c), Line(c, a)

    z = b + (c - b) * np.random.uniform(-0.5, 1.5)

    z_ = z * 2 - c
    l = z_.parallel_line(ca)
    x = line_line_intersection(l, ab)
    y = z * 2 - x
    return [x, y, z]

sketch_angle_bisector(*args)

Ray that bisects angle ABC.

Source code in pyeuclid/formalization/diagram.py
191
192
193
194
195
196
197
198
def sketch_angle_bisector(self, *args: list[Point]) -> Ray:
    """Ray that bisects angle ABC."""
    a, b, c = args
    dist_ab = a.distance(b)
    dist_bc = b.distance(c)
    x = b + (c - b) * (dist_ab / dist_bc)
    m = (a + x) * 0.5
    return Ray(b, m)

sketch_angle_mirror(*args)

Mirror of ray BA across BC.

Source code in pyeuclid/formalization/diagram.py
200
201
202
203
204
205
206
207
208
209
210
211
212
213
def sketch_angle_mirror(self, *args: list[Point]) -> Ray:
    """Mirror of ray BA across BC."""
    a, b, c = args
    ab = a - b
    cb = c - b

    dist_ab = a.distance(b)
    ang_ab = np.arctan2(ab.y / dist_ab, ab.x / dist_ab)
    dist_cb = c.distance(b)
    ang_bc = np.arctan2(cb.y / dist_cb, cb.x / dist_cb)

    ang_bx = 2 * ang_bc - ang_ab
    x = b + Point(np.cos(ang_bx), np.sin(ang_bx))
    return Ray(b, x)

sketch_centroid(*args)

Mid-segment points and centroid of triangle ABC.

Source code in pyeuclid/formalization/diagram.py
357
358
359
360
361
362
363
364
def sketch_centroid(self, *args) -> list[Point]:
    """Mid-segment points and centroid of triangle ABC."""
    a, b, c = args
    x = (b + c) * 0.5
    y = (c + a) * 0.5
    z = (a + b) * 0.5
    i = line_line_intersection(Line(a, x), Line(b, y))
    return [x, y, z, i]

sketch_circle(*args)

Center of circle through three points.

Source code in pyeuclid/formalization/diagram.py
215
216
217
218
219
220
221
def sketch_circle(self, *args: list[Point]) -> Point:
    """Center of circle through three points."""
    a, b, c = args
    l1 = perpendicular_bisector(a, b)
    l2 = perpendicular_bisector(b, c)
    x = line_line_intersection(l1, l2)
    return x

sketch_circumcenter(*args)

Circumcenter of triangle ABC.

Source code in pyeuclid/formalization/diagram.py
223
224
225
226
227
228
229
def sketch_circumcenter(self, *args: list[Point]) -> Point:
    """Circumcenter of triangle ABC."""
    a, b, c = args
    l1 = perpendicular_bisector(a, b)
    l2 = perpendicular_bisector(b, c)
    x = line_line_intersection(l1, l2)
    return x

sketch_e5128(*args)

Problem-specific construction e5128.

Source code in pyeuclid/formalization/diagram.py
650
651
652
653
654
655
656
657
658
659
660
def sketch_e5128(self, *args) -> list[Point]:
    """Problem-specific construction e5128."""
    a, b, c, d = args
    g = (a + b) * 0.5
    de = Line(d, g)

    e, f = line_circle_intersection(de, Circle(c, c.distance(b)))

    if e.distance(d) < f.distance(d):
        e = f
    return [e, g]

sketch_eq_quadrangle(*args)

Randomly sample a quadrilateral with opposite sides equal.

Source code in pyeuclid/formalization/diagram.py
231
232
233
234
235
236
237
238
239
240
241
242
243
244
def sketch_eq_quadrangle(self, *args: list[Point]) -> list[Point]:
    """Randomly sample a quadrilateral with opposite sides equal."""
    a = Point(0.0, 0.0)
    b = Point(1.0, 0.0)

    length = np.random.uniform(0.5, 2.0)
    ang = np.random.uniform(np.pi / 3, np.pi * 2 / 3)
    d = head_from(a, ang, length)

    ang = ang_of(b, d)
    ang = np.random.uniform(ang / 10, ang / 9)
    c = head_from(b, ang, length)
    a, b, c, d = random_rfss(a, b, c, d)
    return [a, b, c, d]

sketch_eq_trapezoid(*args)

Randomly sample an isosceles trapezoid.

Source code in pyeuclid/formalization/diagram.py
246
247
248
249
250
251
252
253
254
255
256
257
def sketch_eq_trapezoid(self, *args: list[Point]) -> list[Point]:
    """Randomly sample an isosceles trapezoid."""
    a = Point(0.0, 0.0)
    b = Point(1.0, 0.0)
    l = unif(0.5, 2.0)

    height = unif(0.5, 2.0)
    c = Point(0.5 + l / 2.0, height)
    d = Point(0.5 - l / 2.0, height)

    a, b, c, d = random_rfss(a, b, c, d)
    return [a, b, c, d]

sketch_eq_triangle(*args)

Circles defining an equilateral triangle on BC.

Source code in pyeuclid/formalization/diagram.py
259
260
261
262
def sketch_eq_triangle(self, *args: list[Point]) -> list[Circle]:
    """Circles defining an equilateral triangle on BC."""
    b, c = args
    return [Circle(center=b, radius=b.distance(c)), Circle(center=c, radius=b.distance(c))]

sketch_eqangle2(*args)

Point X such that angle ABX equals angle XCB.

Source code in pyeuclid/formalization/diagram.py
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
def sketch_eqangle2(self, *args: list[Point]) -> Point:
    """Point X such that angle ABX equals angle XCB."""
    a, b, c = args

    ba = b.distance(a)
    bc = b.distance(c)
    l = ba * ba / bc

    if unif(0.0, 1.0) < 0.5:
        be = min(l, bc)
        be = unif(be * 0.1, be * 0.9)
    else:
        be = max(l, bc)
        be = unif(be * 1.1, be * 1.5)

    e = b + (c - b) * (be / bc)
    y = b + (a - b) * (be / l)
    return line_line_intersection(Line(c, y), Line(a, e))

sketch_eqdia_quadrangle(*args)

Quadrilateral with equal diagonals.

Source code in pyeuclid/formalization/diagram.py
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
def sketch_eqdia_quadrangle(self, *args) -> list[Point]:
    """Quadrilateral with equal diagonals."""
    m = unif(0.3, 0.7)
    n = unif(0.3, 0.7)
    a = Point(-m, 0.0)
    c = Point(1 - m, 0.0)
    b = Point(0.0, -n)
    d = Point(0.0, 1 - n)

    ang = unif(-0.25 * np.pi, 0.25 * np.pi)
    sin, cos = np.sin(ang), np.cos(ang)
    b = b.rotate(sin, cos)
    d = d.rotate(sin, cos)
    a, b, c, d = random_rfss(a, b, c, d)
    return [a, b, c, d]

sketch_eqdistance(*args)

Circle centered at A with radius BC.

Source code in pyeuclid/formalization/diagram.py
299
300
301
302
def sketch_eqdistance(self, *args) -> Circle:
    """Circle centered at A with radius BC."""
    a, b, c = args
    return Circle(center=a, radius=b.distance(c))

sketch_eqdistance2(*args)

Circle centered at A with radius alpha*BC.

Source code in pyeuclid/formalization/diagram.py
304
305
306
307
def sketch_eqdistance2(self, *args) -> Circle:
    """Circle centered at A with radius alpha*BC."""
    a, b, c, alpha = args
    return Circle(center=a, radius=alpha*b.distance(c))

sketch_eqdistance3(*args)

Circle centered at A with fixed radius alpha.

Source code in pyeuclid/formalization/diagram.py
309
310
311
312
def sketch_eqdistance3(self, *args) -> Circle:
    """Circle centered at A with fixed radius alpha."""
    a, alpha = args
    return Circle(center=a, radius=alpha)

sketch_excenter(*args)

Excenter opposite B in triangle ABC.

Source code in pyeuclid/formalization/diagram.py
341
342
343
344
345
346
def sketch_excenter(self, *args) -> Point:
    """Excenter opposite B in triangle ABC."""
    a, b, c = args
    l1 = self.sketch_angle_bisector(b, a, c)
    l2 = self.sketch_angle_bisector(a, b, c).perpendicular_line(b)
    return line_line_intersection(l1, l2)

sketch_excenter2(*args)

Excenter plus touch points on extended sides.

Source code in pyeuclid/formalization/diagram.py
348
349
350
351
352
353
354
355
def sketch_excenter2(self, *args) -> list[Point]:
    """Excenter plus touch points on extended sides."""
    a, b, c = args
    i = self.sketch_excenter(a, b, c)
    x = i.foot(Line(b, c))
    y = i.foot(Line(c, a))
    z = i.foot(Line(a, b))
    return [x, y, z, i]

sketch_foot(*args)

Foot of perpendicular from A to line BC.

Source code in pyeuclid/formalization/diagram.py
314
315
316
317
318
319
def sketch_foot(self, *args) -> Point:
    """Foot of perpendicular from A to line BC."""
    a, b, c = args
    line_bc = Line(b, c)
    tline = a.perpendicular_line(line_bc)
    return line_line_intersection(tline, line_bc)

sketch_free(*args)

Free point uniformly sampled in a box.

Source code in pyeuclid/formalization/diagram.py
321
322
323
def sketch_free(self, *args) -> Point:
    """Free point uniformly sampled in a box."""
    return Point(unif(-1, 1), unif(-1, 1))

sketch_incenter(*args)

Incenter of triangle ABC.

Source code in pyeuclid/formalization/diagram.py
325
326
327
328
329
330
def sketch_incenter(self, *args) -> Point:
    """Incenter of triangle ABC."""
    a, b, c = args
    l1 = self.sketch_angle_bisector(a, b, c)
    l2 = self.sketch_angle_bisector(b, c, a)
    return line_line_intersection(l1, l2)

sketch_incenter2(*args)

Incenter plus touch points on each side.

Source code in pyeuclid/formalization/diagram.py
332
333
334
335
336
337
338
339
def sketch_incenter2(self, *args) -> list[Point]:
    """Incenter plus touch points on each side."""
    a, b, c = args
    i = self.sketch_incenter(a, b, c)
    x = i.foot(Line(b, c))
    y = i.foot(Line(c, a))
    z = i.foot(Line(a, b))
    return [x, y, z, i]

sketch_intersection_cc(*args)

Two circles centered at O and W through A.

Source code in pyeuclid/formalization/diagram.py
366
367
368
369
def sketch_intersection_cc(self, *args) -> list[Circle]:
    """Two circles centered at O and W through A."""
    o, w, a = args
    return [Circle(center=o, radius=o.distance(a)), Circle(center=w, radius=w.distance(a))]

sketch_intersection_lc(*args)

Line and circle defined by A,O,B for intersection.

Source code in pyeuclid/formalization/diagram.py
371
372
373
374
def sketch_intersection_lc(self, *args) -> list:
    """Line and circle defined by A,O,B for intersection."""
    a, o, b = args
    return [Line(b, a), Circle(center=o, radius=o.distance(b))]

sketch_intersection_ll(*args)

Intersection of lines AB and CD.

Source code in pyeuclid/formalization/diagram.py
376
377
378
379
380
381
def sketch_intersection_ll(self, *args) -> Point:
    """Intersection of lines AB and CD."""
    a, b, c, d = args
    l1 = Line(a, b)
    l2 = Line(c, d)
    return line_line_intersection(l1, l2)

sketch_isquare(*args)

Axis-aligned unit square, randomly re-ordered.

Source code in pyeuclid/formalization/diagram.py
584
585
586
587
588
589
590
591
def sketch_isquare(self, *args) -> list[Point]:
    """Axis-aligned unit square, randomly re-ordered."""
    a = Point(0.0, 0.0)
    b = Point(1.0, 0.0)
    c = Point(1.0, 1.0)
    d = Point(0.0, 1.0)
    a, b, c, d = random_rfss(a, b, c, d)
    return [a, b, c, d]

sketch_on_dia(*args)

Circle with diameter AB.

Source code in pyeuclid/formalization/diagram.py
710
711
712
713
714
def sketch_on_dia(self, *args) -> Circle:
    """Circle with diameter AB."""
    a, b = args
    o = (a + b) * 0.5
    return Circle(o, o.distance(a))

sketch_r_trapezoid(*args)

Right trapezoid with AB horizontal and AD vertical.

Source code in pyeuclid/formalization/diagram.py
518
519
520
521
522
523
524
525
def sketch_r_trapezoid(self, *args) -> list[Point]:
    """Right trapezoid with AB horizontal and AD vertical."""
    a = Point(0.0, 1.0)
    d = Point(0.0, 0.0)
    b = Point(unif(0.5, 1.5), 1.0)
    c = Point(unif(0.5, 1.5), 0.0)
    a, b, c, d = random_rfss(a, b, c, d)
    return [a, b, c, d]

sketch_r_triangle(*args)

Random right triangle with legs on axes.

Source code in pyeuclid/formalization/diagram.py
527
528
529
530
531
532
533
def sketch_r_triangle(self, *args) -> list[Point]:
    """Random right triangle with legs on axes."""
    a = Point(0.0, 0.0)
    b = Point(0.0, unif(0.5, 2.0))
    c = Point(unif(0.5, 2.0), 0.0)
    a, b, c = random_rfss(a, b, c)
    return [a, b, c]

sketch_rectangle(*args)

Axis-aligned rectangle with random width/height.

Source code in pyeuclid/formalization/diagram.py
535
536
537
538
539
540
541
542
543
def sketch_rectangle(self, *args) -> list[Point]:
    """Axis-aligned rectangle with random width/height."""
    a = Point(0.0, 0.0)
    b = Point(0.0, 1.0)
    l = unif(0.5, 2.0)
    c = Point(l, 1.0)
    d = Point(l, 0.0)
    a, b, c, d = random_rfss(a, b, c, d)
    return [a, b, c, d]

sketch_reflect(*args)

Reflect point A across line BC.

Source code in pyeuclid/formalization/diagram.py
545
546
547
548
549
def sketch_reflect(self, *args) -> Point:
    """Reflect point A across line BC."""
    a, b, c = args
    m = a.foot(Line(b, c))
    return m * 2 - a

sketch_risos(*args)

Right isosceles triangle.

Source code in pyeuclid/formalization/diagram.py
551
552
553
554
555
556
557
def sketch_risos(self, *args) -> list[Point]:
    """Right isosceles triangle."""
    a = Point(0.0, 0.0)
    b = Point(0.0, 1.0)
    c = Point(1.0, 0.0)
    a, b, c = random_rfss(a, b, c)
    return [a, b, c]

sketch_s_angle(*args)

Ray at point B making angle alpha with BA.

Source code in pyeuclid/formalization/diagram.py
559
560
561
562
563
564
def sketch_s_angle(self, *args) -> Ray:
    """Ray at point B making angle alpha with BA."""
    a, b, alpha = args
    ang = alpha / 180 * np.pi
    x = b + (a - b).rotatea(ang)
    return Ray(b, x)

sketch_segment(*args)

Random segment endpoints in [-1,1] box.

Source code in pyeuclid/formalization/diagram.py
566
567
568
569
570
def sketch_segment(self, *args) -> list[Point]:
    """Random segment endpoints in [-1,1] box."""
    a = Point(unif(-1, 1), unif(-1, 1))
    b = Point(unif(-1, 1), unif(-1, 1))
    return [a, b]

sketch_shift(*args)

Translate C by vector BA.

Source code in pyeuclid/formalization/diagram.py
572
573
574
575
def sketch_shift(self, *args) -> Point:
    """Translate C by vector BA."""
    c, b, a = args
    return c + (b - a)

sketch_square(*args)

Square constructed on segment AB.

Source code in pyeuclid/formalization/diagram.py
577
578
579
580
581
582
def sketch_square(self, *args) -> list[Point]:
    """Square constructed on segment AB."""
    a, b = args
    c = b + (a - b).rotatea(-np.pi / 2)
    d = a + (b - a).rotatea(np.pi / 2)
    return [c, d]

sketch_trapezoid(*args)

Random trapezoid with AB // CD.

Source code in pyeuclid/formalization/diagram.py
593
594
595
596
597
598
599
600
601
602
603
def sketch_trapezoid(self, *args) -> list[Point]:
    """Random trapezoid with AB // CD."""
    d = Point(0.0, 0.0)
    c = Point(1.0, 0.0)

    base = unif(0.5, 2.0)
    height = unif(0.5, 2.0)
    a = Point(unif(0.2, 0.5), height)
    b = Point(a.x + base, height)
    a, b, c, d = random_rfss(a, b, c, d)
    return [a, b, c, d]

sketch_triangle(*args)

Random triangle.

Source code in pyeuclid/formalization/diagram.py
605
606
607
608
609
610
611
612
def sketch_triangle(self, *args) -> list[Point]:
    """Random triangle."""
    a = Point(0.0, 0.0)
    b = Point(1.0, 0.0)
    ac = unif(0.5, 2.0)
    ang = unif(0.2, 0.8) * np.pi
    c = head_from(a, ang, ac)
    return [a, b, c]

sketch_triangle12(*args)

Triangle with side-length ratios near 1:2.

Source code in pyeuclid/formalization/diagram.py
614
615
616
617
618
619
620
def sketch_triangle12(self, *args) -> list[Point]:
    """Triangle with side-length ratios near 1:2."""
    b = Point(0.0, 0.0)
    c = Point(unif(1.5, 2.5), 0.0)
    a, _ = circle_circle_intersection(Circle(b, 1.0), Circle(c, 2.0))
    a, b, c = random_rfss(a, b, c)
    return [a, b, c]

sketch_trisect(*args)

Trisect angle ABC.

Source code in pyeuclid/formalization/diagram.py
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
def sketch_trisect(self, *args) -> list[Point]:
    """Trisect angle ABC."""
    a, b, c = args
    ang1 = ang_of(b, a)
    ang2 = ang_of(b, c)

    swap = 0
    if ang1 > ang2:
        ang1, ang2 = ang2, ang1
        swap += 1

    if ang2 - ang1 > np.pi:
        ang1, ang2 = ang2, ang1 + 2 * np.pi
        swap += 1

    angx = ang1 + (ang2 - ang1) / 3
    angy = ang2 - (ang2 - ang1) / 3

    x = b + Point(np.cos(angx), np.sin(angx))
    y = b + Point(np.cos(angy), np.sin(angy))

    ac = Line(a, c)
    x = line_line_intersection(Line(b, x), ac)
    y = line_line_intersection(Line(b, y), ac)

    if swap == 1:
        return [y, x]
    return [x, y]

sketch_trisegment(*args)

Trisect segment AB.

Source code in pyeuclid/formalization/diagram.py
704
705
706
707
708
def sketch_trisegment(self, *args) -> list[Point]:
    """Trisect segment AB."""
    a, b = args
    x, y = a + (b - a) * (1.0 / 3), a + (b - a) * (2.0 / 3)
    return [x, y]

Different

Bases: Relation

All provided points must be pairwise distinct.

Source code in pyeuclid/formalization/relation.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
class Different(Relation):
    """All provided points must be pairwise distinct."""

    def __init__(self, *ps: list[Point]):
        """
        Args:
            *ps (Point): Points that must be distinct.
        """
        super().__init__()
        self.ps = ps

    def definition(self):
        """Expand to pairwise inequality relations.

        Returns:
            list[Relation]: Negated equalities for every point pair.
        """
        return [Not(Equal(self.ps[i], self.ps[j])) for i in range(len(self.ps)) for j in range(i + 1, len(self.ps))]

__init__(*ps)

Parameters:

Name Type Description Default
*ps Point

Points that must be distinct.

()
Source code in pyeuclid/formalization/relation.py
150
151
152
153
154
155
156
def __init__(self, *ps: list[Point]):
    """
    Args:
        *ps (Point): Points that must be distinct.
    """
    super().__init__()
    self.ps = ps

definition()

Expand to pairwise inequality relations.

Returns:

Type Description

list[Relation]: Negated equalities for every point pair.

Source code in pyeuclid/formalization/relation.py
158
159
160
161
162
163
164
def definition(self):
    """Expand to pairwise inequality relations.

    Returns:
        list[Relation]: Negated equalities for every point pair.
    """
    return [Not(Equal(self.ps[i], self.ps[j])) for i in range(len(self.ps)) for j in range(i + 1, len(self.ps))]

Equal

Bases: Relation

Point equality relation.

Source code in pyeuclid/formalization/relation.py
89
90
91
92
93
94
95
96
97
98
class Equal(Relation):
    """Point equality relation."""

    def __init__(self, v1: Point, v2: Point):
        super().__init__()
        self.v1, self.v2 = sort_points(v1, v2)

    def permutations(self):
        """Enumerate equivalent orderings of the two points."""
        return [(self.v1, self.v2), (self.v2, self.v1)]

permutations()

Enumerate equivalent orderings of the two points.

Source code in pyeuclid/formalization/relation.py
96
97
98
def permutations(self):
    """Enumerate equivalent orderings of the two points."""
    return [(self.v1, self.v2), (self.v2, self.v1)]

HalfPlane

Numerical HalfPlane.

Source code in pyeuclid/formalization/numericals.py
471
472
473
474
475
476
477
478
479
class HalfPlane:
    """Numerical HalfPlane."""

    def __init__(self, a: Point, b: Point, c: Point, opposingsides=False):
        self.line = Line(b, c)
        assert abs(self.line(a)) > ATOM
        self.sign = self.line.sign(a)
        if opposingsides:
            self.sign = -self.sign

Line

Numerical line.

Source code in pyeuclid/formalization/numericals.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
class Line:
    """Numerical line."""
    def __init__(self,
          p1: Point = None,
          p2: Point = None,
          coefficients: tuple[int, int, int] = None
    ):        
        a, b, c = coefficients or (
            p1.y - p2.y,
            p2.x - p1.x,
            p1.x * p2.y - p2.x * p1.y,
        )

        if a < 0.0 or a == 0.0 and b > 0.0:
            a, b, c = -a, -b, -c

        self.coefficients = a, b, c

    def same(self, other: Line) -> bool:
        a, b, c = self.coefficients
        x, y, z = other.coefficients
        return close_enough(a * y, b * x) and close_enough(b * z, c * y)

    def parallel_line(self, p: Point) -> Line:
        a, b, _ = self.coefficients
        return Line(coefficients=(a, b, -a * p.x - b * p.y))

    def perpendicular_line(self, p: Point) -> Line:
        a, b, _ = self.coefficients
        return Line(p, p + Point(a, b))

    def intersect(self, obj):
        if isinstance(obj, Line):
            return line_line_intersection(self, obj)

        if isinstance(obj, Circle):
            return line_circle_intersection(self, obj)

    def distance(self, p: Point) -> float:
        a, b, c = self.coefficients
        return abs(self(p.x, p.y)) / math.sqrt(a * a + b * b)

    def __call__(self, x: Point, y: Point = None) -> float:
        if isinstance(x, Point) and y is None:
            return self(x.x, x.y)
        a, b, c = self.coefficients
        return x * a + y * b + c

    def is_parallel(self, other: Line) -> bool:
        a, b, _ = self.coefficients
        x, y, _ = other.coefficients
        return abs(a * y - b * x) < ATOM

    def is_perp(self, other: Line) -> bool:
        a, b, _ = self.coefficients
        x, y, _ = other.coefficients
        return abs(a * x + b * y) < ATOM

    def diff_side(self, p1: Point, p2: Point) -> Optional[bool]:
        d1 = self(p1.x, p1.y)
        d2 = self(p2.x, p2.y)
        if abs(d1) < ATOM or abs(d2) < ATOM:
            return None
        return d1 * d2 < 0

    def same_side(self, p1: Point, p2: Point) -> Optional[bool]:
        d1 = self(p1.x, p1.y)
        d2 = self(p2.x, p2.y)
        if abs(d1) < ATOM or abs(d2) < ATOM:
            return None
        return d1 * d2 > 0

    def sign(self, point: Point) -> int:
        s = self(point.x, point.y)
        if s > 0:
            return 1
        elif s < 0:
            return -1
        return 0

    def sample_within(self, points: list[Point], n: int = 5) -> list[Point]:
        """Sample a point within the boundary of points."""
        center = sum(points, Point(0.0, 0.0)) * (1.0 / len(points))
        radius = max([p.distance(center) for p in points])
        if close_enough(center.distance(self), radius):
            center = center.foot(self)
        a, b = line_circle_intersection(self, Circle(center.foot(self), radius))
        result = None
        best = -1.0
        for _ in range(n):
            rand = unif(0.0, 1.0)
            x = a + (b - a) * rand
            mind = min([x.distance(p) for p in points])
            if mind > best:
                best = mind
                result = x
        return [result]

    def sample_within_halfplanes(self, points: list[Point], halfplanes: list[HalfPlane], n: int = 5) -> list[Point]:
        """Sample points on the line within the intersection of half-plane constraints and near existing points."""
        # Parameterize the line: L(t) = P0 + t * d
        # P0 is a point on the line, d is the direction vector
        # # Get the direction vector (dx, dy) of the line
        a, b, c = self.coefficients
        if abs(a) > ATOM and abs(b) > ATOM:
            # General case: direction vector perpendicular to normal vector (a, b)
            d = Point(-b, a)
        elif abs(a) > ATOM:
            # Vertical line 
            d = Point(0, 1)
        elif abs(b) > ATOM:
            # Horizontal line
            d = Point(1, 0)
        else:
            raise ValueError("Invalid line with zero coefficients")

        # Find a point P0 on the line

        if abs(a) > ATOM:
            x0 = (-c - b * 0) / a  # Set y = 0
            y0 = 0
        elif abs(b) > ATOM:
            x0 = 0
            y0 = (-c - a * 0) / b  # Set x = 0
        else:
            raise ValueError("Invalid line with zero coefficients")

        P0 = Point(x0, y0)

        # Project existing points onto the line to get an initial interval
        t_points = []
        for p in points:
            # Vector from P0 to p
            vec = p - P0
            # Project vec onto d
            t = (vec.x * d.x + vec.y * d.y) / (d.x ** 2 + d.y ** 2)
            t_points.append(t)
        if not t_points:
            raise ValueError("No existing points provided for sampling")

        # Determine the interval based on existing points
        t_points.sort()
        t_center = sum(t_points) / len(t_points)
        t_radius = max(abs(t - t_center) for t in t_points)

        # Define an initial interval around the existing points
        t_init_min = t_center - t_radius
        t_init_max = t_center + t_radius

        # Initialize the interval as [t_init_min, t_init_max]
        t_min = t_init_min
        t_max = t_init_max

        # Process half-plane constraints
        for hp in halfplanes:
            # For each half-plane, compute K and H0
            a_h, b_h, c_h = hp.line.coefficients
            sign_h = hp.sign  # +1 or -1
            # Compute K = a_h * dx + b_h * dy
            K = a_h * d.x + b_h * d.y
            # Compute H0 = a_h * x0 + b_h * y0 + c_h
            H0 = a_h * P0.x + b_h * P0.y + c_h
            # The half-plane inequality is sign_h * (K * t + H0) >= 0
            S = sign_h
            if abs(K) < ATOM:
                # K is zero
                if S * H0 >= 0:
                    # The entire line satisfies the constraint
                    continue
                else:
                    # The line is entirely outside the half-plane
                    return []
            else:
                t0 = -H0 / K
                if K * S > 0:
                    # Inequality is t >= t0
                    t_min = max(t_min, t0)
                else:
                    # Inequality is t <= t0
                    t_max = min(t_max, t0)
        # After processing all half-planes, check if the interval is valid
        if t_min > t_max:
            # Empty interval
            return []
        else:
            # The intersection is [t_min, t_max]
            # Sample n points within this interval
            result = None
            best = -1.0
            for _ in range(n):
                t = unif(t_min, t_max)
                p = Point(P0.x + t * d.x, P0.y + t * d.y)
                # Calculate the minimum distance to existing points
                mind = min(p.distance(q) for q in points)
                if mind > best:
                    best = mind
                    result = p
            if result is None:
                raise ValueError("Cannot find a suitable point within the constraints")
            return [result]

sample_within(points, n=5)

Sample a point within the boundary of points.

Source code in pyeuclid/formalization/numericals.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
def sample_within(self, points: list[Point], n: int = 5) -> list[Point]:
    """Sample a point within the boundary of points."""
    center = sum(points, Point(0.0, 0.0)) * (1.0 / len(points))
    radius = max([p.distance(center) for p in points])
    if close_enough(center.distance(self), radius):
        center = center.foot(self)
    a, b = line_circle_intersection(self, Circle(center.foot(self), radius))
    result = None
    best = -1.0
    for _ in range(n):
        rand = unif(0.0, 1.0)
        x = a + (b - a) * rand
        mind = min([x.distance(p) for p in points])
        if mind > best:
            best = mind
            result = x
    return [result]

sample_within_halfplanes(points, halfplanes, n=5)

Sample points on the line within the intersection of half-plane constraints and near existing points.

Source code in pyeuclid/formalization/numericals.py
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
def sample_within_halfplanes(self, points: list[Point], halfplanes: list[HalfPlane], n: int = 5) -> list[Point]:
    """Sample points on the line within the intersection of half-plane constraints and near existing points."""
    # Parameterize the line: L(t) = P0 + t * d
    # P0 is a point on the line, d is the direction vector
    # # Get the direction vector (dx, dy) of the line
    a, b, c = self.coefficients
    if abs(a) > ATOM and abs(b) > ATOM:
        # General case: direction vector perpendicular to normal vector (a, b)
        d = Point(-b, a)
    elif abs(a) > ATOM:
        # Vertical line 
        d = Point(0, 1)
    elif abs(b) > ATOM:
        # Horizontal line
        d = Point(1, 0)
    else:
        raise ValueError("Invalid line with zero coefficients")

    # Find a point P0 on the line

    if abs(a) > ATOM:
        x0 = (-c - b * 0) / a  # Set y = 0
        y0 = 0
    elif abs(b) > ATOM:
        x0 = 0
        y0 = (-c - a * 0) / b  # Set x = 0
    else:
        raise ValueError("Invalid line with zero coefficients")

    P0 = Point(x0, y0)

    # Project existing points onto the line to get an initial interval
    t_points = []
    for p in points:
        # Vector from P0 to p
        vec = p - P0
        # Project vec onto d
        t = (vec.x * d.x + vec.y * d.y) / (d.x ** 2 + d.y ** 2)
        t_points.append(t)
    if not t_points:
        raise ValueError("No existing points provided for sampling")

    # Determine the interval based on existing points
    t_points.sort()
    t_center = sum(t_points) / len(t_points)
    t_radius = max(abs(t - t_center) for t in t_points)

    # Define an initial interval around the existing points
    t_init_min = t_center - t_radius
    t_init_max = t_center + t_radius

    # Initialize the interval as [t_init_min, t_init_max]
    t_min = t_init_min
    t_max = t_init_max

    # Process half-plane constraints
    for hp in halfplanes:
        # For each half-plane, compute K and H0
        a_h, b_h, c_h = hp.line.coefficients
        sign_h = hp.sign  # +1 or -1
        # Compute K = a_h * dx + b_h * dy
        K = a_h * d.x + b_h * d.y
        # Compute H0 = a_h * x0 + b_h * y0 + c_h
        H0 = a_h * P0.x + b_h * P0.y + c_h
        # The half-plane inequality is sign_h * (K * t + H0) >= 0
        S = sign_h
        if abs(K) < ATOM:
            # K is zero
            if S * H0 >= 0:
                # The entire line satisfies the constraint
                continue
            else:
                # The line is entirely outside the half-plane
                return []
        else:
            t0 = -H0 / K
            if K * S > 0:
                # Inequality is t >= t0
                t_min = max(t_min, t0)
            else:
                # Inequality is t <= t0
                t_max = min(t_max, t0)
    # After processing all half-planes, check if the interval is valid
    if t_min > t_max:
        # Empty interval
        return []
    else:
        # The intersection is [t_min, t_max]
        # Sample n points within this interval
        result = None
        best = -1.0
        for _ in range(n):
            t = unif(t_min, t_max)
            p = Point(P0.x + t * d.x, P0.y + t * d.y)
            # Calculate the minimum distance to existing points
            mind = min(p.distance(q) for q in points)
            if mind > best:
                best = mind
                result = p
        if result is None:
            raise ValueError("Cannot find a suitable point within the constraints")
        return [result]

Lt

Bases: Relation

Source code in pyeuclid/formalization/relation.py
81
82
83
84
85
86
class Lt(Relation):
    def __init__(self, v1: Point, v2: Point):
        """Ordering helper used to canonicalize inference rule assignments."""
        super().__init__()
        self.v1 = v1
        self.v2 = v2

__init__(v1, v2)

Ordering helper used to canonicalize inference rule assignments.

Source code in pyeuclid/formalization/relation.py
82
83
84
85
86
def __init__(self, v1: Point, v2: Point):
    """Ordering helper used to canonicalize inference rule assignments."""
    super().__init__()
    self.v1 = v1
    self.v2 = v2

Midpoint

Bases: Relation

p1 is the midpoint of segment p2p3.

Source code in pyeuclid/formalization/relation.py
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
class Midpoint(Relation):
    """p1 is the midpoint of segment p2p3."""

    def __init__(self, p1: Point, p2: Point, p3: Point):
        """
        Args:
            p1 (Point): Candidate midpoint.
            p2 (Point): Segment endpoint.
            p3 (Point): Segment endpoint.
        """
        super().__init__()
        self.p1 = p1
        self.p2, self.p3 = sort_points(p2, p3)

    def definition(self):
        """Midpoint expressed via equal lengths, collinearity, and betweenness.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) - Length(self.p1, self.p3),
            Collinear(self.p1, self.p2, self.p3),
            Different(self.p2, self.p3),
            Between(self.p1, self.p2, self.p3),
        ]

__init__(p1, p2, p3)

Parameters:

Name Type Description Default
p1 Point

Candidate midpoint.

required
p2 Point

Segment endpoint.

required
p3 Point

Segment endpoint.

required
Source code in pyeuclid/formalization/relation.py
291
292
293
294
295
296
297
298
299
300
def __init__(self, p1: Point, p2: Point, p3: Point):
    """
    Args:
        p1 (Point): Candidate midpoint.
        p2 (Point): Segment endpoint.
        p3 (Point): Segment endpoint.
    """
    super().__init__()
    self.p1 = p1
    self.p2, self.p3 = sort_points(p2, p3)

definition()

Midpoint expressed via equal lengths, collinearity, and betweenness.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
302
303
304
305
306
307
308
309
310
311
312
313
def definition(self):
    """Midpoint expressed via equal lengths, collinearity, and betweenness.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) - Length(self.p1, self.p3),
        Collinear(self.p1, self.p2, self.p3),
        Different(self.p2, self.p3),
        Between(self.p1, self.p2, self.p3),
    ]

NotCollinear

Bases: Relation

Points p1,p2,p3 are not collinear.

Source code in pyeuclid/formalization/relation.py
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
class NotCollinear(Relation):
    """Points p1,p2,p3 are not collinear."""

    def __init__(self, p1, p2, p3):
        """
        Args:
            p1 (Point)
            p2 (Point)
            p3 (Point)
        """
        super().__init__()
        self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

    def definition(self):
        """Expand non-collinearity into primitive constraints."""
        return [
            Not(Collinear(self.p1, self.p2, self.p3)),
            Different(self.p1, self.p2, self.p3)
        ]

__init__(p1, p2, p3)

Source code in pyeuclid/formalization/relation.py
270
271
272
273
274
275
276
277
278
def __init__(self, p1, p2, p3):
    """
    Args:
        p1 (Point)
        p2 (Point)
        p3 (Point)
    """
    super().__init__()
    self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

definition()

Expand non-collinearity into primitive constraints.

Source code in pyeuclid/formalization/relation.py
280
281
282
283
284
285
def definition(self):
    """Expand non-collinearity into primitive constraints."""
    return [
        Not(Collinear(self.p1, self.p2, self.p3)),
        Different(self.p1, self.p2, self.p3)
    ]

OppositeSide

Bases: Relation

Points p1,p2 lie on opposite sides of the line (p3,p4).

Source code in pyeuclid/formalization/relation.py
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
class OppositeSide(Relation):
    """Points p1,p2 lie on opposite sides of the line (p3,p4)."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1 (Point): First query point.
            p2 (Point): Second query point.
            p3 (Point): Line endpoint 1.
            p4 (Point): Line endpoint 2.
        """
        super().__init__()
        self.p1, self.p2 = sort_points(p1, p2)
        self.p3, self.p4 = sort_points(p3, p4)

    def definition(self):
        """Logical expansion expressing opposite-side constraints.

        Returns:
            list[Relation]: Primitive relations defining opposite sides.
        """
        return [
            Not(Collinear(self.p1, self.p3, self.p4)),
            Not(Collinear(self.p2, self.p3, self.p4)),
            Not(SameSide(self.p1, self.p2, self.p3, self.p4)),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1 Point

First query point.

required
p2 Point

Second query point.

required
p3 Point

Line endpoint 1.

required
p4 Point

Line endpoint 2.

required
Source code in pyeuclid/formalization/relation.py
220
221
222
223
224
225
226
227
228
229
230
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1 (Point): First query point.
        p2 (Point): Second query point.
        p3 (Point): Line endpoint 1.
        p4 (Point): Line endpoint 2.
    """
    super().__init__()
    self.p1, self.p2 = sort_points(p1, p2)
    self.p3, self.p4 = sort_points(p3, p4)

definition()

Logical expansion expressing opposite-side constraints.

Returns:

Type Description

list[Relation]: Primitive relations defining opposite sides.

Source code in pyeuclid/formalization/relation.py
232
233
234
235
236
237
238
239
240
241
242
def definition(self):
    """Logical expansion expressing opposite-side constraints.

    Returns:
        list[Relation]: Primitive relations defining opposite sides.
    """
    return [
        Not(Collinear(self.p1, self.p3, self.p4)),
        Not(Collinear(self.p2, self.p3, self.p4)),
        Not(SameSide(self.p1, self.p2, self.p3, self.p4)),
    ]

Parallel

Bases: Relation

Segments (p1,p2) and (p3,p4) are parallel.

Source code in pyeuclid/formalization/relation.py
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
class Parallel(Relation):
    """Segments (p1,p2) and (p3,p4) are parallel."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1, p2 (Point): First segment endpoints.
            p3, p4 (Point): Second segment endpoints.
        """
        super().__init__()
        p1, p2 = sort_points(p1, p2)
        p3, p4 = sort_points(p3, p4)
        self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

    def permutations(self):
        """Enumerate symmetric endpoint permutations preserving segment groups.

        Returns:
            list[tuple[Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p1, self.p2, self.p4, self.p3),
            (self.p2, self.p1, self.p3, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
            (self.p3, self.p4, self.p1, self.p2),
            (self.p4, self.p3, self.p1, self.p2),
            (self.p3, self.p4, self.p2, self.p1),
            (self.p4, self.p3, self.p2, self.p1),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1, p2 Point

First segment endpoints.

required
p3, p4 Point

Second segment endpoints.

required
Source code in pyeuclid/formalization/relation.py
396
397
398
399
400
401
402
403
404
405
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1, p2 (Point): First segment endpoints.
        p3, p4 (Point): Second segment endpoints.
    """
    super().__init__()
    p1, p2 = sort_points(p1, p2)
    p3, p4 = sort_points(p3, p4)
    self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

permutations()

Enumerate symmetric endpoint permutations preserving segment groups.

Returns:

Type Description

list[tuple[Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
def permutations(self):
    """Enumerate symmetric endpoint permutations preserving segment groups.

    Returns:
        list[tuple[Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p1, self.p2, self.p4, self.p3),
        (self.p2, self.p1, self.p3, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
        (self.p3, self.p4, self.p1, self.p2),
        (self.p4, self.p3, self.p1, self.p2),
        (self.p3, self.p4, self.p2, self.p1),
        (self.p4, self.p3, self.p2, self.p1),
    ]

Pentagon

Bases: Relation

Points form a cyclically ordered pentagon.

Source code in pyeuclid/formalization/relation.py
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
class Pentagon(Relation):
    """Points form a cyclically ordered pentagon."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point):
        """
        Args:
            p1, p2, p3, p4, p5 (Point): Vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5 = sort_cyclic_points(p1, p2, p3, p4, p5)

    def permutations(self):
        """Enumerate cyclic and reversed vertex orderings.

        Returns:
            list[tuple[Point, Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4, self.p5),
            (self.p2, self.p3, self.p4, self.p5, self.p1),
            (self.p3, self.p4, self.p5, self.p1, self.p2),
            (self.p4, self.p5, self.p1, self.p2, self.p3),
            (self.p5, self.p1, self.p2, self.p3, self.p4),
            (self.p5, self.p4, self.p3, self.p2, self.p1),
            (self.p4, self.p3, self.p2, self.p1, self.p5),
            (self.p3, self.p2, self.p1, self.p5, self.p4),
            (self.p2, self.p1, self.p5, self.p4, self.p3),
            (self.p1, self.p5, self.p4, self.p3, self.p2),
        ]

__init__(p1, p2, p3, p4, p5)

Parameters:

Name Type Description Default
p1, p2, p3, p4, p5 Point

Vertices.

required
Source code in pyeuclid/formalization/relation.py
500
501
502
503
504
505
506
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point):
    """
    Args:
        p1, p2, p3, p4, p5 (Point): Vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5 = sort_cyclic_points(p1, p2, p3, p4, p5)

permutations()

Enumerate cyclic and reversed vertex orderings.

Returns:

Type Description

list[tuple[Point, Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
def permutations(self):
    """Enumerate cyclic and reversed vertex orderings.

    Returns:
        list[tuple[Point, Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4, self.p5),
        (self.p2, self.p3, self.p4, self.p5, self.p1),
        (self.p3, self.p4, self.p5, self.p1, self.p2),
        (self.p4, self.p5, self.p1, self.p2, self.p3),
        (self.p5, self.p1, self.p2, self.p3, self.p4),
        (self.p5, self.p4, self.p3, self.p2, self.p1),
        (self.p4, self.p3, self.p2, self.p1, self.p5),
        (self.p3, self.p2, self.p1, self.p5, self.p4),
        (self.p2, self.p1, self.p5, self.p4, self.p3),
        (self.p1, self.p5, self.p4, self.p3, self.p2),
    ]

Perpendicular

Bases: Relation

Segments (p1,p2) and (p3,p4) are perpendicular.

Source code in pyeuclid/formalization/relation.py
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
class Perpendicular(Relation):
    """Segments (p1,p2) and (p3,p4) are perpendicular."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1, p2 (Point): First segment endpoints.
            p3, p4 (Point): Second segment endpoints.
        """
        super().__init__()
        p1, p2 = sort_points(p1, p2)
        p3, p4 = sort_points(p3, p4)
        self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

    def permutations(self):
        """Enumerate symmetric endpoint permutations preserving segment groups.

        Returns:
            list[tuple[Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p1, self.p2, self.p4, self.p3),
            (self.p2, self.p1, self.p3, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
            (self.p3, self.p4, self.p1, self.p2),
            (self.p4, self.p3, self.p1, self.p2),
            (self.p3, self.p4, self.p2, self.p1),
            (self.p4, self.p3, self.p2, self.p1),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1, p2 Point

First segment endpoints.

required
p3, p4 Point

Second segment endpoints.

required
Source code in pyeuclid/formalization/relation.py
428
429
430
431
432
433
434
435
436
437
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1, p2 (Point): First segment endpoints.
        p3, p4 (Point): Second segment endpoints.
    """
    super().__init__()
    p1, p2 = sort_points(p1, p2)
    p3, p4 = sort_points(p3, p4)
    self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

permutations()

Enumerate symmetric endpoint permutations preserving segment groups.

Returns:

Type Description

list[tuple[Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
def permutations(self):
    """Enumerate symmetric endpoint permutations preserving segment groups.

    Returns:
        list[tuple[Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p1, self.p2, self.p4, self.p3),
        (self.p2, self.p1, self.p3, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
        (self.p3, self.p4, self.p1, self.p2),
        (self.p4, self.p3, self.p1, self.p2),
        (self.p3, self.p4, self.p2, self.p1),
        (self.p4, self.p3, self.p2, self.p1),
    ]

Point

Named geometric point used throughout the formalization.

Source code in pyeuclid/formalization/relation.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Point:
    """Named geometric point used throughout the formalization."""

    def __init__(self, name: str):
        """Create a point.

        Args:
            name (str): Identifier (underscores are not allowed).
        """
        self.name = name
        assert "_" not in name

    def __str__(self):
        return self.name

    def __eq__(self, other):
        return str(self) == str(other)

    def __repr__(self):
        return str(self)

    def __hash__(self):
        return hash(str(self))

__init__(name)

Create a point.

Parameters:

Name Type Description Default
name str

Identifier (underscores are not allowed).

required
Source code in pyeuclid/formalization/relation.py
16
17
18
19
20
21
22
23
def __init__(self, name: str):
    """Create a point.

    Args:
        name (str): Identifier (underscores are not allowed).
    """
    self.name = name
    assert "_" not in name

Quadrilateral

Bases: Relation

Points form a cyclically ordered quadrilateral.

Source code in pyeuclid/formalization/relation.py
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
class Quadrilateral(Relation):
    """Points form a cyclically ordered quadrilateral."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1, p2, p3, p4 (Point): Vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4 = sort_cyclic_points(p1, p2, p3, p4)

    def permutations(self):
        """Enumerate cyclic and reversed vertex orderings.

        Returns:
            list[tuple[Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p2, self.p3, self.p4, self.p1),
            (self.p3, self.p4, self.p1, self.p2),
            (self.p4, self.p1, self.p2, self.p3),
            (self.p4, self.p3, self.p2, self.p1),
            (self.p3, self.p2, self.p1, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
            (self.p1, self.p4, self.p3, self.p2),
        ]

    def definition(self):
        """Opposite sides must lie on opposite sides of diagonals.

        Returns:
            list[Relation]: Primitive opposite-side relations.
        """
        return [
            OppositeSide(self.p1, self.p3, self.p2, self.p4),
            OppositeSide(self.p2, self.p4, self.p1, self.p3),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1, p2, p3, p4 Point

Vertices.

required
Source code in pyeuclid/formalization/relation.py
460
461
462
463
464
465
466
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1, p2, p3, p4 (Point): Vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4 = sort_cyclic_points(p1, p2, p3, p4)

definition()

Opposite sides must lie on opposite sides of diagonals.

Returns:

Type Description

list[Relation]: Primitive opposite-side relations.

Source code in pyeuclid/formalization/relation.py
485
486
487
488
489
490
491
492
493
494
def definition(self):
    """Opposite sides must lie on opposite sides of diagonals.

    Returns:
        list[Relation]: Primitive opposite-side relations.
    """
    return [
        OppositeSide(self.p1, self.p3, self.p2, self.p4),
        OppositeSide(self.p2, self.p4, self.p1, self.p3),
    ]

permutations()

Enumerate cyclic and reversed vertex orderings.

Returns:

Type Description

list[tuple[Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
def permutations(self):
    """Enumerate cyclic and reversed vertex orderings.

    Returns:
        list[tuple[Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p2, self.p3, self.p4, self.p1),
        (self.p3, self.p4, self.p1, self.p2),
        (self.p4, self.p1, self.p2, self.p3),
        (self.p4, self.p3, self.p2, self.p1),
        (self.p3, self.p2, self.p1, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
        (self.p1, self.p4, self.p3, self.p2),
    ]

Ray

Bases: Line

Numerical ray.

Source code in pyeuclid/formalization/numericals.py
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
class Ray(Line):
    """Numerical ray."""

    def __init__(self, tail: Point, head: Point):
        self.line = Line(tail, head)
        self.coefficients = self.line.coefficients
        self.tail = tail
        self.head = head

    def intersect(self, obj) -> Point:
        if isinstance(obj, (Ray, Line)):
            return line_line_intersection(self.line, obj)

        a, b = line_circle_intersection(self.line, obj)

        if a.close(self.tail):
            return b
        if b.close(self.tail):
            return a

        v = self.head - self.tail
        va = a - self.tail
        vb = b - self.tail

        if v.dot(va) > 0:
            return a
        if v.dot(vb) > 0:
            return b

        raise Exception()

    def sample_within_halfplanes(self, points: list[Point], halfplanes: list[HalfPlane], n: int = 5) -> list[Point]:
        """Sample points on the half-line within the intersection of half-plane constraints and near existing points."""

        # Parameterize the half-line: L(t) = tail + t * d, t >= 0
        d = self.head - self.tail
        d_norm_sq = d.x ** 2 + d.y ** 2
        if d_norm_sq < ATOM:
            raise ValueError("Invalid HalfLine with zero length")

        # Project existing points onto the half-line to get an initial interval
        t_points = []
        for p in points:
            # Vector from tail to p
            vec = p - self.tail
            # Project vec onto d
            t = (vec.x * d.x + vec.y * d.y) / d_norm_sq
            if t >= 0:
                t_points.append(t)
        if not t_points:
            # If no existing points project onto the half-line, define a default interval
            t_init_min = 0
            t_init_max = 1  # For example, length 1 along the half-line
        else:
            # Determine the interval based on existing points
            t_points.sort()
            t_center = sum(t_points) / len(t_points)
            t_radius = max(abs(t - t_center) for t in t_points)
            # Define an initial interval around the existing points
            t_init_min = max(0, t_center - t_radius)
            t_init_max = t_center + t_radius

        # Initialize the interval as [t_init_min, t_init_max]
        t_min = t_init_min
        t_max = t_init_max

        # Process half-plane constraints
        for hp in halfplanes:
            a_h, b_h, c_h = hp.line.coefficients
            sign_h = hp.sign  # +1 or -1

            # Compute K = a_h * dx + b_h * dy
            K = a_h * d.x + b_h * d.y

            # Compute H0 = a_h * tail.x + b_h * tail.y + c_h
            H0 = a_h * self.tail.x + b_h * self.tail.y + c_h

            # The half-plane inequality is sign_h * (K * t + H0) >= 0
            S = sign_h

            if abs(K) < ATOM:
                # K is zero
                if S * H0 >= 0:
                    # The entire half-line satisfies the constraint
                    continue
                else:
                    # The half-line is entirely outside the half-plane
                    return []
            else:
                t0 = -H0 / K
                if K * S > 0:
                    # Inequality is t >= t0
                    if t0 >= 0:
                        t_min = max(t_min, t0)
                    else:
                        t_min = t_min  # t_min remains as is (t >= 0)
                else:
                    # Inequality is t <= t0
                    t_max = min(t_max, t0)
                    if t_max < 0:
                        # Entire interval is before the tail (t < 0), no valid t
                        return []

        # After processing all half-planes, check if the interval is valid
        if t_min > t_max:
            # Empty interval
            return []
        else:
            # The intersection is [t_min, t_max]
            # Ensure t_min >= 0
            t_min = max(t_min, 0)
            if t_min > t_max:
                # No valid t
                return []
            # Sample n points within this interval
            result = None
            best = -1.0
            for _ in range(n):
                t = unif(t_min, t_max)
                p = Point(self.tail.x + t * d.x, self.tail.y + t * d.y)
                # Calculate the minimum distance to existing points
                mind = min(p.distance(q) for q in points)
                if mind > best:
                    best = mind
                    result = p
            if result is None:
                raise ValueError("Cannot find a suitable point within the constraints")
            return [result]

sample_within_halfplanes(points, halfplanes, n=5)

Sample points on the half-line within the intersection of half-plane constraints and near existing points.

Source code in pyeuclid/formalization/numericals.py
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
def sample_within_halfplanes(self, points: list[Point], halfplanes: list[HalfPlane], n: int = 5) -> list[Point]:
    """Sample points on the half-line within the intersection of half-plane constraints and near existing points."""

    # Parameterize the half-line: L(t) = tail + t * d, t >= 0
    d = self.head - self.tail
    d_norm_sq = d.x ** 2 + d.y ** 2
    if d_norm_sq < ATOM:
        raise ValueError("Invalid HalfLine with zero length")

    # Project existing points onto the half-line to get an initial interval
    t_points = []
    for p in points:
        # Vector from tail to p
        vec = p - self.tail
        # Project vec onto d
        t = (vec.x * d.x + vec.y * d.y) / d_norm_sq
        if t >= 0:
            t_points.append(t)
    if not t_points:
        # If no existing points project onto the half-line, define a default interval
        t_init_min = 0
        t_init_max = 1  # For example, length 1 along the half-line
    else:
        # Determine the interval based on existing points
        t_points.sort()
        t_center = sum(t_points) / len(t_points)
        t_radius = max(abs(t - t_center) for t in t_points)
        # Define an initial interval around the existing points
        t_init_min = max(0, t_center - t_radius)
        t_init_max = t_center + t_radius

    # Initialize the interval as [t_init_min, t_init_max]
    t_min = t_init_min
    t_max = t_init_max

    # Process half-plane constraints
    for hp in halfplanes:
        a_h, b_h, c_h = hp.line.coefficients
        sign_h = hp.sign  # +1 or -1

        # Compute K = a_h * dx + b_h * dy
        K = a_h * d.x + b_h * d.y

        # Compute H0 = a_h * tail.x + b_h * tail.y + c_h
        H0 = a_h * self.tail.x + b_h * self.tail.y + c_h

        # The half-plane inequality is sign_h * (K * t + H0) >= 0
        S = sign_h

        if abs(K) < ATOM:
            # K is zero
            if S * H0 >= 0:
                # The entire half-line satisfies the constraint
                continue
            else:
                # The half-line is entirely outside the half-plane
                return []
        else:
            t0 = -H0 / K
            if K * S > 0:
                # Inequality is t >= t0
                if t0 >= 0:
                    t_min = max(t_min, t0)
                else:
                    t_min = t_min  # t_min remains as is (t >= 0)
            else:
                # Inequality is t <= t0
                t_max = min(t_max, t0)
                if t_max < 0:
                    # Entire interval is before the tail (t < 0), no valid t
                    return []

    # After processing all half-planes, check if the interval is valid
    if t_min > t_max:
        # Empty interval
        return []
    else:
        # The intersection is [t_min, t_max]
        # Ensure t_min >= 0
        t_min = max(t_min, 0)
        if t_min > t_max:
            # No valid t
            return []
        # Sample n points within this interval
        result = None
        best = -1.0
        for _ in range(n):
            t = unif(t_min, t_max)
            p = Point(self.tail.x + t * d.x, self.tail.y + t * d.y)
            # Calculate the minimum distance to existing points
            mind = min(p.distance(q) for q in points)
            if mind > best:
                best = mind
                result = p
        if result is None:
            raise ValueError("Cannot find a suitable point within the constraints")
        return [result]

Relation

Base class for logical relations over points.

Source code in pyeuclid/formalization/relation.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class Relation:
    """Base class for logical relations over points."""

    def __init__(self):
        self.negated = False

    def negate(self):
        """Toggle negation flag in-place."""
        self.negated = not self.negated

    def get_points(self):
        """Return all point instances contained in the relation."""
        points = []
        for v in vars(self).values():
            if isinstance(v, Point):
                points.append(v)
            elif isinstance(v, list):
                for p in v:
                    if isinstance(p, Point):
                        points.append(p)
        return points

    def __str__(self):
        """Readable representation, prefixed with Not() when negated."""
        class_name = self.__class__.__name__
        points = self.get_points()
        args_name = ",".join([p.name for p in points])

        if not self.negated:
            return f"{class_name}({args_name})"
        else:
            return f"Not({class_name}({args_name}))"

    def __repr__(self):
        return str(self)

    def __eq__(self, other):
        return str(self) == str(other)

    def __hash__(self):
        return hash(str(self))

__str__()

Readable representation, prefixed with Not() when negated.

Source code in pyeuclid/formalization/relation.py
60
61
62
63
64
65
66
67
68
69
def __str__(self):
    """Readable representation, prefixed with Not() when negated."""
    class_name = self.__class__.__name__
    points = self.get_points()
    args_name = ",".join([p.name for p in points])

    if not self.negated:
        return f"{class_name}({args_name})"
    else:
        return f"Not({class_name}({args_name}))"

get_points()

Return all point instances contained in the relation.

Source code in pyeuclid/formalization/relation.py
48
49
50
51
52
53
54
55
56
57
58
def get_points(self):
    """Return all point instances contained in the relation."""
    points = []
    for v in vars(self).values():
        if isinstance(v, Point):
            points.append(v)
        elif isinstance(v, list):
            for p in v:
                if isinstance(p, Point):
                    points.append(p)
    return points

negate()

Toggle negation flag in-place.

Source code in pyeuclid/formalization/relation.py
44
45
46
def negate(self):
    """Toggle negation flag in-place."""
    self.negated = not self.negated

SameSide

Bases: Relation

Points p1,p2 lie on the same side of the line (p3,p4).

Source code in pyeuclid/formalization/relation.py
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
class SameSide(Relation):
    """Points p1,p2 lie on the same side of the line (p3,p4)."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1 (Point): First query point.
            p2 (Point): Second query point.
            p3 (Point): Line endpoint 1.
            p4 (Point): Line endpoint 2.
        """
        super().__init__()
        self.p1, self.p2 = sort_points(p1, p2)
        self.p3, self.p4 = sort_points(p3, p4)

    def permutations(self):
        """Enumerate symmetric orderings for same-side tests."""
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p1, self.p2, self.p4, self.p3),
            (self.p2, self.p1, self.p3, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1 Point

First query point.

required
p2 Point

Second query point.

required
p3 Point

Line endpoint 1.

required
p4 Point

Line endpoint 2.

required
Source code in pyeuclid/formalization/relation.py
195
196
197
198
199
200
201
202
203
204
205
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1 (Point): First query point.
        p2 (Point): Second query point.
        p3 (Point): Line endpoint 1.
        p4 (Point): Line endpoint 2.
    """
    super().__init__()
    self.p1, self.p2 = sort_points(p1, p2)
    self.p3, self.p4 = sort_points(p3, p4)

permutations()

Enumerate symmetric orderings for same-side tests.

Source code in pyeuclid/formalization/relation.py
207
208
209
210
211
212
213
214
def permutations(self):
    """Enumerate symmetric orderings for same-side tests."""
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p1, self.p2, self.p4, self.p3),
        (self.p2, self.p1, self.p3, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
    ]

Similar

Bases: Relation

Triangles (p1,p2,p3) and (p4,p5,p6) are similar.

Source code in pyeuclid/formalization/relation.py
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
class Similar(Relation):
    """Triangles (p1,p2,p3) and (p4,p5,p6) are similar."""

    def __init__(
        self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
    ):
        """
        Args:
            p1, p2, p3 (Point): First triangle vertices.
            p4, p5, p6 (Point): Second triangle vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

    def definition(self):
        """Similarity expressed via length ratios and non-collinearity.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) / Length(self.p4, self.p5)
            - Length(self.p2, self.p3) / Length(self.p5, self.p6),
            Length(self.p1, self.p2) / Length(self.p4, self.p5)
            - Length(self.p3, self.p1) / Length(self.p6, self.p4),
            NotCollinear(self.p1, self.p2, self.p3),
        ]

__init__(p1, p2, p3, p4, p5, p6)

Parameters:

Name Type Description Default
p1, p2, p3 Point

First triangle vertices.

required
p4, p5, p6 Point

Second triangle vertices.

required
Source code in pyeuclid/formalization/relation.py
347
348
349
350
351
352
353
354
355
356
def __init__(
    self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
):
    """
    Args:
        p1, p2, p3 (Point): First triangle vertices.
        p4, p5, p6 (Point): Second triangle vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

definition()

Similarity expressed via length ratios and non-collinearity.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
358
359
360
361
362
363
364
365
366
367
368
369
370
def definition(self):
    """Similarity expressed via length ratios and non-collinearity.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) / Length(self.p4, self.p5)
        - Length(self.p2, self.p3) / Length(self.p5, self.p6),
        Length(self.p1, self.p2) / Length(self.p4, self.p5)
        - Length(self.p3, self.p1) / Length(self.p6, self.p4),
        NotCollinear(self.p1, self.p2, self.p3),
    ]

Similar4P

Bases: Relation

Two quadrilaterals are similar.

Source code in pyeuclid/formalization/relation.py
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
class Similar4P(Relation):
    """Two quadrilaterals are similar."""

    def __init__(
        self,
        p1: Point,
        p2: Point,
        p3: Point,
        p4: Point,
        p5: Point,
        p6: Point,
        p7: Point,
        p8: Point,
    ):
        """
        Args:
            p1..p4 (Point): First quadrilateral.
            p5..p8 (Point): Second quadrilateral.
        """
        super().__init__()
        point_map_12 = {p1: p5, p2: p6, p3: p7, p4: p8}
        point_map_21 = {p5: p1, p6: p2, p7: p3, p8: p4}

        sorted_1 = sort_cyclic_points(p1, p2, p3, p4)
        sorted_2 = sort_cyclic_points(p5, p6, p7, p8)
        if compare_names(sorted_1, sorted_2) == 0:
            self.p1, self.p2, self.p3, self.p4 = sorted_1
            self.p5,self.p6,self.p7,self.p8 = point_map_12[self.p1], point_map_12[
                self.p2], point_map_12[self.p3], point_map_12[self.p4]
        else:
            self.p1, self.p2, self.p3, self.p4 = sorted_2
            self.p5, self.p6, self.p7, self.p8 = point_map_21[self.p1], point_map_21[
                self.p2], point_map_21[self.p3], point_map_21[self.p4]

    def definition(self):
        """Similarity expressed via side ratios and angle equalities.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) / Length(self.p5, self.p6)
            - Length(self.p2, self.p3) / Length(self.p6, self.p7),
            Length(self.p1, self.p2) / Length(self.p5, self.p6)
            - Length(self.p3, self.p4) / Length(self.p7, self.p8),
            Length(self.p1, self.p2) / Length(self.p5, self.p6)
            - Length(self.p4, self.p1) / Length(self.p8, self.p5),
            Angle(self.p1, self.p2, self.p3) - Angle(self.p5, self.p6, self.p7),
            Angle(self.p2, self.p3, self.p4) - Angle(self.p6, self.p7, self.p8),
            Angle(self.p3, self.p4, self.p1) - Angle(self.p7, self.p8, self.p5),
            Angle(self.p4, self.p1, self.p2) - Angle(self.p8, self.p5, self.p6),
        ]

__init__(p1, p2, p3, p4, p5, p6, p7, p8)

Parameters:

Name Type Description Default
p1..p4 Point

First quadrilateral.

required
p5..p8 Point

Second quadrilateral.

required
Source code in pyeuclid/formalization/relation.py
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
def __init__(
    self,
    p1: Point,
    p2: Point,
    p3: Point,
    p4: Point,
    p5: Point,
    p6: Point,
    p7: Point,
    p8: Point,
):
    """
    Args:
        p1..p4 (Point): First quadrilateral.
        p5..p8 (Point): Second quadrilateral.
    """
    super().__init__()
    point_map_12 = {p1: p5, p2: p6, p3: p7, p4: p8}
    point_map_21 = {p5: p1, p6: p2, p7: p3, p8: p4}

    sorted_1 = sort_cyclic_points(p1, p2, p3, p4)
    sorted_2 = sort_cyclic_points(p5, p6, p7, p8)
    if compare_names(sorted_1, sorted_2) == 0:
        self.p1, self.p2, self.p3, self.p4 = sorted_1
        self.p5,self.p6,self.p7,self.p8 = point_map_12[self.p1], point_map_12[
            self.p2], point_map_12[self.p3], point_map_12[self.p4]
    else:
        self.p1, self.p2, self.p3, self.p4 = sorted_2
        self.p5, self.p6, self.p7, self.p8 = point_map_21[self.p1], point_map_21[
            self.p2], point_map_21[self.p3], point_map_21[self.p4]

definition()

Similarity expressed via side ratios and angle equalities.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
def definition(self):
    """Similarity expressed via side ratios and angle equalities.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) / Length(self.p5, self.p6)
        - Length(self.p2, self.p3) / Length(self.p6, self.p7),
        Length(self.p1, self.p2) / Length(self.p5, self.p6)
        - Length(self.p3, self.p4) / Length(self.p7, self.p8),
        Length(self.p1, self.p2) / Length(self.p5, self.p6)
        - Length(self.p4, self.p1) / Length(self.p8, self.p5),
        Angle(self.p1, self.p2, self.p3) - Angle(self.p5, self.p6, self.p7),
        Angle(self.p2, self.p3, self.p4) - Angle(self.p6, self.p7, self.p8),
        Angle(self.p3, self.p4, self.p1) - Angle(self.p7, self.p8, self.p5),
        Angle(self.p4, self.p1, self.p2) - Angle(self.p8, self.p5, self.p6),
    ]

Similar5P

Bases: Relation

Two pentagons are similar.

Source code in pyeuclid/formalization/relation.py
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
class Similar5P(Relation):
    """Two pentagons are similar."""

    def __init__(
        self,
        p1: Point,
        p2: Point,
        p3: Point,
        p4: Point,
        p5: Point,
        p6: Point,
        p7: Point,
        p8: Point,
        p9: Point,
        p10: Point,
    ):
        """
        Args:
            p1..p5 (Point): First pentagon.
            p6..p10 (Point): Second pentagon.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5, self.p6, self.p7, self.p8, self.p9, self.p10 = p1, p2, p3, p4, p5, p6, p7, p8, p9, p10

    def definition(self):
        """Similarity expressed via consecutive side ratios and angle equalities.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) / Length(self.p6, self.p7)
            - Length(self.p2, self.p3) / Length(self.p7, self.p8),
            Length(self.p2, self.p3) / Length(self.p7, self.p8)
            - Length(self.p3, self.p4) / Length(self.p8, self.p9),
            Length(self.p3, self.p4) / Length(self.p8, self.p9)
            - Length(self.p4, self.p5) / Length(self.p9, self.p10),
            Length(self.p4, self.p5) / Length(self.p9, self.p10)
            - Length(self.p5, self.p1) / Length(self.p10, self.p6),
            Length(self.p5, self.p1) / Length(self.p10, self.p6)
            - Length(self.p1, self.p2) / Length(self.p6, self.p7),
            Angle(self.p1, self.p2, self.p3) - Angle(self.p6, self.p7, self.p8),
            Angle(self.p2, self.p3, self.p4) - Angle(self.p7, self.p8, self.p9),
            Angle(self.p3, self.p4, self.p5) - Angle(self.p8, self.p9, self.p10),
            Angle(self.p4, self.p5, self.p1) - Angle(self.p9, self.p10, self.p6),
            Angle(self.p5, self.p1, self.p2) - Angle(self.p10, self.p6, self.p7),
        ]

__init__(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10)

Parameters:

Name Type Description Default
p1..p5 Point

First pentagon.

required
p6..p10 Point

Second pentagon.

required
Source code in pyeuclid/formalization/relation.py
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
def __init__(
    self,
    p1: Point,
    p2: Point,
    p3: Point,
    p4: Point,
    p5: Point,
    p6: Point,
    p7: Point,
    p8: Point,
    p9: Point,
    p10: Point,
):
    """
    Args:
        p1..p5 (Point): First pentagon.
        p6..p10 (Point): Second pentagon.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5, self.p6, self.p7, self.p8, self.p9, self.p10 = p1, p2, p3, p4, p5, p6, p7, p8, p9, p10

definition()

Similarity expressed via consecutive side ratios and angle equalities.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
def definition(self):
    """Similarity expressed via consecutive side ratios and angle equalities.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) / Length(self.p6, self.p7)
        - Length(self.p2, self.p3) / Length(self.p7, self.p8),
        Length(self.p2, self.p3) / Length(self.p7, self.p8)
        - Length(self.p3, self.p4) / Length(self.p8, self.p9),
        Length(self.p3, self.p4) / Length(self.p8, self.p9)
        - Length(self.p4, self.p5) / Length(self.p9, self.p10),
        Length(self.p4, self.p5) / Length(self.p9, self.p10)
        - Length(self.p5, self.p1) / Length(self.p10, self.p6),
        Length(self.p5, self.p1) / Length(self.p10, self.p6)
        - Length(self.p1, self.p2) / Length(self.p6, self.p7),
        Angle(self.p1, self.p2, self.p3) - Angle(self.p6, self.p7, self.p8),
        Angle(self.p2, self.p3, self.p4) - Angle(self.p7, self.p8, self.p9),
        Angle(self.p3, self.p4, self.p5) - Angle(self.p8, self.p9, self.p10),
        Angle(self.p4, self.p5, self.p1) - Angle(self.p9, self.p10, self.p6),
        Angle(self.p5, self.p1, self.p2) - Angle(self.p10, self.p6, self.p7),
    ]

State

Mutable state holding points, relations, equations, and goal/solution status.

Source code in pyeuclid/formalization/state.py
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
class State:
    """Mutable state holding points, relations, equations, and goal/solution status."""

    def __init__(self):
        self.goal = None
        self.diagram = None
        self.points = set()
        self.relations = set()
        self.equations = []
        self.lengths = UnionFind()
        self.angles = UnionFind()
        self.var_types = {}
        self.ratios = {}
        self.angle_sums = {}

        self.current_depth = 0
        self.solutions = []
        self.solvers = {}
        self.try_complex = False
        self.silent = False
        self.logger = logging.getLogger(__name__)
        self.set_logger(logging.DEBUG)

    def load_problem(self, conditions=None, goal=None, diagram=None):        
        """Seed the state with initial conditions, goal, and optional diagram.

        Adds relations/equations, infers variable categories, sets the goal, and
        records an optional diagram instance.

        Args:
            conditions (Iterable | None): Relations/equations to seed the state.
            goal (Relation | sympy.Expr | None): Target to satisfy.
            diagram (Diagram | None): Optional diagram object.

        Returns:
            None
        """
        if conditions:
            self.add_relations(conditions)
            old_size = 0
            self.categorize_variable()
            size = len(self.var_types)
            while(size > old_size):
                self.categorize_variable()
                old_size = size
                size = len(self.var_types)
        if goal:
            self.goal = goal
        if diagram:
            self.diagram = diagram

    def set_logger(self, level):
        """Configure the state logger; rank-aware for MPI runs.

        Args:
            level (int): Logging level (e.g., logging.INFO).

        Returns:
            None
        """
        self.logger.setLevel(level)
        rank = os.environ.get("OMPI_COMM_WORLD_RANK", None)
        if not len(self.logger.handlers):
            handler = logging.StreamHandler(sys.stdout)
            if rank is None:
                formatter = logging.Formatter(
                    '%(levelname)s - %(message)s')  # %(asctime)s - %(name)s -
            else:
                formatter = logging.Formatter(
                    rank+' %(levelname)s - %(message)s')
            handler.setFormatter(formatter)
            self.logger.addHandler(handler)

    def add_relations(self, relations):
        """Add one or more relations/equations, expanding definitions as needed.

        Args:
            relations: Relation or sympy expression or iterable of them. Composite
                relations with a `definition()` are expanded before insertion.

        Returns:
            None
        """
        if not isinstance(relations, (tuple, list, set)):
            relations = [relations]
        for item in relations:
            if hasattr(item, "definition") and not item.negated:
                self.add_relations(item.definition())
            else:
                if isinstance(item, Relation):
                    self.add_relation(item)
                else:
                    self.add_equation(item)

    def add_relation(self, relation):
        """Insert a relation, ensuring its points are tracked."""
        if relation in self.relations:
            return
        points = relation.get_points()
        for p in points:
            self.add_point(p)
        self.relations.add(relation)

    def add_point(self, p):
        """Track a new point and initialize length union-find edges to existing points.

        Args:
            p (Point): Point to register.

        Returns:
            None
        """
        if not p in self.points:
            for point in self.points:
                self.lengths.add(Length(point, p))
            self.points.add(p)

    def add_equation(self, equation):
        """Insert an equation, tracing its depth and registering involved symbols.

        Args:
            equation (sympy.Expr): Equation to add.

        Returns:
            None
        """
        # allow redundant equations for neat proofs
        equation = Traced(equation, depth=self.current_depth)
        for item in self.equations:
            if equation.expr - item.expr == 0:
                return
        points, quantities = get_points_and_symbols(equation)
        for p in points:
            self.add_point(p)
        unionfind = None
        for quantity in quantities:
            if "Angle" in str(quantity):
                unionfind = self.angles
                unionfind.add(quantity)
            elif "Length" in str(quantity):
                unionfind = self.lengths
                unionfind.add(quantity)
        self.equations.append(equation)

    def categorize_variable(self):
        """Infer variable types (Angle/Length) from existing equations.

        Returns:
            None
        """
        angle_linear, length_linear, length_ratio, others = classify_equations(self.equations, self.var_types)
        for eq in self.equations:
            if "Variable" not in str(eq):
                continue
            _, entities = get_points_and_symbols(eq)
            label = None
            if eq in angle_linear and ("Angle" in str(eq) or "pi" in str(eq)):
                label = "Angle"
            elif eq in length_linear and "Length" in str(eq):
                label = "Length"
            elif eq in length_ratio and "Length" in str(eq):
                label = "Length"
            else:
                continue
            for entity in entities:
                if label is not None:
                    if entity in self.var_types:
                        if self.var_types[entity] is None: # dimensionless variable
                            continue
                        elif self.var_types[entity] != label:
                            self.var_types[entity] = None
                    else:
                        self.var_types[entity] = label

    def load_problem_from_text(self, text, diagram_path=None, resample=False):
        """Parse a textual benchmark instance and populate state+diagram.

        Builds a diagram, verifies numerical consistency with the goal, and
        populates points/relations deduced from construction rules and sampling.

        Args:
            text (str): Problem description string.
            diagram_path (str | None): Optional path for saving diagram.
            resample (bool): Force resampling even if cache exists.

        Returns:
            None
        Raises:
            Exception: If a consistent diagram cannot be generated in allotted attempts.
        """
        constructions_list = get_constructions_list_from_text(text)
        goal = get_goal_from_text(text)

        diagram = Diagram(constructions_list, diagram_path, resample=resample)
        satisfied, satisfied_goal = diagram.numerical_check_goal(goal)

        for _ in range(MAX_DIAGRAM_ATTEMPTS):
            if satisfied:
                break
            diagram = Diagram(constructions_list, diagram_path, resample=True)
            satisfied, satisfied_goal = diagram.numerical_check_goal(goal)

        if not satisfied:
            raise Exception(f"Failed to satisfy goal after {MAX_DIAGRAM_ATTEMPTS} attempts.")

        self.diagram = diagram
        self.goal = satisfied_goal
        # self.diagram.show()

        for constructions in constructions_list:
            for construction in constructions:
                for p in construction.constructed_points():
                    self.add_point(p)

                relations = construction.conclusions()
                if isinstance(relations, tuple):
                    if self.diagram.numerical_check(relations[0]):
                        assert not self.diagram.numerical_check(relations[1])
                        self.add_relations(relations[0])
                    else:
                        assert self.diagram.numerical_check(relations[1])
                        self.add_relations(relations[1])
                else:
                    self.add_relations(relations)

        for perm in permutations(self.points, 3):
            between_relation = Between(*perm)
            if self.diagram.numerical_check(between_relation):
                self.add_relations(between_relation)

            notcollinear_relation = NotCollinear(*perm)
            if self.diagram.numerical_check(notcollinear_relation):
                self.add_relations(notcollinear_relation)

        for perm in permutations(self.points, 4):
            sameside_relation = SameSide(*perm)
            if self.diagram.numerical_check(sameside_relation):
                self.add_relations(sameside_relation)

            oppositeside_relation = OppositeSide(*perm)
            if self.diagram.numerical_check(oppositeside_relation):
                self.add_relations(oppositeside_relation)

    def complete(self):
        """Return solved status: True/expr if goal satisfied, else None.

        Returns:
            bool | sympy.Expr | None: True or numeric expression if solved; otherwise None.
        """
        if isinstance(self.goal, Relation):
            if self.check_conditions(self.goal):
                return True
            else:
                return None
        else:
            assert isinstance(self.goal, sympy.core.expr.Expr)
            solution = self.simplify_equation(self.goal)
            if len(solution.free_symbols) == 0:
                return solution
            return None

    def simplify_equation(self, expr, depth=None):
        """Substitute solved variables into an expression.

        Args:
            expr (sympy.Expr): Expression to simplify.
            depth (int | None): Solution depth to use; defaults to latest.

        Returns:
            sympy.Expr: Simplified expression.
        """
        if depth is None:
            depth = len(self.solutions) - 1
        solved_vars = self.solutions[depth]
        expr = getattr(expr, "expr", expr)
        for symbol in expr.free_symbols:
            if symbol in solved_vars:
                value = solved_vars[symbol].expr
                expr = expr.subs(symbol, value)
        return expr

    def check_conditions(self, conditions):
        """Verify that a set of relations/equations holds in the current state.

        Expands relation definitions, checks presence in `relations`, and
        simplifies equations via solved variables.

        Args:
            conditions (Iterable | Relation | sympy.Expr): Conditions to verify.

        Returns:
            bool: True if all conditions hold; False otherwise.
        """
        if not type(conditions) in (list, tuple, set):
            conditions = [conditions]
        conditional_relations, conditional_equations = set(), []
        i = 0
        while i < len(conditions):
            item = conditions[i]
            if isinstance(item, Equal):
                if not ((item.v1 == item.v2) ^ item.negated):
                    return False
            elif hasattr(item, "definition") and not item.negated:
                unrolled = item.definition()
                if not (isinstance(unrolled, tuple) or isinstance(unrolled, list)):
                    unrolled = unrolled,
                conditions += unrolled
            # auxillary predicate for canonical ordering of inference rule params, does not used for checking
            elif isinstance(item, Lt):
                pass
            elif isinstance(item, Between):
                if item.negated:
                    if Not(item) in self.relations:
                        return False
                else:
                    if not item in self.relations:
                        return False
                    if item.p1 == item.p2 or item.p2 == item.p3 or item.p3 == item.p1:
                        return False
            elif isinstance(item, Relation):
                if isinstance(item, Collinear) and (item.p1 == item.p2 or item.p2 == item.p3 or item.p3 == item.p1):
                    pass
                elif not item in self.relations:
                    return False
            else:
                conditional_equations.append(self.simplify_equation(item))
            i += 1
        equation_satisfied = check_equalities(conditional_equations)
        return equation_satisfied

add_equation(equation)

Insert an equation, tracing its depth and registering involved symbols.

Parameters:

Name Type Description Default
equation Expr

Equation to add.

required

Returns:

Type Description

None

Source code in pyeuclid/formalization/state.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
def add_equation(self, equation):
    """Insert an equation, tracing its depth and registering involved symbols.

    Args:
        equation (sympy.Expr): Equation to add.

    Returns:
        None
    """
    # allow redundant equations for neat proofs
    equation = Traced(equation, depth=self.current_depth)
    for item in self.equations:
        if equation.expr - item.expr == 0:
            return
    points, quantities = get_points_and_symbols(equation)
    for p in points:
        self.add_point(p)
    unionfind = None
    for quantity in quantities:
        if "Angle" in str(quantity):
            unionfind = self.angles
            unionfind.add(quantity)
        elif "Length" in str(quantity):
            unionfind = self.lengths
            unionfind.add(quantity)
    self.equations.append(equation)

add_point(p)

Track a new point and initialize length union-find edges to existing points.

Parameters:

Name Type Description Default
p Point

Point to register.

required

Returns:

Type Description

None

Source code in pyeuclid/formalization/state.py
118
119
120
121
122
123
124
125
126
127
128
129
130
def add_point(self, p):
    """Track a new point and initialize length union-find edges to existing points.

    Args:
        p (Point): Point to register.

    Returns:
        None
    """
    if not p in self.points:
        for point in self.points:
            self.lengths.add(Length(point, p))
        self.points.add(p)

add_relation(relation)

Insert a relation, ensuring its points are tracked.

Source code in pyeuclid/formalization/state.py
109
110
111
112
113
114
115
116
def add_relation(self, relation):
    """Insert a relation, ensuring its points are tracked."""
    if relation in self.relations:
        return
    points = relation.get_points()
    for p in points:
        self.add_point(p)
    self.relations.add(relation)

add_relations(relations)

Add one or more relations/equations, expanding definitions as needed.

Parameters:

Name Type Description Default
relations

Relation or sympy expression or iterable of them. Composite relations with a definition() are expanded before insertion.

required

Returns:

Type Description

None

Source code in pyeuclid/formalization/state.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
def add_relations(self, relations):
    """Add one or more relations/equations, expanding definitions as needed.

    Args:
        relations: Relation or sympy expression or iterable of them. Composite
            relations with a `definition()` are expanded before insertion.

    Returns:
        None
    """
    if not isinstance(relations, (tuple, list, set)):
        relations = [relations]
    for item in relations:
        if hasattr(item, "definition") and not item.negated:
            self.add_relations(item.definition())
        else:
            if isinstance(item, Relation):
                self.add_relation(item)
            else:
                self.add_equation(item)

categorize_variable()

Infer variable types (Angle/Length) from existing equations.

Returns:

Type Description

None

Source code in pyeuclid/formalization/state.py
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
def categorize_variable(self):
    """Infer variable types (Angle/Length) from existing equations.

    Returns:
        None
    """
    angle_linear, length_linear, length_ratio, others = classify_equations(self.equations, self.var_types)
    for eq in self.equations:
        if "Variable" not in str(eq):
            continue
        _, entities = get_points_and_symbols(eq)
        label = None
        if eq in angle_linear and ("Angle" in str(eq) or "pi" in str(eq)):
            label = "Angle"
        elif eq in length_linear and "Length" in str(eq):
            label = "Length"
        elif eq in length_ratio and "Length" in str(eq):
            label = "Length"
        else:
            continue
        for entity in entities:
            if label is not None:
                if entity in self.var_types:
                    if self.var_types[entity] is None: # dimensionless variable
                        continue
                    elif self.var_types[entity] != label:
                        self.var_types[entity] = None
                else:
                    self.var_types[entity] = label

check_conditions(conditions)

Verify that a set of relations/equations holds in the current state.

Expands relation definitions, checks presence in relations, and simplifies equations via solved variables.

Parameters:

Name Type Description Default
conditions Iterable | Relation | Expr

Conditions to verify.

required

Returns:

Name Type Description
bool

True if all conditions hold; False otherwise.

Source code in pyeuclid/formalization/state.py
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
def check_conditions(self, conditions):
    """Verify that a set of relations/equations holds in the current state.

    Expands relation definitions, checks presence in `relations`, and
    simplifies equations via solved variables.

    Args:
        conditions (Iterable | Relation | sympy.Expr): Conditions to verify.

    Returns:
        bool: True if all conditions hold; False otherwise.
    """
    if not type(conditions) in (list, tuple, set):
        conditions = [conditions]
    conditional_relations, conditional_equations = set(), []
    i = 0
    while i < len(conditions):
        item = conditions[i]
        if isinstance(item, Equal):
            if not ((item.v1 == item.v2) ^ item.negated):
                return False
        elif hasattr(item, "definition") and not item.negated:
            unrolled = item.definition()
            if not (isinstance(unrolled, tuple) or isinstance(unrolled, list)):
                unrolled = unrolled,
            conditions += unrolled
        # auxillary predicate for canonical ordering of inference rule params, does not used for checking
        elif isinstance(item, Lt):
            pass
        elif isinstance(item, Between):
            if item.negated:
                if Not(item) in self.relations:
                    return False
            else:
                if not item in self.relations:
                    return False
                if item.p1 == item.p2 or item.p2 == item.p3 or item.p3 == item.p1:
                    return False
        elif isinstance(item, Relation):
            if isinstance(item, Collinear) and (item.p1 == item.p2 or item.p2 == item.p3 or item.p3 == item.p1):
                pass
            elif not item in self.relations:
                return False
        else:
            conditional_equations.append(self.simplify_equation(item))
        i += 1
    equation_satisfied = check_equalities(conditional_equations)
    return equation_satisfied

complete()

Return solved status: True/expr if goal satisfied, else None.

Returns:

Type Description

bool | sympy.Expr | None: True or numeric expression if solved; otherwise None.

Source code in pyeuclid/formalization/state.py
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
def complete(self):
    """Return solved status: True/expr if goal satisfied, else None.

    Returns:
        bool | sympy.Expr | None: True or numeric expression if solved; otherwise None.
    """
    if isinstance(self.goal, Relation):
        if self.check_conditions(self.goal):
            return True
        else:
            return None
    else:
        assert isinstance(self.goal, sympy.core.expr.Expr)
        solution = self.simplify_equation(self.goal)
        if len(solution.free_symbols) == 0:
            return solution
        return None

load_problem(conditions=None, goal=None, diagram=None)

Seed the state with initial conditions, goal, and optional diagram.

Adds relations/equations, infers variable categories, sets the goal, and records an optional diagram instance.

Parameters:

Name Type Description Default
conditions Iterable | None

Relations/equations to seed the state.

None
goal Relation | Expr | None

Target to satisfy.

None
diagram Diagram | None

Optional diagram object.

None

Returns:

Type Description

None

Source code in pyeuclid/formalization/state.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
def load_problem(self, conditions=None, goal=None, diagram=None):        
    """Seed the state with initial conditions, goal, and optional diagram.

    Adds relations/equations, infers variable categories, sets the goal, and
    records an optional diagram instance.

    Args:
        conditions (Iterable | None): Relations/equations to seed the state.
        goal (Relation | sympy.Expr | None): Target to satisfy.
        diagram (Diagram | None): Optional diagram object.

    Returns:
        None
    """
    if conditions:
        self.add_relations(conditions)
        old_size = 0
        self.categorize_variable()
        size = len(self.var_types)
        while(size > old_size):
            self.categorize_variable()
            old_size = size
            size = len(self.var_types)
    if goal:
        self.goal = goal
    if diagram:
        self.diagram = diagram

load_problem_from_text(text, diagram_path=None, resample=False)

Parse a textual benchmark instance and populate state+diagram.

Builds a diagram, verifies numerical consistency with the goal, and populates points/relations deduced from construction rules and sampling.

Parameters:

Name Type Description Default
text str

Problem description string.

required
diagram_path str | None

Optional path for saving diagram.

None
resample bool

Force resampling even if cache exists.

False

Returns:

Type Description

None

Raises: Exception: If a consistent diagram cannot be generated in allotted attempts.

Source code in pyeuclid/formalization/state.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
def load_problem_from_text(self, text, diagram_path=None, resample=False):
    """Parse a textual benchmark instance and populate state+diagram.

    Builds a diagram, verifies numerical consistency with the goal, and
    populates points/relations deduced from construction rules and sampling.

    Args:
        text (str): Problem description string.
        diagram_path (str | None): Optional path for saving diagram.
        resample (bool): Force resampling even if cache exists.

    Returns:
        None
    Raises:
        Exception: If a consistent diagram cannot be generated in allotted attempts.
    """
    constructions_list = get_constructions_list_from_text(text)
    goal = get_goal_from_text(text)

    diagram = Diagram(constructions_list, diagram_path, resample=resample)
    satisfied, satisfied_goal = diagram.numerical_check_goal(goal)

    for _ in range(MAX_DIAGRAM_ATTEMPTS):
        if satisfied:
            break
        diagram = Diagram(constructions_list, diagram_path, resample=True)
        satisfied, satisfied_goal = diagram.numerical_check_goal(goal)

    if not satisfied:
        raise Exception(f"Failed to satisfy goal after {MAX_DIAGRAM_ATTEMPTS} attempts.")

    self.diagram = diagram
    self.goal = satisfied_goal
    # self.diagram.show()

    for constructions in constructions_list:
        for construction in constructions:
            for p in construction.constructed_points():
                self.add_point(p)

            relations = construction.conclusions()
            if isinstance(relations, tuple):
                if self.diagram.numerical_check(relations[0]):
                    assert not self.diagram.numerical_check(relations[1])
                    self.add_relations(relations[0])
                else:
                    assert self.diagram.numerical_check(relations[1])
                    self.add_relations(relations[1])
            else:
                self.add_relations(relations)

    for perm in permutations(self.points, 3):
        between_relation = Between(*perm)
        if self.diagram.numerical_check(between_relation):
            self.add_relations(between_relation)

        notcollinear_relation = NotCollinear(*perm)
        if self.diagram.numerical_check(notcollinear_relation):
            self.add_relations(notcollinear_relation)

    for perm in permutations(self.points, 4):
        sameside_relation = SameSide(*perm)
        if self.diagram.numerical_check(sameside_relation):
            self.add_relations(sameside_relation)

        oppositeside_relation = OppositeSide(*perm)
        if self.diagram.numerical_check(oppositeside_relation):
            self.add_relations(oppositeside_relation)

set_logger(level)

Configure the state logger; rank-aware for MPI runs.

Parameters:

Name Type Description Default
level int

Logging level (e.g., logging.INFO).

required

Returns:

Type Description

None

Source code in pyeuclid/formalization/state.py
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
def set_logger(self, level):
    """Configure the state logger; rank-aware for MPI runs.

    Args:
        level (int): Logging level (e.g., logging.INFO).

    Returns:
        None
    """
    self.logger.setLevel(level)
    rank = os.environ.get("OMPI_COMM_WORLD_RANK", None)
    if not len(self.logger.handlers):
        handler = logging.StreamHandler(sys.stdout)
        if rank is None:
            formatter = logging.Formatter(
                '%(levelname)s - %(message)s')  # %(asctime)s - %(name)s -
        else:
            formatter = logging.Formatter(
                rank+' %(levelname)s - %(message)s')
        handler.setFormatter(formatter)
        self.logger.addHandler(handler)

simplify_equation(expr, depth=None)

Substitute solved variables into an expression.

Parameters:

Name Type Description Default
expr Expr

Expression to simplify.

required
depth int | None

Solution depth to use; defaults to latest.

None

Returns:

Type Description

sympy.Expr: Simplified expression.

Source code in pyeuclid/formalization/state.py
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
def simplify_equation(self, expr, depth=None):
    """Substitute solved variables into an expression.

    Args:
        expr (sympy.Expr): Expression to simplify.
        depth (int | None): Solution depth to use; defaults to latest.

    Returns:
        sympy.Expr: Simplified expression.
    """
    if depth is None:
        depth = len(self.solutions) - 1
    solved_vars = self.solutions[depth]
    expr = getattr(expr, "expr", expr)
    for symbol in expr.free_symbols:
        if symbol in solved_vars:
            value = solved_vars[symbol].expr
            expr = expr.subs(symbol, value)
    return expr

construct_angle_bisector

Bases: ConstructionRule

Construct the bisector point X of angle ABC.

Source code in pyeuclid/formalization/construction_rule.py
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
@register("AG")
class construct_angle_bisector(ConstructionRule):
    """Construct the bisector point X of angle ABC."""
    def __init__(self, x, a, b, c):
        self.x, self.a, self.b, self.c = x, a, b, c

    def arguments(self):
        return [self.a, self.b, self.c]

    def constructed_points(self):
        return [self.x]

    def conditions(self):
        return [NotCollinear(self.a, self.b, self.c)]

    def conclusions(self):
        return [Angle(self.a, self.b, self.x) - Angle(self.x, self.b, self.c)]

construct_angle_mirror

Bases: ConstructionRule

Construct point X as the mirror of BA across BC.

Source code in pyeuclid/formalization/construction_rule.py
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
@register("AG")
class construct_angle_mirror(ConstructionRule):
    """Construct point X as the mirror of BA across BC."""
    def __init__(self, x, a, b, c):
        self.x, self.a, self.b, self.c = x, a, b, c

    def arguments(self):
        return [self.a, self.b, self.c]

    def constructed_points(self):
        return [self.x]

    def conditions(self):
        return [NotCollinear(self.a, self.b, self.c)]

    def conclusions(self):
        return [Angle(self.a, self.b, self.c) - Angle(self.c, self.b, self.x)]

construct_circle

Bases: ConstructionRule

Construct circle center X equidistant from A, B, C.

Source code in pyeuclid/formalization/construction_rule.py
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
@register("AG")
class construct_circle(ConstructionRule):
    """Construct circle center X equidistant from A, B, C."""
    def __init__(self, x, a, b, c):
        self.x, self.a, self.b, self.c = x, a, b, c

    def arguments(self):
        return [self.a, self.b, self.c]

    def constructed_points(self):
        return [self.x]

    def conditions(self):
        return [NotCollinear(self.a, self.b, self.c)]

    def conclusions(self):
        return [
            Length(self.x, self.a) - Length(self.x, self.b),
            Length(self.x, self.b) - Length(self.x, self.c),
        ]

construct_circumcenter

Bases: ConstructionRule

Construct circumcenter X of triangle ABC.

Source code in pyeuclid/formalization/construction_rule.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
@register("AG")
class construct_circumcenter(ConstructionRule):
    """Construct circumcenter X of triangle ABC."""
    def __init__(self, x, a, b, c):
        self.x, self.a, self.b, self.c = x, a, b, c

    def arguments(self):
        return [self.a, self.b, self.c]

    def constructed_points(self):
        return [self.x]

    def conditions(self):
        return [NotCollinear(self.a, self.b, self.c)]

    def conclusions(self):
        return [
            Length(self.x, self.a) - Length(self.x, self.b),
            Length(self.x, self.b) - Length(self.x, self.c),
        ]

construct_eq_quadrangle

Bases: ConstructionRule

Construct quadrilateral ABCD with equal diagonals.

Source code in pyeuclid/formalization/construction_rule.py
141
142
143
144
145
146
147
148
149
150
151
152
153
@register("AG")
class construct_eq_quadrangle(ConstructionRule):
    """Construct quadrilateral ABCD with equal diagonals."""
    def __init__(self, a, b, c, d):
        self.a, self.b, self.c, self.d = a, b, c, d

    def constructed_points(self):
        return [self.a, self.b, self.c, self.d]

    def conclusions(self):
        return [
            Length(self.a, self.d) - Length(self.b, self.c)
        ]

construct_eq_trapezoid

Bases: ConstructionRule

Construct isosceles trapezoid ABCD (AB ∥ CD).

Source code in pyeuclid/formalization/construction_rule.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
@register("AG")
class construct_eq_trapezoid(ConstructionRule):
    """Construct isosceles trapezoid ABCD (AB ∥ CD)."""
    def __init__(self, a, b, c, d):
        self.a, self.b, self.c, self.d = a, b, c, d

    def constructed_points(self):
        return [self.a, self.b, self.c, self.d]

    def conclusions(self):
        return [
            Length(self.a, self.d) - Length(self.b, self.c),
            Parallel(self.a, self.b, self.c, self.d),
            Angle(self.d, self.a, self.b) - Angle(self.a, self.b, self.c),
            Angle(self.b, self.c, self.d) - Angle(self.c, self.d, self.a),
        ]

construct_eq_triangle

Bases: ConstructionRule

Construct equilateral triangle with vertex X and base BC.

Source code in pyeuclid/formalization/construction_rule.py
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
@register("AG")
class construct_eq_triangle(ConstructionRule):
    """Construct equilateral triangle with vertex X and base BC."""
    def __init__(self, x, b, c):
        self.x, self.b, self.c = x, b, c

    def arguments(self):
        return [self.b, self.c]

    def constructed_points(self):
        return [self.x]

    def conditions(self):
        return [Different(self.b, self.c)]

    def conclusions(self):
        return [
            Length(self.x, self.b) - Length(self.b, self.c),
            Length(self.b, self.c) - Length(self.c, self.x),
            Angle(self.x, self.b, self.c) - Angle(self.b, self.c, self.x),
            Angle(self.c, self.x, self.b) - Angle(self.x, self.b, self.c),
        ]

construct_eqangle2

Bases: ConstructionRule

Construct X so that angle ABX equals angle XCB.

Source code in pyeuclid/formalization/construction_rule.py
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
@register("AG")
class construct_eqangle2(ConstructionRule):
    """Construct X so that angle ABX equals angle XCB."""
    def __init__(self, x, a, b, c):
        self.x, self.a, self.b, self.c = x, a, b, c

    def arguments(self):
        return [self.a, self.b, self.c]

    def constructed_points(self):
        return [self.x]

    def conditions(self):
        return [NotCollinear(self.a, self.b, self.c)]

    def conclusions(self):
        return [Angle(self.b, self.a, self.x) - Angle(self.x, self.c, self.b)]

register

Decorator that registers a construction rule into labeled sets.

Source code in pyeuclid/formalization/construction_rule.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class register:
    """Decorator that registers a construction rule into labeled sets."""

    def __init__(self, *annotations):
        self.annotations = annotations

    def __call__(self, cls):
        for item in self.annotations:
            if not item in construction_rule_sets:
                construction_rule_sets[item] = [cls]
            else:
                construction_rule_sets[item].append(cls)

        def expanded_conditions(self):
            return expand_definition(self._conditions())

        cls._conditions = cls.conditions
        cls.conditions = expanded_conditions
        return cls

Angle(p1, p2, p3)

Symbolic angle at p2.

Returns:

Type Description

sympy.Symbol: Non-negative angle symbol.

Source code in pyeuclid/formalization/relation.py
108
109
110
111
112
113
114
115
def Angle(p1: Point, p2: Point, p3: Point):
    """Symbolic angle at p2.

    Returns:
        sympy.Symbol: Non-negative angle symbol.
    """
    p1, p3 = sort_points(p1, p3)
    return Symbol(f"Angle_{p1}_{p2}_{p3}", non_negative=True)

Area(*ps)

Symbolic polygonal area over an ordered point cycle.

Returns:

Type Description

sympy.Symbol: Positive area symbol.

Source code in pyeuclid/formalization/relation.py
128
129
130
131
132
133
134
135
def Area(*ps: list[Point]):
    """Symbolic polygonal area over an ordered point cycle.

    Returns:
        sympy.Symbol: Positive area symbol.
    """
    ps = sort_cyclic_points(*ps)
    return Symbol("_".join(["Area"] + [str(item) for item in ps]), positive=True)

Length(p1, p2)

Symbolic length between two points.

Returns:

Type Description

sympy.Symbol: Positive length symbol.

Source code in pyeuclid/formalization/relation.py
118
119
120
121
122
123
124
125
def Length(p1: Point, p2: Point):
    """Symbolic length between two points.

    Returns:
        sympy.Symbol: Positive length symbol.
    """
    p1, p2 = sort_points(p1, p2)
    return Symbol(f"Length_{str(p1)}_{str(p2)}", positive=True)

Not(p)

Return a negated shallow copy of a relation.

Source code in pyeuclid/formalization/relation.py
101
102
103
104
105
def Not(p: Relation) -> Relation:
    """Return a negated shallow copy of a relation."""
    other = copy.copy(p)
    other.negate()
    return other

Variable(name)

Free symbolic variable placeholder.

Returns:

Type Description

sympy.Symbol: Dimensionless variable symbol.

Source code in pyeuclid/formalization/relation.py
138
139
140
141
142
143
144
def Variable(name: str):
    """Free symbolic variable placeholder.

    Returns:
        sympy.Symbol: Dimensionless variable symbol.
    """
    return Symbol(f"Variable_{name}")

circle_circle_intersection(c1, c2)

Returns a pair of Points as intersections of c1 and c2.

Source code in pyeuclid/formalization/numericals.py
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
def circle_circle_intersection(c1: Circle, c2: Circle) -> tuple[Point, Point]:
    """Returns a pair of Points as intersections of c1 and c2."""
    # circle 1: (x0, y0), radius r0
    # circle 2: (x1, y1), radius r1
    x0, y0, r0 = c1.center.x, c1.center.y, c1.radius
    x1, y1, r1 = c2.center.x, c2.center.y, c2.radius

    d = math.sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2)
    if d == 0:
        raise Exception()

    a = (r0**2 - r1**2 + d**2) / (2 * d)
    h = r0**2 - a**2
    if h < 0:
        raise Exception()
    h = np.sqrt(h)
    x2 = x0 + a * (x1 - x0) / d
    y2 = y0 + a * (y1 - y0) / d
    x3 = x2 + h * (y1 - y0) / d
    y3 = y2 - h * (x1 - x0) / d
    x4 = x2 - h * (y1 - y0) / d
    y4 = y2 + h * (x1 - x0) / d

    return Point(x3, y3), Point(x4, y4)

get_constructions_list_from_text(text)

Parse the constructions section of a text instance into rule objects.

Parameters:

Name Type Description Default
text str

Full benchmark line containing constructions and goal separated by ' ? '.

required

Returns:

Type Description

list[list[ConstructionRule]]: Nested list of construction batches.

Source code in pyeuclid/formalization/translation.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def get_constructions_list_from_text(text):
    """Parse the constructions section of a text instance into rule objects.

    Args:
        text (str): Full benchmark line containing constructions and goal separated by ' ? '.

    Returns:
        list[list[ConstructionRule]]: Nested list of construction batches.
    """
    parts = text.split(' ? ')
    constructions_text_list = parts[0].split('; ')
    constructions_list = []

    for constructions_text in constructions_text_list:
        constructions_text = constructions_text.split(' = ')[1]
        construction_text_list = constructions_text.split(', ')
        constructions = []
        for construction_text in construction_text_list:
            construction_text = construction_text.split(' ')
            rule_name = construction_text[0]
            arg_names = [name.replace('_', '') for name in construction_text[1:]]
            rule = globals()['construct_'+rule_name]
            args = [float(arg_name) if is_float(arg_name) else Point(arg_name) for arg_name in arg_names]
            construction = rule(*args)
            constructions.append(construction)
        constructions_list.append(constructions)

    return constructions_list

get_goal_from_text(text)

Parse the goal portion of a text instance into a Relation or expression.

Parameters:

Name Type Description Default
text str

Full benchmark line containing constructions and goal.

required

Returns:

Type Description

Relation | sympy.Expr | tuple[Relation, Relation] | None: Parsed goal or None.

Source code in pyeuclid/formalization/translation.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
def get_goal_from_text(text):
    """Parse the goal portion of a text instance into a Relation or expression.

    Args:
        text (str): Full benchmark line containing constructions and goal.

    Returns:
        Relation | sympy.Expr | tuple[Relation, Relation] | None: Parsed goal or None.
    """
    parts = text.split(' ? ')
    goal_text = parts[1] if len(parts) > 1 else None
    goal = None
    if goal_text:
        goal_text = goal_text.split(' ')
        goal_name = goal_text[0]
        arg_names = [name.replace('_', '') for name in goal_text[1:]]
        args = [Point(arg_name) for arg_name in arg_names]
        if goal_name == 'cong':
            goal = Length(*args[:2]) - Length(*args[2:])
        elif goal_name == 'cyclic':
            goal = Concyclic(*args)
        elif goal_name == 'coll':
            goal = Collinear(*args)
        elif goal_name == 'perp':
            goal = Perpendicular(*args)
        elif goal_name == 'para':
            goal = Parallel(*args)
        elif goal_name == 'eqratio':
            goal = Length(*args[:2])/Length(*args[2:4]) - Length(*args[4:6])/Length(*args[6:8])
        elif goal_name == 'eqangle':
            def extract_angle(points):
                count = Counter(points)
                repeating = next(p for p, c in count.items() if c == 2)
                singles = [p for p, c in count.items() if c == 1]
                return singles[0], repeating, singles[1]
            angle1 = Angle(*extract_angle(args[:4]))
            angle2 = Angle(*extract_angle(args[4:]))
            # The goal may involve either equal angles or supplementary angles
            goal = (angle1 - angle2, angle1 + angle2 - pi)
        elif goal_name == 'midp':
            goal = Midpoint(*args)
        elif goal_name == 'simtri':
            goal = Similar(*args)
        elif goal_name == 'contri':
            goal = Congruent(*args)

    return goal

line_circle_intersection(line, circle)

Returns a pair of points as intersections of line and circle.

Source code in pyeuclid/formalization/numericals.py
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
def line_circle_intersection(line: Line, circle: Circle) -> tuple[Point, Point]:
    """Returns a pair of points as intersections of line and circle."""
    a, b, c = line.coefficients
    r = float(circle.radius)
    center = circle.center
    p, q = center.x, center.y

    if b == 0:
        x = -c / a
        x_p = x - p
        x_p2 = x_p * x_p
        y = solve_quad(1, -2 * q, q * q + x_p2 - r * r)
        if y is None:
            raise Exception()
        y1, y2 = y
        return (Point(x, y1), Point(x, y2))

    if a == 0:
        y = -c / b
        y_q = y - q
        y_q2 = y_q * y_q
        x = solve_quad(1, -2 * p, p * p + y_q2 - r * r)
        if x is None:
            raise Exception()
        x1, x2 = x
        return (Point(x1, y), Point(x2, y))

    c_ap = c + a * p
    a2 = a * a
    y = solve_quad(
        a2 + b * b, 2 * (b * c_ap - a2 * q), c_ap * c_ap + a2 * (q * q - r * r)
    )
    if y is None:
        raise Exception()
    y1, y2 = y

    return Point(-(b * y1 + c) / a, y1), Point(-(b * y2 + c) / a, y2)

parse_texts_from_file(file_name)

Load every other line from a benchmark file as a problem description.

Parameters:

Name Type Description Default
file_name str

Path to benchmark text file.

required

Returns:

Type Description

list[str]: Problem description strings.

Source code in pyeuclid/formalization/translation.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
def parse_texts_from_file(file_name):
    """Load every other line from a benchmark file as a problem description.

    Args:
        file_name (str): Path to benchmark text file.

    Returns:
        list[str]: Problem description strings.
    """
    with open(file_name, "r") as f:
        lines = f.readlines()

    texts = [lines[i].strip() for i in range(1, len(lines), 2)]
    return texts

random_rfss(*points)

Random rotate-flip-scale-shift a point cloud.

Source code in pyeuclid/formalization/numericals.py
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
def random_rfss(*points: list[Point]) -> list[Point]:
    """Random rotate-flip-scale-shift a point cloud."""
    # center point cloud.
    average = sum(points, Point(0.0, 0.0)) * (1.0 / len(points))
    points = [p - average for p in points]

    # rotate
    ang = unif(0.0, 2 * np.pi)
    sin, cos = np.sin(ang), np.cos(ang)
    # scale and shift
    scale = unif(0.5, 2.0)
    shift = Point(unif(-1, 1), unif(-1, 1))
    points = [p.rotate(sin, cos) * scale + shift for p in points]

    # randomly flip
    if np.random.rand() < 0.5:
        points = [p.flip() for p in points]

    return points

solve_quad(a, b, c)

Solve a x^2 + bx + c = 0.

Source code in pyeuclid/formalization/numericals.py
513
514
515
516
517
518
519
520
521
def solve_quad(a: float, b: float, c: float) -> tuple[float, float]:
    """Solve a x^2 + bx + c = 0."""
    a = 2 * a
    d = b * b - 2 * a * c
    if d < 0:
        return None  # the caller should expect this result.

    y = math.sqrt(d)
    return (-b - y) / a, (-b + y) / a

Geometric primitive types and relations used throughout PyEuclid.

Between

Bases: Relation

p1 lies between p2 and p3 (on the same line).

Source code in pyeuclid/formalization/relation.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
class Between(Relation):
    """p1 lies between p2 and p3 (on the same line)."""

    def __init__(self, p1: Point, p2: Point, p3: Point):
        """
        p1 is between p2 and p3.

        Args:
            p1 (Point): Middle point.
            p2 (Point): Endpoint 1.
            p3 (Point): Endpoint 2.
        """
        super().__init__()
        p2, p3 = sort_points(p2, p3)
        self.p1, self.p2, self.p3 = p1, p2, p3

    def permutations(self):
        """Enumerate equivalent point orderings for the between relation.

        Returns:
            list[tuple[Point, Point, Point]]: Permitted permutations.
        """
        return [(self.p1, self.p2, self.p3), (self.p1, self.p3, self.p2)]

__init__(p1, p2, p3)

p1 is between p2 and p3.

Parameters:

Name Type Description Default
p1 Point

Middle point.

required
p2 Point

Endpoint 1.

required
p3 Point

Endpoint 2.

required
Source code in pyeuclid/formalization/relation.py
170
171
172
173
174
175
176
177
178
179
180
181
def __init__(self, p1: Point, p2: Point, p3: Point):
    """
    p1 is between p2 and p3.

    Args:
        p1 (Point): Middle point.
        p2 (Point): Endpoint 1.
        p3 (Point): Endpoint 2.
    """
    super().__init__()
    p2, p3 = sort_points(p2, p3)
    self.p1, self.p2, self.p3 = p1, p2, p3

permutations()

Enumerate equivalent point orderings for the between relation.

Returns:

Type Description

list[tuple[Point, Point, Point]]: Permitted permutations.

Source code in pyeuclid/formalization/relation.py
183
184
185
186
187
188
189
def permutations(self):
    """Enumerate equivalent point orderings for the between relation.

    Returns:
        list[tuple[Point, Point, Point]]: Permitted permutations.
    """
    return [(self.p1, self.p2, self.p3), (self.p1, self.p3, self.p2)]

Collinear

Bases: Relation

Points p1,p2,p3 are collinear.

Source code in pyeuclid/formalization/relation.py
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
class Collinear(Relation):
    """Points p1,p2,p3 are collinear."""

    def __init__(self, p1, p2, p3):
        """
        Args:
            p1 (Point)
            p2 (Point)
            p3 (Point)
        """
        super().__init__()
        self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

    def permutations(self):
        """Enumerate collinearity argument permutations.

        Returns:
            itertools.permutations: All orderings of the three points.
        """
        return itertools.permutations([self.p1, self.p2, self.p3])

__init__(p1, p2, p3)

Source code in pyeuclid/formalization/relation.py
248
249
250
251
252
253
254
255
256
def __init__(self, p1, p2, p3):
    """
    Args:
        p1 (Point)
        p2 (Point)
        p3 (Point)
    """
    super().__init__()
    self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

permutations()

Enumerate collinearity argument permutations.

Returns:

Type Description

itertools.permutations: All orderings of the three points.

Source code in pyeuclid/formalization/relation.py
258
259
260
261
262
263
264
def permutations(self):
    """Enumerate collinearity argument permutations.

    Returns:
        itertools.permutations: All orderings of the three points.
    """
    return itertools.permutations([self.p1, self.p2, self.p3])

Concyclic

Bases: Relation

All given points lie on the same circle.

Source code in pyeuclid/formalization/relation.py
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
class Concyclic(Relation):
    """All given points lie on the same circle."""

    def __init__(self, *ps: Point):
        """
        Args:
            *ps (Point): Points to be tested for concyclicity.
        """
        super().__init__()
        self.ps = list(sort_points(*ps))

    def permutations(self):
        """Enumerate symmetric permutations of the point set.

        Returns:
            itertools.permutations: All orderings of the points.
        """
        return itertools.permutations(self.ps)

__init__(*ps)

Parameters:

Name Type Description Default
*ps Point

Points to be tested for concyclicity.

()
Source code in pyeuclid/formalization/relation.py
376
377
378
379
380
381
382
def __init__(self, *ps: Point):
    """
    Args:
        *ps (Point): Points to be tested for concyclicity.
    """
    super().__init__()
    self.ps = list(sort_points(*ps))

permutations()

Enumerate symmetric permutations of the point set.

Returns:

Type Description

itertools.permutations: All orderings of the points.

Source code in pyeuclid/formalization/relation.py
384
385
386
387
388
389
390
def permutations(self):
    """Enumerate symmetric permutations of the point set.

    Returns:
        itertools.permutations: All orderings of the points.
    """
    return itertools.permutations(self.ps)

Congruent

Bases: Relation

Triangles (p1,p2,p3) and (p4,p5,p6) are congruent.

Source code in pyeuclid/formalization/relation.py
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
class Congruent(Relation):
    """Triangles (p1,p2,p3) and (p4,p5,p6) are congruent."""

    def __init__(
        self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
    ):
        """
        Args:
            p1, p2, p3 (Point): First triangle vertices.
            p4, p5, p6 (Point): Second triangle vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

    def definition(self):
        """Congruence expressed as equal side lengths and non-collinearity.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) - Length(self.p4, self.p5),
            Length(self.p2, self.p3) - Length(self.p5, self.p6),
            Length(self.p1, self.p3) - Length(self.p4, self.p6),
            NotCollinear(self.p1, self.p2, self.p3),
        ]

__init__(p1, p2, p3, p4, p5, p6)

Parameters:

Name Type Description Default
p1, p2, p3 Point

First triangle vertices.

required
p4, p5, p6 Point

Second triangle vertices.

required
Source code in pyeuclid/formalization/relation.py
319
320
321
322
323
324
325
326
327
328
def __init__(
    self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
):
    """
    Args:
        p1, p2, p3 (Point): First triangle vertices.
        p4, p5, p6 (Point): Second triangle vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

definition()

Congruence expressed as equal side lengths and non-collinearity.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
330
331
332
333
334
335
336
337
338
339
340
341
def definition(self):
    """Congruence expressed as equal side lengths and non-collinearity.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) - Length(self.p4, self.p5),
        Length(self.p2, self.p3) - Length(self.p5, self.p6),
        Length(self.p1, self.p3) - Length(self.p4, self.p6),
        NotCollinear(self.p1, self.p2, self.p3),
    ]

Different

Bases: Relation

All provided points must be pairwise distinct.

Source code in pyeuclid/formalization/relation.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
class Different(Relation):
    """All provided points must be pairwise distinct."""

    def __init__(self, *ps: list[Point]):
        """
        Args:
            *ps (Point): Points that must be distinct.
        """
        super().__init__()
        self.ps = ps

    def definition(self):
        """Expand to pairwise inequality relations.

        Returns:
            list[Relation]: Negated equalities for every point pair.
        """
        return [Not(Equal(self.ps[i], self.ps[j])) for i in range(len(self.ps)) for j in range(i + 1, len(self.ps))]

__init__(*ps)

Parameters:

Name Type Description Default
*ps Point

Points that must be distinct.

()
Source code in pyeuclid/formalization/relation.py
150
151
152
153
154
155
156
def __init__(self, *ps: list[Point]):
    """
    Args:
        *ps (Point): Points that must be distinct.
    """
    super().__init__()
    self.ps = ps

definition()

Expand to pairwise inequality relations.

Returns:

Type Description

list[Relation]: Negated equalities for every point pair.

Source code in pyeuclid/formalization/relation.py
158
159
160
161
162
163
164
def definition(self):
    """Expand to pairwise inequality relations.

    Returns:
        list[Relation]: Negated equalities for every point pair.
    """
    return [Not(Equal(self.ps[i], self.ps[j])) for i in range(len(self.ps)) for j in range(i + 1, len(self.ps))]

Equal

Bases: Relation

Point equality relation.

Source code in pyeuclid/formalization/relation.py
89
90
91
92
93
94
95
96
97
98
class Equal(Relation):
    """Point equality relation."""

    def __init__(self, v1: Point, v2: Point):
        super().__init__()
        self.v1, self.v2 = sort_points(v1, v2)

    def permutations(self):
        """Enumerate equivalent orderings of the two points."""
        return [(self.v1, self.v2), (self.v2, self.v1)]

permutations()

Enumerate equivalent orderings of the two points.

Source code in pyeuclid/formalization/relation.py
96
97
98
def permutations(self):
    """Enumerate equivalent orderings of the two points."""
    return [(self.v1, self.v2), (self.v2, self.v1)]

Lt

Bases: Relation

Source code in pyeuclid/formalization/relation.py
81
82
83
84
85
86
class Lt(Relation):
    def __init__(self, v1: Point, v2: Point):
        """Ordering helper used to canonicalize inference rule assignments."""
        super().__init__()
        self.v1 = v1
        self.v2 = v2

__init__(v1, v2)

Ordering helper used to canonicalize inference rule assignments.

Source code in pyeuclid/formalization/relation.py
82
83
84
85
86
def __init__(self, v1: Point, v2: Point):
    """Ordering helper used to canonicalize inference rule assignments."""
    super().__init__()
    self.v1 = v1
    self.v2 = v2

Midpoint

Bases: Relation

p1 is the midpoint of segment p2p3.

Source code in pyeuclid/formalization/relation.py
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
class Midpoint(Relation):
    """p1 is the midpoint of segment p2p3."""

    def __init__(self, p1: Point, p2: Point, p3: Point):
        """
        Args:
            p1 (Point): Candidate midpoint.
            p2 (Point): Segment endpoint.
            p3 (Point): Segment endpoint.
        """
        super().__init__()
        self.p1 = p1
        self.p2, self.p3 = sort_points(p2, p3)

    def definition(self):
        """Midpoint expressed via equal lengths, collinearity, and betweenness.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) - Length(self.p1, self.p3),
            Collinear(self.p1, self.p2, self.p3),
            Different(self.p2, self.p3),
            Between(self.p1, self.p2, self.p3),
        ]

__init__(p1, p2, p3)

Parameters:

Name Type Description Default
p1 Point

Candidate midpoint.

required
p2 Point

Segment endpoint.

required
p3 Point

Segment endpoint.

required
Source code in pyeuclid/formalization/relation.py
291
292
293
294
295
296
297
298
299
300
def __init__(self, p1: Point, p2: Point, p3: Point):
    """
    Args:
        p1 (Point): Candidate midpoint.
        p2 (Point): Segment endpoint.
        p3 (Point): Segment endpoint.
    """
    super().__init__()
    self.p1 = p1
    self.p2, self.p3 = sort_points(p2, p3)

definition()

Midpoint expressed via equal lengths, collinearity, and betweenness.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
302
303
304
305
306
307
308
309
310
311
312
313
def definition(self):
    """Midpoint expressed via equal lengths, collinearity, and betweenness.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) - Length(self.p1, self.p3),
        Collinear(self.p1, self.p2, self.p3),
        Different(self.p2, self.p3),
        Between(self.p1, self.p2, self.p3),
    ]

NotCollinear

Bases: Relation

Points p1,p2,p3 are not collinear.

Source code in pyeuclid/formalization/relation.py
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
class NotCollinear(Relation):
    """Points p1,p2,p3 are not collinear."""

    def __init__(self, p1, p2, p3):
        """
        Args:
            p1 (Point)
            p2 (Point)
            p3 (Point)
        """
        super().__init__()
        self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

    def definition(self):
        """Expand non-collinearity into primitive constraints."""
        return [
            Not(Collinear(self.p1, self.p2, self.p3)),
            Different(self.p1, self.p2, self.p3)
        ]

__init__(p1, p2, p3)

Source code in pyeuclid/formalization/relation.py
270
271
272
273
274
275
276
277
278
def __init__(self, p1, p2, p3):
    """
    Args:
        p1 (Point)
        p2 (Point)
        p3 (Point)
    """
    super().__init__()
    self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

definition()

Expand non-collinearity into primitive constraints.

Source code in pyeuclid/formalization/relation.py
280
281
282
283
284
285
def definition(self):
    """Expand non-collinearity into primitive constraints."""
    return [
        Not(Collinear(self.p1, self.p2, self.p3)),
        Different(self.p1, self.p2, self.p3)
    ]

OppositeSide

Bases: Relation

Points p1,p2 lie on opposite sides of the line (p3,p4).

Source code in pyeuclid/formalization/relation.py
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
class OppositeSide(Relation):
    """Points p1,p2 lie on opposite sides of the line (p3,p4)."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1 (Point): First query point.
            p2 (Point): Second query point.
            p3 (Point): Line endpoint 1.
            p4 (Point): Line endpoint 2.
        """
        super().__init__()
        self.p1, self.p2 = sort_points(p1, p2)
        self.p3, self.p4 = sort_points(p3, p4)

    def definition(self):
        """Logical expansion expressing opposite-side constraints.

        Returns:
            list[Relation]: Primitive relations defining opposite sides.
        """
        return [
            Not(Collinear(self.p1, self.p3, self.p4)),
            Not(Collinear(self.p2, self.p3, self.p4)),
            Not(SameSide(self.p1, self.p2, self.p3, self.p4)),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1 Point

First query point.

required
p2 Point

Second query point.

required
p3 Point

Line endpoint 1.

required
p4 Point

Line endpoint 2.

required
Source code in pyeuclid/formalization/relation.py
220
221
222
223
224
225
226
227
228
229
230
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1 (Point): First query point.
        p2 (Point): Second query point.
        p3 (Point): Line endpoint 1.
        p4 (Point): Line endpoint 2.
    """
    super().__init__()
    self.p1, self.p2 = sort_points(p1, p2)
    self.p3, self.p4 = sort_points(p3, p4)

definition()

Logical expansion expressing opposite-side constraints.

Returns:

Type Description

list[Relation]: Primitive relations defining opposite sides.

Source code in pyeuclid/formalization/relation.py
232
233
234
235
236
237
238
239
240
241
242
def definition(self):
    """Logical expansion expressing opposite-side constraints.

    Returns:
        list[Relation]: Primitive relations defining opposite sides.
    """
    return [
        Not(Collinear(self.p1, self.p3, self.p4)),
        Not(Collinear(self.p2, self.p3, self.p4)),
        Not(SameSide(self.p1, self.p2, self.p3, self.p4)),
    ]

Parallel

Bases: Relation

Segments (p1,p2) and (p3,p4) are parallel.

Source code in pyeuclid/formalization/relation.py
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
class Parallel(Relation):
    """Segments (p1,p2) and (p3,p4) are parallel."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1, p2 (Point): First segment endpoints.
            p3, p4 (Point): Second segment endpoints.
        """
        super().__init__()
        p1, p2 = sort_points(p1, p2)
        p3, p4 = sort_points(p3, p4)
        self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

    def permutations(self):
        """Enumerate symmetric endpoint permutations preserving segment groups.

        Returns:
            list[tuple[Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p1, self.p2, self.p4, self.p3),
            (self.p2, self.p1, self.p3, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
            (self.p3, self.p4, self.p1, self.p2),
            (self.p4, self.p3, self.p1, self.p2),
            (self.p3, self.p4, self.p2, self.p1),
            (self.p4, self.p3, self.p2, self.p1),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1, p2 Point

First segment endpoints.

required
p3, p4 Point

Second segment endpoints.

required
Source code in pyeuclid/formalization/relation.py
396
397
398
399
400
401
402
403
404
405
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1, p2 (Point): First segment endpoints.
        p3, p4 (Point): Second segment endpoints.
    """
    super().__init__()
    p1, p2 = sort_points(p1, p2)
    p3, p4 = sort_points(p3, p4)
    self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

permutations()

Enumerate symmetric endpoint permutations preserving segment groups.

Returns:

Type Description

list[tuple[Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
def permutations(self):
    """Enumerate symmetric endpoint permutations preserving segment groups.

    Returns:
        list[tuple[Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p1, self.p2, self.p4, self.p3),
        (self.p2, self.p1, self.p3, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
        (self.p3, self.p4, self.p1, self.p2),
        (self.p4, self.p3, self.p1, self.p2),
        (self.p3, self.p4, self.p2, self.p1),
        (self.p4, self.p3, self.p2, self.p1),
    ]

Pentagon

Bases: Relation

Points form a cyclically ordered pentagon.

Source code in pyeuclid/formalization/relation.py
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
class Pentagon(Relation):
    """Points form a cyclically ordered pentagon."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point):
        """
        Args:
            p1, p2, p3, p4, p5 (Point): Vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5 = sort_cyclic_points(p1, p2, p3, p4, p5)

    def permutations(self):
        """Enumerate cyclic and reversed vertex orderings.

        Returns:
            list[tuple[Point, Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4, self.p5),
            (self.p2, self.p3, self.p4, self.p5, self.p1),
            (self.p3, self.p4, self.p5, self.p1, self.p2),
            (self.p4, self.p5, self.p1, self.p2, self.p3),
            (self.p5, self.p1, self.p2, self.p3, self.p4),
            (self.p5, self.p4, self.p3, self.p2, self.p1),
            (self.p4, self.p3, self.p2, self.p1, self.p5),
            (self.p3, self.p2, self.p1, self.p5, self.p4),
            (self.p2, self.p1, self.p5, self.p4, self.p3),
            (self.p1, self.p5, self.p4, self.p3, self.p2),
        ]

__init__(p1, p2, p3, p4, p5)

Parameters:

Name Type Description Default
p1, p2, p3, p4, p5 Point

Vertices.

required
Source code in pyeuclid/formalization/relation.py
500
501
502
503
504
505
506
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point):
    """
    Args:
        p1, p2, p3, p4, p5 (Point): Vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5 = sort_cyclic_points(p1, p2, p3, p4, p5)

permutations()

Enumerate cyclic and reversed vertex orderings.

Returns:

Type Description

list[tuple[Point, Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
def permutations(self):
    """Enumerate cyclic and reversed vertex orderings.

    Returns:
        list[tuple[Point, Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4, self.p5),
        (self.p2, self.p3, self.p4, self.p5, self.p1),
        (self.p3, self.p4, self.p5, self.p1, self.p2),
        (self.p4, self.p5, self.p1, self.p2, self.p3),
        (self.p5, self.p1, self.p2, self.p3, self.p4),
        (self.p5, self.p4, self.p3, self.p2, self.p1),
        (self.p4, self.p3, self.p2, self.p1, self.p5),
        (self.p3, self.p2, self.p1, self.p5, self.p4),
        (self.p2, self.p1, self.p5, self.p4, self.p3),
        (self.p1, self.p5, self.p4, self.p3, self.p2),
    ]

Perpendicular

Bases: Relation

Segments (p1,p2) and (p3,p4) are perpendicular.

Source code in pyeuclid/formalization/relation.py
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
class Perpendicular(Relation):
    """Segments (p1,p2) and (p3,p4) are perpendicular."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1, p2 (Point): First segment endpoints.
            p3, p4 (Point): Second segment endpoints.
        """
        super().__init__()
        p1, p2 = sort_points(p1, p2)
        p3, p4 = sort_points(p3, p4)
        self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

    def permutations(self):
        """Enumerate symmetric endpoint permutations preserving segment groups.

        Returns:
            list[tuple[Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p1, self.p2, self.p4, self.p3),
            (self.p2, self.p1, self.p3, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
            (self.p3, self.p4, self.p1, self.p2),
            (self.p4, self.p3, self.p1, self.p2),
            (self.p3, self.p4, self.p2, self.p1),
            (self.p4, self.p3, self.p2, self.p1),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1, p2 Point

First segment endpoints.

required
p3, p4 Point

Second segment endpoints.

required
Source code in pyeuclid/formalization/relation.py
428
429
430
431
432
433
434
435
436
437
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1, p2 (Point): First segment endpoints.
        p3, p4 (Point): Second segment endpoints.
    """
    super().__init__()
    p1, p2 = sort_points(p1, p2)
    p3, p4 = sort_points(p3, p4)
    self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

permutations()

Enumerate symmetric endpoint permutations preserving segment groups.

Returns:

Type Description

list[tuple[Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
def permutations(self):
    """Enumerate symmetric endpoint permutations preserving segment groups.

    Returns:
        list[tuple[Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p1, self.p2, self.p4, self.p3),
        (self.p2, self.p1, self.p3, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
        (self.p3, self.p4, self.p1, self.p2),
        (self.p4, self.p3, self.p1, self.p2),
        (self.p3, self.p4, self.p2, self.p1),
        (self.p4, self.p3, self.p2, self.p1),
    ]

Point

Named geometric point used throughout the formalization.

Source code in pyeuclid/formalization/relation.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Point:
    """Named geometric point used throughout the formalization."""

    def __init__(self, name: str):
        """Create a point.

        Args:
            name (str): Identifier (underscores are not allowed).
        """
        self.name = name
        assert "_" not in name

    def __str__(self):
        return self.name

    def __eq__(self, other):
        return str(self) == str(other)

    def __repr__(self):
        return str(self)

    def __hash__(self):
        return hash(str(self))

__init__(name)

Create a point.

Parameters:

Name Type Description Default
name str

Identifier (underscores are not allowed).

required
Source code in pyeuclid/formalization/relation.py
16
17
18
19
20
21
22
23
def __init__(self, name: str):
    """Create a point.

    Args:
        name (str): Identifier (underscores are not allowed).
    """
    self.name = name
    assert "_" not in name

Quadrilateral

Bases: Relation

Points form a cyclically ordered quadrilateral.

Source code in pyeuclid/formalization/relation.py
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
class Quadrilateral(Relation):
    """Points form a cyclically ordered quadrilateral."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1, p2, p3, p4 (Point): Vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4 = sort_cyclic_points(p1, p2, p3, p4)

    def permutations(self):
        """Enumerate cyclic and reversed vertex orderings.

        Returns:
            list[tuple[Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p2, self.p3, self.p4, self.p1),
            (self.p3, self.p4, self.p1, self.p2),
            (self.p4, self.p1, self.p2, self.p3),
            (self.p4, self.p3, self.p2, self.p1),
            (self.p3, self.p2, self.p1, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
            (self.p1, self.p4, self.p3, self.p2),
        ]

    def definition(self):
        """Opposite sides must lie on opposite sides of diagonals.

        Returns:
            list[Relation]: Primitive opposite-side relations.
        """
        return [
            OppositeSide(self.p1, self.p3, self.p2, self.p4),
            OppositeSide(self.p2, self.p4, self.p1, self.p3),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1, p2, p3, p4 Point

Vertices.

required
Source code in pyeuclid/formalization/relation.py
460
461
462
463
464
465
466
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1, p2, p3, p4 (Point): Vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4 = sort_cyclic_points(p1, p2, p3, p4)

definition()

Opposite sides must lie on opposite sides of diagonals.

Returns:

Type Description

list[Relation]: Primitive opposite-side relations.

Source code in pyeuclid/formalization/relation.py
485
486
487
488
489
490
491
492
493
494
def definition(self):
    """Opposite sides must lie on opposite sides of diagonals.

    Returns:
        list[Relation]: Primitive opposite-side relations.
    """
    return [
        OppositeSide(self.p1, self.p3, self.p2, self.p4),
        OppositeSide(self.p2, self.p4, self.p1, self.p3),
    ]

permutations()

Enumerate cyclic and reversed vertex orderings.

Returns:

Type Description

list[tuple[Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
def permutations(self):
    """Enumerate cyclic and reversed vertex orderings.

    Returns:
        list[tuple[Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p2, self.p3, self.p4, self.p1),
        (self.p3, self.p4, self.p1, self.p2),
        (self.p4, self.p1, self.p2, self.p3),
        (self.p4, self.p3, self.p2, self.p1),
        (self.p3, self.p2, self.p1, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
        (self.p1, self.p4, self.p3, self.p2),
    ]

Relation

Base class for logical relations over points.

Source code in pyeuclid/formalization/relation.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class Relation:
    """Base class for logical relations over points."""

    def __init__(self):
        self.negated = False

    def negate(self):
        """Toggle negation flag in-place."""
        self.negated = not self.negated

    def get_points(self):
        """Return all point instances contained in the relation."""
        points = []
        for v in vars(self).values():
            if isinstance(v, Point):
                points.append(v)
            elif isinstance(v, list):
                for p in v:
                    if isinstance(p, Point):
                        points.append(p)
        return points

    def __str__(self):
        """Readable representation, prefixed with Not() when negated."""
        class_name = self.__class__.__name__
        points = self.get_points()
        args_name = ",".join([p.name for p in points])

        if not self.negated:
            return f"{class_name}({args_name})"
        else:
            return f"Not({class_name}({args_name}))"

    def __repr__(self):
        return str(self)

    def __eq__(self, other):
        return str(self) == str(other)

    def __hash__(self):
        return hash(str(self))

__str__()

Readable representation, prefixed with Not() when negated.

Source code in pyeuclid/formalization/relation.py
60
61
62
63
64
65
66
67
68
69
def __str__(self):
    """Readable representation, prefixed with Not() when negated."""
    class_name = self.__class__.__name__
    points = self.get_points()
    args_name = ",".join([p.name for p in points])

    if not self.negated:
        return f"{class_name}({args_name})"
    else:
        return f"Not({class_name}({args_name}))"

get_points()

Return all point instances contained in the relation.

Source code in pyeuclid/formalization/relation.py
48
49
50
51
52
53
54
55
56
57
58
def get_points(self):
    """Return all point instances contained in the relation."""
    points = []
    for v in vars(self).values():
        if isinstance(v, Point):
            points.append(v)
        elif isinstance(v, list):
            for p in v:
                if isinstance(p, Point):
                    points.append(p)
    return points

negate()

Toggle negation flag in-place.

Source code in pyeuclid/formalization/relation.py
44
45
46
def negate(self):
    """Toggle negation flag in-place."""
    self.negated = not self.negated

SameSide

Bases: Relation

Points p1,p2 lie on the same side of the line (p3,p4).

Source code in pyeuclid/formalization/relation.py
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
class SameSide(Relation):
    """Points p1,p2 lie on the same side of the line (p3,p4)."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1 (Point): First query point.
            p2 (Point): Second query point.
            p3 (Point): Line endpoint 1.
            p4 (Point): Line endpoint 2.
        """
        super().__init__()
        self.p1, self.p2 = sort_points(p1, p2)
        self.p3, self.p4 = sort_points(p3, p4)

    def permutations(self):
        """Enumerate symmetric orderings for same-side tests."""
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p1, self.p2, self.p4, self.p3),
            (self.p2, self.p1, self.p3, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1 Point

First query point.

required
p2 Point

Second query point.

required
p3 Point

Line endpoint 1.

required
p4 Point

Line endpoint 2.

required
Source code in pyeuclid/formalization/relation.py
195
196
197
198
199
200
201
202
203
204
205
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1 (Point): First query point.
        p2 (Point): Second query point.
        p3 (Point): Line endpoint 1.
        p4 (Point): Line endpoint 2.
    """
    super().__init__()
    self.p1, self.p2 = sort_points(p1, p2)
    self.p3, self.p4 = sort_points(p3, p4)

permutations()

Enumerate symmetric orderings for same-side tests.

Source code in pyeuclid/formalization/relation.py
207
208
209
210
211
212
213
214
def permutations(self):
    """Enumerate symmetric orderings for same-side tests."""
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p1, self.p2, self.p4, self.p3),
        (self.p2, self.p1, self.p3, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
    ]

Similar

Bases: Relation

Triangles (p1,p2,p3) and (p4,p5,p6) are similar.

Source code in pyeuclid/formalization/relation.py
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
class Similar(Relation):
    """Triangles (p1,p2,p3) and (p4,p5,p6) are similar."""

    def __init__(
        self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
    ):
        """
        Args:
            p1, p2, p3 (Point): First triangle vertices.
            p4, p5, p6 (Point): Second triangle vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

    def definition(self):
        """Similarity expressed via length ratios and non-collinearity.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) / Length(self.p4, self.p5)
            - Length(self.p2, self.p3) / Length(self.p5, self.p6),
            Length(self.p1, self.p2) / Length(self.p4, self.p5)
            - Length(self.p3, self.p1) / Length(self.p6, self.p4),
            NotCollinear(self.p1, self.p2, self.p3),
        ]

__init__(p1, p2, p3, p4, p5, p6)

Parameters:

Name Type Description Default
p1, p2, p3 Point

First triangle vertices.

required
p4, p5, p6 Point

Second triangle vertices.

required
Source code in pyeuclid/formalization/relation.py
347
348
349
350
351
352
353
354
355
356
def __init__(
    self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
):
    """
    Args:
        p1, p2, p3 (Point): First triangle vertices.
        p4, p5, p6 (Point): Second triangle vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

definition()

Similarity expressed via length ratios and non-collinearity.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
358
359
360
361
362
363
364
365
366
367
368
369
370
def definition(self):
    """Similarity expressed via length ratios and non-collinearity.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) / Length(self.p4, self.p5)
        - Length(self.p2, self.p3) / Length(self.p5, self.p6),
        Length(self.p1, self.p2) / Length(self.p4, self.p5)
        - Length(self.p3, self.p1) / Length(self.p6, self.p4),
        NotCollinear(self.p1, self.p2, self.p3),
    ]

Similar4P

Bases: Relation

Two quadrilaterals are similar.

Source code in pyeuclid/formalization/relation.py
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
class Similar4P(Relation):
    """Two quadrilaterals are similar."""

    def __init__(
        self,
        p1: Point,
        p2: Point,
        p3: Point,
        p4: Point,
        p5: Point,
        p6: Point,
        p7: Point,
        p8: Point,
    ):
        """
        Args:
            p1..p4 (Point): First quadrilateral.
            p5..p8 (Point): Second quadrilateral.
        """
        super().__init__()
        point_map_12 = {p1: p5, p2: p6, p3: p7, p4: p8}
        point_map_21 = {p5: p1, p6: p2, p7: p3, p8: p4}

        sorted_1 = sort_cyclic_points(p1, p2, p3, p4)
        sorted_2 = sort_cyclic_points(p5, p6, p7, p8)
        if compare_names(sorted_1, sorted_2) == 0:
            self.p1, self.p2, self.p3, self.p4 = sorted_1
            self.p5,self.p6,self.p7,self.p8 = point_map_12[self.p1], point_map_12[
                self.p2], point_map_12[self.p3], point_map_12[self.p4]
        else:
            self.p1, self.p2, self.p3, self.p4 = sorted_2
            self.p5, self.p6, self.p7, self.p8 = point_map_21[self.p1], point_map_21[
                self.p2], point_map_21[self.p3], point_map_21[self.p4]

    def definition(self):
        """Similarity expressed via side ratios and angle equalities.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) / Length(self.p5, self.p6)
            - Length(self.p2, self.p3) / Length(self.p6, self.p7),
            Length(self.p1, self.p2) / Length(self.p5, self.p6)
            - Length(self.p3, self.p4) / Length(self.p7, self.p8),
            Length(self.p1, self.p2) / Length(self.p5, self.p6)
            - Length(self.p4, self.p1) / Length(self.p8, self.p5),
            Angle(self.p1, self.p2, self.p3) - Angle(self.p5, self.p6, self.p7),
            Angle(self.p2, self.p3, self.p4) - Angle(self.p6, self.p7, self.p8),
            Angle(self.p3, self.p4, self.p1) - Angle(self.p7, self.p8, self.p5),
            Angle(self.p4, self.p1, self.p2) - Angle(self.p8, self.p5, self.p6),
        ]

__init__(p1, p2, p3, p4, p5, p6, p7, p8)

Parameters:

Name Type Description Default
p1..p4 Point

First quadrilateral.

required
p5..p8 Point

Second quadrilateral.

required
Source code in pyeuclid/formalization/relation.py
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
def __init__(
    self,
    p1: Point,
    p2: Point,
    p3: Point,
    p4: Point,
    p5: Point,
    p6: Point,
    p7: Point,
    p8: Point,
):
    """
    Args:
        p1..p4 (Point): First quadrilateral.
        p5..p8 (Point): Second quadrilateral.
    """
    super().__init__()
    point_map_12 = {p1: p5, p2: p6, p3: p7, p4: p8}
    point_map_21 = {p5: p1, p6: p2, p7: p3, p8: p4}

    sorted_1 = sort_cyclic_points(p1, p2, p3, p4)
    sorted_2 = sort_cyclic_points(p5, p6, p7, p8)
    if compare_names(sorted_1, sorted_2) == 0:
        self.p1, self.p2, self.p3, self.p4 = sorted_1
        self.p5,self.p6,self.p7,self.p8 = point_map_12[self.p1], point_map_12[
            self.p2], point_map_12[self.p3], point_map_12[self.p4]
    else:
        self.p1, self.p2, self.p3, self.p4 = sorted_2
        self.p5, self.p6, self.p7, self.p8 = point_map_21[self.p1], point_map_21[
            self.p2], point_map_21[self.p3], point_map_21[self.p4]

definition()

Similarity expressed via side ratios and angle equalities.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
def definition(self):
    """Similarity expressed via side ratios and angle equalities.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) / Length(self.p5, self.p6)
        - Length(self.p2, self.p3) / Length(self.p6, self.p7),
        Length(self.p1, self.p2) / Length(self.p5, self.p6)
        - Length(self.p3, self.p4) / Length(self.p7, self.p8),
        Length(self.p1, self.p2) / Length(self.p5, self.p6)
        - Length(self.p4, self.p1) / Length(self.p8, self.p5),
        Angle(self.p1, self.p2, self.p3) - Angle(self.p5, self.p6, self.p7),
        Angle(self.p2, self.p3, self.p4) - Angle(self.p6, self.p7, self.p8),
        Angle(self.p3, self.p4, self.p1) - Angle(self.p7, self.p8, self.p5),
        Angle(self.p4, self.p1, self.p2) - Angle(self.p8, self.p5, self.p6),
    ]

Similar5P

Bases: Relation

Two pentagons are similar.

Source code in pyeuclid/formalization/relation.py
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
class Similar5P(Relation):
    """Two pentagons are similar."""

    def __init__(
        self,
        p1: Point,
        p2: Point,
        p3: Point,
        p4: Point,
        p5: Point,
        p6: Point,
        p7: Point,
        p8: Point,
        p9: Point,
        p10: Point,
    ):
        """
        Args:
            p1..p5 (Point): First pentagon.
            p6..p10 (Point): Second pentagon.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5, self.p6, self.p7, self.p8, self.p9, self.p10 = p1, p2, p3, p4, p5, p6, p7, p8, p9, p10

    def definition(self):
        """Similarity expressed via consecutive side ratios and angle equalities.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) / Length(self.p6, self.p7)
            - Length(self.p2, self.p3) / Length(self.p7, self.p8),
            Length(self.p2, self.p3) / Length(self.p7, self.p8)
            - Length(self.p3, self.p4) / Length(self.p8, self.p9),
            Length(self.p3, self.p4) / Length(self.p8, self.p9)
            - Length(self.p4, self.p5) / Length(self.p9, self.p10),
            Length(self.p4, self.p5) / Length(self.p9, self.p10)
            - Length(self.p5, self.p1) / Length(self.p10, self.p6),
            Length(self.p5, self.p1) / Length(self.p10, self.p6)
            - Length(self.p1, self.p2) / Length(self.p6, self.p7),
            Angle(self.p1, self.p2, self.p3) - Angle(self.p6, self.p7, self.p8),
            Angle(self.p2, self.p3, self.p4) - Angle(self.p7, self.p8, self.p9),
            Angle(self.p3, self.p4, self.p5) - Angle(self.p8, self.p9, self.p10),
            Angle(self.p4, self.p5, self.p1) - Angle(self.p9, self.p10, self.p6),
            Angle(self.p5, self.p1, self.p2) - Angle(self.p10, self.p6, self.p7),
        ]

__init__(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10)

Parameters:

Name Type Description Default
p1..p5 Point

First pentagon.

required
p6..p10 Point

Second pentagon.

required
Source code in pyeuclid/formalization/relation.py
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
def __init__(
    self,
    p1: Point,
    p2: Point,
    p3: Point,
    p4: Point,
    p5: Point,
    p6: Point,
    p7: Point,
    p8: Point,
    p9: Point,
    p10: Point,
):
    """
    Args:
        p1..p5 (Point): First pentagon.
        p6..p10 (Point): Second pentagon.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5, self.p6, self.p7, self.p8, self.p9, self.p10 = p1, p2, p3, p4, p5, p6, p7, p8, p9, p10

definition()

Similarity expressed via consecutive side ratios and angle equalities.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
def definition(self):
    """Similarity expressed via consecutive side ratios and angle equalities.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) / Length(self.p6, self.p7)
        - Length(self.p2, self.p3) / Length(self.p7, self.p8),
        Length(self.p2, self.p3) / Length(self.p7, self.p8)
        - Length(self.p3, self.p4) / Length(self.p8, self.p9),
        Length(self.p3, self.p4) / Length(self.p8, self.p9)
        - Length(self.p4, self.p5) / Length(self.p9, self.p10),
        Length(self.p4, self.p5) / Length(self.p9, self.p10)
        - Length(self.p5, self.p1) / Length(self.p10, self.p6),
        Length(self.p5, self.p1) / Length(self.p10, self.p6)
        - Length(self.p1, self.p2) / Length(self.p6, self.p7),
        Angle(self.p1, self.p2, self.p3) - Angle(self.p6, self.p7, self.p8),
        Angle(self.p2, self.p3, self.p4) - Angle(self.p7, self.p8, self.p9),
        Angle(self.p3, self.p4, self.p5) - Angle(self.p8, self.p9, self.p10),
        Angle(self.p4, self.p5, self.p1) - Angle(self.p9, self.p10, self.p6),
        Angle(self.p5, self.p1, self.p2) - Angle(self.p10, self.p6, self.p7),
    ]

Angle(p1, p2, p3)

Symbolic angle at p2.

Returns:

Type Description

sympy.Symbol: Non-negative angle symbol.

Source code in pyeuclid/formalization/relation.py
108
109
110
111
112
113
114
115
def Angle(p1: Point, p2: Point, p3: Point):
    """Symbolic angle at p2.

    Returns:
        sympy.Symbol: Non-negative angle symbol.
    """
    p1, p3 = sort_points(p1, p3)
    return Symbol(f"Angle_{p1}_{p2}_{p3}", non_negative=True)

Area(*ps)

Symbolic polygonal area over an ordered point cycle.

Returns:

Type Description

sympy.Symbol: Positive area symbol.

Source code in pyeuclid/formalization/relation.py
128
129
130
131
132
133
134
135
def Area(*ps: list[Point]):
    """Symbolic polygonal area over an ordered point cycle.

    Returns:
        sympy.Symbol: Positive area symbol.
    """
    ps = sort_cyclic_points(*ps)
    return Symbol("_".join(["Area"] + [str(item) for item in ps]), positive=True)

Length(p1, p2)

Symbolic length between two points.

Returns:

Type Description

sympy.Symbol: Positive length symbol.

Source code in pyeuclid/formalization/relation.py
118
119
120
121
122
123
124
125
def Length(p1: Point, p2: Point):
    """Symbolic length between two points.

    Returns:
        sympy.Symbol: Positive length symbol.
    """
    p1, p2 = sort_points(p1, p2)
    return Symbol(f"Length_{str(p1)}_{str(p2)}", positive=True)

Not(p)

Return a negated shallow copy of a relation.

Source code in pyeuclid/formalization/relation.py
101
102
103
104
105
def Not(p: Relation) -> Relation:
    """Return a negated shallow copy of a relation."""
    other = copy.copy(p)
    other.negate()
    return other

Variable(name)

Free symbolic variable placeholder.

Returns:

Type Description

sympy.Symbol: Dimensionless variable symbol.

Source code in pyeuclid/formalization/relation.py
138
139
140
141
142
143
144
def Variable(name: str):
    """Free symbolic variable placeholder.

    Returns:
        sympy.Symbol: Dimensionless variable symbol.
    """
    return Symbol(f"Variable_{name}")

Parsing utilities for benchmark text descriptions into constructions/goals.

Between

Bases: Relation

p1 lies between p2 and p3 (on the same line).

Source code in pyeuclid/formalization/relation.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
class Between(Relation):
    """p1 lies between p2 and p3 (on the same line)."""

    def __init__(self, p1: Point, p2: Point, p3: Point):
        """
        p1 is between p2 and p3.

        Args:
            p1 (Point): Middle point.
            p2 (Point): Endpoint 1.
            p3 (Point): Endpoint 2.
        """
        super().__init__()
        p2, p3 = sort_points(p2, p3)
        self.p1, self.p2, self.p3 = p1, p2, p3

    def permutations(self):
        """Enumerate equivalent point orderings for the between relation.

        Returns:
            list[tuple[Point, Point, Point]]: Permitted permutations.
        """
        return [(self.p1, self.p2, self.p3), (self.p1, self.p3, self.p2)]

__init__(p1, p2, p3)

p1 is between p2 and p3.

Parameters:

Name Type Description Default
p1 Point

Middle point.

required
p2 Point

Endpoint 1.

required
p3 Point

Endpoint 2.

required
Source code in pyeuclid/formalization/relation.py
170
171
172
173
174
175
176
177
178
179
180
181
def __init__(self, p1: Point, p2: Point, p3: Point):
    """
    p1 is between p2 and p3.

    Args:
        p1 (Point): Middle point.
        p2 (Point): Endpoint 1.
        p3 (Point): Endpoint 2.
    """
    super().__init__()
    p2, p3 = sort_points(p2, p3)
    self.p1, self.p2, self.p3 = p1, p2, p3

permutations()

Enumerate equivalent point orderings for the between relation.

Returns:

Type Description

list[tuple[Point, Point, Point]]: Permitted permutations.

Source code in pyeuclid/formalization/relation.py
183
184
185
186
187
188
189
def permutations(self):
    """Enumerate equivalent point orderings for the between relation.

    Returns:
        list[tuple[Point, Point, Point]]: Permitted permutations.
    """
    return [(self.p1, self.p2, self.p3), (self.p1, self.p3, self.p2)]

Collinear

Bases: Relation

Points p1,p2,p3 are collinear.

Source code in pyeuclid/formalization/relation.py
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
class Collinear(Relation):
    """Points p1,p2,p3 are collinear."""

    def __init__(self, p1, p2, p3):
        """
        Args:
            p1 (Point)
            p2 (Point)
            p3 (Point)
        """
        super().__init__()
        self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

    def permutations(self):
        """Enumerate collinearity argument permutations.

        Returns:
            itertools.permutations: All orderings of the three points.
        """
        return itertools.permutations([self.p1, self.p2, self.p3])

__init__(p1, p2, p3)

Source code in pyeuclid/formalization/relation.py
248
249
250
251
252
253
254
255
256
def __init__(self, p1, p2, p3):
    """
    Args:
        p1 (Point)
        p2 (Point)
        p3 (Point)
    """
    super().__init__()
    self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

permutations()

Enumerate collinearity argument permutations.

Returns:

Type Description

itertools.permutations: All orderings of the three points.

Source code in pyeuclid/formalization/relation.py
258
259
260
261
262
263
264
def permutations(self):
    """Enumerate collinearity argument permutations.

    Returns:
        itertools.permutations: All orderings of the three points.
    """
    return itertools.permutations([self.p1, self.p2, self.p3])

Concyclic

Bases: Relation

All given points lie on the same circle.

Source code in pyeuclid/formalization/relation.py
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
class Concyclic(Relation):
    """All given points lie on the same circle."""

    def __init__(self, *ps: Point):
        """
        Args:
            *ps (Point): Points to be tested for concyclicity.
        """
        super().__init__()
        self.ps = list(sort_points(*ps))

    def permutations(self):
        """Enumerate symmetric permutations of the point set.

        Returns:
            itertools.permutations: All orderings of the points.
        """
        return itertools.permutations(self.ps)

__init__(*ps)

Parameters:

Name Type Description Default
*ps Point

Points to be tested for concyclicity.

()
Source code in pyeuclid/formalization/relation.py
376
377
378
379
380
381
382
def __init__(self, *ps: Point):
    """
    Args:
        *ps (Point): Points to be tested for concyclicity.
    """
    super().__init__()
    self.ps = list(sort_points(*ps))

permutations()

Enumerate symmetric permutations of the point set.

Returns:

Type Description

itertools.permutations: All orderings of the points.

Source code in pyeuclid/formalization/relation.py
384
385
386
387
388
389
390
def permutations(self):
    """Enumerate symmetric permutations of the point set.

    Returns:
        itertools.permutations: All orderings of the points.
    """
    return itertools.permutations(self.ps)

Congruent

Bases: Relation

Triangles (p1,p2,p3) and (p4,p5,p6) are congruent.

Source code in pyeuclid/formalization/relation.py
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
class Congruent(Relation):
    """Triangles (p1,p2,p3) and (p4,p5,p6) are congruent."""

    def __init__(
        self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
    ):
        """
        Args:
            p1, p2, p3 (Point): First triangle vertices.
            p4, p5, p6 (Point): Second triangle vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

    def definition(self):
        """Congruence expressed as equal side lengths and non-collinearity.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) - Length(self.p4, self.p5),
            Length(self.p2, self.p3) - Length(self.p5, self.p6),
            Length(self.p1, self.p3) - Length(self.p4, self.p6),
            NotCollinear(self.p1, self.p2, self.p3),
        ]

__init__(p1, p2, p3, p4, p5, p6)

Parameters:

Name Type Description Default
p1, p2, p3 Point

First triangle vertices.

required
p4, p5, p6 Point

Second triangle vertices.

required
Source code in pyeuclid/formalization/relation.py
319
320
321
322
323
324
325
326
327
328
def __init__(
    self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
):
    """
    Args:
        p1, p2, p3 (Point): First triangle vertices.
        p4, p5, p6 (Point): Second triangle vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

definition()

Congruence expressed as equal side lengths and non-collinearity.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
330
331
332
333
334
335
336
337
338
339
340
341
def definition(self):
    """Congruence expressed as equal side lengths and non-collinearity.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) - Length(self.p4, self.p5),
        Length(self.p2, self.p3) - Length(self.p5, self.p6),
        Length(self.p1, self.p3) - Length(self.p4, self.p6),
        NotCollinear(self.p1, self.p2, self.p3),
    ]

ConstructionRule

Base class for geometric construction rules.

Source code in pyeuclid/formalization/construction_rule.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class ConstructionRule:
    """Base class for geometric construction rules."""

    def __init__(self):
        """Initialize an empty construction rule."""
        pass

    def arguments(self):
        """Return the input entities required for the construction."""
        return []

    def constructed_points(self):
        """Return the points constructed by this rule."""
        return []

    def conditions(self):
        """Return prerequisite relations for the construction to be valid."""
        return []

    def conclusions(self):
        """Return relations implied after applying the construction."""
        return []

    def __str__(self):
        class_name = self.__class__.__name__
        attributes = ",".join(str(value) for _, value in vars(self).items())
        return f"{class_name}({attributes})"

__init__()

Initialize an empty construction rule.

Source code in pyeuclid/formalization/construction_rule.py
12
13
14
def __init__(self):
    """Initialize an empty construction rule."""
    pass

arguments()

Return the input entities required for the construction.

Source code in pyeuclid/formalization/construction_rule.py
16
17
18
def arguments(self):
    """Return the input entities required for the construction."""
    return []

conclusions()

Return relations implied after applying the construction.

Source code in pyeuclid/formalization/construction_rule.py
28
29
30
def conclusions(self):
    """Return relations implied after applying the construction."""
    return []

conditions()

Return prerequisite relations for the construction to be valid.

Source code in pyeuclid/formalization/construction_rule.py
24
25
26
def conditions(self):
    """Return prerequisite relations for the construction to be valid."""
    return []

constructed_points()

Return the points constructed by this rule.

Source code in pyeuclid/formalization/construction_rule.py
20
21
22
def constructed_points(self):
    """Return the points constructed by this rule."""
    return []

Different

Bases: Relation

All provided points must be pairwise distinct.

Source code in pyeuclid/formalization/relation.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
class Different(Relation):
    """All provided points must be pairwise distinct."""

    def __init__(self, *ps: list[Point]):
        """
        Args:
            *ps (Point): Points that must be distinct.
        """
        super().__init__()
        self.ps = ps

    def definition(self):
        """Expand to pairwise inequality relations.

        Returns:
            list[Relation]: Negated equalities for every point pair.
        """
        return [Not(Equal(self.ps[i], self.ps[j])) for i in range(len(self.ps)) for j in range(i + 1, len(self.ps))]

__init__(*ps)

Parameters:

Name Type Description Default
*ps Point

Points that must be distinct.

()
Source code in pyeuclid/formalization/relation.py
150
151
152
153
154
155
156
def __init__(self, *ps: list[Point]):
    """
    Args:
        *ps (Point): Points that must be distinct.
    """
    super().__init__()
    self.ps = ps

definition()

Expand to pairwise inequality relations.

Returns:

Type Description

list[Relation]: Negated equalities for every point pair.

Source code in pyeuclid/formalization/relation.py
158
159
160
161
162
163
164
def definition(self):
    """Expand to pairwise inequality relations.

    Returns:
        list[Relation]: Negated equalities for every point pair.
    """
    return [Not(Equal(self.ps[i], self.ps[j])) for i in range(len(self.ps)) for j in range(i + 1, len(self.ps))]

Equal

Bases: Relation

Point equality relation.

Source code in pyeuclid/formalization/relation.py
89
90
91
92
93
94
95
96
97
98
class Equal(Relation):
    """Point equality relation."""

    def __init__(self, v1: Point, v2: Point):
        super().__init__()
        self.v1, self.v2 = sort_points(v1, v2)

    def permutations(self):
        """Enumerate equivalent orderings of the two points."""
        return [(self.v1, self.v2), (self.v2, self.v1)]

permutations()

Enumerate equivalent orderings of the two points.

Source code in pyeuclid/formalization/relation.py
96
97
98
def permutations(self):
    """Enumerate equivalent orderings of the two points."""
    return [(self.v1, self.v2), (self.v2, self.v1)]

Lt

Bases: Relation

Source code in pyeuclid/formalization/relation.py
81
82
83
84
85
86
class Lt(Relation):
    def __init__(self, v1: Point, v2: Point):
        """Ordering helper used to canonicalize inference rule assignments."""
        super().__init__()
        self.v1 = v1
        self.v2 = v2

__init__(v1, v2)

Ordering helper used to canonicalize inference rule assignments.

Source code in pyeuclid/formalization/relation.py
82
83
84
85
86
def __init__(self, v1: Point, v2: Point):
    """Ordering helper used to canonicalize inference rule assignments."""
    super().__init__()
    self.v1 = v1
    self.v2 = v2

Midpoint

Bases: Relation

p1 is the midpoint of segment p2p3.

Source code in pyeuclid/formalization/relation.py
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
class Midpoint(Relation):
    """p1 is the midpoint of segment p2p3."""

    def __init__(self, p1: Point, p2: Point, p3: Point):
        """
        Args:
            p1 (Point): Candidate midpoint.
            p2 (Point): Segment endpoint.
            p3 (Point): Segment endpoint.
        """
        super().__init__()
        self.p1 = p1
        self.p2, self.p3 = sort_points(p2, p3)

    def definition(self):
        """Midpoint expressed via equal lengths, collinearity, and betweenness.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) - Length(self.p1, self.p3),
            Collinear(self.p1, self.p2, self.p3),
            Different(self.p2, self.p3),
            Between(self.p1, self.p2, self.p3),
        ]

__init__(p1, p2, p3)

Parameters:

Name Type Description Default
p1 Point

Candidate midpoint.

required
p2 Point

Segment endpoint.

required
p3 Point

Segment endpoint.

required
Source code in pyeuclid/formalization/relation.py
291
292
293
294
295
296
297
298
299
300
def __init__(self, p1: Point, p2: Point, p3: Point):
    """
    Args:
        p1 (Point): Candidate midpoint.
        p2 (Point): Segment endpoint.
        p3 (Point): Segment endpoint.
    """
    super().__init__()
    self.p1 = p1
    self.p2, self.p3 = sort_points(p2, p3)

definition()

Midpoint expressed via equal lengths, collinearity, and betweenness.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
302
303
304
305
306
307
308
309
310
311
312
313
def definition(self):
    """Midpoint expressed via equal lengths, collinearity, and betweenness.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) - Length(self.p1, self.p3),
        Collinear(self.p1, self.p2, self.p3),
        Different(self.p2, self.p3),
        Between(self.p1, self.p2, self.p3),
    ]

NotCollinear

Bases: Relation

Points p1,p2,p3 are not collinear.

Source code in pyeuclid/formalization/relation.py
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
class NotCollinear(Relation):
    """Points p1,p2,p3 are not collinear."""

    def __init__(self, p1, p2, p3):
        """
        Args:
            p1 (Point)
            p2 (Point)
            p3 (Point)
        """
        super().__init__()
        self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

    def definition(self):
        """Expand non-collinearity into primitive constraints."""
        return [
            Not(Collinear(self.p1, self.p2, self.p3)),
            Different(self.p1, self.p2, self.p3)
        ]

__init__(p1, p2, p3)

Source code in pyeuclid/formalization/relation.py
270
271
272
273
274
275
276
277
278
def __init__(self, p1, p2, p3):
    """
    Args:
        p1 (Point)
        p2 (Point)
        p3 (Point)
    """
    super().__init__()
    self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

definition()

Expand non-collinearity into primitive constraints.

Source code in pyeuclid/formalization/relation.py
280
281
282
283
284
285
def definition(self):
    """Expand non-collinearity into primitive constraints."""
    return [
        Not(Collinear(self.p1, self.p2, self.p3)),
        Different(self.p1, self.p2, self.p3)
    ]

OppositeSide

Bases: Relation

Points p1,p2 lie on opposite sides of the line (p3,p4).

Source code in pyeuclid/formalization/relation.py
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
class OppositeSide(Relation):
    """Points p1,p2 lie on opposite sides of the line (p3,p4)."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1 (Point): First query point.
            p2 (Point): Second query point.
            p3 (Point): Line endpoint 1.
            p4 (Point): Line endpoint 2.
        """
        super().__init__()
        self.p1, self.p2 = sort_points(p1, p2)
        self.p3, self.p4 = sort_points(p3, p4)

    def definition(self):
        """Logical expansion expressing opposite-side constraints.

        Returns:
            list[Relation]: Primitive relations defining opposite sides.
        """
        return [
            Not(Collinear(self.p1, self.p3, self.p4)),
            Not(Collinear(self.p2, self.p3, self.p4)),
            Not(SameSide(self.p1, self.p2, self.p3, self.p4)),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1 Point

First query point.

required
p2 Point

Second query point.

required
p3 Point

Line endpoint 1.

required
p4 Point

Line endpoint 2.

required
Source code in pyeuclid/formalization/relation.py
220
221
222
223
224
225
226
227
228
229
230
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1 (Point): First query point.
        p2 (Point): Second query point.
        p3 (Point): Line endpoint 1.
        p4 (Point): Line endpoint 2.
    """
    super().__init__()
    self.p1, self.p2 = sort_points(p1, p2)
    self.p3, self.p4 = sort_points(p3, p4)

definition()

Logical expansion expressing opposite-side constraints.

Returns:

Type Description

list[Relation]: Primitive relations defining opposite sides.

Source code in pyeuclid/formalization/relation.py
232
233
234
235
236
237
238
239
240
241
242
def definition(self):
    """Logical expansion expressing opposite-side constraints.

    Returns:
        list[Relation]: Primitive relations defining opposite sides.
    """
    return [
        Not(Collinear(self.p1, self.p3, self.p4)),
        Not(Collinear(self.p2, self.p3, self.p4)),
        Not(SameSide(self.p1, self.p2, self.p3, self.p4)),
    ]

Parallel

Bases: Relation

Segments (p1,p2) and (p3,p4) are parallel.

Source code in pyeuclid/formalization/relation.py
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
class Parallel(Relation):
    """Segments (p1,p2) and (p3,p4) are parallel."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1, p2 (Point): First segment endpoints.
            p3, p4 (Point): Second segment endpoints.
        """
        super().__init__()
        p1, p2 = sort_points(p1, p2)
        p3, p4 = sort_points(p3, p4)
        self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

    def permutations(self):
        """Enumerate symmetric endpoint permutations preserving segment groups.

        Returns:
            list[tuple[Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p1, self.p2, self.p4, self.p3),
            (self.p2, self.p1, self.p3, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
            (self.p3, self.p4, self.p1, self.p2),
            (self.p4, self.p3, self.p1, self.p2),
            (self.p3, self.p4, self.p2, self.p1),
            (self.p4, self.p3, self.p2, self.p1),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1, p2 Point

First segment endpoints.

required
p3, p4 Point

Second segment endpoints.

required
Source code in pyeuclid/formalization/relation.py
396
397
398
399
400
401
402
403
404
405
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1, p2 (Point): First segment endpoints.
        p3, p4 (Point): Second segment endpoints.
    """
    super().__init__()
    p1, p2 = sort_points(p1, p2)
    p3, p4 = sort_points(p3, p4)
    self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

permutations()

Enumerate symmetric endpoint permutations preserving segment groups.

Returns:

Type Description

list[tuple[Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
def permutations(self):
    """Enumerate symmetric endpoint permutations preserving segment groups.

    Returns:
        list[tuple[Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p1, self.p2, self.p4, self.p3),
        (self.p2, self.p1, self.p3, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
        (self.p3, self.p4, self.p1, self.p2),
        (self.p4, self.p3, self.p1, self.p2),
        (self.p3, self.p4, self.p2, self.p1),
        (self.p4, self.p3, self.p2, self.p1),
    ]

Pentagon

Bases: Relation

Points form a cyclically ordered pentagon.

Source code in pyeuclid/formalization/relation.py
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
class Pentagon(Relation):
    """Points form a cyclically ordered pentagon."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point):
        """
        Args:
            p1, p2, p3, p4, p5 (Point): Vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5 = sort_cyclic_points(p1, p2, p3, p4, p5)

    def permutations(self):
        """Enumerate cyclic and reversed vertex orderings.

        Returns:
            list[tuple[Point, Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4, self.p5),
            (self.p2, self.p3, self.p4, self.p5, self.p1),
            (self.p3, self.p4, self.p5, self.p1, self.p2),
            (self.p4, self.p5, self.p1, self.p2, self.p3),
            (self.p5, self.p1, self.p2, self.p3, self.p4),
            (self.p5, self.p4, self.p3, self.p2, self.p1),
            (self.p4, self.p3, self.p2, self.p1, self.p5),
            (self.p3, self.p2, self.p1, self.p5, self.p4),
            (self.p2, self.p1, self.p5, self.p4, self.p3),
            (self.p1, self.p5, self.p4, self.p3, self.p2),
        ]

__init__(p1, p2, p3, p4, p5)

Parameters:

Name Type Description Default
p1, p2, p3, p4, p5 Point

Vertices.

required
Source code in pyeuclid/formalization/relation.py
500
501
502
503
504
505
506
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point):
    """
    Args:
        p1, p2, p3, p4, p5 (Point): Vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5 = sort_cyclic_points(p1, p2, p3, p4, p5)

permutations()

Enumerate cyclic and reversed vertex orderings.

Returns:

Type Description

list[tuple[Point, Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
def permutations(self):
    """Enumerate cyclic and reversed vertex orderings.

    Returns:
        list[tuple[Point, Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4, self.p5),
        (self.p2, self.p3, self.p4, self.p5, self.p1),
        (self.p3, self.p4, self.p5, self.p1, self.p2),
        (self.p4, self.p5, self.p1, self.p2, self.p3),
        (self.p5, self.p1, self.p2, self.p3, self.p4),
        (self.p5, self.p4, self.p3, self.p2, self.p1),
        (self.p4, self.p3, self.p2, self.p1, self.p5),
        (self.p3, self.p2, self.p1, self.p5, self.p4),
        (self.p2, self.p1, self.p5, self.p4, self.p3),
        (self.p1, self.p5, self.p4, self.p3, self.p2),
    ]

Perpendicular

Bases: Relation

Segments (p1,p2) and (p3,p4) are perpendicular.

Source code in pyeuclid/formalization/relation.py
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
class Perpendicular(Relation):
    """Segments (p1,p2) and (p3,p4) are perpendicular."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1, p2 (Point): First segment endpoints.
            p3, p4 (Point): Second segment endpoints.
        """
        super().__init__()
        p1, p2 = sort_points(p1, p2)
        p3, p4 = sort_points(p3, p4)
        self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

    def permutations(self):
        """Enumerate symmetric endpoint permutations preserving segment groups.

        Returns:
            list[tuple[Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p1, self.p2, self.p4, self.p3),
            (self.p2, self.p1, self.p3, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
            (self.p3, self.p4, self.p1, self.p2),
            (self.p4, self.p3, self.p1, self.p2),
            (self.p3, self.p4, self.p2, self.p1),
            (self.p4, self.p3, self.p2, self.p1),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1, p2 Point

First segment endpoints.

required
p3, p4 Point

Second segment endpoints.

required
Source code in pyeuclid/formalization/relation.py
428
429
430
431
432
433
434
435
436
437
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1, p2 (Point): First segment endpoints.
        p3, p4 (Point): Second segment endpoints.
    """
    super().__init__()
    p1, p2 = sort_points(p1, p2)
    p3, p4 = sort_points(p3, p4)
    self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

permutations()

Enumerate symmetric endpoint permutations preserving segment groups.

Returns:

Type Description

list[tuple[Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
def permutations(self):
    """Enumerate symmetric endpoint permutations preserving segment groups.

    Returns:
        list[tuple[Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p1, self.p2, self.p4, self.p3),
        (self.p2, self.p1, self.p3, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
        (self.p3, self.p4, self.p1, self.p2),
        (self.p4, self.p3, self.p1, self.p2),
        (self.p3, self.p4, self.p2, self.p1),
        (self.p4, self.p3, self.p2, self.p1),
    ]

Point

Named geometric point used throughout the formalization.

Source code in pyeuclid/formalization/relation.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Point:
    """Named geometric point used throughout the formalization."""

    def __init__(self, name: str):
        """Create a point.

        Args:
            name (str): Identifier (underscores are not allowed).
        """
        self.name = name
        assert "_" not in name

    def __str__(self):
        return self.name

    def __eq__(self, other):
        return str(self) == str(other)

    def __repr__(self):
        return str(self)

    def __hash__(self):
        return hash(str(self))

__init__(name)

Create a point.

Parameters:

Name Type Description Default
name str

Identifier (underscores are not allowed).

required
Source code in pyeuclid/formalization/relation.py
16
17
18
19
20
21
22
23
def __init__(self, name: str):
    """Create a point.

    Args:
        name (str): Identifier (underscores are not allowed).
    """
    self.name = name
    assert "_" not in name

Quadrilateral

Bases: Relation

Points form a cyclically ordered quadrilateral.

Source code in pyeuclid/formalization/relation.py
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
class Quadrilateral(Relation):
    """Points form a cyclically ordered quadrilateral."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1, p2, p3, p4 (Point): Vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4 = sort_cyclic_points(p1, p2, p3, p4)

    def permutations(self):
        """Enumerate cyclic and reversed vertex orderings.

        Returns:
            list[tuple[Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p2, self.p3, self.p4, self.p1),
            (self.p3, self.p4, self.p1, self.p2),
            (self.p4, self.p1, self.p2, self.p3),
            (self.p4, self.p3, self.p2, self.p1),
            (self.p3, self.p2, self.p1, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
            (self.p1, self.p4, self.p3, self.p2),
        ]

    def definition(self):
        """Opposite sides must lie on opposite sides of diagonals.

        Returns:
            list[Relation]: Primitive opposite-side relations.
        """
        return [
            OppositeSide(self.p1, self.p3, self.p2, self.p4),
            OppositeSide(self.p2, self.p4, self.p1, self.p3),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1, p2, p3, p4 Point

Vertices.

required
Source code in pyeuclid/formalization/relation.py
460
461
462
463
464
465
466
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1, p2, p3, p4 (Point): Vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4 = sort_cyclic_points(p1, p2, p3, p4)

definition()

Opposite sides must lie on opposite sides of diagonals.

Returns:

Type Description

list[Relation]: Primitive opposite-side relations.

Source code in pyeuclid/formalization/relation.py
485
486
487
488
489
490
491
492
493
494
def definition(self):
    """Opposite sides must lie on opposite sides of diagonals.

    Returns:
        list[Relation]: Primitive opposite-side relations.
    """
    return [
        OppositeSide(self.p1, self.p3, self.p2, self.p4),
        OppositeSide(self.p2, self.p4, self.p1, self.p3),
    ]

permutations()

Enumerate cyclic and reversed vertex orderings.

Returns:

Type Description

list[tuple[Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
def permutations(self):
    """Enumerate cyclic and reversed vertex orderings.

    Returns:
        list[tuple[Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p2, self.p3, self.p4, self.p1),
        (self.p3, self.p4, self.p1, self.p2),
        (self.p4, self.p1, self.p2, self.p3),
        (self.p4, self.p3, self.p2, self.p1),
        (self.p3, self.p2, self.p1, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
        (self.p1, self.p4, self.p3, self.p2),
    ]

Relation

Base class for logical relations over points.

Source code in pyeuclid/formalization/relation.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class Relation:
    """Base class for logical relations over points."""

    def __init__(self):
        self.negated = False

    def negate(self):
        """Toggle negation flag in-place."""
        self.negated = not self.negated

    def get_points(self):
        """Return all point instances contained in the relation."""
        points = []
        for v in vars(self).values():
            if isinstance(v, Point):
                points.append(v)
            elif isinstance(v, list):
                for p in v:
                    if isinstance(p, Point):
                        points.append(p)
        return points

    def __str__(self):
        """Readable representation, prefixed with Not() when negated."""
        class_name = self.__class__.__name__
        points = self.get_points()
        args_name = ",".join([p.name for p in points])

        if not self.negated:
            return f"{class_name}({args_name})"
        else:
            return f"Not({class_name}({args_name}))"

    def __repr__(self):
        return str(self)

    def __eq__(self, other):
        return str(self) == str(other)

    def __hash__(self):
        return hash(str(self))

__str__()

Readable representation, prefixed with Not() when negated.

Source code in pyeuclid/formalization/relation.py
60
61
62
63
64
65
66
67
68
69
def __str__(self):
    """Readable representation, prefixed with Not() when negated."""
    class_name = self.__class__.__name__
    points = self.get_points()
    args_name = ",".join([p.name for p in points])

    if not self.negated:
        return f"{class_name}({args_name})"
    else:
        return f"Not({class_name}({args_name}))"

get_points()

Return all point instances contained in the relation.

Source code in pyeuclid/formalization/relation.py
48
49
50
51
52
53
54
55
56
57
58
def get_points(self):
    """Return all point instances contained in the relation."""
    points = []
    for v in vars(self).values():
        if isinstance(v, Point):
            points.append(v)
        elif isinstance(v, list):
            for p in v:
                if isinstance(p, Point):
                    points.append(p)
    return points

negate()

Toggle negation flag in-place.

Source code in pyeuclid/formalization/relation.py
44
45
46
def negate(self):
    """Toggle negation flag in-place."""
    self.negated = not self.negated

SameSide

Bases: Relation

Points p1,p2 lie on the same side of the line (p3,p4).

Source code in pyeuclid/formalization/relation.py
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
class SameSide(Relation):
    """Points p1,p2 lie on the same side of the line (p3,p4)."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1 (Point): First query point.
            p2 (Point): Second query point.
            p3 (Point): Line endpoint 1.
            p4 (Point): Line endpoint 2.
        """
        super().__init__()
        self.p1, self.p2 = sort_points(p1, p2)
        self.p3, self.p4 = sort_points(p3, p4)

    def permutations(self):
        """Enumerate symmetric orderings for same-side tests."""
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p1, self.p2, self.p4, self.p3),
            (self.p2, self.p1, self.p3, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1 Point

First query point.

required
p2 Point

Second query point.

required
p3 Point

Line endpoint 1.

required
p4 Point

Line endpoint 2.

required
Source code in pyeuclid/formalization/relation.py
195
196
197
198
199
200
201
202
203
204
205
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1 (Point): First query point.
        p2 (Point): Second query point.
        p3 (Point): Line endpoint 1.
        p4 (Point): Line endpoint 2.
    """
    super().__init__()
    self.p1, self.p2 = sort_points(p1, p2)
    self.p3, self.p4 = sort_points(p3, p4)

permutations()

Enumerate symmetric orderings for same-side tests.

Source code in pyeuclid/formalization/relation.py
207
208
209
210
211
212
213
214
def permutations(self):
    """Enumerate symmetric orderings for same-side tests."""
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p1, self.p2, self.p4, self.p3),
        (self.p2, self.p1, self.p3, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
    ]

Similar

Bases: Relation

Triangles (p1,p2,p3) and (p4,p5,p6) are similar.

Source code in pyeuclid/formalization/relation.py
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
class Similar(Relation):
    """Triangles (p1,p2,p3) and (p4,p5,p6) are similar."""

    def __init__(
        self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
    ):
        """
        Args:
            p1, p2, p3 (Point): First triangle vertices.
            p4, p5, p6 (Point): Second triangle vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

    def definition(self):
        """Similarity expressed via length ratios and non-collinearity.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) / Length(self.p4, self.p5)
            - Length(self.p2, self.p3) / Length(self.p5, self.p6),
            Length(self.p1, self.p2) / Length(self.p4, self.p5)
            - Length(self.p3, self.p1) / Length(self.p6, self.p4),
            NotCollinear(self.p1, self.p2, self.p3),
        ]

__init__(p1, p2, p3, p4, p5, p6)

Parameters:

Name Type Description Default
p1, p2, p3 Point

First triangle vertices.

required
p4, p5, p6 Point

Second triangle vertices.

required
Source code in pyeuclid/formalization/relation.py
347
348
349
350
351
352
353
354
355
356
def __init__(
    self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
):
    """
    Args:
        p1, p2, p3 (Point): First triangle vertices.
        p4, p5, p6 (Point): Second triangle vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

definition()

Similarity expressed via length ratios and non-collinearity.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
358
359
360
361
362
363
364
365
366
367
368
369
370
def definition(self):
    """Similarity expressed via length ratios and non-collinearity.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) / Length(self.p4, self.p5)
        - Length(self.p2, self.p3) / Length(self.p5, self.p6),
        Length(self.p1, self.p2) / Length(self.p4, self.p5)
        - Length(self.p3, self.p1) / Length(self.p6, self.p4),
        NotCollinear(self.p1, self.p2, self.p3),
    ]

Similar4P

Bases: Relation

Two quadrilaterals are similar.

Source code in pyeuclid/formalization/relation.py
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
class Similar4P(Relation):
    """Two quadrilaterals are similar."""

    def __init__(
        self,
        p1: Point,
        p2: Point,
        p3: Point,
        p4: Point,
        p5: Point,
        p6: Point,
        p7: Point,
        p8: Point,
    ):
        """
        Args:
            p1..p4 (Point): First quadrilateral.
            p5..p8 (Point): Second quadrilateral.
        """
        super().__init__()
        point_map_12 = {p1: p5, p2: p6, p3: p7, p4: p8}
        point_map_21 = {p5: p1, p6: p2, p7: p3, p8: p4}

        sorted_1 = sort_cyclic_points(p1, p2, p3, p4)
        sorted_2 = sort_cyclic_points(p5, p6, p7, p8)
        if compare_names(sorted_1, sorted_2) == 0:
            self.p1, self.p2, self.p3, self.p4 = sorted_1
            self.p5,self.p6,self.p7,self.p8 = point_map_12[self.p1], point_map_12[
                self.p2], point_map_12[self.p3], point_map_12[self.p4]
        else:
            self.p1, self.p2, self.p3, self.p4 = sorted_2
            self.p5, self.p6, self.p7, self.p8 = point_map_21[self.p1], point_map_21[
                self.p2], point_map_21[self.p3], point_map_21[self.p4]

    def definition(self):
        """Similarity expressed via side ratios and angle equalities.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) / Length(self.p5, self.p6)
            - Length(self.p2, self.p3) / Length(self.p6, self.p7),
            Length(self.p1, self.p2) / Length(self.p5, self.p6)
            - Length(self.p3, self.p4) / Length(self.p7, self.p8),
            Length(self.p1, self.p2) / Length(self.p5, self.p6)
            - Length(self.p4, self.p1) / Length(self.p8, self.p5),
            Angle(self.p1, self.p2, self.p3) - Angle(self.p5, self.p6, self.p7),
            Angle(self.p2, self.p3, self.p4) - Angle(self.p6, self.p7, self.p8),
            Angle(self.p3, self.p4, self.p1) - Angle(self.p7, self.p8, self.p5),
            Angle(self.p4, self.p1, self.p2) - Angle(self.p8, self.p5, self.p6),
        ]

__init__(p1, p2, p3, p4, p5, p6, p7, p8)

Parameters:

Name Type Description Default
p1..p4 Point

First quadrilateral.

required
p5..p8 Point

Second quadrilateral.

required
Source code in pyeuclid/formalization/relation.py
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
def __init__(
    self,
    p1: Point,
    p2: Point,
    p3: Point,
    p4: Point,
    p5: Point,
    p6: Point,
    p7: Point,
    p8: Point,
):
    """
    Args:
        p1..p4 (Point): First quadrilateral.
        p5..p8 (Point): Second quadrilateral.
    """
    super().__init__()
    point_map_12 = {p1: p5, p2: p6, p3: p7, p4: p8}
    point_map_21 = {p5: p1, p6: p2, p7: p3, p8: p4}

    sorted_1 = sort_cyclic_points(p1, p2, p3, p4)
    sorted_2 = sort_cyclic_points(p5, p6, p7, p8)
    if compare_names(sorted_1, sorted_2) == 0:
        self.p1, self.p2, self.p3, self.p4 = sorted_1
        self.p5,self.p6,self.p7,self.p8 = point_map_12[self.p1], point_map_12[
            self.p2], point_map_12[self.p3], point_map_12[self.p4]
    else:
        self.p1, self.p2, self.p3, self.p4 = sorted_2
        self.p5, self.p6, self.p7, self.p8 = point_map_21[self.p1], point_map_21[
            self.p2], point_map_21[self.p3], point_map_21[self.p4]

definition()

Similarity expressed via side ratios and angle equalities.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
def definition(self):
    """Similarity expressed via side ratios and angle equalities.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) / Length(self.p5, self.p6)
        - Length(self.p2, self.p3) / Length(self.p6, self.p7),
        Length(self.p1, self.p2) / Length(self.p5, self.p6)
        - Length(self.p3, self.p4) / Length(self.p7, self.p8),
        Length(self.p1, self.p2) / Length(self.p5, self.p6)
        - Length(self.p4, self.p1) / Length(self.p8, self.p5),
        Angle(self.p1, self.p2, self.p3) - Angle(self.p5, self.p6, self.p7),
        Angle(self.p2, self.p3, self.p4) - Angle(self.p6, self.p7, self.p8),
        Angle(self.p3, self.p4, self.p1) - Angle(self.p7, self.p8, self.p5),
        Angle(self.p4, self.p1, self.p2) - Angle(self.p8, self.p5, self.p6),
    ]

Similar5P

Bases: Relation

Two pentagons are similar.

Source code in pyeuclid/formalization/relation.py
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
class Similar5P(Relation):
    """Two pentagons are similar."""

    def __init__(
        self,
        p1: Point,
        p2: Point,
        p3: Point,
        p4: Point,
        p5: Point,
        p6: Point,
        p7: Point,
        p8: Point,
        p9: Point,
        p10: Point,
    ):
        """
        Args:
            p1..p5 (Point): First pentagon.
            p6..p10 (Point): Second pentagon.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5, self.p6, self.p7, self.p8, self.p9, self.p10 = p1, p2, p3, p4, p5, p6, p7, p8, p9, p10

    def definition(self):
        """Similarity expressed via consecutive side ratios and angle equalities.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) / Length(self.p6, self.p7)
            - Length(self.p2, self.p3) / Length(self.p7, self.p8),
            Length(self.p2, self.p3) / Length(self.p7, self.p8)
            - Length(self.p3, self.p4) / Length(self.p8, self.p9),
            Length(self.p3, self.p4) / Length(self.p8, self.p9)
            - Length(self.p4, self.p5) / Length(self.p9, self.p10),
            Length(self.p4, self.p5) / Length(self.p9, self.p10)
            - Length(self.p5, self.p1) / Length(self.p10, self.p6),
            Length(self.p5, self.p1) / Length(self.p10, self.p6)
            - Length(self.p1, self.p2) / Length(self.p6, self.p7),
            Angle(self.p1, self.p2, self.p3) - Angle(self.p6, self.p7, self.p8),
            Angle(self.p2, self.p3, self.p4) - Angle(self.p7, self.p8, self.p9),
            Angle(self.p3, self.p4, self.p5) - Angle(self.p8, self.p9, self.p10),
            Angle(self.p4, self.p5, self.p1) - Angle(self.p9, self.p10, self.p6),
            Angle(self.p5, self.p1, self.p2) - Angle(self.p10, self.p6, self.p7),
        ]

__init__(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10)

Parameters:

Name Type Description Default
p1..p5 Point

First pentagon.

required
p6..p10 Point

Second pentagon.

required
Source code in pyeuclid/formalization/relation.py
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
def __init__(
    self,
    p1: Point,
    p2: Point,
    p3: Point,
    p4: Point,
    p5: Point,
    p6: Point,
    p7: Point,
    p8: Point,
    p9: Point,
    p10: Point,
):
    """
    Args:
        p1..p5 (Point): First pentagon.
        p6..p10 (Point): Second pentagon.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5, self.p6, self.p7, self.p8, self.p9, self.p10 = p1, p2, p3, p4, p5, p6, p7, p8, p9, p10

definition()

Similarity expressed via consecutive side ratios and angle equalities.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
def definition(self):
    """Similarity expressed via consecutive side ratios and angle equalities.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) / Length(self.p6, self.p7)
        - Length(self.p2, self.p3) / Length(self.p7, self.p8),
        Length(self.p2, self.p3) / Length(self.p7, self.p8)
        - Length(self.p3, self.p4) / Length(self.p8, self.p9),
        Length(self.p3, self.p4) / Length(self.p8, self.p9)
        - Length(self.p4, self.p5) / Length(self.p9, self.p10),
        Length(self.p4, self.p5) / Length(self.p9, self.p10)
        - Length(self.p5, self.p1) / Length(self.p10, self.p6),
        Length(self.p5, self.p1) / Length(self.p10, self.p6)
        - Length(self.p1, self.p2) / Length(self.p6, self.p7),
        Angle(self.p1, self.p2, self.p3) - Angle(self.p6, self.p7, self.p8),
        Angle(self.p2, self.p3, self.p4) - Angle(self.p7, self.p8, self.p9),
        Angle(self.p3, self.p4, self.p5) - Angle(self.p8, self.p9, self.p10),
        Angle(self.p4, self.p5, self.p1) - Angle(self.p9, self.p10, self.p6),
        Angle(self.p5, self.p1, self.p2) - Angle(self.p10, self.p6, self.p7),
    ]

construct_angle_bisector

Bases: ConstructionRule

Construct the bisector point X of angle ABC.

Source code in pyeuclid/formalization/construction_rule.py
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
@register("AG")
class construct_angle_bisector(ConstructionRule):
    """Construct the bisector point X of angle ABC."""
    def __init__(self, x, a, b, c):
        self.x, self.a, self.b, self.c = x, a, b, c

    def arguments(self):
        return [self.a, self.b, self.c]

    def constructed_points(self):
        return [self.x]

    def conditions(self):
        return [NotCollinear(self.a, self.b, self.c)]

    def conclusions(self):
        return [Angle(self.a, self.b, self.x) - Angle(self.x, self.b, self.c)]

construct_angle_mirror

Bases: ConstructionRule

Construct point X as the mirror of BA across BC.

Source code in pyeuclid/formalization/construction_rule.py
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
@register("AG")
class construct_angle_mirror(ConstructionRule):
    """Construct point X as the mirror of BA across BC."""
    def __init__(self, x, a, b, c):
        self.x, self.a, self.b, self.c = x, a, b, c

    def arguments(self):
        return [self.a, self.b, self.c]

    def constructed_points(self):
        return [self.x]

    def conditions(self):
        return [NotCollinear(self.a, self.b, self.c)]

    def conclusions(self):
        return [Angle(self.a, self.b, self.c) - Angle(self.c, self.b, self.x)]

construct_circle

Bases: ConstructionRule

Construct circle center X equidistant from A, B, C.

Source code in pyeuclid/formalization/construction_rule.py
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
@register("AG")
class construct_circle(ConstructionRule):
    """Construct circle center X equidistant from A, B, C."""
    def __init__(self, x, a, b, c):
        self.x, self.a, self.b, self.c = x, a, b, c

    def arguments(self):
        return [self.a, self.b, self.c]

    def constructed_points(self):
        return [self.x]

    def conditions(self):
        return [NotCollinear(self.a, self.b, self.c)]

    def conclusions(self):
        return [
            Length(self.x, self.a) - Length(self.x, self.b),
            Length(self.x, self.b) - Length(self.x, self.c),
        ]

construct_circumcenter

Bases: ConstructionRule

Construct circumcenter X of triangle ABC.

Source code in pyeuclid/formalization/construction_rule.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
@register("AG")
class construct_circumcenter(ConstructionRule):
    """Construct circumcenter X of triangle ABC."""
    def __init__(self, x, a, b, c):
        self.x, self.a, self.b, self.c = x, a, b, c

    def arguments(self):
        return [self.a, self.b, self.c]

    def constructed_points(self):
        return [self.x]

    def conditions(self):
        return [NotCollinear(self.a, self.b, self.c)]

    def conclusions(self):
        return [
            Length(self.x, self.a) - Length(self.x, self.b),
            Length(self.x, self.b) - Length(self.x, self.c),
        ]

construct_eq_quadrangle

Bases: ConstructionRule

Construct quadrilateral ABCD with equal diagonals.

Source code in pyeuclid/formalization/construction_rule.py
141
142
143
144
145
146
147
148
149
150
151
152
153
@register("AG")
class construct_eq_quadrangle(ConstructionRule):
    """Construct quadrilateral ABCD with equal diagonals."""
    def __init__(self, a, b, c, d):
        self.a, self.b, self.c, self.d = a, b, c, d

    def constructed_points(self):
        return [self.a, self.b, self.c, self.d]

    def conclusions(self):
        return [
            Length(self.a, self.d) - Length(self.b, self.c)
        ]

construct_eq_trapezoid

Bases: ConstructionRule

Construct isosceles trapezoid ABCD (AB ∥ CD).

Source code in pyeuclid/formalization/construction_rule.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
@register("AG")
class construct_eq_trapezoid(ConstructionRule):
    """Construct isosceles trapezoid ABCD (AB ∥ CD)."""
    def __init__(self, a, b, c, d):
        self.a, self.b, self.c, self.d = a, b, c, d

    def constructed_points(self):
        return [self.a, self.b, self.c, self.d]

    def conclusions(self):
        return [
            Length(self.a, self.d) - Length(self.b, self.c),
            Parallel(self.a, self.b, self.c, self.d),
            Angle(self.d, self.a, self.b) - Angle(self.a, self.b, self.c),
            Angle(self.b, self.c, self.d) - Angle(self.c, self.d, self.a),
        ]

construct_eq_triangle

Bases: ConstructionRule

Construct equilateral triangle with vertex X and base BC.

Source code in pyeuclid/formalization/construction_rule.py
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
@register("AG")
class construct_eq_triangle(ConstructionRule):
    """Construct equilateral triangle with vertex X and base BC."""
    def __init__(self, x, b, c):
        self.x, self.b, self.c = x, b, c

    def arguments(self):
        return [self.b, self.c]

    def constructed_points(self):
        return [self.x]

    def conditions(self):
        return [Different(self.b, self.c)]

    def conclusions(self):
        return [
            Length(self.x, self.b) - Length(self.b, self.c),
            Length(self.b, self.c) - Length(self.c, self.x),
            Angle(self.x, self.b, self.c) - Angle(self.b, self.c, self.x),
            Angle(self.c, self.x, self.b) - Angle(self.x, self.b, self.c),
        ]

construct_eqangle2

Bases: ConstructionRule

Construct X so that angle ABX equals angle XCB.

Source code in pyeuclid/formalization/construction_rule.py
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
@register("AG")
class construct_eqangle2(ConstructionRule):
    """Construct X so that angle ABX equals angle XCB."""
    def __init__(self, x, a, b, c):
        self.x, self.a, self.b, self.c = x, a, b, c

    def arguments(self):
        return [self.a, self.b, self.c]

    def constructed_points(self):
        return [self.x]

    def conditions(self):
        return [NotCollinear(self.a, self.b, self.c)]

    def conclusions(self):
        return [Angle(self.b, self.a, self.x) - Angle(self.x, self.c, self.b)]

register

Decorator that registers a construction rule into labeled sets.

Source code in pyeuclid/formalization/construction_rule.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class register:
    """Decorator that registers a construction rule into labeled sets."""

    def __init__(self, *annotations):
        self.annotations = annotations

    def __call__(self, cls):
        for item in self.annotations:
            if not item in construction_rule_sets:
                construction_rule_sets[item] = [cls]
            else:
                construction_rule_sets[item].append(cls)

        def expanded_conditions(self):
            return expand_definition(self._conditions())

        cls._conditions = cls.conditions
        cls.conditions = expanded_conditions
        return cls

Angle(p1, p2, p3)

Symbolic angle at p2.

Returns:

Type Description

sympy.Symbol: Non-negative angle symbol.

Source code in pyeuclid/formalization/relation.py
108
109
110
111
112
113
114
115
def Angle(p1: Point, p2: Point, p3: Point):
    """Symbolic angle at p2.

    Returns:
        sympy.Symbol: Non-negative angle symbol.
    """
    p1, p3 = sort_points(p1, p3)
    return Symbol(f"Angle_{p1}_{p2}_{p3}", non_negative=True)

Area(*ps)

Symbolic polygonal area over an ordered point cycle.

Returns:

Type Description

sympy.Symbol: Positive area symbol.

Source code in pyeuclid/formalization/relation.py
128
129
130
131
132
133
134
135
def Area(*ps: list[Point]):
    """Symbolic polygonal area over an ordered point cycle.

    Returns:
        sympy.Symbol: Positive area symbol.
    """
    ps = sort_cyclic_points(*ps)
    return Symbol("_".join(["Area"] + [str(item) for item in ps]), positive=True)

Length(p1, p2)

Symbolic length between two points.

Returns:

Type Description

sympy.Symbol: Positive length symbol.

Source code in pyeuclid/formalization/relation.py
118
119
120
121
122
123
124
125
def Length(p1: Point, p2: Point):
    """Symbolic length between two points.

    Returns:
        sympy.Symbol: Positive length symbol.
    """
    p1, p2 = sort_points(p1, p2)
    return Symbol(f"Length_{str(p1)}_{str(p2)}", positive=True)

Not(p)

Return a negated shallow copy of a relation.

Source code in pyeuclid/formalization/relation.py
101
102
103
104
105
def Not(p: Relation) -> Relation:
    """Return a negated shallow copy of a relation."""
    other = copy.copy(p)
    other.negate()
    return other

Variable(name)

Free symbolic variable placeholder.

Returns:

Type Description

sympy.Symbol: Dimensionless variable symbol.

Source code in pyeuclid/formalization/relation.py
138
139
140
141
142
143
144
def Variable(name: str):
    """Free symbolic variable placeholder.

    Returns:
        sympy.Symbol: Dimensionless variable symbol.
    """
    return Symbol(f"Variable_{name}")

get_constructions_list_from_text(text)

Parse the constructions section of a text instance into rule objects.

Parameters:

Name Type Description Default
text str

Full benchmark line containing constructions and goal separated by ' ? '.

required

Returns:

Type Description

list[list[ConstructionRule]]: Nested list of construction batches.

Source code in pyeuclid/formalization/translation.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def get_constructions_list_from_text(text):
    """Parse the constructions section of a text instance into rule objects.

    Args:
        text (str): Full benchmark line containing constructions and goal separated by ' ? '.

    Returns:
        list[list[ConstructionRule]]: Nested list of construction batches.
    """
    parts = text.split(' ? ')
    constructions_text_list = parts[0].split('; ')
    constructions_list = []

    for constructions_text in constructions_text_list:
        constructions_text = constructions_text.split(' = ')[1]
        construction_text_list = constructions_text.split(', ')
        constructions = []
        for construction_text in construction_text_list:
            construction_text = construction_text.split(' ')
            rule_name = construction_text[0]
            arg_names = [name.replace('_', '') for name in construction_text[1:]]
            rule = globals()['construct_'+rule_name]
            args = [float(arg_name) if is_float(arg_name) else Point(arg_name) for arg_name in arg_names]
            construction = rule(*args)
            constructions.append(construction)
        constructions_list.append(constructions)

    return constructions_list

get_goal_from_text(text)

Parse the goal portion of a text instance into a Relation or expression.

Parameters:

Name Type Description Default
text str

Full benchmark line containing constructions and goal.

required

Returns:

Type Description

Relation | sympy.Expr | tuple[Relation, Relation] | None: Parsed goal or None.

Source code in pyeuclid/formalization/translation.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
def get_goal_from_text(text):
    """Parse the goal portion of a text instance into a Relation or expression.

    Args:
        text (str): Full benchmark line containing constructions and goal.

    Returns:
        Relation | sympy.Expr | tuple[Relation, Relation] | None: Parsed goal or None.
    """
    parts = text.split(' ? ')
    goal_text = parts[1] if len(parts) > 1 else None
    goal = None
    if goal_text:
        goal_text = goal_text.split(' ')
        goal_name = goal_text[0]
        arg_names = [name.replace('_', '') for name in goal_text[1:]]
        args = [Point(arg_name) for arg_name in arg_names]
        if goal_name == 'cong':
            goal = Length(*args[:2]) - Length(*args[2:])
        elif goal_name == 'cyclic':
            goal = Concyclic(*args)
        elif goal_name == 'coll':
            goal = Collinear(*args)
        elif goal_name == 'perp':
            goal = Perpendicular(*args)
        elif goal_name == 'para':
            goal = Parallel(*args)
        elif goal_name == 'eqratio':
            goal = Length(*args[:2])/Length(*args[2:4]) - Length(*args[4:6])/Length(*args[6:8])
        elif goal_name == 'eqangle':
            def extract_angle(points):
                count = Counter(points)
                repeating = next(p for p, c in count.items() if c == 2)
                singles = [p for p, c in count.items() if c == 1]
                return singles[0], repeating, singles[1]
            angle1 = Angle(*extract_angle(args[:4]))
            angle2 = Angle(*extract_angle(args[4:]))
            # The goal may involve either equal angles or supplementary angles
            goal = (angle1 - angle2, angle1 + angle2 - pi)
        elif goal_name == 'midp':
            goal = Midpoint(*args)
        elif goal_name == 'simtri':
            goal = Similar(*args)
        elif goal_name == 'contri':
            goal = Congruent(*args)

    return goal

parse_texts_from_file(file_name)

Load every other line from a benchmark file as a problem description.

Parameters:

Name Type Description Default
file_name str

Path to benchmark text file.

required

Returns:

Type Description

list[str]: Problem description strings.

Source code in pyeuclid/formalization/translation.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
def parse_texts_from_file(file_name):
    """Load every other line from a benchmark file as a problem description.

    Args:
        file_name (str): Path to benchmark text file.

    Returns:
        list[str]: Problem description strings.
    """
    with open(file_name, "r") as f:
        lines = f.readlines()

    texts = [lines[i].strip() for i in range(1, len(lines), 2)]
    return texts

Numerical diagram construction and validation for geometric problems.

Between

Bases: Relation

p1 lies between p2 and p3 (on the same line).

Source code in pyeuclid/formalization/relation.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
class Between(Relation):
    """p1 lies between p2 and p3 (on the same line)."""

    def __init__(self, p1: Point, p2: Point, p3: Point):
        """
        p1 is between p2 and p3.

        Args:
            p1 (Point): Middle point.
            p2 (Point): Endpoint 1.
            p3 (Point): Endpoint 2.
        """
        super().__init__()
        p2, p3 = sort_points(p2, p3)
        self.p1, self.p2, self.p3 = p1, p2, p3

    def permutations(self):
        """Enumerate equivalent point orderings for the between relation.

        Returns:
            list[tuple[Point, Point, Point]]: Permitted permutations.
        """
        return [(self.p1, self.p2, self.p3), (self.p1, self.p3, self.p2)]

__init__(p1, p2, p3)

p1 is between p2 and p3.

Parameters:

Name Type Description Default
p1 Point

Middle point.

required
p2 Point

Endpoint 1.

required
p3 Point

Endpoint 2.

required
Source code in pyeuclid/formalization/relation.py
170
171
172
173
174
175
176
177
178
179
180
181
def __init__(self, p1: Point, p2: Point, p3: Point):
    """
    p1 is between p2 and p3.

    Args:
        p1 (Point): Middle point.
        p2 (Point): Endpoint 1.
        p3 (Point): Endpoint 2.
    """
    super().__init__()
    p2, p3 = sort_points(p2, p3)
    self.p1, self.p2, self.p3 = p1, p2, p3

permutations()

Enumerate equivalent point orderings for the between relation.

Returns:

Type Description

list[tuple[Point, Point, Point]]: Permitted permutations.

Source code in pyeuclid/formalization/relation.py
183
184
185
186
187
188
189
def permutations(self):
    """Enumerate equivalent point orderings for the between relation.

    Returns:
        list[tuple[Point, Point, Point]]: Permitted permutations.
    """
    return [(self.p1, self.p2, self.p3), (self.p1, self.p3, self.p2)]

Circle

Numerical circle.

Source code in pyeuclid/formalization/numericals.py
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
class Circle:
    """Numerical circle."""

    def __init__(
        self,
        center: Optional[Point] = None,
        radius: Optional[float] = None,
        p1: Optional[Point] = None,
        p2: Optional[Point] = None,
        p3: Optional[Point] = None,
    ):
        if not center:
            l12 = perpendicular_bisector(p1, p2)
            l23 = perpendicular_bisector(p2, p3)
            center = line_line_intersection(l12, l23)

        if not radius:
            p = p1 or p2 or p3
            radius = center.distance(p)

        self.center = center
        self.radius = radius

    def intersect(self, obj: Union[Line, Circle]) -> tuple[Point, ...]:
        if isinstance(obj, Line):
            return obj.intersect(self)

        if isinstance(obj, Circle):
            return circle_circle_intersection(self, obj)

    def sample_within(self, points: list[Point], n: int = 5) -> list[Point]:
        """Sample a point within the boundary of points."""
        result = None
        best = -1.0
        for _ in range(n):
            ang = unif(0.0, 2.0) * np.pi
            x = self.center + Point(np.cos(ang), np.sin(ang)) * self.radius
            mind = min([x.distance(p) for p in points])
            if mind > best:
                best = mind
                result = x
        return [result]

sample_within(points, n=5)

Sample a point within the boundary of points.

Source code in pyeuclid/formalization/numericals.py
457
458
459
460
461
462
463
464
465
466
467
468
def sample_within(self, points: list[Point], n: int = 5) -> list[Point]:
    """Sample a point within the boundary of points."""
    result = None
    best = -1.0
    for _ in range(n):
        ang = unif(0.0, 2.0) * np.pi
        x = self.center + Point(np.cos(ang), np.sin(ang)) * self.radius
        mind = min([x.distance(p) for p in points])
        if mind > best:
            best = mind
            result = x
    return [result]

Collinear

Bases: Relation

Points p1,p2,p3 are collinear.

Source code in pyeuclid/formalization/relation.py
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
class Collinear(Relation):
    """Points p1,p2,p3 are collinear."""

    def __init__(self, p1, p2, p3):
        """
        Args:
            p1 (Point)
            p2 (Point)
            p3 (Point)
        """
        super().__init__()
        self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

    def permutations(self):
        """Enumerate collinearity argument permutations.

        Returns:
            itertools.permutations: All orderings of the three points.
        """
        return itertools.permutations([self.p1, self.p2, self.p3])

__init__(p1, p2, p3)

Source code in pyeuclid/formalization/relation.py
248
249
250
251
252
253
254
255
256
def __init__(self, p1, p2, p3):
    """
    Args:
        p1 (Point)
        p2 (Point)
        p3 (Point)
    """
    super().__init__()
    self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

permutations()

Enumerate collinearity argument permutations.

Returns:

Type Description

itertools.permutations: All orderings of the three points.

Source code in pyeuclid/formalization/relation.py
258
259
260
261
262
263
264
def permutations(self):
    """Enumerate collinearity argument permutations.

    Returns:
        itertools.permutations: All orderings of the three points.
    """
    return itertools.permutations([self.p1, self.p2, self.p3])

Concyclic

Bases: Relation

All given points lie on the same circle.

Source code in pyeuclid/formalization/relation.py
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
class Concyclic(Relation):
    """All given points lie on the same circle."""

    def __init__(self, *ps: Point):
        """
        Args:
            *ps (Point): Points to be tested for concyclicity.
        """
        super().__init__()
        self.ps = list(sort_points(*ps))

    def permutations(self):
        """Enumerate symmetric permutations of the point set.

        Returns:
            itertools.permutations: All orderings of the points.
        """
        return itertools.permutations(self.ps)

__init__(*ps)

Parameters:

Name Type Description Default
*ps Point

Points to be tested for concyclicity.

()
Source code in pyeuclid/formalization/relation.py
376
377
378
379
380
381
382
def __init__(self, *ps: Point):
    """
    Args:
        *ps (Point): Points to be tested for concyclicity.
    """
    super().__init__()
    self.ps = list(sort_points(*ps))

permutations()

Enumerate symmetric permutations of the point set.

Returns:

Type Description

itertools.permutations: All orderings of the points.

Source code in pyeuclid/formalization/relation.py
384
385
386
387
388
389
390
def permutations(self):
    """Enumerate symmetric permutations of the point set.

    Returns:
        itertools.permutations: All orderings of the points.
    """
    return itertools.permutations(self.ps)

Congruent

Bases: Relation

Triangles (p1,p2,p3) and (p4,p5,p6) are congruent.

Source code in pyeuclid/formalization/relation.py
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
class Congruent(Relation):
    """Triangles (p1,p2,p3) and (p4,p5,p6) are congruent."""

    def __init__(
        self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
    ):
        """
        Args:
            p1, p2, p3 (Point): First triangle vertices.
            p4, p5, p6 (Point): Second triangle vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

    def definition(self):
        """Congruence expressed as equal side lengths and non-collinearity.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) - Length(self.p4, self.p5),
            Length(self.p2, self.p3) - Length(self.p5, self.p6),
            Length(self.p1, self.p3) - Length(self.p4, self.p6),
            NotCollinear(self.p1, self.p2, self.p3),
        ]

__init__(p1, p2, p3, p4, p5, p6)

Parameters:

Name Type Description Default
p1, p2, p3 Point

First triangle vertices.

required
p4, p5, p6 Point

Second triangle vertices.

required
Source code in pyeuclid/formalization/relation.py
319
320
321
322
323
324
325
326
327
328
def __init__(
    self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
):
    """
    Args:
        p1, p2, p3 (Point): First triangle vertices.
        p4, p5, p6 (Point): Second triangle vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

definition()

Congruence expressed as equal side lengths and non-collinearity.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
330
331
332
333
334
335
336
337
338
339
340
341
def definition(self):
    """Congruence expressed as equal side lengths and non-collinearity.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) - Length(self.p4, self.p5),
        Length(self.p2, self.p3) - Length(self.p5, self.p6),
        Length(self.p1, self.p3) - Length(self.p4, self.p6),
        NotCollinear(self.p1, self.p2, self.p3),
    ]

ConstructionRule

Base class for geometric construction rules.

Source code in pyeuclid/formalization/construction_rule.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class ConstructionRule:
    """Base class for geometric construction rules."""

    def __init__(self):
        """Initialize an empty construction rule."""
        pass

    def arguments(self):
        """Return the input entities required for the construction."""
        return []

    def constructed_points(self):
        """Return the points constructed by this rule."""
        return []

    def conditions(self):
        """Return prerequisite relations for the construction to be valid."""
        return []

    def conclusions(self):
        """Return relations implied after applying the construction."""
        return []

    def __str__(self):
        class_name = self.__class__.__name__
        attributes = ",".join(str(value) for _, value in vars(self).items())
        return f"{class_name}({attributes})"

__init__()

Initialize an empty construction rule.

Source code in pyeuclid/formalization/construction_rule.py
12
13
14
def __init__(self):
    """Initialize an empty construction rule."""
    pass

arguments()

Return the input entities required for the construction.

Source code in pyeuclid/formalization/construction_rule.py
16
17
18
def arguments(self):
    """Return the input entities required for the construction."""
    return []

conclusions()

Return relations implied after applying the construction.

Source code in pyeuclid/formalization/construction_rule.py
28
29
30
def conclusions(self):
    """Return relations implied after applying the construction."""
    return []

conditions()

Return prerequisite relations for the construction to be valid.

Source code in pyeuclid/formalization/construction_rule.py
24
25
26
def conditions(self):
    """Return prerequisite relations for the construction to be valid."""
    return []

constructed_points()

Return the points constructed by this rule.

Source code in pyeuclid/formalization/construction_rule.py
20
21
22
def constructed_points(self):
    """Return the points constructed by this rule."""
    return []

Diagram

Source code in pyeuclid/formalization/diagram.py
  22
  23
  24
  25
  26
  27
  28
  29
  30
  31
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
class Diagram:    
    def __new__(cls, constructions_list:list[list[ConstructionRule]]=None, save_path=None, cache_folder=os.path.join(ROOT_DIR, 'cache'), resample=False):
        """Load from cache if available, otherwise construct a new diagram instance."""
        if not resample and cache_folder is not None:
            if not os.path.exists(cache_folder):
                os.makedirs(cache_folder)

            if constructions_list is not None:
                file_name = f"{hash_constructions_list(constructions_list)}.pkl"
                file_path = os.path.join(cache_folder, file_name)
                try:
                    if os.path.exists(file_path):
                        with open(file_path, 'rb') as f:
                            instance = pickle.load(f)
                            instance.save_path = save_path
                            instance.save_diagram()
                            return instance
                except:
                    pass

        instance = super().__new__(cls)
        return instance

    def __init__(self, constructions_list:list[list[ConstructionRule]]=None, save_path=None, cache_folder=os.path.join(ROOT_DIR, 'cache'), resample=False):
        if hasattr(self, 'cache_folder'):
            return

        self.points = []
        self.segments = []
        self.circles = []

        self.name2point = {}
        self.point2name = {}

        self.fig, self.ax = None, None

        self.constructions_list = constructions_list
        self.save_path = save_path
        self.cache_folder = cache_folder

        if constructions_list is not None:                
            self.construct_diagram()

    def clear(self):
        """Reset all stored points, segments, circles, and name mappings."""
        self.points.clear()
        self.segments.clear()
        self.circles.clear()

        self.name2point.clear()
        self.point2name.clear()

    def show(self):
        """Render the diagram with matplotlib."""
        self.draw_diagram(show=True)

    def save_to_cache(self):
        """Persist the diagram to cache if caching is enabled."""
        if self.cache_folder is not None:
            file_name = f"{hash_constructions_list(self.constructions_list)}.pkl"
            file_path = os.path.join(self.cache_folder, file_name)
            with open(file_path, 'wb') as f:
                pickle.dump(self, f)

    def add_constructions(self, constructions):
        """Add a new batch of constructions, retrying if degeneracy occurs."""
        for _ in range(MAX_DIAGRAM_ATTEMPTS):
            try:
                self.construct(constructions)
                self.constructions_list.append(constructions)
                return
            except:
                continue

        print(f"Failed to add the constructions after {MAX_DIAGRAM_ATTEMPTS} attempts.")
        raise Exception()

    def construct_diagram(self):
        """Construct the full diagram from all construction batches, with retries."""
        for _ in range(MAX_DIAGRAM_ATTEMPTS):
            try:
                self.clear()
                for constructions in self.constructions_list:
                    self.construct(constructions)
                self.draw_diagram()
                self.save_to_cache()
                return
            except:
                continue

        print(f"Failed to construct a diagram after {MAX_DIAGRAM_ATTEMPTS} attempts.")
        raise Exception()

    def construct(self, constructions: list[ConstructionRule]):
        """Apply a single batch of construction rules to extend the diagram."""
        constructed_points = constructions[0].constructed_points()
        if any(construction.constructed_points() != constructed_points for construction in constructions[1:]):
            raise Exception()

        to_be_intersected = []
        for construction in constructions:
            # print(construction.__class__.__name__ + '('+','.join([str(name) for name in construction.arguments()])+')')
            # for c in construction.conditions:
            #     if not self.numerical_check(c):
            #         raise Exception()

            to_be_intersected += self.sketch(construction)

        new_points = self.reduce(to_be_intersected, self.points)

        if check_too_close(new_points, self.points):
            raise Exception()

        if check_too_far(new_points, self.points):
            raise Exception()

        self.points += new_points

        for p, np in zip(constructed_points, new_points):
            self.name2point[p.name] = np
            self.point2name[np] = p.name

        for construction in constructions:
            self.draw(new_points, construction)

    def numerical_check_goal(self, goal):
        """Check if the current diagram satisfies a goal relation/expression."""
        if isinstance(goal, tuple):
            for g in goal:
                if self.numerical_check(g):
                    return True, g
        else:
            if self.numerical_check(goal):
                return True, goal
        return False, goal

    def numerical_check(self, relation):
        """Numerically evaluate whether a relation/expression holds in the diagram."""
        if isinstance(relation, Relation):
            func = globals()['check_' + relation.__class__.__name__.lower()]
            args = [self.name2point[p.name] for p in relation.get_points()]
            return func(args)
        else:
            symbol_to_value = {}
            symbols, symbol_names = parse_expression(relation)

            for angle_symbol, angle_name in zip(symbols['Angle'], symbol_names['Angle']):
                angle_value = calculate_angle(*[self.name2point[n] for n in angle_name])
                symbol_to_value[angle_symbol] = angle_value

            for length_symbol, length_name in zip(symbols['Length'], symbol_names['Length']):
                length_value = calculate_length(*[self.name2point[n] for n in length_name])
                symbol_to_value[length_symbol] = length_value

            evaluated_expr = relation.subs(symbol_to_value)
            if close_enough(float(evaluated_expr.evalf()), 0):
                return True
            else:
                return False

    def sketch(self, construction):
        func = getattr(self, 'sketch_' + construction.__class__.__name__[10:])
        args = [arg if isinstance(arg, float) else self.name2point[arg.name] for arg in construction.arguments()]
        result = func(*args)
        if isinstance(result, list):
            return result
        else:
            return [result]

    def sketch_angle_bisector(self, *args: list[Point]) -> Ray:
        """Ray that bisects angle ABC."""
        a, b, c = args
        dist_ab = a.distance(b)
        dist_bc = b.distance(c)
        x = b + (c - b) * (dist_ab / dist_bc)
        m = (a + x) * 0.5
        return Ray(b, m)

    def sketch_angle_mirror(self, *args: list[Point]) -> Ray:
        """Mirror of ray BA across BC."""
        a, b, c = args
        ab = a - b
        cb = c - b

        dist_ab = a.distance(b)
        ang_ab = np.arctan2(ab.y / dist_ab, ab.x / dist_ab)
        dist_cb = c.distance(b)
        ang_bc = np.arctan2(cb.y / dist_cb, cb.x / dist_cb)

        ang_bx = 2 * ang_bc - ang_ab
        x = b + Point(np.cos(ang_bx), np.sin(ang_bx))
        return Ray(b, x)

    def sketch_circle(self, *args: list[Point]) -> Point:
        """Center of circle through three points."""
        a, b, c = args
        l1 = perpendicular_bisector(a, b)
        l2 = perpendicular_bisector(b, c)
        x = line_line_intersection(l1, l2)
        return x

    def sketch_circumcenter(self, *args: list[Point]) -> Point:
        """Circumcenter of triangle ABC."""
        a, b, c = args
        l1 = perpendicular_bisector(a, b)
        l2 = perpendicular_bisector(b, c)
        x = line_line_intersection(l1, l2)
        return x

    def sketch_eq_quadrangle(self, *args: list[Point]) -> list[Point]:
        """Randomly sample a quadrilateral with opposite sides equal."""
        a = Point(0.0, 0.0)
        b = Point(1.0, 0.0)

        length = np.random.uniform(0.5, 2.0)
        ang = np.random.uniform(np.pi / 3, np.pi * 2 / 3)
        d = head_from(a, ang, length)

        ang = ang_of(b, d)
        ang = np.random.uniform(ang / 10, ang / 9)
        c = head_from(b, ang, length)
        a, b, c, d = random_rfss(a, b, c, d)
        return [a, b, c, d]

    def sketch_eq_trapezoid(self, *args: list[Point]) -> list[Point]:
        """Randomly sample an isosceles trapezoid."""
        a = Point(0.0, 0.0)
        b = Point(1.0, 0.0)
        l = unif(0.5, 2.0)

        height = unif(0.5, 2.0)
        c = Point(0.5 + l / 2.0, height)
        d = Point(0.5 - l / 2.0, height)

        a, b, c, d = random_rfss(a, b, c, d)
        return [a, b, c, d]

    def sketch_eq_triangle(self, *args: list[Point]) -> list[Circle]:
        """Circles defining an equilateral triangle on BC."""
        b, c = args
        return [Circle(center=b, radius=b.distance(c)), Circle(center=c, radius=b.distance(c))]

    def sketch_eqangle2(self, *args: list[Point]) -> Point:
        """Point X such that angle ABX equals angle XCB."""
        a, b, c = args

        ba = b.distance(a)
        bc = b.distance(c)
        l = ba * ba / bc

        if unif(0.0, 1.0) < 0.5:
            be = min(l, bc)
            be = unif(be * 0.1, be * 0.9)
        else:
            be = max(l, bc)
            be = unif(be * 1.1, be * 1.5)

        e = b + (c - b) * (be / bc)
        y = b + (a - b) * (be / l)
        return line_line_intersection(Line(c, y), Line(a, e))

    def sketch_eqdia_quadrangle(self, *args) -> list[Point]:
        """Quadrilateral with equal diagonals."""
        m = unif(0.3, 0.7)
        n = unif(0.3, 0.7)
        a = Point(-m, 0.0)
        c = Point(1 - m, 0.0)
        b = Point(0.0, -n)
        d = Point(0.0, 1 - n)

        ang = unif(-0.25 * np.pi, 0.25 * np.pi)
        sin, cos = np.sin(ang), np.cos(ang)
        b = b.rotate(sin, cos)
        d = d.rotate(sin, cos)
        a, b, c, d = random_rfss(a, b, c, d)
        return [a, b, c, d]

    def sketch_eqdistance(self, *args) -> Circle:
        """Circle centered at A with radius BC."""
        a, b, c = args
        return Circle(center=a, radius=b.distance(c))

    def sketch_eqdistance2(self, *args) -> Circle:
        """Circle centered at A with radius alpha*BC."""
        a, b, c, alpha = args
        return Circle(center=a, radius=alpha*b.distance(c))

    def sketch_eqdistance3(self, *args) -> Circle:
        """Circle centered at A with fixed radius alpha."""
        a, alpha = args
        return Circle(center=a, radius=alpha)

    def sketch_foot(self, *args) -> Point:
        """Foot of perpendicular from A to line BC."""
        a, b, c = args
        line_bc = Line(b, c)
        tline = a.perpendicular_line(line_bc)
        return line_line_intersection(tline, line_bc)

    def sketch_free(self, *args) -> Point:
        """Free point uniformly sampled in a box."""
        return Point(unif(-1, 1), unif(-1, 1))

    def sketch_incenter(self, *args) -> Point:
        """Incenter of triangle ABC."""
        a, b, c = args
        l1 = self.sketch_angle_bisector(a, b, c)
        l2 = self.sketch_angle_bisector(b, c, a)
        return line_line_intersection(l1, l2)

    def sketch_incenter2(self, *args) -> list[Point]:
        """Incenter plus touch points on each side."""
        a, b, c = args
        i = self.sketch_incenter(a, b, c)
        x = i.foot(Line(b, c))
        y = i.foot(Line(c, a))
        z = i.foot(Line(a, b))
        return [x, y, z, i]

    def sketch_excenter(self, *args) -> Point:
        """Excenter opposite B in triangle ABC."""
        a, b, c = args
        l1 = self.sketch_angle_bisector(b, a, c)
        l2 = self.sketch_angle_bisector(a, b, c).perpendicular_line(b)
        return line_line_intersection(l1, l2)

    def sketch_excenter2(self, *args) -> list[Point]:
        """Excenter plus touch points on extended sides."""
        a, b, c = args
        i = self.sketch_excenter(a, b, c)
        x = i.foot(Line(b, c))
        y = i.foot(Line(c, a))
        z = i.foot(Line(a, b))
        return [x, y, z, i]

    def sketch_centroid(self, *args) -> list[Point]:
        """Mid-segment points and centroid of triangle ABC."""
        a, b, c = args
        x = (b + c) * 0.5
        y = (c + a) * 0.5
        z = (a + b) * 0.5
        i = line_line_intersection(Line(a, x), Line(b, y))
        return [x, y, z, i]

    def sketch_intersection_cc(self, *args) -> list[Circle]:
        """Two circles centered at O and W through A."""
        o, w, a = args
        return [Circle(center=o, radius=o.distance(a)), Circle(center=w, radius=w.distance(a))]

    def sketch_intersection_lc(self, *args) -> list:
        """Line and circle defined by A,O,B for intersection."""
        a, o, b = args
        return [Line(b, a), Circle(center=o, radius=o.distance(b))]

    def sketch_intersection_ll(self, *args) -> Point:
        """Intersection of lines AB and CD."""
        a, b, c, d = args
        l1 = Line(a, b)
        l2 = Line(c, d)
        return line_line_intersection(l1, l2)

    def sketch_intersection_lp(self, *args) -> Point:
        a, b, c, m, n = args
        l1 = Line(a,b)
        l2 = self.sketch_on_pline(c, m, n)
        return line_line_intersection(l1, l2)

    def sketch_intersection_lt(self, *args) -> Point:
        a, b, c, d, e = args
        l1 = Line(a, b)
        l2 = self.sketch_on_tline(c, d, e)
        return line_line_intersection(l1, l2)

    def sketch_intersection_pp(self, *args) -> Point:
        a, b, c, d, e, f = args
        l1 = self.sketch_on_pline(a, b, c)
        l2 = self.sketch_on_pline(d, e, f)
        return line_line_intersection(l1, l2)

    def sketch_intersection_tt(self, *args) -> Point:
        a, b, c, d, e, f = args
        l1 = self.sketch_on_tline(a, b, c)
        l2 = self.sketch_on_tline(d, e, f)
        return line_line_intersection(l1, l2)

    def sketch_iso_triangle(self, *args) -> list[Point]:
        base = unif(0.5, 1.5)
        height = unif(0.5, 1.5)

        b = Point(-base / 2, 0.0)
        c = Point(base / 2, 0.0)
        a = Point(0.0, height)
        a, b, c = random_rfss(a, b, c)
        return [a, b, c]

    def sketch_lc_tangent(self, *args) -> Line:
        a, o = args
        return self.sketch_on_tline(a, a, o)

    def sketch_midpoint(self, *args) -> Point:
        a, b = args
        return (a + b) * 0.5

    def sketch_mirror(self, *args) -> Point:
        a, b = args
        return b * 2 - a

    def sketch_nsquare(self, *args) -> Point:
        a, b = args
        ang = -np.pi / 2
        return a + (b - a).rotate(np.sin(ang), np.cos(ang))

    def sketch_on_aline(self, *args) -> Line:
        e, d, c, b, a = args
        ab = a - b
        cb = c - b
        de = d - e

        dab = a.distance(b)
        ang_ab = np.arctan2(ab.y / dab, ab.x / dab)

        dcb = c.distance(b)
        ang_bc = np.arctan2(cb.y / dcb, cb.x / dcb)

        dde = d.distance(e)
        ang_de = np.arctan2(de.y / dde, de.x / dde)

        ang_ex = ang_de + ang_bc - ang_ab
        x = e + Point(np.cos(ang_ex), np.sin(ang_ex))
        return Ray(e, x)

    def sketch_on_bline(self, *args) -> Line:
        a, b = args
        m = (a + b) * 0.5
        return m.perpendicular_line(Line(a, b))

    def sketch_on_circle(self, *args) -> Circle:
        o, a = args
        return Circle(o, o.distance(a))

    def sketch_on_line(self, *args) -> Line:
        a, b = args
        return Line(a, b)

    def sketch_on_pline(self, *args) -> Line:
        a, b, c = args
        return a.parallel_line(Line(b, c))

    def sketch_on_tline(self, *args) -> Line:
        a, b, c = args
        return a.perpendicular_line(Line(b, c))

    def sketch_orthocenter(self, *args) -> Point:
        a, b, c = args
        l1 = self.sketch_on_tline(a, b, c)
        l2 = self.sketch_on_tline(b, c, a)
        return line_line_intersection(l1, l2)

    def sketch_parallelogram(self, *args) -> Point:
        a, b, c = args
        l1 = self.sketch_on_pline(a, b, c)
        l2 = self.sketch_on_pline(c, a, b)
        return line_line_intersection(l1, l2)

    def sketch_pentagon(self, *args) -> list[Point]:
        points = [Point(1.0, 0.0)]
        ang = 0.0

        for i in range(4):
            ang += (2 * np.pi - ang) / (5 - i) * unif(0.5, 1.5)
            point = Point(np.cos(ang), np.sin(ang))
            points.append(point)

        a, b, c, d, e = points  # pylint: disable=unbalanced-tuple-unpacking
        a, b, c, d, e = random_rfss(a, b, c, d, e)
        return [a, b, c, d, e]

    def sketch_psquare(self, *args) -> Point:
        a, b = args
        ang = np.pi / 2
        return a + (b - a).rotate(np.sin(ang), np.cos(ang))

    def sketch_quadrangle(self, *args) -> list[Point]:
        a = Point(0.0, 0.0)
        b = Point(1.0, 0.0)

        length = np.random.uniform(0.5, 2.0)
        ang = np.random.uniform(np.pi / 3, np.pi * 2 / 3)
        d = head_from(a, ang, length)

        ang = ang_of(b, d)
        ang = np.random.uniform(ang / 10, ang / 9)
        c = head_from(b, ang, length)
        a, b, c, d = random_rfss(a, b, c, d)
        return [a, b, c, d]

    def sketch_r_trapezoid(self, *args) -> list[Point]:
        """Right trapezoid with AB horizontal and AD vertical."""
        a = Point(0.0, 1.0)
        d = Point(0.0, 0.0)
        b = Point(unif(0.5, 1.5), 1.0)
        c = Point(unif(0.5, 1.5), 0.0)
        a, b, c, d = random_rfss(a, b, c, d)
        return [a, b, c, d]

    def sketch_r_triangle(self, *args) -> list[Point]:
        """Random right triangle with legs on axes."""
        a = Point(0.0, 0.0)
        b = Point(0.0, unif(0.5, 2.0))
        c = Point(unif(0.5, 2.0), 0.0)
        a, b, c = random_rfss(a, b, c)
        return [a, b, c]

    def sketch_rectangle(self, *args) -> list[Point]:
        """Axis-aligned rectangle with random width/height."""
        a = Point(0.0, 0.0)
        b = Point(0.0, 1.0)
        l = unif(0.5, 2.0)
        c = Point(l, 1.0)
        d = Point(l, 0.0)
        a, b, c, d = random_rfss(a, b, c, d)
        return [a, b, c, d]

    def sketch_reflect(self, *args) -> Point:
        """Reflect point A across line BC."""
        a, b, c = args
        m = a.foot(Line(b, c))
        return m * 2 - a

    def sketch_risos(self, *args) -> list[Point]:
        """Right isosceles triangle."""
        a = Point(0.0, 0.0)
        b = Point(0.0, 1.0)
        c = Point(1.0, 0.0)
        a, b, c = random_rfss(a, b, c)
        return [a, b, c]

    def sketch_s_angle(self, *args) -> Ray:
        """Ray at point B making angle alpha with BA."""
        a, b, alpha = args
        ang = alpha / 180 * np.pi
        x = b + (a - b).rotatea(ang)
        return Ray(b, x)

    def sketch_segment(self, *args) -> list[Point]:
        """Random segment endpoints in [-1,1] box."""
        a = Point(unif(-1, 1), unif(-1, 1))
        b = Point(unif(-1, 1), unif(-1, 1))
        return [a, b]

    def sketch_shift(self, *args) -> Point:
        """Translate C by vector BA."""
        c, b, a = args
        return c + (b - a)

    def sketch_square(self, *args) -> list[Point]:
        """Square constructed on segment AB."""
        a, b = args
        c = b + (a - b).rotatea(-np.pi / 2)
        d = a + (b - a).rotatea(np.pi / 2)
        return [c, d]

    def sketch_isquare(self, *args) -> list[Point]:
        """Axis-aligned unit square, randomly re-ordered."""
        a = Point(0.0, 0.0)
        b = Point(1.0, 0.0)
        c = Point(1.0, 1.0)
        d = Point(0.0, 1.0)
        a, b, c, d = random_rfss(a, b, c, d)
        return [a, b, c, d]

    def sketch_trapezoid(self, *args) -> list[Point]:
        """Random trapezoid with AB // CD."""
        d = Point(0.0, 0.0)
        c = Point(1.0, 0.0)

        base = unif(0.5, 2.0)
        height = unif(0.5, 2.0)
        a = Point(unif(0.2, 0.5), height)
        b = Point(a.x + base, height)
        a, b, c, d = random_rfss(a, b, c, d)
        return [a, b, c, d]

    def sketch_triangle(self, *args) -> list[Point]:
        """Random triangle."""
        a = Point(0.0, 0.0)
        b = Point(1.0, 0.0)
        ac = unif(0.5, 2.0)
        ang = unif(0.2, 0.8) * np.pi
        c = head_from(a, ang, ac)
        return [a, b, c]

    def sketch_triangle12(self, *args) -> list[Point]:
        """Triangle with side-length ratios near 1:2."""
        b = Point(0.0, 0.0)
        c = Point(unif(1.5, 2.5), 0.0)
        a, _ = circle_circle_intersection(Circle(b, 1.0), Circle(c, 2.0))
        a, b, c = random_rfss(a, b, c)
        return [a, b, c]

    def sketch_2l1c(self, *args) -> list[Point]:
        """Intersections of perpendiculars from P to AC/BC with circle centered at P."""
        a, b, c, p = args
        bc, ac = Line(b, c), Line(a, c)
        circle = Circle(p, p.distance(a))

        d, d_ = line_circle_intersection(p.perpendicular_line(bc), circle)
        if bc.diff_side(d_, a):
            d = d_

        e, e_ = line_circle_intersection(p.perpendicular_line(ac), circle)
        if ac.diff_side(e_, b):
            e = e_

        df = d.perpendicular_line(Line(p, d))
        ef = e.perpendicular_line(Line(p, e))
        f = line_line_intersection(df, ef)

        g, g_ = line_circle_intersection(Line(c, f), circle)
        if bc.same_side(g_, a):
            g = g_

        b_ = c + (b - c) / b.distance(c)
        a_ = c + (a - c) / a.distance(c)
        m = (a_ + b_) * 0.5
        x = line_line_intersection(Line(c, m), Line(p, g))
        return [x.foot(ac), x.foot(bc), g, x]

    def sketch_e5128(self, *args) -> list[Point]:
        """Problem-specific construction e5128."""
        a, b, c, d = args
        g = (a + b) * 0.5
        de = Line(d, g)

        e, f = line_circle_intersection(de, Circle(c, c.distance(b)))

        if e.distance(d) < f.distance(d):
            e = f
        return [e, g]

    def sketch_3peq(self, *args) -> list[Point]:
        """Three-point equidistance construction."""
        a, b, c = args
        ab, bc, ca = Line(a, b), Line(b, c), Line(c, a)

        z = b + (c - b) * np.random.uniform(-0.5, 1.5)

        z_ = z * 2 - c
        l = z_.parallel_line(ca)
        x = line_line_intersection(l, ab)
        y = z * 2 - x
        return [x, y, z]

    def sketch_trisect(self, *args) -> list[Point]:
        """Trisect angle ABC."""
        a, b, c = args
        ang1 = ang_of(b, a)
        ang2 = ang_of(b, c)

        swap = 0
        if ang1 > ang2:
            ang1, ang2 = ang2, ang1
            swap += 1

        if ang2 - ang1 > np.pi:
            ang1, ang2 = ang2, ang1 + 2 * np.pi
            swap += 1

        angx = ang1 + (ang2 - ang1) / 3
        angy = ang2 - (ang2 - ang1) / 3

        x = b + Point(np.cos(angx), np.sin(angx))
        y = b + Point(np.cos(angy), np.sin(angy))

        ac = Line(a, c)
        x = line_line_intersection(Line(b, x), ac)
        y = line_line_intersection(Line(b, y), ac)

        if swap == 1:
            return [y, x]
        return [x, y]

    def sketch_trisegment(self, *args) -> list[Point]:
        """Trisect segment AB."""
        a, b = args
        x, y = a + (b - a) * (1.0 / 3), a + (b - a) * (2.0 / 3)
        return [x, y]

    def sketch_on_dia(self, *args) -> Circle:
        """Circle with diameter AB."""
        a, b = args
        o = (a + b) * 0.5
        return Circle(o, o.distance(a))

    def sketch_ieq_triangle(self, *args) -> list[Point]:
        a = Point(0.0, 0.0)
        b = Point(1.0, 0.0)

        c, _ = Circle(a, a.distance(b)).intersect(Circle(b, b.distance(a)))
        return [a, b, c]

    def sketch_on_opline(self, *args) -> Ray:
        a, b = args
        return Ray(a, a + a - b)

    def sketch_cc_tangent(self, *args) -> list[Point]:
        o, a, w, b = args
        ra, rb = o.distance(a), w.distance(b)

        ow = Line(o, w)
        if close_enough(ra, rb):
            oo = ow.perpendicular_line(o)
            oa = Circle(o, ra)
            x, z = line_circle_intersection(oo, oa)
            y = x + w - o
            t = z + w - o
            return [x, y, z, t]

    def sketch_cc_tangent0(self, *args) -> Ray:
        o, a, w, b = args
        return self.sketch_cc_tangent(o, a, w, b)[:2]

    def sketch_eqangle3(self, *args) -> list[Point]:
        a, b, d, e, f = args
        de = d.distance(e)
        ef = e.distance(f)
        ab = b.distance(a)
        ang_ax = ang_of(a, b) + ang_between(e, d, f)
        x = head_from(a, ang_ax, length=de / ef * ab)   
        o = self.sketch_circle(a, b, x)
        return Circle(o, o.distance(a))

    def sketch_tangent(self, *args) -> list[Point]:
        a, o, b = args
        dia = self.sketch_dia([a, o])
        return list(circle_circle_intersection(Circle(o, o.distance(b)), dia))

    def sketch_on_circum(self, *args) -> Circle:
        a, b, c = args
        o = self.sketch_circle(a, b, c)
        return Circle(o, o.distance(a))

    def sketch_sameside(self, *args) -> HalfPlane:
        a, b, c = args
        return HalfPlane(a, b, c)

    def sketch_opposingsides(self, *args) -> HalfPlane:
        a, b, c = args
        return HalfPlane(a, b, c, opposingsides=True)

    def reduce(self, objs, existing_points) -> list[Point]:
        """Reduce intersecting objects into sampled intersection points.

        Filters half-planes, handles point-only cases, samples within half-planes,
        or intersects pairs of essential geometric objects.
        """
        essential_objs = [i for i in objs if not isinstance(i, HalfPlane)]
        halfplane_objs = [i for i in objs if isinstance(i, HalfPlane)]

        if all(isinstance(o, Point) for o in objs):
            return objs

        elif all(isinstance(o, HalfPlane) for o in objs):
            if len(objs) == 1:
                return objs[0].sample_within_halfplanes(existing_points,[])
            else:
                return objs[0].sample_within_halfplanes(existing_points,objs[1:])

        elif len(essential_objs) == 1:
            if not halfplane_objs:
                return objs[0].sample_within(existing_points)
            else:
                return objs[0].sample_within_halfplanes(existing_points,halfplane_objs)

        elif len(essential_objs) == 2:
            a, b = essential_objs
            result = a.intersect(b)

            if isinstance(result, Point):
                if halfplane_objs and not all(i.contains(result) for i in halfplane_objs):
                    raise Exception()
                return [result]

            a, b = result

            if halfplane_objs:
                a_correct_side = all(i.contains(a) for i in halfplane_objs)
                b_correct_side = all(i.contains(b) for i in halfplane_objs)

                if a_correct_side and not b_correct_side:
                    return [a]
                elif b_correct_side and not a_correct_side:
                    return [b]
                elif not a_correct_side and not b_correct_side:
                    raise Exception()

            a_close = any([a.close(x) for x in existing_points])
            b_close = any([b.close(x) for x in existing_points])

            if a_close and not b_close:
                return [b]

            elif b_close and not a_close:
                return [a]
            else:
                return [np.random.choice([a, b])]

    def draw(self, new_points, construction):
        func = getattr(self, 'draw_' + construction.__class__.__name__[10:])
        args = [arg if isinstance(arg, float) else self.name2point[arg.name] for arg in construction.arguments()]
        func(*new_points, *args)

    def draw_angle_bisector(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(b, x))

    def draw_angle_mirror(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(b, x))

    def draw_circle(self, *args):
        x, a, b, c = args
        # self.segments.append(Segment(a, x))
        # self.segments.append(Segment(b, x))
        # self.segments.append(Segment(c, x))
        self.circles.append(Circle(x, x.distance(a)))

    def draw_circumcenter(self, *args):
        x, a, b, c = args
        # self.segments.append(Segment(a, x))
        # self.segments.append(Segment(b, x))
        # self.segments.append(Segment(c, x))
        self.circles.append(Circle(x, x.distance(a)))

    def draw_eq_quadrangle(self, *args):
        a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, a))

    def draw_eq_trapezoid(self, *args):
        a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, a))

    def draw_eq_triangle(self, *args):
        x, b, c = args
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, x))
        self.segments.append(Segment(x, b))

    def draw_eqangle2(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(b, a))
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(c, b))
        self.segments.append(Segment(b, b))

    def draw_eqdia_quadrangle(self, *args):
        a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, a))
        self.segments.append(Segment(b, d))
        self.segments.append(Segment(a, c))

    def draw_eqdistance(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(b, c))

    def draw_eqdistance2(self, *args):
        x, a, b, c, alpha = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(b, c))

    def draw_eqdistance2(self, *args):
        x, a, alpha = args
        self.segments.append(Segment(x, a))

    def draw_foot(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(x, b))
        self.segments.append(Segment(x, c))
        self.segments.append(Segment(b, c))

    def draw_free(self, *args):
        x = args

    def draw_incenter(self, *args):
        i, a, b, c = args
        x = i.foot(Line(b, c))
        y = i.foot(Line(c, a))
        z = i.foot(Line(a, b))
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))
        self.circles.append(Circle(p1=x, p2=y, p3=z))

    def draw_incenter2(self, *args):
        x, y, z, i, a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))
        self.circles.append(Circle(p1=x, p2=y, p3=z))

    def draw_excenter(self, *args):
        i, a, b, c = args
        x = i.foot(Line(b, c))
        y = i.foot(Line(c, a))
        z = i.foot(Line(a, b))
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))
        self.circles.append(Circle(p1=x, p2=y, p3=z))

    def draw_excenter2(self, *args):
        x, y, z, i, a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))
        self.circles.append(Circle(p1=x, p2=y, p3=z))

    def draw_centroid(self, *args):
        x, y, z, i, a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, y))
        self.segments.append(Segment(c, z))

    def draw_intersection_cc(self, *args):
        x, o, w, a = args
        self.segments.append(Segment(o, a))
        self.segments.append(Segment(o, x))
        self.segments.append(Segment(w, a))
        self.segments.append(Segment(w, x))
        self.circles.append(Circle(o, o.distance(a)))
        self.circles.append(Circle(w, w.distance(a)))

    def draw_intersection_lc(self, *args):
        x, a, o, b = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(x, b))
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(o, b))
        self.segments.append(Segment(o, x))
        self.circles.append(Circle(o, o.distance(b)))

    def draw_intersection_ll(self, *args):
        x, a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, x))
        self.segments.append(Segment(c, x))
        self.segments.append(Segment(d, x))

    def draw_intersection_lp(self, *args):
        x, a, b, c, m, n = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(m, n))
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, x))
        self.segments.append(Segment(c, x))

    def draw_intersection_lp(self, *args):
        x, a, b, c, m, n = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(m, n))
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, x))
        self.segments.append(Segment(c, x))

    def draw_intersection_lt(self, *args):
        x, a, b, c, d, e = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(d, e))
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, x))
        self.segments.append(Segment(c, x))

    def draw_intersection_pp(self, *args):
        x, a, b, c, d, e, f = args
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(d, x))
        self.segments.append(Segment(e, f))

    def draw_intersection_tt(self, *args):
        x, a, b, c, d, e, f = args
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(d, x))
        self.segments.append(Segment(e, f))

    def draw_iso_triangle(self, *args):
        a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))

    def draw_lc_tangent(self, *args):
        x, a, o = args
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(a, o))
        self.circles.append(Circle(o, o.distance(a)))

    def draw_midpoint(self, *args):
        x, a, b = args
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, x))

    def draw_mirror(self, *args):
        x, a, b = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, x))

    def draw_nsquare(self, *args):
        x, a, b = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, x))

    def draw_on_aline(self, *args):
        x, a, b, c, d, e = args
        self.segments.append(Segment(e, d))
        self.segments.append(Segment(d, c))
        self.segments.append(Segment(b, a))
        self.segments.append(Segment(a, x))

    def draw_on_bline(self, *args):
        x, a, b = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, x))

    def draw_on_circle(self, *args):
        x, o, a = args
        self.segments.append(Segment(o, a))
        self.segments.append(Segment(o, x))
        self.circles.append(Circle(o, o.distance(x)))

    def draw_on_line(self, *args):
        x, a, b = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(x, b))
        self.segments.append(Segment(a, b))

    def draw_on_pline(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(b, c))

    def draw_on_tline(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(b, c))

    def draw_orthocenter(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))

    def draw_parallelogram(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, x))

    def draw_parallelogram(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, x))

    def draw_pentagon(self, *args):
        a, b, c, d, e = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, e))
        self.segments.append(Segment(e, a))

    def draw_psquare(self, *args):
        x, a, b = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(a, b))

    def draw_quadrangle(self, *args):
        a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, a))

    def draw_r_trapezoid(self, *args):
        a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, a))

    def draw_r_triangle(self, *args):
        a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))

    def draw_rectangle(self, *args):
        a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, a))

    def draw_reflect(self, *args):
        x, a, b, c = args
        self.segments.append(Segment(b, c))

    def draw_risos(self, *args):
        a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))

    def draw_s_angle(self, *args):
        x, a, b, alpha = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, x))

    def draw_segment(self, *args):
        a, b = args
        self.segments.append(Segment(a, b))

    def draw_s_segment(self, *args):
        a, b, alpha = args
        self.segments.append(Segment(a, b))

    def draw_shift(self, *args):
        x, b, c, d = args
        self.segments.append(Segment(x, b))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(x, c))
        self.segments.append(Segment(b, d))

    def draw_square(self, *args):
        x, y, a, b = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, x))
        self.segments.append(Segment(x, y))
        self.segments.append(Segment(y, a))

    def draw_isquare(self, *args):
        a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, a))

    def draw_trapezoid(self, *args):
        a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, a))

    def draw_triangle(self, *args):
        a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))

    def draw_triangle12(self, *args):
        a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))

    def draw_2l1c(self, *args):
        x, y, z, i, a, b, c, o = args
        self.segments.append(Segment(a, c))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(a, o))
        self.segments.append(Segment(b, o))

        self.segments.append(Segment(i, x))
        self.segments.append(Segment(i, y))
        self.segments.append(Segment(i, z))

        self.segments.append(Segment(c, x))
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(c, y))
        self.segments.append(Segment(b, y))
        self.segments.append(Segment(o, z))

        self.circles.append(Circle(i, i.distance(x)))
        self.circles.append(Circle(o, o.distance(a)))

    def draw_e5128(self, *args):
        x, y, a, b, c, d = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, d))
        self.segments.append(Segment(d, a))

        self.segments.append(Segment(a, x))
        self.segments.append(Segment(x, y))
        self.segments.append(Segment(c, x))

    def draw_3peq(self, *args):
        x, y, z, a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))

        self.segments.append(Segment(a, x))
        self.segments.append(Segment(b, x))

        self.segments.append(Segment(a, y))
        self.segments.append(Segment(c, y))

        self.segments.append(Segment(c, z))
        self.segments.append(Segment(b, z))

        self.segments.append(Segment(x, y))
        self.segments.append(Segment(y, z))
        self.segments.append(Segment(z, x))

    def draw_trisect(self, *args):
        x, y, a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))

        self.segments.append(Segment(b, x))
        self.segments.append(Segment(b, y))

        self.segments.append(Segment(a, x))
        self.segments.append(Segment(x, y))
        self.segments.append(Segment(y, c))

    def draw_trisegment(self, *args):
        x, y, a, b = args
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(x, y))
        self.segments.append(Segment(y, b))

    def draw_on_dia(self, *args):
        x, a, b = args
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(x, b))

    def draw_ieq_triangle(self, *args):
        a, b, c = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(b, c))
        self.segments.append(Segment(c, a))

    def draw_on_opline(self, *args):
        x, a, b = args
        self.segments.append(Segment(a, b))
        self.segments.append(Segment(x, a))
        self.segments.append(Segment(x, b))

    def draw_cc_tangent0(self, *args):
        x, y, o, a, w, b = args
        self.segments.append(Segment(o, a))
        self.segments.append(Segment(o, x))

        self.segments.append(Segment(w, b))
        self.segments.append(Segment(w, y))

        self.segments.append(Segment(x, y))

        self.circles.append(Circle(o, o.distance(a)))
        self.circles.append(Circle(w, w.distance(b)))

    def draw_cc_tangent(self, *args):
        x, y, z, i, o, a, w, b = args
        self.segments.append(Segment(o, a))
        self.segments.append(Segment(o, x))
        self.segments.append(Segment(o, z))

        self.segments.append(Segment(w, b))
        self.segments.append(Segment(w, y))
        self.segments.append(Segment(w, i))

        self.segments.append(Segment(x, y))
        self.segments.append(Segment(z, i))

        self.circles.append(Circle(o, o.distance(a)))
        self.circles.append(Circle(w, w.distance(b)))

    def draw_eqangle3(self, *args):
        x, a, b, d, e, f = args
        self.segments.append(Segment(f, d))
        self.segments.append(Segment(d, e))

        self.segments.append(Segment(a, x))
        self.segments.append(Segment(x, b))

    def draw_tangent(self, *args):
        x, y, a, o, b = args
        self.segments.append(Segment(o, b))
        self.segments.append(Segment(o, x))
        self.segments.append(Segment(o, y))
        self.segments.append(Segment(a, x))
        self.segments.append(Segment(a, y))

        self.circles.append(Circle(o, o.distance(b)))

    def draw_on_circum(self, *args):
        x, a, b, c = args
        self.circles.append(Circle(p1=a, p2=b, p3=c))

    def draw_sameside(self, *args):
        x, a, b, c = args

    def draw_opposingsides(self, *args):
        x, a, b, c = args

    def draw_diagram(self, show=False):
        """Draw the current diagram; optionally display the matplotlib figure."""
        imsize = 512 / 100
        self.fig, self.ax = plt.subplots(figsize=(imsize, imsize), dpi=300)
        self.ax.set_facecolor((1.0, 1.0, 1.0))

        for segment in self.segments:
            p1, p2 = segment.p1, segment.p2
            lx, ly = (p1.x, p2.x), (p1.y, p2.y)
            self.ax.plot(lx, ly, color='black', lw=1.2, alpha=0.8, ls='-')

        for circle in self.circles:
            self.ax.add_patch(
                plt.Circle(
                    (circle.center.x, circle.center.y),
                    circle.radius,
                    color='red',
                    alpha=0.8,
                    fill=False,
                    lw=1.2,
                    ls='-'
                )
            )

        for p in self.points:
            self.ax.scatter(p.x, p.y, color='black', s=15)
            self.ax.annotate(self.point2name[p], (p.x+0.015, p.y+0.015), color='black', fontsize=8)

        self.ax.set_aspect('equal')
        self.ax.set_axis_off()
        self.fig.subplots_adjust(left=0.05, right=0.95, top=0.95, bottom=0.05, wspace=0, hspace=0)
        xmin = min([p.x for p in self.points])
        xmax = max([p.x for p in self.points])
        ymin = min([p.y for p in self.points])
        ymax = max([p.y for p in self.points])
        x_margin = (xmax - xmin) * 0.1
        y_margin = (ymax - ymin) * 0.1

        self.ax.margins(x_margin, y_margin)

        self.save_diagram()

        if show:
            plt.show()

        plt.close(self.fig)

    def save_diagram(self):
        if self.save_path is not None:
            parent_dir = os.path.dirname(self.save_path)
            if parent_dir and not os.path.exists(parent_dir):
                os.makedirs(parent_dir)
            self.fig.savefig(self.save_path)

__new__(constructions_list=None, save_path=None, cache_folder=os.path.join(ROOT_DIR, 'cache'), resample=False)

Load from cache if available, otherwise construct a new diagram instance.

Source code in pyeuclid/formalization/diagram.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def __new__(cls, constructions_list:list[list[ConstructionRule]]=None, save_path=None, cache_folder=os.path.join(ROOT_DIR, 'cache'), resample=False):
    """Load from cache if available, otherwise construct a new diagram instance."""
    if not resample and cache_folder is not None:
        if not os.path.exists(cache_folder):
            os.makedirs(cache_folder)

        if constructions_list is not None:
            file_name = f"{hash_constructions_list(constructions_list)}.pkl"
            file_path = os.path.join(cache_folder, file_name)
            try:
                if os.path.exists(file_path):
                    with open(file_path, 'rb') as f:
                        instance = pickle.load(f)
                        instance.save_path = save_path
                        instance.save_diagram()
                        return instance
            except:
                pass

    instance = super().__new__(cls)
    return instance

add_constructions(constructions)

Add a new batch of constructions, retrying if degeneracy occurs.

Source code in pyeuclid/formalization/diagram.py
86
87
88
89
90
91
92
93
94
95
96
97
def add_constructions(self, constructions):
    """Add a new batch of constructions, retrying if degeneracy occurs."""
    for _ in range(MAX_DIAGRAM_ATTEMPTS):
        try:
            self.construct(constructions)
            self.constructions_list.append(constructions)
            return
        except:
            continue

    print(f"Failed to add the constructions after {MAX_DIAGRAM_ATTEMPTS} attempts.")
    raise Exception()

clear()

Reset all stored points, segments, circles, and name mappings.

Source code in pyeuclid/formalization/diagram.py
65
66
67
68
69
70
71
72
def clear(self):
    """Reset all stored points, segments, circles, and name mappings."""
    self.points.clear()
    self.segments.clear()
    self.circles.clear()

    self.name2point.clear()
    self.point2name.clear()

construct(constructions)

Apply a single batch of construction rules to extend the diagram.

Source code in pyeuclid/formalization/diagram.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
def construct(self, constructions: list[ConstructionRule]):
    """Apply a single batch of construction rules to extend the diagram."""
    constructed_points = constructions[0].constructed_points()
    if any(construction.constructed_points() != constructed_points for construction in constructions[1:]):
        raise Exception()

    to_be_intersected = []
    for construction in constructions:
        # print(construction.__class__.__name__ + '('+','.join([str(name) for name in construction.arguments()])+')')
        # for c in construction.conditions:
        #     if not self.numerical_check(c):
        #         raise Exception()

        to_be_intersected += self.sketch(construction)

    new_points = self.reduce(to_be_intersected, self.points)

    if check_too_close(new_points, self.points):
        raise Exception()

    if check_too_far(new_points, self.points):
        raise Exception()

    self.points += new_points

    for p, np in zip(constructed_points, new_points):
        self.name2point[p.name] = np
        self.point2name[np] = p.name

    for construction in constructions:
        self.draw(new_points, construction)

construct_diagram()

Construct the full diagram from all construction batches, with retries.

Source code in pyeuclid/formalization/diagram.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def construct_diagram(self):
    """Construct the full diagram from all construction batches, with retries."""
    for _ in range(MAX_DIAGRAM_ATTEMPTS):
        try:
            self.clear()
            for constructions in self.constructions_list:
                self.construct(constructions)
            self.draw_diagram()
            self.save_to_cache()
            return
        except:
            continue

    print(f"Failed to construct a diagram after {MAX_DIAGRAM_ATTEMPTS} attempts.")
    raise Exception()

draw_diagram(show=False)

Draw the current diagram; optionally display the matplotlib figure.

Source code in pyeuclid/formalization/diagram.py
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
def draw_diagram(self, show=False):
    """Draw the current diagram; optionally display the matplotlib figure."""
    imsize = 512 / 100
    self.fig, self.ax = plt.subplots(figsize=(imsize, imsize), dpi=300)
    self.ax.set_facecolor((1.0, 1.0, 1.0))

    for segment in self.segments:
        p1, p2 = segment.p1, segment.p2
        lx, ly = (p1.x, p2.x), (p1.y, p2.y)
        self.ax.plot(lx, ly, color='black', lw=1.2, alpha=0.8, ls='-')

    for circle in self.circles:
        self.ax.add_patch(
            plt.Circle(
                (circle.center.x, circle.center.y),
                circle.radius,
                color='red',
                alpha=0.8,
                fill=False,
                lw=1.2,
                ls='-'
            )
        )

    for p in self.points:
        self.ax.scatter(p.x, p.y, color='black', s=15)
        self.ax.annotate(self.point2name[p], (p.x+0.015, p.y+0.015), color='black', fontsize=8)

    self.ax.set_aspect('equal')
    self.ax.set_axis_off()
    self.fig.subplots_adjust(left=0.05, right=0.95, top=0.95, bottom=0.05, wspace=0, hspace=0)
    xmin = min([p.x for p in self.points])
    xmax = max([p.x for p in self.points])
    ymin = min([p.y for p in self.points])
    ymax = max([p.y for p in self.points])
    x_margin = (xmax - xmin) * 0.1
    y_margin = (ymax - ymin) * 0.1

    self.ax.margins(x_margin, y_margin)

    self.save_diagram()

    if show:
        plt.show()

    plt.close(self.fig)

numerical_check(relation)

Numerically evaluate whether a relation/expression holds in the diagram.

Source code in pyeuclid/formalization/diagram.py
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
def numerical_check(self, relation):
    """Numerically evaluate whether a relation/expression holds in the diagram."""
    if isinstance(relation, Relation):
        func = globals()['check_' + relation.__class__.__name__.lower()]
        args = [self.name2point[p.name] for p in relation.get_points()]
        return func(args)
    else:
        symbol_to_value = {}
        symbols, symbol_names = parse_expression(relation)

        for angle_symbol, angle_name in zip(symbols['Angle'], symbol_names['Angle']):
            angle_value = calculate_angle(*[self.name2point[n] for n in angle_name])
            symbol_to_value[angle_symbol] = angle_value

        for length_symbol, length_name in zip(symbols['Length'], symbol_names['Length']):
            length_value = calculate_length(*[self.name2point[n] for n in length_name])
            symbol_to_value[length_symbol] = length_value

        evaluated_expr = relation.subs(symbol_to_value)
        if close_enough(float(evaluated_expr.evalf()), 0):
            return True
        else:
            return False

numerical_check_goal(goal)

Check if the current diagram satisfies a goal relation/expression.

Source code in pyeuclid/formalization/diagram.py
147
148
149
150
151
152
153
154
155
156
def numerical_check_goal(self, goal):
    """Check if the current diagram satisfies a goal relation/expression."""
    if isinstance(goal, tuple):
        for g in goal:
            if self.numerical_check(g):
                return True, g
    else:
        if self.numerical_check(goal):
            return True, goal
    return False, goal

reduce(objs, existing_points)

Reduce intersecting objects into sampled intersection points.

Filters half-planes, handles point-only cases, samples within half-planes, or intersects pairs of essential geometric objects.

Source code in pyeuclid/formalization/diagram.py
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
def reduce(self, objs, existing_points) -> list[Point]:
    """Reduce intersecting objects into sampled intersection points.

    Filters half-planes, handles point-only cases, samples within half-planes,
    or intersects pairs of essential geometric objects.
    """
    essential_objs = [i for i in objs if not isinstance(i, HalfPlane)]
    halfplane_objs = [i for i in objs if isinstance(i, HalfPlane)]

    if all(isinstance(o, Point) for o in objs):
        return objs

    elif all(isinstance(o, HalfPlane) for o in objs):
        if len(objs) == 1:
            return objs[0].sample_within_halfplanes(existing_points,[])
        else:
            return objs[0].sample_within_halfplanes(existing_points,objs[1:])

    elif len(essential_objs) == 1:
        if not halfplane_objs:
            return objs[0].sample_within(existing_points)
        else:
            return objs[0].sample_within_halfplanes(existing_points,halfplane_objs)

    elif len(essential_objs) == 2:
        a, b = essential_objs
        result = a.intersect(b)

        if isinstance(result, Point):
            if halfplane_objs and not all(i.contains(result) for i in halfplane_objs):
                raise Exception()
            return [result]

        a, b = result

        if halfplane_objs:
            a_correct_side = all(i.contains(a) for i in halfplane_objs)
            b_correct_side = all(i.contains(b) for i in halfplane_objs)

            if a_correct_side and not b_correct_side:
                return [a]
            elif b_correct_side and not a_correct_side:
                return [b]
            elif not a_correct_side and not b_correct_side:
                raise Exception()

        a_close = any([a.close(x) for x in existing_points])
        b_close = any([b.close(x) for x in existing_points])

        if a_close and not b_close:
            return [b]

        elif b_close and not a_close:
            return [a]
        else:
            return [np.random.choice([a, b])]

save_to_cache()

Persist the diagram to cache if caching is enabled.

Source code in pyeuclid/formalization/diagram.py
78
79
80
81
82
83
84
def save_to_cache(self):
    """Persist the diagram to cache if caching is enabled."""
    if self.cache_folder is not None:
        file_name = f"{hash_constructions_list(self.constructions_list)}.pkl"
        file_path = os.path.join(self.cache_folder, file_name)
        with open(file_path, 'wb') as f:
            pickle.dump(self, f)

show()

Render the diagram with matplotlib.

Source code in pyeuclid/formalization/diagram.py
74
75
76
def show(self):
    """Render the diagram with matplotlib."""
    self.draw_diagram(show=True)

sketch_2l1c(*args)

Intersections of perpendiculars from P to AC/BC with circle centered at P.

Source code in pyeuclid/formalization/diagram.py
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
def sketch_2l1c(self, *args) -> list[Point]:
    """Intersections of perpendiculars from P to AC/BC with circle centered at P."""
    a, b, c, p = args
    bc, ac = Line(b, c), Line(a, c)
    circle = Circle(p, p.distance(a))

    d, d_ = line_circle_intersection(p.perpendicular_line(bc), circle)
    if bc.diff_side(d_, a):
        d = d_

    e, e_ = line_circle_intersection(p.perpendicular_line(ac), circle)
    if ac.diff_side(e_, b):
        e = e_

    df = d.perpendicular_line(Line(p, d))
    ef = e.perpendicular_line(Line(p, e))
    f = line_line_intersection(df, ef)

    g, g_ = line_circle_intersection(Line(c, f), circle)
    if bc.same_side(g_, a):
        g = g_

    b_ = c + (b - c) / b.distance(c)
    a_ = c + (a - c) / a.distance(c)
    m = (a_ + b_) * 0.5
    x = line_line_intersection(Line(c, m), Line(p, g))
    return [x.foot(ac), x.foot(bc), g, x]

sketch_3peq(*args)

Three-point equidistance construction.

Source code in pyeuclid/formalization/diagram.py
662
663
664
665
666
667
668
669
670
671
672
673
def sketch_3peq(self, *args) -> list[Point]:
    """Three-point equidistance construction."""
    a, b, c = args
    ab, bc, ca = Line(a, b), Line(b, c), Line(c, a)

    z = b + (c - b) * np.random.uniform(-0.5, 1.5)

    z_ = z * 2 - c
    l = z_.parallel_line(ca)
    x = line_line_intersection(l, ab)
    y = z * 2 - x
    return [x, y, z]

sketch_angle_bisector(*args)

Ray that bisects angle ABC.

Source code in pyeuclid/formalization/diagram.py
191
192
193
194
195
196
197
198
def sketch_angle_bisector(self, *args: list[Point]) -> Ray:
    """Ray that bisects angle ABC."""
    a, b, c = args
    dist_ab = a.distance(b)
    dist_bc = b.distance(c)
    x = b + (c - b) * (dist_ab / dist_bc)
    m = (a + x) * 0.5
    return Ray(b, m)

sketch_angle_mirror(*args)

Mirror of ray BA across BC.

Source code in pyeuclid/formalization/diagram.py
200
201
202
203
204
205
206
207
208
209
210
211
212
213
def sketch_angle_mirror(self, *args: list[Point]) -> Ray:
    """Mirror of ray BA across BC."""
    a, b, c = args
    ab = a - b
    cb = c - b

    dist_ab = a.distance(b)
    ang_ab = np.arctan2(ab.y / dist_ab, ab.x / dist_ab)
    dist_cb = c.distance(b)
    ang_bc = np.arctan2(cb.y / dist_cb, cb.x / dist_cb)

    ang_bx = 2 * ang_bc - ang_ab
    x = b + Point(np.cos(ang_bx), np.sin(ang_bx))
    return Ray(b, x)

sketch_centroid(*args)

Mid-segment points and centroid of triangle ABC.

Source code in pyeuclid/formalization/diagram.py
357
358
359
360
361
362
363
364
def sketch_centroid(self, *args) -> list[Point]:
    """Mid-segment points and centroid of triangle ABC."""
    a, b, c = args
    x = (b + c) * 0.5
    y = (c + a) * 0.5
    z = (a + b) * 0.5
    i = line_line_intersection(Line(a, x), Line(b, y))
    return [x, y, z, i]

sketch_circle(*args)

Center of circle through three points.

Source code in pyeuclid/formalization/diagram.py
215
216
217
218
219
220
221
def sketch_circle(self, *args: list[Point]) -> Point:
    """Center of circle through three points."""
    a, b, c = args
    l1 = perpendicular_bisector(a, b)
    l2 = perpendicular_bisector(b, c)
    x = line_line_intersection(l1, l2)
    return x

sketch_circumcenter(*args)

Circumcenter of triangle ABC.

Source code in pyeuclid/formalization/diagram.py
223
224
225
226
227
228
229
def sketch_circumcenter(self, *args: list[Point]) -> Point:
    """Circumcenter of triangle ABC."""
    a, b, c = args
    l1 = perpendicular_bisector(a, b)
    l2 = perpendicular_bisector(b, c)
    x = line_line_intersection(l1, l2)
    return x

sketch_e5128(*args)

Problem-specific construction e5128.

Source code in pyeuclid/formalization/diagram.py
650
651
652
653
654
655
656
657
658
659
660
def sketch_e5128(self, *args) -> list[Point]:
    """Problem-specific construction e5128."""
    a, b, c, d = args
    g = (a + b) * 0.5
    de = Line(d, g)

    e, f = line_circle_intersection(de, Circle(c, c.distance(b)))

    if e.distance(d) < f.distance(d):
        e = f
    return [e, g]

sketch_eq_quadrangle(*args)

Randomly sample a quadrilateral with opposite sides equal.

Source code in pyeuclid/formalization/diagram.py
231
232
233
234
235
236
237
238
239
240
241
242
243
244
def sketch_eq_quadrangle(self, *args: list[Point]) -> list[Point]:
    """Randomly sample a quadrilateral with opposite sides equal."""
    a = Point(0.0, 0.0)
    b = Point(1.0, 0.0)

    length = np.random.uniform(0.5, 2.0)
    ang = np.random.uniform(np.pi / 3, np.pi * 2 / 3)
    d = head_from(a, ang, length)

    ang = ang_of(b, d)
    ang = np.random.uniform(ang / 10, ang / 9)
    c = head_from(b, ang, length)
    a, b, c, d = random_rfss(a, b, c, d)
    return [a, b, c, d]

sketch_eq_trapezoid(*args)

Randomly sample an isosceles trapezoid.

Source code in pyeuclid/formalization/diagram.py
246
247
248
249
250
251
252
253
254
255
256
257
def sketch_eq_trapezoid(self, *args: list[Point]) -> list[Point]:
    """Randomly sample an isosceles trapezoid."""
    a = Point(0.0, 0.0)
    b = Point(1.0, 0.0)
    l = unif(0.5, 2.0)

    height = unif(0.5, 2.0)
    c = Point(0.5 + l / 2.0, height)
    d = Point(0.5 - l / 2.0, height)

    a, b, c, d = random_rfss(a, b, c, d)
    return [a, b, c, d]

sketch_eq_triangle(*args)

Circles defining an equilateral triangle on BC.

Source code in pyeuclid/formalization/diagram.py
259
260
261
262
def sketch_eq_triangle(self, *args: list[Point]) -> list[Circle]:
    """Circles defining an equilateral triangle on BC."""
    b, c = args
    return [Circle(center=b, radius=b.distance(c)), Circle(center=c, radius=b.distance(c))]

sketch_eqangle2(*args)

Point X such that angle ABX equals angle XCB.

Source code in pyeuclid/formalization/diagram.py
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
def sketch_eqangle2(self, *args: list[Point]) -> Point:
    """Point X such that angle ABX equals angle XCB."""
    a, b, c = args

    ba = b.distance(a)
    bc = b.distance(c)
    l = ba * ba / bc

    if unif(0.0, 1.0) < 0.5:
        be = min(l, bc)
        be = unif(be * 0.1, be * 0.9)
    else:
        be = max(l, bc)
        be = unif(be * 1.1, be * 1.5)

    e = b + (c - b) * (be / bc)
    y = b + (a - b) * (be / l)
    return line_line_intersection(Line(c, y), Line(a, e))

sketch_eqdia_quadrangle(*args)

Quadrilateral with equal diagonals.

Source code in pyeuclid/formalization/diagram.py
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
def sketch_eqdia_quadrangle(self, *args) -> list[Point]:
    """Quadrilateral with equal diagonals."""
    m = unif(0.3, 0.7)
    n = unif(0.3, 0.7)
    a = Point(-m, 0.0)
    c = Point(1 - m, 0.0)
    b = Point(0.0, -n)
    d = Point(0.0, 1 - n)

    ang = unif(-0.25 * np.pi, 0.25 * np.pi)
    sin, cos = np.sin(ang), np.cos(ang)
    b = b.rotate(sin, cos)
    d = d.rotate(sin, cos)
    a, b, c, d = random_rfss(a, b, c, d)
    return [a, b, c, d]

sketch_eqdistance(*args)

Circle centered at A with radius BC.

Source code in pyeuclid/formalization/diagram.py
299
300
301
302
def sketch_eqdistance(self, *args) -> Circle:
    """Circle centered at A with radius BC."""
    a, b, c = args
    return Circle(center=a, radius=b.distance(c))

sketch_eqdistance2(*args)

Circle centered at A with radius alpha*BC.

Source code in pyeuclid/formalization/diagram.py
304
305
306
307
def sketch_eqdistance2(self, *args) -> Circle:
    """Circle centered at A with radius alpha*BC."""
    a, b, c, alpha = args
    return Circle(center=a, radius=alpha*b.distance(c))

sketch_eqdistance3(*args)

Circle centered at A with fixed radius alpha.

Source code in pyeuclid/formalization/diagram.py
309
310
311
312
def sketch_eqdistance3(self, *args) -> Circle:
    """Circle centered at A with fixed radius alpha."""
    a, alpha = args
    return Circle(center=a, radius=alpha)

sketch_excenter(*args)

Excenter opposite B in triangle ABC.

Source code in pyeuclid/formalization/diagram.py
341
342
343
344
345
346
def sketch_excenter(self, *args) -> Point:
    """Excenter opposite B in triangle ABC."""
    a, b, c = args
    l1 = self.sketch_angle_bisector(b, a, c)
    l2 = self.sketch_angle_bisector(a, b, c).perpendicular_line(b)
    return line_line_intersection(l1, l2)

sketch_excenter2(*args)

Excenter plus touch points on extended sides.

Source code in pyeuclid/formalization/diagram.py
348
349
350
351
352
353
354
355
def sketch_excenter2(self, *args) -> list[Point]:
    """Excenter plus touch points on extended sides."""
    a, b, c = args
    i = self.sketch_excenter(a, b, c)
    x = i.foot(Line(b, c))
    y = i.foot(Line(c, a))
    z = i.foot(Line(a, b))
    return [x, y, z, i]

sketch_foot(*args)

Foot of perpendicular from A to line BC.

Source code in pyeuclid/formalization/diagram.py
314
315
316
317
318
319
def sketch_foot(self, *args) -> Point:
    """Foot of perpendicular from A to line BC."""
    a, b, c = args
    line_bc = Line(b, c)
    tline = a.perpendicular_line(line_bc)
    return line_line_intersection(tline, line_bc)

sketch_free(*args)

Free point uniformly sampled in a box.

Source code in pyeuclid/formalization/diagram.py
321
322
323
def sketch_free(self, *args) -> Point:
    """Free point uniformly sampled in a box."""
    return Point(unif(-1, 1), unif(-1, 1))

sketch_incenter(*args)

Incenter of triangle ABC.

Source code in pyeuclid/formalization/diagram.py
325
326
327
328
329
330
def sketch_incenter(self, *args) -> Point:
    """Incenter of triangle ABC."""
    a, b, c = args
    l1 = self.sketch_angle_bisector(a, b, c)
    l2 = self.sketch_angle_bisector(b, c, a)
    return line_line_intersection(l1, l2)

sketch_incenter2(*args)

Incenter plus touch points on each side.

Source code in pyeuclid/formalization/diagram.py
332
333
334
335
336
337
338
339
def sketch_incenter2(self, *args) -> list[Point]:
    """Incenter plus touch points on each side."""
    a, b, c = args
    i = self.sketch_incenter(a, b, c)
    x = i.foot(Line(b, c))
    y = i.foot(Line(c, a))
    z = i.foot(Line(a, b))
    return [x, y, z, i]

sketch_intersection_cc(*args)

Two circles centered at O and W through A.

Source code in pyeuclid/formalization/diagram.py
366
367
368
369
def sketch_intersection_cc(self, *args) -> list[Circle]:
    """Two circles centered at O and W through A."""
    o, w, a = args
    return [Circle(center=o, radius=o.distance(a)), Circle(center=w, radius=w.distance(a))]

sketch_intersection_lc(*args)

Line and circle defined by A,O,B for intersection.

Source code in pyeuclid/formalization/diagram.py
371
372
373
374
def sketch_intersection_lc(self, *args) -> list:
    """Line and circle defined by A,O,B for intersection."""
    a, o, b = args
    return [Line(b, a), Circle(center=o, radius=o.distance(b))]

sketch_intersection_ll(*args)

Intersection of lines AB and CD.

Source code in pyeuclid/formalization/diagram.py
376
377
378
379
380
381
def sketch_intersection_ll(self, *args) -> Point:
    """Intersection of lines AB and CD."""
    a, b, c, d = args
    l1 = Line(a, b)
    l2 = Line(c, d)
    return line_line_intersection(l1, l2)

sketch_isquare(*args)

Axis-aligned unit square, randomly re-ordered.

Source code in pyeuclid/formalization/diagram.py
584
585
586
587
588
589
590
591
def sketch_isquare(self, *args) -> list[Point]:
    """Axis-aligned unit square, randomly re-ordered."""
    a = Point(0.0, 0.0)
    b = Point(1.0, 0.0)
    c = Point(1.0, 1.0)
    d = Point(0.0, 1.0)
    a, b, c, d = random_rfss(a, b, c, d)
    return [a, b, c, d]

sketch_on_dia(*args)

Circle with diameter AB.

Source code in pyeuclid/formalization/diagram.py
710
711
712
713
714
def sketch_on_dia(self, *args) -> Circle:
    """Circle with diameter AB."""
    a, b = args
    o = (a + b) * 0.5
    return Circle(o, o.distance(a))

sketch_r_trapezoid(*args)

Right trapezoid with AB horizontal and AD vertical.

Source code in pyeuclid/formalization/diagram.py
518
519
520
521
522
523
524
525
def sketch_r_trapezoid(self, *args) -> list[Point]:
    """Right trapezoid with AB horizontal and AD vertical."""
    a = Point(0.0, 1.0)
    d = Point(0.0, 0.0)
    b = Point(unif(0.5, 1.5), 1.0)
    c = Point(unif(0.5, 1.5), 0.0)
    a, b, c, d = random_rfss(a, b, c, d)
    return [a, b, c, d]

sketch_r_triangle(*args)

Random right triangle with legs on axes.

Source code in pyeuclid/formalization/diagram.py
527
528
529
530
531
532
533
def sketch_r_triangle(self, *args) -> list[Point]:
    """Random right triangle with legs on axes."""
    a = Point(0.0, 0.0)
    b = Point(0.0, unif(0.5, 2.0))
    c = Point(unif(0.5, 2.0), 0.0)
    a, b, c = random_rfss(a, b, c)
    return [a, b, c]

sketch_rectangle(*args)

Axis-aligned rectangle with random width/height.

Source code in pyeuclid/formalization/diagram.py
535
536
537
538
539
540
541
542
543
def sketch_rectangle(self, *args) -> list[Point]:
    """Axis-aligned rectangle with random width/height."""
    a = Point(0.0, 0.0)
    b = Point(0.0, 1.0)
    l = unif(0.5, 2.0)
    c = Point(l, 1.0)
    d = Point(l, 0.0)
    a, b, c, d = random_rfss(a, b, c, d)
    return [a, b, c, d]

sketch_reflect(*args)

Reflect point A across line BC.

Source code in pyeuclid/formalization/diagram.py
545
546
547
548
549
def sketch_reflect(self, *args) -> Point:
    """Reflect point A across line BC."""
    a, b, c = args
    m = a.foot(Line(b, c))
    return m * 2 - a

sketch_risos(*args)

Right isosceles triangle.

Source code in pyeuclid/formalization/diagram.py
551
552
553
554
555
556
557
def sketch_risos(self, *args) -> list[Point]:
    """Right isosceles triangle."""
    a = Point(0.0, 0.0)
    b = Point(0.0, 1.0)
    c = Point(1.0, 0.0)
    a, b, c = random_rfss(a, b, c)
    return [a, b, c]

sketch_s_angle(*args)

Ray at point B making angle alpha with BA.

Source code in pyeuclid/formalization/diagram.py
559
560
561
562
563
564
def sketch_s_angle(self, *args) -> Ray:
    """Ray at point B making angle alpha with BA."""
    a, b, alpha = args
    ang = alpha / 180 * np.pi
    x = b + (a - b).rotatea(ang)
    return Ray(b, x)

sketch_segment(*args)

Random segment endpoints in [-1,1] box.

Source code in pyeuclid/formalization/diagram.py
566
567
568
569
570
def sketch_segment(self, *args) -> list[Point]:
    """Random segment endpoints in [-1,1] box."""
    a = Point(unif(-1, 1), unif(-1, 1))
    b = Point(unif(-1, 1), unif(-1, 1))
    return [a, b]

sketch_shift(*args)

Translate C by vector BA.

Source code in pyeuclid/formalization/diagram.py
572
573
574
575
def sketch_shift(self, *args) -> Point:
    """Translate C by vector BA."""
    c, b, a = args
    return c + (b - a)

sketch_square(*args)

Square constructed on segment AB.

Source code in pyeuclid/formalization/diagram.py
577
578
579
580
581
582
def sketch_square(self, *args) -> list[Point]:
    """Square constructed on segment AB."""
    a, b = args
    c = b + (a - b).rotatea(-np.pi / 2)
    d = a + (b - a).rotatea(np.pi / 2)
    return [c, d]

sketch_trapezoid(*args)

Random trapezoid with AB // CD.

Source code in pyeuclid/formalization/diagram.py
593
594
595
596
597
598
599
600
601
602
603
def sketch_trapezoid(self, *args) -> list[Point]:
    """Random trapezoid with AB // CD."""
    d = Point(0.0, 0.0)
    c = Point(1.0, 0.0)

    base = unif(0.5, 2.0)
    height = unif(0.5, 2.0)
    a = Point(unif(0.2, 0.5), height)
    b = Point(a.x + base, height)
    a, b, c, d = random_rfss(a, b, c, d)
    return [a, b, c, d]

sketch_triangle(*args)

Random triangle.

Source code in pyeuclid/formalization/diagram.py
605
606
607
608
609
610
611
612
def sketch_triangle(self, *args) -> list[Point]:
    """Random triangle."""
    a = Point(0.0, 0.0)
    b = Point(1.0, 0.0)
    ac = unif(0.5, 2.0)
    ang = unif(0.2, 0.8) * np.pi
    c = head_from(a, ang, ac)
    return [a, b, c]

sketch_triangle12(*args)

Triangle with side-length ratios near 1:2.

Source code in pyeuclid/formalization/diagram.py
614
615
616
617
618
619
620
def sketch_triangle12(self, *args) -> list[Point]:
    """Triangle with side-length ratios near 1:2."""
    b = Point(0.0, 0.0)
    c = Point(unif(1.5, 2.5), 0.0)
    a, _ = circle_circle_intersection(Circle(b, 1.0), Circle(c, 2.0))
    a, b, c = random_rfss(a, b, c)
    return [a, b, c]

sketch_trisect(*args)

Trisect angle ABC.

Source code in pyeuclid/formalization/diagram.py
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
def sketch_trisect(self, *args) -> list[Point]:
    """Trisect angle ABC."""
    a, b, c = args
    ang1 = ang_of(b, a)
    ang2 = ang_of(b, c)

    swap = 0
    if ang1 > ang2:
        ang1, ang2 = ang2, ang1
        swap += 1

    if ang2 - ang1 > np.pi:
        ang1, ang2 = ang2, ang1 + 2 * np.pi
        swap += 1

    angx = ang1 + (ang2 - ang1) / 3
    angy = ang2 - (ang2 - ang1) / 3

    x = b + Point(np.cos(angx), np.sin(angx))
    y = b + Point(np.cos(angy), np.sin(angy))

    ac = Line(a, c)
    x = line_line_intersection(Line(b, x), ac)
    y = line_line_intersection(Line(b, y), ac)

    if swap == 1:
        return [y, x]
    return [x, y]

sketch_trisegment(*args)

Trisect segment AB.

Source code in pyeuclid/formalization/diagram.py
704
705
706
707
708
def sketch_trisegment(self, *args) -> list[Point]:
    """Trisect segment AB."""
    a, b = args
    x, y = a + (b - a) * (1.0 / 3), a + (b - a) * (2.0 / 3)
    return [x, y]

Different

Bases: Relation

All provided points must be pairwise distinct.

Source code in pyeuclid/formalization/relation.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
class Different(Relation):
    """All provided points must be pairwise distinct."""

    def __init__(self, *ps: list[Point]):
        """
        Args:
            *ps (Point): Points that must be distinct.
        """
        super().__init__()
        self.ps = ps

    def definition(self):
        """Expand to pairwise inequality relations.

        Returns:
            list[Relation]: Negated equalities for every point pair.
        """
        return [Not(Equal(self.ps[i], self.ps[j])) for i in range(len(self.ps)) for j in range(i + 1, len(self.ps))]

__init__(*ps)

Parameters:

Name Type Description Default
*ps Point

Points that must be distinct.

()
Source code in pyeuclid/formalization/relation.py
150
151
152
153
154
155
156
def __init__(self, *ps: list[Point]):
    """
    Args:
        *ps (Point): Points that must be distinct.
    """
    super().__init__()
    self.ps = ps

definition()

Expand to pairwise inequality relations.

Returns:

Type Description

list[Relation]: Negated equalities for every point pair.

Source code in pyeuclid/formalization/relation.py
158
159
160
161
162
163
164
def definition(self):
    """Expand to pairwise inequality relations.

    Returns:
        list[Relation]: Negated equalities for every point pair.
    """
    return [Not(Equal(self.ps[i], self.ps[j])) for i in range(len(self.ps)) for j in range(i + 1, len(self.ps))]

Equal

Bases: Relation

Point equality relation.

Source code in pyeuclid/formalization/relation.py
89
90
91
92
93
94
95
96
97
98
class Equal(Relation):
    """Point equality relation."""

    def __init__(self, v1: Point, v2: Point):
        super().__init__()
        self.v1, self.v2 = sort_points(v1, v2)

    def permutations(self):
        """Enumerate equivalent orderings of the two points."""
        return [(self.v1, self.v2), (self.v2, self.v1)]

permutations()

Enumerate equivalent orderings of the two points.

Source code in pyeuclid/formalization/relation.py
96
97
98
def permutations(self):
    """Enumerate equivalent orderings of the two points."""
    return [(self.v1, self.v2), (self.v2, self.v1)]

HalfPlane

Numerical HalfPlane.

Source code in pyeuclid/formalization/numericals.py
471
472
473
474
475
476
477
478
479
class HalfPlane:
    """Numerical HalfPlane."""

    def __init__(self, a: Point, b: Point, c: Point, opposingsides=False):
        self.line = Line(b, c)
        assert abs(self.line(a)) > ATOM
        self.sign = self.line.sign(a)
        if opposingsides:
            self.sign = -self.sign

Line

Numerical line.

Source code in pyeuclid/formalization/numericals.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
class Line:
    """Numerical line."""
    def __init__(self,
          p1: Point = None,
          p2: Point = None,
          coefficients: tuple[int, int, int] = None
    ):        
        a, b, c = coefficients or (
            p1.y - p2.y,
            p2.x - p1.x,
            p1.x * p2.y - p2.x * p1.y,
        )

        if a < 0.0 or a == 0.0 and b > 0.0:
            a, b, c = -a, -b, -c

        self.coefficients = a, b, c

    def same(self, other: Line) -> bool:
        a, b, c = self.coefficients
        x, y, z = other.coefficients
        return close_enough(a * y, b * x) and close_enough(b * z, c * y)

    def parallel_line(self, p: Point) -> Line:
        a, b, _ = self.coefficients
        return Line(coefficients=(a, b, -a * p.x - b * p.y))

    def perpendicular_line(self, p: Point) -> Line:
        a, b, _ = self.coefficients
        return Line(p, p + Point(a, b))

    def intersect(self, obj):
        if isinstance(obj, Line):
            return line_line_intersection(self, obj)

        if isinstance(obj, Circle):
            return line_circle_intersection(self, obj)

    def distance(self, p: Point) -> float:
        a, b, c = self.coefficients
        return abs(self(p.x, p.y)) / math.sqrt(a * a + b * b)

    def __call__(self, x: Point, y: Point = None) -> float:
        if isinstance(x, Point) and y is None:
            return self(x.x, x.y)
        a, b, c = self.coefficients
        return x * a + y * b + c

    def is_parallel(self, other: Line) -> bool:
        a, b, _ = self.coefficients
        x, y, _ = other.coefficients
        return abs(a * y - b * x) < ATOM

    def is_perp(self, other: Line) -> bool:
        a, b, _ = self.coefficients
        x, y, _ = other.coefficients
        return abs(a * x + b * y) < ATOM

    def diff_side(self, p1: Point, p2: Point) -> Optional[bool]:
        d1 = self(p1.x, p1.y)
        d2 = self(p2.x, p2.y)
        if abs(d1) < ATOM or abs(d2) < ATOM:
            return None
        return d1 * d2 < 0

    def same_side(self, p1: Point, p2: Point) -> Optional[bool]:
        d1 = self(p1.x, p1.y)
        d2 = self(p2.x, p2.y)
        if abs(d1) < ATOM or abs(d2) < ATOM:
            return None
        return d1 * d2 > 0

    def sign(self, point: Point) -> int:
        s = self(point.x, point.y)
        if s > 0:
            return 1
        elif s < 0:
            return -1
        return 0

    def sample_within(self, points: list[Point], n: int = 5) -> list[Point]:
        """Sample a point within the boundary of points."""
        center = sum(points, Point(0.0, 0.0)) * (1.0 / len(points))
        radius = max([p.distance(center) for p in points])
        if close_enough(center.distance(self), radius):
            center = center.foot(self)
        a, b = line_circle_intersection(self, Circle(center.foot(self), radius))
        result = None
        best = -1.0
        for _ in range(n):
            rand = unif(0.0, 1.0)
            x = a + (b - a) * rand
            mind = min([x.distance(p) for p in points])
            if mind > best:
                best = mind
                result = x
        return [result]

    def sample_within_halfplanes(self, points: list[Point], halfplanes: list[HalfPlane], n: int = 5) -> list[Point]:
        """Sample points on the line within the intersection of half-plane constraints and near existing points."""
        # Parameterize the line: L(t) = P0 + t * d
        # P0 is a point on the line, d is the direction vector
        # # Get the direction vector (dx, dy) of the line
        a, b, c = self.coefficients
        if abs(a) > ATOM and abs(b) > ATOM:
            # General case: direction vector perpendicular to normal vector (a, b)
            d = Point(-b, a)
        elif abs(a) > ATOM:
            # Vertical line 
            d = Point(0, 1)
        elif abs(b) > ATOM:
            # Horizontal line
            d = Point(1, 0)
        else:
            raise ValueError("Invalid line with zero coefficients")

        # Find a point P0 on the line

        if abs(a) > ATOM:
            x0 = (-c - b * 0) / a  # Set y = 0
            y0 = 0
        elif abs(b) > ATOM:
            x0 = 0
            y0 = (-c - a * 0) / b  # Set x = 0
        else:
            raise ValueError("Invalid line with zero coefficients")

        P0 = Point(x0, y0)

        # Project existing points onto the line to get an initial interval
        t_points = []
        for p in points:
            # Vector from P0 to p
            vec = p - P0
            # Project vec onto d
            t = (vec.x * d.x + vec.y * d.y) / (d.x ** 2 + d.y ** 2)
            t_points.append(t)
        if not t_points:
            raise ValueError("No existing points provided for sampling")

        # Determine the interval based on existing points
        t_points.sort()
        t_center = sum(t_points) / len(t_points)
        t_radius = max(abs(t - t_center) for t in t_points)

        # Define an initial interval around the existing points
        t_init_min = t_center - t_radius
        t_init_max = t_center + t_radius

        # Initialize the interval as [t_init_min, t_init_max]
        t_min = t_init_min
        t_max = t_init_max

        # Process half-plane constraints
        for hp in halfplanes:
            # For each half-plane, compute K and H0
            a_h, b_h, c_h = hp.line.coefficients
            sign_h = hp.sign  # +1 or -1
            # Compute K = a_h * dx + b_h * dy
            K = a_h * d.x + b_h * d.y
            # Compute H0 = a_h * x0 + b_h * y0 + c_h
            H0 = a_h * P0.x + b_h * P0.y + c_h
            # The half-plane inequality is sign_h * (K * t + H0) >= 0
            S = sign_h
            if abs(K) < ATOM:
                # K is zero
                if S * H0 >= 0:
                    # The entire line satisfies the constraint
                    continue
                else:
                    # The line is entirely outside the half-plane
                    return []
            else:
                t0 = -H0 / K
                if K * S > 0:
                    # Inequality is t >= t0
                    t_min = max(t_min, t0)
                else:
                    # Inequality is t <= t0
                    t_max = min(t_max, t0)
        # After processing all half-planes, check if the interval is valid
        if t_min > t_max:
            # Empty interval
            return []
        else:
            # The intersection is [t_min, t_max]
            # Sample n points within this interval
            result = None
            best = -1.0
            for _ in range(n):
                t = unif(t_min, t_max)
                p = Point(P0.x + t * d.x, P0.y + t * d.y)
                # Calculate the minimum distance to existing points
                mind = min(p.distance(q) for q in points)
                if mind > best:
                    best = mind
                    result = p
            if result is None:
                raise ValueError("Cannot find a suitable point within the constraints")
            return [result]

sample_within(points, n=5)

Sample a point within the boundary of points.

Source code in pyeuclid/formalization/numericals.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
def sample_within(self, points: list[Point], n: int = 5) -> list[Point]:
    """Sample a point within the boundary of points."""
    center = sum(points, Point(0.0, 0.0)) * (1.0 / len(points))
    radius = max([p.distance(center) for p in points])
    if close_enough(center.distance(self), radius):
        center = center.foot(self)
    a, b = line_circle_intersection(self, Circle(center.foot(self), radius))
    result = None
    best = -1.0
    for _ in range(n):
        rand = unif(0.0, 1.0)
        x = a + (b - a) * rand
        mind = min([x.distance(p) for p in points])
        if mind > best:
            best = mind
            result = x
    return [result]

sample_within_halfplanes(points, halfplanes, n=5)

Sample points on the line within the intersection of half-plane constraints and near existing points.

Source code in pyeuclid/formalization/numericals.py
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
def sample_within_halfplanes(self, points: list[Point], halfplanes: list[HalfPlane], n: int = 5) -> list[Point]:
    """Sample points on the line within the intersection of half-plane constraints and near existing points."""
    # Parameterize the line: L(t) = P0 + t * d
    # P0 is a point on the line, d is the direction vector
    # # Get the direction vector (dx, dy) of the line
    a, b, c = self.coefficients
    if abs(a) > ATOM and abs(b) > ATOM:
        # General case: direction vector perpendicular to normal vector (a, b)
        d = Point(-b, a)
    elif abs(a) > ATOM:
        # Vertical line 
        d = Point(0, 1)
    elif abs(b) > ATOM:
        # Horizontal line
        d = Point(1, 0)
    else:
        raise ValueError("Invalid line with zero coefficients")

    # Find a point P0 on the line

    if abs(a) > ATOM:
        x0 = (-c - b * 0) / a  # Set y = 0
        y0 = 0
    elif abs(b) > ATOM:
        x0 = 0
        y0 = (-c - a * 0) / b  # Set x = 0
    else:
        raise ValueError("Invalid line with zero coefficients")

    P0 = Point(x0, y0)

    # Project existing points onto the line to get an initial interval
    t_points = []
    for p in points:
        # Vector from P0 to p
        vec = p - P0
        # Project vec onto d
        t = (vec.x * d.x + vec.y * d.y) / (d.x ** 2 + d.y ** 2)
        t_points.append(t)
    if not t_points:
        raise ValueError("No existing points provided for sampling")

    # Determine the interval based on existing points
    t_points.sort()
    t_center = sum(t_points) / len(t_points)
    t_radius = max(abs(t - t_center) for t in t_points)

    # Define an initial interval around the existing points
    t_init_min = t_center - t_radius
    t_init_max = t_center + t_radius

    # Initialize the interval as [t_init_min, t_init_max]
    t_min = t_init_min
    t_max = t_init_max

    # Process half-plane constraints
    for hp in halfplanes:
        # For each half-plane, compute K and H0
        a_h, b_h, c_h = hp.line.coefficients
        sign_h = hp.sign  # +1 or -1
        # Compute K = a_h * dx + b_h * dy
        K = a_h * d.x + b_h * d.y
        # Compute H0 = a_h * x0 + b_h * y0 + c_h
        H0 = a_h * P0.x + b_h * P0.y + c_h
        # The half-plane inequality is sign_h * (K * t + H0) >= 0
        S = sign_h
        if abs(K) < ATOM:
            # K is zero
            if S * H0 >= 0:
                # The entire line satisfies the constraint
                continue
            else:
                # The line is entirely outside the half-plane
                return []
        else:
            t0 = -H0 / K
            if K * S > 0:
                # Inequality is t >= t0
                t_min = max(t_min, t0)
            else:
                # Inequality is t <= t0
                t_max = min(t_max, t0)
    # After processing all half-planes, check if the interval is valid
    if t_min > t_max:
        # Empty interval
        return []
    else:
        # The intersection is [t_min, t_max]
        # Sample n points within this interval
        result = None
        best = -1.0
        for _ in range(n):
            t = unif(t_min, t_max)
            p = Point(P0.x + t * d.x, P0.y + t * d.y)
            # Calculate the minimum distance to existing points
            mind = min(p.distance(q) for q in points)
            if mind > best:
                best = mind
                result = p
        if result is None:
            raise ValueError("Cannot find a suitable point within the constraints")
        return [result]

Lt

Bases: Relation

Source code in pyeuclid/formalization/relation.py
81
82
83
84
85
86
class Lt(Relation):
    def __init__(self, v1: Point, v2: Point):
        """Ordering helper used to canonicalize inference rule assignments."""
        super().__init__()
        self.v1 = v1
        self.v2 = v2

__init__(v1, v2)

Ordering helper used to canonicalize inference rule assignments.

Source code in pyeuclid/formalization/relation.py
82
83
84
85
86
def __init__(self, v1: Point, v2: Point):
    """Ordering helper used to canonicalize inference rule assignments."""
    super().__init__()
    self.v1 = v1
    self.v2 = v2

Midpoint

Bases: Relation

p1 is the midpoint of segment p2p3.

Source code in pyeuclid/formalization/relation.py
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
class Midpoint(Relation):
    """p1 is the midpoint of segment p2p3."""

    def __init__(self, p1: Point, p2: Point, p3: Point):
        """
        Args:
            p1 (Point): Candidate midpoint.
            p2 (Point): Segment endpoint.
            p3 (Point): Segment endpoint.
        """
        super().__init__()
        self.p1 = p1
        self.p2, self.p3 = sort_points(p2, p3)

    def definition(self):
        """Midpoint expressed via equal lengths, collinearity, and betweenness.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) - Length(self.p1, self.p3),
            Collinear(self.p1, self.p2, self.p3),
            Different(self.p2, self.p3),
            Between(self.p1, self.p2, self.p3),
        ]

__init__(p1, p2, p3)

Parameters:

Name Type Description Default
p1 Point

Candidate midpoint.

required
p2 Point

Segment endpoint.

required
p3 Point

Segment endpoint.

required
Source code in pyeuclid/formalization/relation.py
291
292
293
294
295
296
297
298
299
300
def __init__(self, p1: Point, p2: Point, p3: Point):
    """
    Args:
        p1 (Point): Candidate midpoint.
        p2 (Point): Segment endpoint.
        p3 (Point): Segment endpoint.
    """
    super().__init__()
    self.p1 = p1
    self.p2, self.p3 = sort_points(p2, p3)

definition()

Midpoint expressed via equal lengths, collinearity, and betweenness.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
302
303
304
305
306
307
308
309
310
311
312
313
def definition(self):
    """Midpoint expressed via equal lengths, collinearity, and betweenness.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) - Length(self.p1, self.p3),
        Collinear(self.p1, self.p2, self.p3),
        Different(self.p2, self.p3),
        Between(self.p1, self.p2, self.p3),
    ]

NotCollinear

Bases: Relation

Points p1,p2,p3 are not collinear.

Source code in pyeuclid/formalization/relation.py
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
class NotCollinear(Relation):
    """Points p1,p2,p3 are not collinear."""

    def __init__(self, p1, p2, p3):
        """
        Args:
            p1 (Point)
            p2 (Point)
            p3 (Point)
        """
        super().__init__()
        self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

    def definition(self):
        """Expand non-collinearity into primitive constraints."""
        return [
            Not(Collinear(self.p1, self.p2, self.p3)),
            Different(self.p1, self.p2, self.p3)
        ]

__init__(p1, p2, p3)

Source code in pyeuclid/formalization/relation.py
270
271
272
273
274
275
276
277
278
def __init__(self, p1, p2, p3):
    """
    Args:
        p1 (Point)
        p2 (Point)
        p3 (Point)
    """
    super().__init__()
    self.p1, self.p2, self.p3 = sort_points(p1, p2, p3)

definition()

Expand non-collinearity into primitive constraints.

Source code in pyeuclid/formalization/relation.py
280
281
282
283
284
285
def definition(self):
    """Expand non-collinearity into primitive constraints."""
    return [
        Not(Collinear(self.p1, self.p2, self.p3)),
        Different(self.p1, self.p2, self.p3)
    ]

OppositeSide

Bases: Relation

Points p1,p2 lie on opposite sides of the line (p3,p4).

Source code in pyeuclid/formalization/relation.py
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
class OppositeSide(Relation):
    """Points p1,p2 lie on opposite sides of the line (p3,p4)."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1 (Point): First query point.
            p2 (Point): Second query point.
            p3 (Point): Line endpoint 1.
            p4 (Point): Line endpoint 2.
        """
        super().__init__()
        self.p1, self.p2 = sort_points(p1, p2)
        self.p3, self.p4 = sort_points(p3, p4)

    def definition(self):
        """Logical expansion expressing opposite-side constraints.

        Returns:
            list[Relation]: Primitive relations defining opposite sides.
        """
        return [
            Not(Collinear(self.p1, self.p3, self.p4)),
            Not(Collinear(self.p2, self.p3, self.p4)),
            Not(SameSide(self.p1, self.p2, self.p3, self.p4)),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1 Point

First query point.

required
p2 Point

Second query point.

required
p3 Point

Line endpoint 1.

required
p4 Point

Line endpoint 2.

required
Source code in pyeuclid/formalization/relation.py
220
221
222
223
224
225
226
227
228
229
230
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1 (Point): First query point.
        p2 (Point): Second query point.
        p3 (Point): Line endpoint 1.
        p4 (Point): Line endpoint 2.
    """
    super().__init__()
    self.p1, self.p2 = sort_points(p1, p2)
    self.p3, self.p4 = sort_points(p3, p4)

definition()

Logical expansion expressing opposite-side constraints.

Returns:

Type Description

list[Relation]: Primitive relations defining opposite sides.

Source code in pyeuclid/formalization/relation.py
232
233
234
235
236
237
238
239
240
241
242
def definition(self):
    """Logical expansion expressing opposite-side constraints.

    Returns:
        list[Relation]: Primitive relations defining opposite sides.
    """
    return [
        Not(Collinear(self.p1, self.p3, self.p4)),
        Not(Collinear(self.p2, self.p3, self.p4)),
        Not(SameSide(self.p1, self.p2, self.p3, self.p4)),
    ]

Parallel

Bases: Relation

Segments (p1,p2) and (p3,p4) are parallel.

Source code in pyeuclid/formalization/relation.py
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
class Parallel(Relation):
    """Segments (p1,p2) and (p3,p4) are parallel."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1, p2 (Point): First segment endpoints.
            p3, p4 (Point): Second segment endpoints.
        """
        super().__init__()
        p1, p2 = sort_points(p1, p2)
        p3, p4 = sort_points(p3, p4)
        self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

    def permutations(self):
        """Enumerate symmetric endpoint permutations preserving segment groups.

        Returns:
            list[tuple[Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p1, self.p2, self.p4, self.p3),
            (self.p2, self.p1, self.p3, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
            (self.p3, self.p4, self.p1, self.p2),
            (self.p4, self.p3, self.p1, self.p2),
            (self.p3, self.p4, self.p2, self.p1),
            (self.p4, self.p3, self.p2, self.p1),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1, p2 Point

First segment endpoints.

required
p3, p4 Point

Second segment endpoints.

required
Source code in pyeuclid/formalization/relation.py
396
397
398
399
400
401
402
403
404
405
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1, p2 (Point): First segment endpoints.
        p3, p4 (Point): Second segment endpoints.
    """
    super().__init__()
    p1, p2 = sort_points(p1, p2)
    p3, p4 = sort_points(p3, p4)
    self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

permutations()

Enumerate symmetric endpoint permutations preserving segment groups.

Returns:

Type Description

list[tuple[Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
def permutations(self):
    """Enumerate symmetric endpoint permutations preserving segment groups.

    Returns:
        list[tuple[Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p1, self.p2, self.p4, self.p3),
        (self.p2, self.p1, self.p3, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
        (self.p3, self.p4, self.p1, self.p2),
        (self.p4, self.p3, self.p1, self.p2),
        (self.p3, self.p4, self.p2, self.p1),
        (self.p4, self.p3, self.p2, self.p1),
    ]

Pentagon

Bases: Relation

Points form a cyclically ordered pentagon.

Source code in pyeuclid/formalization/relation.py
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
class Pentagon(Relation):
    """Points form a cyclically ordered pentagon."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point):
        """
        Args:
            p1, p2, p3, p4, p5 (Point): Vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5 = sort_cyclic_points(p1, p2, p3, p4, p5)

    def permutations(self):
        """Enumerate cyclic and reversed vertex orderings.

        Returns:
            list[tuple[Point, Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4, self.p5),
            (self.p2, self.p3, self.p4, self.p5, self.p1),
            (self.p3, self.p4, self.p5, self.p1, self.p2),
            (self.p4, self.p5, self.p1, self.p2, self.p3),
            (self.p5, self.p1, self.p2, self.p3, self.p4),
            (self.p5, self.p4, self.p3, self.p2, self.p1),
            (self.p4, self.p3, self.p2, self.p1, self.p5),
            (self.p3, self.p2, self.p1, self.p5, self.p4),
            (self.p2, self.p1, self.p5, self.p4, self.p3),
            (self.p1, self.p5, self.p4, self.p3, self.p2),
        ]

__init__(p1, p2, p3, p4, p5)

Parameters:

Name Type Description Default
p1, p2, p3, p4, p5 Point

Vertices.

required
Source code in pyeuclid/formalization/relation.py
500
501
502
503
504
505
506
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point):
    """
    Args:
        p1, p2, p3, p4, p5 (Point): Vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5 = sort_cyclic_points(p1, p2, p3, p4, p5)

permutations()

Enumerate cyclic and reversed vertex orderings.

Returns:

Type Description

list[tuple[Point, Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
def permutations(self):
    """Enumerate cyclic and reversed vertex orderings.

    Returns:
        list[tuple[Point, Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4, self.p5),
        (self.p2, self.p3, self.p4, self.p5, self.p1),
        (self.p3, self.p4, self.p5, self.p1, self.p2),
        (self.p4, self.p5, self.p1, self.p2, self.p3),
        (self.p5, self.p1, self.p2, self.p3, self.p4),
        (self.p5, self.p4, self.p3, self.p2, self.p1),
        (self.p4, self.p3, self.p2, self.p1, self.p5),
        (self.p3, self.p2, self.p1, self.p5, self.p4),
        (self.p2, self.p1, self.p5, self.p4, self.p3),
        (self.p1, self.p5, self.p4, self.p3, self.p2),
    ]

Perpendicular

Bases: Relation

Segments (p1,p2) and (p3,p4) are perpendicular.

Source code in pyeuclid/formalization/relation.py
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
class Perpendicular(Relation):
    """Segments (p1,p2) and (p3,p4) are perpendicular."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1, p2 (Point): First segment endpoints.
            p3, p4 (Point): Second segment endpoints.
        """
        super().__init__()
        p1, p2 = sort_points(p1, p2)
        p3, p4 = sort_points(p3, p4)
        self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

    def permutations(self):
        """Enumerate symmetric endpoint permutations preserving segment groups.

        Returns:
            list[tuple[Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p1, self.p2, self.p4, self.p3),
            (self.p2, self.p1, self.p3, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
            (self.p3, self.p4, self.p1, self.p2),
            (self.p4, self.p3, self.p1, self.p2),
            (self.p3, self.p4, self.p2, self.p1),
            (self.p4, self.p3, self.p2, self.p1),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1, p2 Point

First segment endpoints.

required
p3, p4 Point

Second segment endpoints.

required
Source code in pyeuclid/formalization/relation.py
428
429
430
431
432
433
434
435
436
437
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1, p2 (Point): First segment endpoints.
        p3, p4 (Point): Second segment endpoints.
    """
    super().__init__()
    p1, p2 = sort_points(p1, p2)
    p3, p4 = sort_points(p3, p4)
    self.p1, self.p2, self.p3, self.p4 = sort_point_groups([p1, p2], [p3, p4])

permutations()

Enumerate symmetric endpoint permutations preserving segment groups.

Returns:

Type Description

list[tuple[Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
def permutations(self):
    """Enumerate symmetric endpoint permutations preserving segment groups.

    Returns:
        list[tuple[Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p1, self.p2, self.p4, self.p3),
        (self.p2, self.p1, self.p3, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
        (self.p3, self.p4, self.p1, self.p2),
        (self.p4, self.p3, self.p1, self.p2),
        (self.p3, self.p4, self.p2, self.p1),
        (self.p4, self.p3, self.p2, self.p1),
    ]

Point

Numerical point.

Source code in pyeuclid/formalization/numericals.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
class Point:
    """Numerical point."""

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __lt__(self, other: Point) -> bool:
        return (self.x, self.y) < (other.x, other.y)

    def __gt__(self, other: Point) -> bool:
        return (self.x, self.y) > (other.x, other.y)

    def __add__(self, p: Point) -> Point:
        return Point(self.x + p.x, self.y + p.y)

    def __sub__(self, p: Point) -> Point:
        return Point(self.x - p.x, self.y - p.y)

    def __mul__(self, f: float) -> Point:
        return Point(self.x * f, self.y * f)

    def __rmul__(self, f: float) -> Point:
        return self * f

    def __truediv__(self, f: float) -> Point:
        return Point(self.x / f, self.y / f)

    def __floordiv__(self, f: float) -> Point:
        div = self / f  # true div
        return Point(int(div.x), int(div.y))

    def __str__(self) -> str:
        return "P({},{})".format(self.x, self.y)

    def close(self, point: Point, tol: float = 1e-12) -> bool:
        return abs(self.x - point.x) < tol and abs(self.y - point.y) < tol

    def distance(self, p) -> float:
        if isinstance(p, Line):
            return p.distance(self)
        if isinstance(p, Circle):
            return abs(p.radius - self.distance(p.center))
        dx = self.x - p.x
        dy = self.y - p.y
        return np.sqrt(dx * dx + dy * dy)

    def rotatea(self, ang: float) -> Point:
        sinb, cosb = np.sin(ang), np.cos(ang)
        return self.rotate(sinb, cosb)

    def rotate(self, sinb: float, cosb: float) -> Point:
        x, y = self.x, self.y
        return Point(x * cosb - y * sinb, x * sinb + y * cosb)

    def flip(self) -> Point:
        return Point(-self.x, self.y)

    def foot(self, line: Line) -> Point:
        l = line.perpendicular_line(self)
        return line_line_intersection(l, line)

    def perpendicular_line(self, line: Line) -> Line:
        return line.perpendicular_line(self)

    def parallel_line(self, line: Line) -> Line:
        return line.parallel_line(self)

    def dot(self, other: Point) -> float:
        return self.x * other.x + self.y * other.y

    def sign(self, line: Line) -> int:
        return line.sign(self)

Quadrilateral

Bases: Relation

Points form a cyclically ordered quadrilateral.

Source code in pyeuclid/formalization/relation.py
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
class Quadrilateral(Relation):
    """Points form a cyclically ordered quadrilateral."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1, p2, p3, p4 (Point): Vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4 = sort_cyclic_points(p1, p2, p3, p4)

    def permutations(self):
        """Enumerate cyclic and reversed vertex orderings.

        Returns:
            list[tuple[Point, Point, Point, Point]]: Valid permutations.
        """
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p2, self.p3, self.p4, self.p1),
            (self.p3, self.p4, self.p1, self.p2),
            (self.p4, self.p1, self.p2, self.p3),
            (self.p4, self.p3, self.p2, self.p1),
            (self.p3, self.p2, self.p1, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
            (self.p1, self.p4, self.p3, self.p2),
        ]

    def definition(self):
        """Opposite sides must lie on opposite sides of diagonals.

        Returns:
            list[Relation]: Primitive opposite-side relations.
        """
        return [
            OppositeSide(self.p1, self.p3, self.p2, self.p4),
            OppositeSide(self.p2, self.p4, self.p1, self.p3),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1, p2, p3, p4 Point

Vertices.

required
Source code in pyeuclid/formalization/relation.py
460
461
462
463
464
465
466
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1, p2, p3, p4 (Point): Vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4 = sort_cyclic_points(p1, p2, p3, p4)

definition()

Opposite sides must lie on opposite sides of diagonals.

Returns:

Type Description

list[Relation]: Primitive opposite-side relations.

Source code in pyeuclid/formalization/relation.py
485
486
487
488
489
490
491
492
493
494
def definition(self):
    """Opposite sides must lie on opposite sides of diagonals.

    Returns:
        list[Relation]: Primitive opposite-side relations.
    """
    return [
        OppositeSide(self.p1, self.p3, self.p2, self.p4),
        OppositeSide(self.p2, self.p4, self.p1, self.p3),
    ]

permutations()

Enumerate cyclic and reversed vertex orderings.

Returns:

Type Description

list[tuple[Point, Point, Point, Point]]: Valid permutations.

Source code in pyeuclid/formalization/relation.py
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
def permutations(self):
    """Enumerate cyclic and reversed vertex orderings.

    Returns:
        list[tuple[Point, Point, Point, Point]]: Valid permutations.
    """
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p2, self.p3, self.p4, self.p1),
        (self.p3, self.p4, self.p1, self.p2),
        (self.p4, self.p1, self.p2, self.p3),
        (self.p4, self.p3, self.p2, self.p1),
        (self.p3, self.p2, self.p1, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
        (self.p1, self.p4, self.p3, self.p2),
    ]

Ray

Bases: Line

Numerical ray.

Source code in pyeuclid/formalization/numericals.py
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
class Ray(Line):
    """Numerical ray."""

    def __init__(self, tail: Point, head: Point):
        self.line = Line(tail, head)
        self.coefficients = self.line.coefficients
        self.tail = tail
        self.head = head

    def intersect(self, obj) -> Point:
        if isinstance(obj, (Ray, Line)):
            return line_line_intersection(self.line, obj)

        a, b = line_circle_intersection(self.line, obj)

        if a.close(self.tail):
            return b
        if b.close(self.tail):
            return a

        v = self.head - self.tail
        va = a - self.tail
        vb = b - self.tail

        if v.dot(va) > 0:
            return a
        if v.dot(vb) > 0:
            return b

        raise Exception()

    def sample_within_halfplanes(self, points: list[Point], halfplanes: list[HalfPlane], n: int = 5) -> list[Point]:
        """Sample points on the half-line within the intersection of half-plane constraints and near existing points."""

        # Parameterize the half-line: L(t) = tail + t * d, t >= 0
        d = self.head - self.tail
        d_norm_sq = d.x ** 2 + d.y ** 2
        if d_norm_sq < ATOM:
            raise ValueError("Invalid HalfLine with zero length")

        # Project existing points onto the half-line to get an initial interval
        t_points = []
        for p in points:
            # Vector from tail to p
            vec = p - self.tail
            # Project vec onto d
            t = (vec.x * d.x + vec.y * d.y) / d_norm_sq
            if t >= 0:
                t_points.append(t)
        if not t_points:
            # If no existing points project onto the half-line, define a default interval
            t_init_min = 0
            t_init_max = 1  # For example, length 1 along the half-line
        else:
            # Determine the interval based on existing points
            t_points.sort()
            t_center = sum(t_points) / len(t_points)
            t_radius = max(abs(t - t_center) for t in t_points)
            # Define an initial interval around the existing points
            t_init_min = max(0, t_center - t_radius)
            t_init_max = t_center + t_radius

        # Initialize the interval as [t_init_min, t_init_max]
        t_min = t_init_min
        t_max = t_init_max

        # Process half-plane constraints
        for hp in halfplanes:
            a_h, b_h, c_h = hp.line.coefficients
            sign_h = hp.sign  # +1 or -1

            # Compute K = a_h * dx + b_h * dy
            K = a_h * d.x + b_h * d.y

            # Compute H0 = a_h * tail.x + b_h * tail.y + c_h
            H0 = a_h * self.tail.x + b_h * self.tail.y + c_h

            # The half-plane inequality is sign_h * (K * t + H0) >= 0
            S = sign_h

            if abs(K) < ATOM:
                # K is zero
                if S * H0 >= 0:
                    # The entire half-line satisfies the constraint
                    continue
                else:
                    # The half-line is entirely outside the half-plane
                    return []
            else:
                t0 = -H0 / K
                if K * S > 0:
                    # Inequality is t >= t0
                    if t0 >= 0:
                        t_min = max(t_min, t0)
                    else:
                        t_min = t_min  # t_min remains as is (t >= 0)
                else:
                    # Inequality is t <= t0
                    t_max = min(t_max, t0)
                    if t_max < 0:
                        # Entire interval is before the tail (t < 0), no valid t
                        return []

        # After processing all half-planes, check if the interval is valid
        if t_min > t_max:
            # Empty interval
            return []
        else:
            # The intersection is [t_min, t_max]
            # Ensure t_min >= 0
            t_min = max(t_min, 0)
            if t_min > t_max:
                # No valid t
                return []
            # Sample n points within this interval
            result = None
            best = -1.0
            for _ in range(n):
                t = unif(t_min, t_max)
                p = Point(self.tail.x + t * d.x, self.tail.y + t * d.y)
                # Calculate the minimum distance to existing points
                mind = min(p.distance(q) for q in points)
                if mind > best:
                    best = mind
                    result = p
            if result is None:
                raise ValueError("Cannot find a suitable point within the constraints")
            return [result]

sample_within_halfplanes(points, halfplanes, n=5)

Sample points on the half-line within the intersection of half-plane constraints and near existing points.

Source code in pyeuclid/formalization/numericals.py
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
def sample_within_halfplanes(self, points: list[Point], halfplanes: list[HalfPlane], n: int = 5) -> list[Point]:
    """Sample points on the half-line within the intersection of half-plane constraints and near existing points."""

    # Parameterize the half-line: L(t) = tail + t * d, t >= 0
    d = self.head - self.tail
    d_norm_sq = d.x ** 2 + d.y ** 2
    if d_norm_sq < ATOM:
        raise ValueError("Invalid HalfLine with zero length")

    # Project existing points onto the half-line to get an initial interval
    t_points = []
    for p in points:
        # Vector from tail to p
        vec = p - self.tail
        # Project vec onto d
        t = (vec.x * d.x + vec.y * d.y) / d_norm_sq
        if t >= 0:
            t_points.append(t)
    if not t_points:
        # If no existing points project onto the half-line, define a default interval
        t_init_min = 0
        t_init_max = 1  # For example, length 1 along the half-line
    else:
        # Determine the interval based on existing points
        t_points.sort()
        t_center = sum(t_points) / len(t_points)
        t_radius = max(abs(t - t_center) for t in t_points)
        # Define an initial interval around the existing points
        t_init_min = max(0, t_center - t_radius)
        t_init_max = t_center + t_radius

    # Initialize the interval as [t_init_min, t_init_max]
    t_min = t_init_min
    t_max = t_init_max

    # Process half-plane constraints
    for hp in halfplanes:
        a_h, b_h, c_h = hp.line.coefficients
        sign_h = hp.sign  # +1 or -1

        # Compute K = a_h * dx + b_h * dy
        K = a_h * d.x + b_h * d.y

        # Compute H0 = a_h * tail.x + b_h * tail.y + c_h
        H0 = a_h * self.tail.x + b_h * self.tail.y + c_h

        # The half-plane inequality is sign_h * (K * t + H0) >= 0
        S = sign_h

        if abs(K) < ATOM:
            # K is zero
            if S * H0 >= 0:
                # The entire half-line satisfies the constraint
                continue
            else:
                # The half-line is entirely outside the half-plane
                return []
        else:
            t0 = -H0 / K
            if K * S > 0:
                # Inequality is t >= t0
                if t0 >= 0:
                    t_min = max(t_min, t0)
                else:
                    t_min = t_min  # t_min remains as is (t >= 0)
            else:
                # Inequality is t <= t0
                t_max = min(t_max, t0)
                if t_max < 0:
                    # Entire interval is before the tail (t < 0), no valid t
                    return []

    # After processing all half-planes, check if the interval is valid
    if t_min > t_max:
        # Empty interval
        return []
    else:
        # The intersection is [t_min, t_max]
        # Ensure t_min >= 0
        t_min = max(t_min, 0)
        if t_min > t_max:
            # No valid t
            return []
        # Sample n points within this interval
        result = None
        best = -1.0
        for _ in range(n):
            t = unif(t_min, t_max)
            p = Point(self.tail.x + t * d.x, self.tail.y + t * d.y)
            # Calculate the minimum distance to existing points
            mind = min(p.distance(q) for q in points)
            if mind > best:
                best = mind
                result = p
        if result is None:
            raise ValueError("Cannot find a suitable point within the constraints")
        return [result]

Relation

Base class for logical relations over points.

Source code in pyeuclid/formalization/relation.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class Relation:
    """Base class for logical relations over points."""

    def __init__(self):
        self.negated = False

    def negate(self):
        """Toggle negation flag in-place."""
        self.negated = not self.negated

    def get_points(self):
        """Return all point instances contained in the relation."""
        points = []
        for v in vars(self).values():
            if isinstance(v, Point):
                points.append(v)
            elif isinstance(v, list):
                for p in v:
                    if isinstance(p, Point):
                        points.append(p)
        return points

    def __str__(self):
        """Readable representation, prefixed with Not() when negated."""
        class_name = self.__class__.__name__
        points = self.get_points()
        args_name = ",".join([p.name for p in points])

        if not self.negated:
            return f"{class_name}({args_name})"
        else:
            return f"Not({class_name}({args_name}))"

    def __repr__(self):
        return str(self)

    def __eq__(self, other):
        return str(self) == str(other)

    def __hash__(self):
        return hash(str(self))

__str__()

Readable representation, prefixed with Not() when negated.

Source code in pyeuclid/formalization/relation.py
60
61
62
63
64
65
66
67
68
69
def __str__(self):
    """Readable representation, prefixed with Not() when negated."""
    class_name = self.__class__.__name__
    points = self.get_points()
    args_name = ",".join([p.name for p in points])

    if not self.negated:
        return f"{class_name}({args_name})"
    else:
        return f"Not({class_name}({args_name}))"

get_points()

Return all point instances contained in the relation.

Source code in pyeuclid/formalization/relation.py
48
49
50
51
52
53
54
55
56
57
58
def get_points(self):
    """Return all point instances contained in the relation."""
    points = []
    for v in vars(self).values():
        if isinstance(v, Point):
            points.append(v)
        elif isinstance(v, list):
            for p in v:
                if isinstance(p, Point):
                    points.append(p)
    return points

negate()

Toggle negation flag in-place.

Source code in pyeuclid/formalization/relation.py
44
45
46
def negate(self):
    """Toggle negation flag in-place."""
    self.negated = not self.negated

SameSide

Bases: Relation

Points p1,p2 lie on the same side of the line (p3,p4).

Source code in pyeuclid/formalization/relation.py
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
class SameSide(Relation):
    """Points p1,p2 lie on the same side of the line (p3,p4)."""

    def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
        """
        Args:
            p1 (Point): First query point.
            p2 (Point): Second query point.
            p3 (Point): Line endpoint 1.
            p4 (Point): Line endpoint 2.
        """
        super().__init__()
        self.p1, self.p2 = sort_points(p1, p2)
        self.p3, self.p4 = sort_points(p3, p4)

    def permutations(self):
        """Enumerate symmetric orderings for same-side tests."""
        return [
            (self.p1, self.p2, self.p3, self.p4),
            (self.p1, self.p2, self.p4, self.p3),
            (self.p2, self.p1, self.p3, self.p4),
            (self.p2, self.p1, self.p4, self.p3),
        ]

__init__(p1, p2, p3, p4)

Parameters:

Name Type Description Default
p1 Point

First query point.

required
p2 Point

Second query point.

required
p3 Point

Line endpoint 1.

required
p4 Point

Line endpoint 2.

required
Source code in pyeuclid/formalization/relation.py
195
196
197
198
199
200
201
202
203
204
205
def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point):
    """
    Args:
        p1 (Point): First query point.
        p2 (Point): Second query point.
        p3 (Point): Line endpoint 1.
        p4 (Point): Line endpoint 2.
    """
    super().__init__()
    self.p1, self.p2 = sort_points(p1, p2)
    self.p3, self.p4 = sort_points(p3, p4)

permutations()

Enumerate symmetric orderings for same-side tests.

Source code in pyeuclid/formalization/relation.py
207
208
209
210
211
212
213
214
def permutations(self):
    """Enumerate symmetric orderings for same-side tests."""
    return [
        (self.p1, self.p2, self.p3, self.p4),
        (self.p1, self.p2, self.p4, self.p3),
        (self.p2, self.p1, self.p3, self.p4),
        (self.p2, self.p1, self.p4, self.p3),
    ]

Similar

Bases: Relation

Triangles (p1,p2,p3) and (p4,p5,p6) are similar.

Source code in pyeuclid/formalization/relation.py
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
class Similar(Relation):
    """Triangles (p1,p2,p3) and (p4,p5,p6) are similar."""

    def __init__(
        self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
    ):
        """
        Args:
            p1, p2, p3 (Point): First triangle vertices.
            p4, p5, p6 (Point): Second triangle vertices.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

    def definition(self):
        """Similarity expressed via length ratios and non-collinearity.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) / Length(self.p4, self.p5)
            - Length(self.p2, self.p3) / Length(self.p5, self.p6),
            Length(self.p1, self.p2) / Length(self.p4, self.p5)
            - Length(self.p3, self.p1) / Length(self.p6, self.p4),
            NotCollinear(self.p1, self.p2, self.p3),
        ]

__init__(p1, p2, p3, p4, p5, p6)

Parameters:

Name Type Description Default
p1, p2, p3 Point

First triangle vertices.

required
p4, p5, p6 Point

Second triangle vertices.

required
Source code in pyeuclid/formalization/relation.py
347
348
349
350
351
352
353
354
355
356
def __init__(
    self, p1: Point, p2: Point, p3: Point, p4: Point, p5: Point, p6: Point
):
    """
    Args:
        p1, p2, p3 (Point): First triangle vertices.
        p4, p5, p6 (Point): Second triangle vertices.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5, self.p6 = p1, p2, p3, p4, p5, p6

definition()

Similarity expressed via length ratios and non-collinearity.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
358
359
360
361
362
363
364
365
366
367
368
369
370
def definition(self):
    """Similarity expressed via length ratios and non-collinearity.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) / Length(self.p4, self.p5)
        - Length(self.p2, self.p3) / Length(self.p5, self.p6),
        Length(self.p1, self.p2) / Length(self.p4, self.p5)
        - Length(self.p3, self.p1) / Length(self.p6, self.p4),
        NotCollinear(self.p1, self.p2, self.p3),
    ]

Similar4P

Bases: Relation

Two quadrilaterals are similar.

Source code in pyeuclid/formalization/relation.py
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
class Similar4P(Relation):
    """Two quadrilaterals are similar."""

    def __init__(
        self,
        p1: Point,
        p2: Point,
        p3: Point,
        p4: Point,
        p5: Point,
        p6: Point,
        p7: Point,
        p8: Point,
    ):
        """
        Args:
            p1..p4 (Point): First quadrilateral.
            p5..p8 (Point): Second quadrilateral.
        """
        super().__init__()
        point_map_12 = {p1: p5, p2: p6, p3: p7, p4: p8}
        point_map_21 = {p5: p1, p6: p2, p7: p3, p8: p4}

        sorted_1 = sort_cyclic_points(p1, p2, p3, p4)
        sorted_2 = sort_cyclic_points(p5, p6, p7, p8)
        if compare_names(sorted_1, sorted_2) == 0:
            self.p1, self.p2, self.p3, self.p4 = sorted_1
            self.p5,self.p6,self.p7,self.p8 = point_map_12[self.p1], point_map_12[
                self.p2], point_map_12[self.p3], point_map_12[self.p4]
        else:
            self.p1, self.p2, self.p3, self.p4 = sorted_2
            self.p5, self.p6, self.p7, self.p8 = point_map_21[self.p1], point_map_21[
                self.p2], point_map_21[self.p3], point_map_21[self.p4]

    def definition(self):
        """Similarity expressed via side ratios and angle equalities.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) / Length(self.p5, self.p6)
            - Length(self.p2, self.p3) / Length(self.p6, self.p7),
            Length(self.p1, self.p2) / Length(self.p5, self.p6)
            - Length(self.p3, self.p4) / Length(self.p7, self.p8),
            Length(self.p1, self.p2) / Length(self.p5, self.p6)
            - Length(self.p4, self.p1) / Length(self.p8, self.p5),
            Angle(self.p1, self.p2, self.p3) - Angle(self.p5, self.p6, self.p7),
            Angle(self.p2, self.p3, self.p4) - Angle(self.p6, self.p7, self.p8),
            Angle(self.p3, self.p4, self.p1) - Angle(self.p7, self.p8, self.p5),
            Angle(self.p4, self.p1, self.p2) - Angle(self.p8, self.p5, self.p6),
        ]

__init__(p1, p2, p3, p4, p5, p6, p7, p8)

Parameters:

Name Type Description Default
p1..p4 Point

First quadrilateral.

required
p5..p8 Point

Second quadrilateral.

required
Source code in pyeuclid/formalization/relation.py
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
def __init__(
    self,
    p1: Point,
    p2: Point,
    p3: Point,
    p4: Point,
    p5: Point,
    p6: Point,
    p7: Point,
    p8: Point,
):
    """
    Args:
        p1..p4 (Point): First quadrilateral.
        p5..p8 (Point): Second quadrilateral.
    """
    super().__init__()
    point_map_12 = {p1: p5, p2: p6, p3: p7, p4: p8}
    point_map_21 = {p5: p1, p6: p2, p7: p3, p8: p4}

    sorted_1 = sort_cyclic_points(p1, p2, p3, p4)
    sorted_2 = sort_cyclic_points(p5, p6, p7, p8)
    if compare_names(sorted_1, sorted_2) == 0:
        self.p1, self.p2, self.p3, self.p4 = sorted_1
        self.p5,self.p6,self.p7,self.p8 = point_map_12[self.p1], point_map_12[
            self.p2], point_map_12[self.p3], point_map_12[self.p4]
    else:
        self.p1, self.p2, self.p3, self.p4 = sorted_2
        self.p5, self.p6, self.p7, self.p8 = point_map_21[self.p1], point_map_21[
            self.p2], point_map_21[self.p3], point_map_21[self.p4]

definition()

Similarity expressed via side ratios and angle equalities.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
def definition(self):
    """Similarity expressed via side ratios and angle equalities.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) / Length(self.p5, self.p6)
        - Length(self.p2, self.p3) / Length(self.p6, self.p7),
        Length(self.p1, self.p2) / Length(self.p5, self.p6)
        - Length(self.p3, self.p4) / Length(self.p7, self.p8),
        Length(self.p1, self.p2) / Length(self.p5, self.p6)
        - Length(self.p4, self.p1) / Length(self.p8, self.p5),
        Angle(self.p1, self.p2, self.p3) - Angle(self.p5, self.p6, self.p7),
        Angle(self.p2, self.p3, self.p4) - Angle(self.p6, self.p7, self.p8),
        Angle(self.p3, self.p4, self.p1) - Angle(self.p7, self.p8, self.p5),
        Angle(self.p4, self.p1, self.p2) - Angle(self.p8, self.p5, self.p6),
    ]

Similar5P

Bases: Relation

Two pentagons are similar.

Source code in pyeuclid/formalization/relation.py
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
class Similar5P(Relation):
    """Two pentagons are similar."""

    def __init__(
        self,
        p1: Point,
        p2: Point,
        p3: Point,
        p4: Point,
        p5: Point,
        p6: Point,
        p7: Point,
        p8: Point,
        p9: Point,
        p10: Point,
    ):
        """
        Args:
            p1..p5 (Point): First pentagon.
            p6..p10 (Point): Second pentagon.
        """
        super().__init__()
        self.p1, self.p2, self.p3, self.p4, self.p5, self.p6, self.p7, self.p8, self.p9, self.p10 = p1, p2, p3, p4, p5, p6, p7, p8, p9, p10

    def definition(self):
        """Similarity expressed via consecutive side ratios and angle equalities.

        Returns:
            list[Relation | sympy.Expr]: Derived relations/equations.
        """
        return [
            Length(self.p1, self.p2) / Length(self.p6, self.p7)
            - Length(self.p2, self.p3) / Length(self.p7, self.p8),
            Length(self.p2, self.p3) / Length(self.p7, self.p8)
            - Length(self.p3, self.p4) / Length(self.p8, self.p9),
            Length(self.p3, self.p4) / Length(self.p8, self.p9)
            - Length(self.p4, self.p5) / Length(self.p9, self.p10),
            Length(self.p4, self.p5) / Length(self.p9, self.p10)
            - Length(self.p5, self.p1) / Length(self.p10, self.p6),
            Length(self.p5, self.p1) / Length(self.p10, self.p6)
            - Length(self.p1, self.p2) / Length(self.p6, self.p7),
            Angle(self.p1, self.p2, self.p3) - Angle(self.p6, self.p7, self.p8),
            Angle(self.p2, self.p3, self.p4) - Angle(self.p7, self.p8, self.p9),
            Angle(self.p3, self.p4, self.p5) - Angle(self.p8, self.p9, self.p10),
            Angle(self.p4, self.p5, self.p1) - Angle(self.p9, self.p10, self.p6),
            Angle(self.p5, self.p1, self.p2) - Angle(self.p10, self.p6, self.p7),
        ]

__init__(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10)

Parameters:

Name Type Description Default
p1..p5 Point

First pentagon.

required
p6..p10 Point

Second pentagon.

required
Source code in pyeuclid/formalization/relation.py
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
def __init__(
    self,
    p1: Point,
    p2: Point,
    p3: Point,
    p4: Point,
    p5: Point,
    p6: Point,
    p7: Point,
    p8: Point,
    p9: Point,
    p10: Point,
):
    """
    Args:
        p1..p5 (Point): First pentagon.
        p6..p10 (Point): Second pentagon.
    """
    super().__init__()
    self.p1, self.p2, self.p3, self.p4, self.p5, self.p6, self.p7, self.p8, self.p9, self.p10 = p1, p2, p3, p4, p5, p6, p7, p8, p9, p10

definition()

Similarity expressed via consecutive side ratios and angle equalities.

Returns:

Type Description

list[Relation | sympy.Expr]: Derived relations/equations.

Source code in pyeuclid/formalization/relation.py
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
def definition(self):
    """Similarity expressed via consecutive side ratios and angle equalities.

    Returns:
        list[Relation | sympy.Expr]: Derived relations/equations.
    """
    return [
        Length(self.p1, self.p2) / Length(self.p6, self.p7)
        - Length(self.p2, self.p3) / Length(self.p7, self.p8),
        Length(self.p2, self.p3) / Length(self.p7, self.p8)
        - Length(self.p3, self.p4) / Length(self.p8, self.p9),
        Length(self.p3, self.p4) / Length(self.p8, self.p9)
        - Length(self.p4, self.p5) / Length(self.p9, self.p10),
        Length(self.p4, self.p5) / Length(self.p9, self.p10)
        - Length(self.p5, self.p1) / Length(self.p10, self.p6),
        Length(self.p5, self.p1) / Length(self.p10, self.p6)
        - Length(self.p1, self.p2) / Length(self.p6, self.p7),
        Angle(self.p1, self.p2, self.p3) - Angle(self.p6, self.p7, self.p8),
        Angle(self.p2, self.p3, self.p4) - Angle(self.p7, self.p8, self.p9),
        Angle(self.p3, self.p4, self.p5) - Angle(self.p8, self.p9, self.p10),
        Angle(self.p4, self.p5, self.p1) - Angle(self.p9, self.p10, self.p6),
        Angle(self.p5, self.p1, self.p2) - Angle(self.p10, self.p6, self.p7),
    ]

construct_angle_bisector

Bases: ConstructionRule

Construct the bisector point X of angle ABC.

Source code in pyeuclid/formalization/construction_rule.py
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
@register("AG")
class construct_angle_bisector(ConstructionRule):
    """Construct the bisector point X of angle ABC."""
    def __init__(self, x, a, b, c):
        self.x, self.a, self.b, self.c = x, a, b, c

    def arguments(self):
        return [self.a, self.b, self.c]

    def constructed_points(self):
        return [self.x]

    def conditions(self):
        return [NotCollinear(self.a, self.b, self.c)]

    def conclusions(self):
        return [Angle(self.a, self.b, self.x) - Angle(self.x, self.b, self.c)]

construct_angle_mirror

Bases: ConstructionRule

Construct point X as the mirror of BA across BC.

Source code in pyeuclid/formalization/construction_rule.py
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
@register("AG")
class construct_angle_mirror(ConstructionRule):
    """Construct point X as the mirror of BA across BC."""
    def __init__(self, x, a, b, c):
        self.x, self.a, self.b, self.c = x, a, b, c

    def arguments(self):
        return [self.a, self.b, self.c]

    def constructed_points(self):
        return [self.x]

    def conditions(self):
        return [NotCollinear(self.a, self.b, self.c)]

    def conclusions(self):
        return [Angle(self.a, self.b, self.c) - Angle(self.c, self.b, self.x)]

construct_circle

Bases: ConstructionRule

Construct circle center X equidistant from A, B, C.

Source code in pyeuclid/formalization/construction_rule.py
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
@register("AG")
class construct_circle(ConstructionRule):
    """Construct circle center X equidistant from A, B, C."""
    def __init__(self, x, a, b, c):
        self.x, self.a, self.b, self.c = x, a, b, c

    def arguments(self):
        return [self.a, self.b, self.c]

    def constructed_points(self):
        return [self.x]

    def conditions(self):
        return [NotCollinear(self.a, self.b, self.c)]

    def conclusions(self):
        return [
            Length(self.x, self.a) - Length(self.x, self.b),
            Length(self.x, self.b) - Length(self.x, self.c),
        ]

construct_circumcenter

Bases: ConstructionRule

Construct circumcenter X of triangle ABC.

Source code in pyeuclid/formalization/construction_rule.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
@register("AG")
class construct_circumcenter(ConstructionRule):
    """Construct circumcenter X of triangle ABC."""
    def __init__(self, x, a, b, c):
        self.x, self.a, self.b, self.c = x, a, b, c

    def arguments(self):
        return [self.a, self.b, self.c]

    def constructed_points(self):
        return [self.x]

    def conditions(self):
        return [NotCollinear(self.a, self.b, self.c)]

    def conclusions(self):
        return [
            Length(self.x, self.a) - Length(self.x, self.b),
            Length(self.x, self.b) - Length(self.x, self.c),
        ]

construct_eq_quadrangle

Bases: ConstructionRule

Construct quadrilateral ABCD with equal diagonals.

Source code in pyeuclid/formalization/construction_rule.py
141
142
143
144
145
146
147
148
149
150
151
152
153
@register("AG")
class construct_eq_quadrangle(ConstructionRule):
    """Construct quadrilateral ABCD with equal diagonals."""
    def __init__(self, a, b, c, d):
        self.a, self.b, self.c, self.d = a, b, c, d

    def constructed_points(self):
        return [self.a, self.b, self.c, self.d]

    def conclusions(self):
        return [
            Length(self.a, self.d) - Length(self.b, self.c)
        ]

construct_eq_trapezoid

Bases: ConstructionRule

Construct isosceles trapezoid ABCD (AB ∥ CD).

Source code in pyeuclid/formalization/construction_rule.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
@register("AG")
class construct_eq_trapezoid(ConstructionRule):
    """Construct isosceles trapezoid ABCD (AB ∥ CD)."""
    def __init__(self, a, b, c, d):
        self.a, self.b, self.c, self.d = a, b, c, d

    def constructed_points(self):
        return [self.a, self.b, self.c, self.d]

    def conclusions(self):
        return [
            Length(self.a, self.d) - Length(self.b, self.c),
            Parallel(self.a, self.b, self.c, self.d),
            Angle(self.d, self.a, self.b) - Angle(self.a, self.b, self.c),
            Angle(self.b, self.c, self.d) - Angle(self.c, self.d, self.a),
        ]

construct_eq_triangle

Bases: ConstructionRule

Construct equilateral triangle with vertex X and base BC.

Source code in pyeuclid/formalization/construction_rule.py
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
@register("AG")
class construct_eq_triangle(ConstructionRule):
    """Construct equilateral triangle with vertex X and base BC."""
    def __init__(self, x, b, c):
        self.x, self.b, self.c = x, b, c

    def arguments(self):
        return [self.b, self.c]

    def constructed_points(self):
        return [self.x]

    def conditions(self):
        return [Different(self.b, self.c)]

    def conclusions(self):
        return [
            Length(self.x, self.b) - Length(self.b, self.c),
            Length(self.b, self.c) - Length(self.c, self.x),
            Angle(self.x, self.b, self.c) - Angle(self.b, self.c, self.x),
            Angle(self.c, self.x, self.b) - Angle(self.x, self.b, self.c),
        ]

construct_eqangle2

Bases: ConstructionRule

Construct X so that angle ABX equals angle XCB.

Source code in pyeuclid/formalization/construction_rule.py
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
@register("AG")
class construct_eqangle2(ConstructionRule):
    """Construct X so that angle ABX equals angle XCB."""
    def __init__(self, x, a, b, c):
        self.x, self.a, self.b, self.c = x, a, b, c

    def arguments(self):
        return [self.a, self.b, self.c]

    def constructed_points(self):
        return [self.x]

    def conditions(self):
        return [NotCollinear(self.a, self.b, self.c)]

    def conclusions(self):
        return [Angle(self.b, self.a, self.x) - Angle(self.x, self.c, self.b)]

register

Decorator that registers a construction rule into labeled sets.

Source code in pyeuclid/formalization/construction_rule.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class register:
    """Decorator that registers a construction rule into labeled sets."""

    def __init__(self, *annotations):
        self.annotations = annotations

    def __call__(self, cls):
        for item in self.annotations:
            if not item in construction_rule_sets:
                construction_rule_sets[item] = [cls]
            else:
                construction_rule_sets[item].append(cls)

        def expanded_conditions(self):
            return expand_definition(self._conditions())

        cls._conditions = cls.conditions
        cls.conditions = expanded_conditions
        return cls

Angle(p1, p2, p3)

Symbolic angle at p2.

Returns:

Type Description

sympy.Symbol: Non-negative angle symbol.

Source code in pyeuclid/formalization/relation.py
108
109
110
111
112
113
114
115
def Angle(p1: Point, p2: Point, p3: Point):
    """Symbolic angle at p2.

    Returns:
        sympy.Symbol: Non-negative angle symbol.
    """
    p1, p3 = sort_points(p1, p3)
    return Symbol(f"Angle_{p1}_{p2}_{p3}", non_negative=True)

Area(*ps)

Symbolic polygonal area over an ordered point cycle.

Returns:

Type Description

sympy.Symbol: Positive area symbol.

Source code in pyeuclid/formalization/relation.py
128
129
130
131
132
133
134
135
def Area(*ps: list[Point]):
    """Symbolic polygonal area over an ordered point cycle.

    Returns:
        sympy.Symbol: Positive area symbol.
    """
    ps = sort_cyclic_points(*ps)
    return Symbol("_".join(["Area"] + [str(item) for item in ps]), positive=True)

Length(p1, p2)

Symbolic length between two points.

Returns:

Type Description

sympy.Symbol: Positive length symbol.

Source code in pyeuclid/formalization/relation.py
118
119
120
121
122
123
124
125
def Length(p1: Point, p2: Point):
    """Symbolic length between two points.

    Returns:
        sympy.Symbol: Positive length symbol.
    """
    p1, p2 = sort_points(p1, p2)
    return Symbol(f"Length_{str(p1)}_{str(p2)}", positive=True)

Not(p)

Return a negated shallow copy of a relation.

Source code in pyeuclid/formalization/relation.py
101
102
103
104
105
def Not(p: Relation) -> Relation:
    """Return a negated shallow copy of a relation."""
    other = copy.copy(p)
    other.negate()
    return other

Variable(name)

Free symbolic variable placeholder.

Returns:

Type Description

sympy.Symbol: Dimensionless variable symbol.

Source code in pyeuclid/formalization/relation.py
138
139
140
141
142
143
144
def Variable(name: str):
    """Free symbolic variable placeholder.

    Returns:
        sympy.Symbol: Dimensionless variable symbol.
    """
    return Symbol(f"Variable_{name}")

circle_circle_intersection(c1, c2)

Returns a pair of Points as intersections of c1 and c2.

Source code in pyeuclid/formalization/numericals.py
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
def circle_circle_intersection(c1: Circle, c2: Circle) -> tuple[Point, Point]:
    """Returns a pair of Points as intersections of c1 and c2."""
    # circle 1: (x0, y0), radius r0
    # circle 2: (x1, y1), radius r1
    x0, y0, r0 = c1.center.x, c1.center.y, c1.radius
    x1, y1, r1 = c2.center.x, c2.center.y, c2.radius

    d = math.sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2)
    if d == 0:
        raise Exception()

    a = (r0**2 - r1**2 + d**2) / (2 * d)
    h = r0**2 - a**2
    if h < 0:
        raise Exception()
    h = np.sqrt(h)
    x2 = x0 + a * (x1 - x0) / d
    y2 = y0 + a * (y1 - y0) / d
    x3 = x2 + h * (y1 - y0) / d
    y3 = y2 - h * (x1 - x0) / d
    x4 = x2 - h * (y1 - y0) / d
    y4 = y2 + h * (x1 - x0) / d

    return Point(x3, y3), Point(x4, y4)

line_circle_intersection(line, circle)

Returns a pair of points as intersections of line and circle.

Source code in pyeuclid/formalization/numericals.py
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
def line_circle_intersection(line: Line, circle: Circle) -> tuple[Point, Point]:
    """Returns a pair of points as intersections of line and circle."""
    a, b, c = line.coefficients
    r = float(circle.radius)
    center = circle.center
    p, q = center.x, center.y

    if b == 0:
        x = -c / a
        x_p = x - p
        x_p2 = x_p * x_p
        y = solve_quad(1, -2 * q, q * q + x_p2 - r * r)
        if y is None:
            raise Exception()
        y1, y2 = y
        return (Point(x, y1), Point(x, y2))

    if a == 0:
        y = -c / b
        y_q = y - q
        y_q2 = y_q * y_q
        x = solve_quad(1, -2 * p, p * p + y_q2 - r * r)
        if x is None:
            raise Exception()
        x1, x2 = x
        return (Point(x1, y), Point(x2, y))

    c_ap = c + a * p
    a2 = a * a
    y = solve_quad(
        a2 + b * b, 2 * (b * c_ap - a2 * q), c_ap * c_ap + a2 * (q * q - r * r)
    )
    if y is None:
        raise Exception()
    y1, y2 = y

    return Point(-(b * y1 + c) / a, y1), Point(-(b * y2 + c) / a, y2)

random_rfss(*points)

Random rotate-flip-scale-shift a point cloud.

Source code in pyeuclid/formalization/numericals.py
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
def random_rfss(*points: list[Point]) -> list[Point]:
    """Random rotate-flip-scale-shift a point cloud."""
    # center point cloud.
    average = sum(points, Point(0.0, 0.0)) * (1.0 / len(points))
    points = [p - average for p in points]

    # rotate
    ang = unif(0.0, 2 * np.pi)
    sin, cos = np.sin(ang), np.cos(ang)
    # scale and shift
    scale = unif(0.5, 2.0)
    shift = Point(unif(-1, 1), unif(-1, 1))
    points = [p.rotate(sin, cos) * scale + shift for p in points]

    # randomly flip
    if np.random.rand() < 0.5:
        points = [p.flip() for p in points]

    return points

solve_quad(a, b, c)

Solve a x^2 + bx + c = 0.

Source code in pyeuclid/formalization/numericals.py
513
514
515
516
517
518
519
520
521
def solve_quad(a: float, b: float, c: float) -> tuple[float, float]:
    """Solve a x^2 + bx + c = 0."""
    a = 2 * a
    d = b * b - 2 * a * c
    if d < 0:
        return None  # the caller should expect this result.

    y = math.sqrt(d)
    return (-b - y) / a, (-b + y) / a