File size: 4,699 Bytes
eab734a | 1 2 3 4 5 6 7 8 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 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 | """Typed intermediate representation for plane geometry diagrams."""
from __future__ import annotations
from enum import Enum
from typing import Annotated, Literal
from pydantic import BaseModel, Field, model_validator
class PointDef(BaseModel):
name: str
"""Optional hint used only for schematic/initial placement."""
hint: tuple[float, float] | None = None
class SegmentDef(BaseModel):
a: str
b: str
draw: bool = True
class LineDef(BaseModel):
a: str
b: str
draw: bool = False
"""If true, draw a longer line through A and B rather than only the segment."""
class CircleDef(BaseModel):
id: str
center: str | None = None
radius: float | None = None
"""Circle defined by three points on the circumference."""
through: list[str] | None = None
draw: bool = True
@model_validator(mode="after")
def _check_definition(self) -> CircleDef:
has_center = self.center is not None
has_through = self.through is not None and len(self.through) >= 3
if not has_center and not has_through:
raise ValueError(f"Circle {self.id!r} needs center or through points")
if has_through and len(self.through or []) < 3:
raise ValueError(f"Circle {self.id!r} through needs at least 3 points")
return self
class AngleMarkDef(BaseModel):
"""Mark angle at vertex with rays through a and b (order a-vertex-b)."""
vertex: str
a: str
b: str
right_angle: bool = False
label: str | None = None
class LabelDef(BaseModel):
point: str
text: str | None = None
"""TikZ anchor hint, e.g. above, below left."""
position: str = "above right"
# --- Constraints ---
class EqualLength(BaseModel):
type: Literal["equal_length"] = "equal_length"
a1: str
a2: str
b1: str
b2: str
class Length(BaseModel):
type: Literal["length"] = "length"
a: str
b: str
value: float
class EqualAngle(BaseModel):
type: Literal["equal_angle"] = "equal_angle"
a1: str
v1: str
b1: str
a2: str
v2: str
b2: str
class AngleMeasure(BaseModel):
type: Literal["angle_measure"] = "angle_measure"
a: str
vertex: str
b: str
degrees: float
class Perpendicular(BaseModel):
type: Literal["perpendicular"] = "perpendicular"
a1: str
a2: str
b1: str
b2: str
class Parallel(BaseModel):
type: Literal["parallel"] = "parallel"
a1: str
a2: str
b1: str
b2: str
class OnLine(BaseModel):
type: Literal["on_line"] = "on_line"
point: str
a: str
b: str
class OnCircle(BaseModel):
type: Literal["on_circle"] = "on_circle"
point: str
circle: str
class Midpoint(BaseModel):
type: Literal["midpoint"] = "midpoint"
point: str
a: str
b: str
class Collinear(BaseModel):
type: Literal["collinear"] = "collinear"
points: list[str]
@model_validator(mode="after")
def _need_three(self) -> Collinear:
if len(self.points) < 3:
raise ValueError("collinear needs at least 3 points")
return self
class Intersection(BaseModel):
"""Point is intersection of lines AB and CD."""
type: Literal["intersection"] = "intersection"
point: str
a: str
b: str
c: str
d: str
Constraint = Annotated[
EqualLength
| Length
| EqualAngle
| AngleMeasure
| Perpendicular
| Parallel
| OnLine
| OnCircle
| Midpoint
| Collinear
| Intersection,
Field(discriminator="type"),
]
class GeometryIR(BaseModel):
"""Formal scene: objects, constraints, and draw directives."""
statement: str = ""
points: list[PointDef]
segments: list[SegmentDef] = Field(default_factory=list)
lines: list[LineDef] = Field(default_factory=list)
circles: list[CircleDef] = Field(default_factory=list)
angle_marks: list[AngleMarkDef] = Field(default_factory=list)
labels: list[LabelDef] = Field(default_factory=list)
constraints: list[Constraint] = Field(default_factory=list)
def point_names(self) -> list[str]:
return [p.name for p in self.points]
def ensure_labels(self) -> None:
existing = {lb.point for lb in self.labels}
for p in self.points:
if p.name not in existing:
self.labels.append(LabelDef(point=p.name, text=p.name))
class SolveMode(str, Enum):
exact = "exact"
schematic = "schematic"
failed = "failed"
class SolvedScene(BaseModel):
ir: GeometryIR
coordinates: dict[str, tuple[float, float]]
mode: SolveMode
max_residual: float
residuals: list[float] = Field(default_factory=list)
message: str = ""
|