BuddyMath / constraint_checks.py
dotandru's picture
feat: implement Geometric Sanity Engine (V1.0) and fix project ID in functions
4de02b8
Raw
History Blame Contribute Delete
6.42 kB
# constraint_checks.py - V1.0 (Geometric Constraint Validator)
# Deterministic algebraic checks using SymPy.
# Each function verifies a specific geometric relationship and logs
# the result to the GeometryAnchor (verified_facts or warnings).
import logging
from sympy import simplify, symbols, Eq, solve, Rational
from geometry_types import Point, Line, Segment, Circle, GeometryAnchor
logger = logging.getLogger(__name__)
def check_point_on_circle(p: Point, circle: Circle, anchor: GeometryAnchor):
"""
Verifies algebraically whether point p lies on the circle boundary.
Injects a verified_fact or a warning into the anchor.
"""
try:
if circle.contains(p):
anchor.add_fact(
f"โœ“ ื”ื ืงื•ื“ื” {p} ื ืžืฆืืช ืขืœ ื”ืžืขื’ืœ (ืžืจื›ื– {circle.center}, r={circle.radius})"
)
else:
lhs = (p.x - circle.center.x)**2 + (p.y - circle.center.y)**2
anchor.add_warning(
f"โœ— ื”ื ืงื•ื“ื” {p} ืื™ื ื” ืขืœ ื”ืžืขื’ืœ โ€” "
f"ื—ื™ืฉื•ื‘: {simplify(lhs)} โ‰  rยฒ={simplify(circle.radius**2)}. "
f"ื™ื™ืชื›ืŸ ืฉื”-OCR ื—ื™ืœืฅ ืงื•ืื•ืจื“ื™ื ื˜ื•ืช ืฉื’ื•ื™ื•ืช."
)
except Exception as e:
logger.error(f"[CONSTRAINT] check_point_on_circle failed: {e}")
anchor.add_warning(f"โš ๏ธ ืœื ื ื™ืชืŸ ืœืืžืช ืฉื”ื ืงื•ื“ื” {p.name} ืขืœ ื”ืžืขื’ืœ: {e}")
def check_point_on_line(p: Point, line: Line, anchor: GeometryAnchor):
"""
Verifies algebraically whether point p lies on the line ax + by + c = 0.
"""
try:
if line.contains(p):
anchor.add_fact(
f"โœ“ ื”ื ืงื•ื“ื” {p} ื ืžืฆืืช ืขืœ ื”ื™ืฉืจ {line}"
)
else:
val = simplify(line.a * p.x + line.b * p.y + line.c)
anchor.add_warning(
f"โœ— ื”ื ืงื•ื“ื” {p} ืื™ื ื” ืขืœ ื”ื™ืฉืจ {line} โ€” "
f"ื”ืฆื‘ื” ื ื•ืชื ืช {val} โ‰  0"
)
except Exception as e:
logger.error(f"[CONSTRAINT] check_point_on_line failed: {e}")
anchor.add_warning(f"โš ๏ธ ืœื ื ื™ืชืŸ ืœืืžืช ืฉื”ื ืงื•ื“ื” {p.name} ืขืœ ื”ื™ืฉืจ: {e}")
def check_is_diameter(segment: Segment, circle: Circle, anchor: GeometryAnchor):
"""
Checks if a segment is a diameter by verifying its midpoint equals the circle center.
This is the most critical check for preventing midpoint hallucination.
"""
try:
mid = segment.midpoint()
center = circle.center
if mid == center:
anchor.add_fact(
f"โœ“ ื”ืงื˜ืข {segment.p1.name}{segment.p2.name} ื”ื•ื ืงื•ื˜ืจ โ€” "
f"ืืžืฆืขื• {mid} = ืžืจื›ื– ื”ืžืขื’ืœ {center}"
)
else:
anchor.add_warning(
f"โœ— ื”ืงื˜ืข {segment.p1.name}{segment.p2.name} ืื™ื ื• ืงื•ื˜ืจ โ€” "
f"ืืžืฆืข ื”ืงื˜ืข ื”ื•ื {mid}, ืืš ืžืจื›ื– ื”ืžืขื’ืœ ื”ื•ื {center}. "
f"โš ๏ธ ืืœ ืชื ื™ื— ืฉืžืจื›ื– ื”ืžืขื’ืœ ื”ื•ื ื ืงื•ื“ืช ื”ืืžืฆืข ืฉืœ ืงื˜ืข ื–ื”!"
)
except Exception as e:
logger.error(f"[CONSTRAINT] check_is_diameter failed: {e}")
anchor.add_warning(f"โš ๏ธ ืœื ื ื™ืชืŸ ืœืืžืช ื”ืื ื”ืงื˜ืข {segment} ื”ื•ื ืงื•ื˜ืจ: {e}")
def check_perpendicular_lines(slope1, slope2, anchor: GeometryAnchor, label1="ื™ืฉืจ 1", label2="ื™ืฉืจ 2"):
"""
Verifies that two lines (given by slopes) are perpendicular: m1 * m2 = -1.
"""
try:
product = simplify(slope1 * slope2)
if product == -1:
anchor.add_fact(
f"โœ“ {label1} ื•-{label2} ืžืื•ื ื›ื™ื โ€” ืžื›ืคืœืช ืฉื™ืคื•ืขื™ื = {product} = -1"
)
else:
anchor.add_warning(
f"โœ— {label1} ื•-{label2} ืื™ื ื ืžืื•ื ื›ื™ื โ€” ืžื›ืคืœืช ืฉื™ืคื•ืขื™ื = {product} โ‰  -1"
)
except Exception as e:
logger.error(f"[CONSTRAINT] check_perpendicular_lines failed: {e}")
anchor.add_warning(f"โš ๏ธ ืœื ื ื™ืชืŸ ืœืืžืช ืื ื›ื™ื•ืช: {e}")
def find_circle_center_from_diameters(
d1: Segment,
d2: Segment,
anchor: GeometryAnchor
) -> Point:
"""
Finds the circle center deterministically as the intersection of two diameter segments.
This eliminates any need for the LLM to 'guess' the center.
"""
try:
x, y = symbols('x y')
def line_eq_through(p1: Point, p2: Point):
if simplify(p2.x - p1.x) == 0:
# Vertical line
return Eq(x, p1.x)
slope = Rational(p2.y - p1.y, p2.x - p1.x)
return Eq(y, slope * (x - p1.x) + p1.y)
eq1 = line_eq_through(d1.p1, d1.p2)
eq2 = line_eq_through(d2.p1, d2.p2)
solution = solve([eq1, eq2], [x, y])
if not solution:
anchor.add_warning(
"[SOLVER] ืœื ื ื™ืชืŸ ืœื—ืฉื‘ ืžืจื›ื– ืžืขื’ืœ ืžื—ื™ืชื•ืš ืฉื ื™ ืงื˜ืจื™ื โ€” ื”ื™ืฉืจื™ื ืžืงื‘ื™ืœื™ื?"
)
return None
cx = solution[x] if isinstance(solution, dict) else solution[0]
cy = solution[y] if isinstance(solution, dict) else solution[1]
center = Point(name="M", x=simplify(cx), y=simplify(cy))
anchor.add_fact(
f"โœ“ ืžืจื›ื– ื”ืžืขื’ืœ M ื—ื•ืฉื‘ ื›ื—ื™ืชื•ืš ืงื˜ืจื™ื: M = {center}"
)
return center
except Exception as e:
logger.error(f"[CONSTRAINT] find_circle_center_from_diameters failed: {e}")
anchor.add_warning(f"โš ๏ธ ื—ื™ืฉื•ื‘ ืžืจื›ื– ืžืžืขื’ืœ ื ื›ืฉืœ: {e}")
return None
def check_distance(p1: Point, p2: Point, expected_distance, anchor: GeometryAnchor, label=""):
"""
Verifies the distance between two points matches an expected value.
"""
try:
actual = simplify(p1.distance_to(p2))
expected = simplify(expected_distance)
desc = label or f"{p1.name}โ†”{p2.name}"
if simplify(actual - expected) == 0:
anchor.add_fact(f"โœ“ ื”ืžืจื—ืง {desc} = {actual} (ืชื•ืื ืœื ืชื•ืŸ)")
else:
anchor.add_warning(
f"โœ— ื”ืžืจื—ืง {desc}: ื—ื™ืฉื•ื‘={actual} โ‰  ื ืชื•ืŸ={expected}"
)
except Exception as e:
logger.error(f"[CONSTRAINT] check_distance failed: {e}")
anchor.add_warning(f"โš ๏ธ ื‘ื“ื™ืงืช ืžืจื—ืง ื ื›ืฉืœื”: {e}")