| |
| |
| |
| |
|
|
| 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: |
| |
| 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}") |
|
|