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