Spaces:
Runtime error
Runtime error
feat(v2.0): Phase C — verification_method on all 6 domains, chord fallback=partial [22/22 tests pass]
Browse files- backend/tests/test_pipeline.py +118 -1
- backend/universal_verifier.py +26 -5
backend/tests/test_pipeline.py
CHANGED
|
@@ -61,12 +61,14 @@ class TestVerifierRobustness(unittest.TestCase):
|
|
| 61 |
constraints=["AD is a diameter"],
|
| 62 |
)
|
| 63 |
result = verifier.verify(schema)
|
| 64 |
-
|
|
|
|
| 65 |
center = result.verified.get("center")
|
| 66 |
self.assertIsNotNone(center, "Center must be in verified")
|
| 67 |
self.assertAlmostEqual(center[0], 0.0, places=4)
|
| 68 |
self.assertAlmostEqual(center[1], 5.0, places=4)
|
| 69 |
self.assertAlmostEqual(result.verified.get("radius"), 4.0, places=4)
|
|
|
|
| 70 |
|
| 71 |
def test_algebra_insufficient_data(self):
|
| 72 |
schema = MathProblemSchema(
|
|
@@ -278,3 +280,118 @@ class TestVariablePinner(unittest.TestCase):
|
|
| 278 |
|
| 279 |
if __name__ == "__main__":
|
| 280 |
unittest.main(verbosity=2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
constraints=["AD is a diameter"],
|
| 62 |
)
|
| 63 |
result = verifier.verify(schema)
|
| 64 |
+
# Chord midpoint fallback is always partial — center is unconfirmed without line eqs
|
| 65 |
+
self.assertEqual(result.verification_status, "partial")
|
| 66 |
center = result.verified.get("center")
|
| 67 |
self.assertIsNotNone(center, "Center must be in verified")
|
| 68 |
self.assertAlmostEqual(center[0], 0.0, places=4)
|
| 69 |
self.assertAlmostEqual(center[1], 5.0, places=4)
|
| 70 |
self.assertAlmostEqual(result.verified.get("radius"), 4.0, places=4)
|
| 71 |
+
self.assertEqual(result.verified.get("verification_method"), "chord_midpoint_fallback")
|
| 72 |
|
| 73 |
def test_algebra_insufficient_data(self):
|
| 74 |
schema = MathProblemSchema(
|
|
|
|
| 280 |
|
| 281 |
if __name__ == "__main__":
|
| 282 |
unittest.main(verbosity=2)
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
# ============================================================
|
| 286 |
+
# Phase C — 5-Domain Acceptance Suite
|
| 287 |
+
# Each test verifies: (1) correct result, (2) verification_method field
|
| 288 |
+
# ============================================================
|
| 289 |
+
class TestPhaseCDomainCoverage(unittest.TestCase):
|
| 290 |
+
"""
|
| 291 |
+
Phase C acceptance criterion:
|
| 292 |
+
Each domain must report WHAT was verified AND HOW (verification_method).
|
| 293 |
+
5 exam-style exercises, one per remaining domain.
|
| 294 |
+
"""
|
| 295 |
+
|
| 296 |
+
# 🔴 ALGEBRA: substitution into original equation
|
| 297 |
+
def test_algebra_cubic_roots_and_method(self):
|
| 298 |
+
"""x^3 - 6x^2 + 11x - 6 = 0 → roots [1, 2, 3]"""
|
| 299 |
+
schema = MathProblemSchema(
|
| 300 |
+
problem_type="algebra",
|
| 301 |
+
sub_type="cubic",
|
| 302 |
+
given={"equation": "x**3 - 6*x**2 + 11*x - 6"},
|
| 303 |
+
find=["roots"],
|
| 304 |
+
constraints=[],
|
| 305 |
+
)
|
| 306 |
+
result = verifier.verify(schema)
|
| 307 |
+
self.assertEqual(result.verification_status, "verified")
|
| 308 |
+
solutions = result.verified.get("solutions", [])
|
| 309 |
+
self.assertIn("1", solutions)
|
| 310 |
+
self.assertIn("2", solutions)
|
| 311 |
+
self.assertIn("3", solutions)
|
| 312 |
+
self.assertEqual(result.verified.get("verification_method"), "sympy_solve_substitution")
|
| 313 |
+
print(f" [algebra] method={result.verified.get('verification_method')}")
|
| 314 |
+
|
| 315 |
+
# 🔴 GEOMETRY: diameter line intersection (already covered, here as Phase C entry)
|
| 316 |
+
def test_geometry_circle_diameter_intersection_and_method(self):
|
| 317 |
+
"""y=2x+1 ∩ y=-2x+9 → center (2, 5)"""
|
| 318 |
+
schema = MathProblemSchema(
|
| 319 |
+
problem_type="geometry",
|
| 320 |
+
sub_type="circle",
|
| 321 |
+
given={"diameter_lines": ["y=2*x+1", "y=-2*x+9"]},
|
| 322 |
+
find=["center"],
|
| 323 |
+
constraints=["given lines are diameters"],
|
| 324 |
+
)
|
| 325 |
+
result = verifier.verify(schema)
|
| 326 |
+
self.assertIn(result.verification_status, ["verified", "partial"])
|
| 327 |
+
M = result.verified.get("M")
|
| 328 |
+
self.assertIsNotNone(M)
|
| 329 |
+
self.assertAlmostEqual(M[0], 2.0, places=3)
|
| 330 |
+
self.assertAlmostEqual(M[1], 5.0, places=3)
|
| 331 |
+
self.assertEqual(result.verified.get("verification_method"), "diameter_line_intersection")
|
| 332 |
+
print(f" [geometry] M={M} method={result.verified.get('verification_method')}")
|
| 333 |
+
|
| 334 |
+
# 🟡 PROBABILITY: sum-to-1 check
|
| 335 |
+
def test_probability_three_event_distribution_and_method(self):
|
| 336 |
+
"""P(A)=1/3, P(B)=1/2, P(C)=1/6 → sum=1"""
|
| 337 |
+
schema = MathProblemSchema(
|
| 338 |
+
problem_type="probability",
|
| 339 |
+
sub_type="discrete_distribution",
|
| 340 |
+
given={"probabilities": ["1/3", "1/2", "1/6"]},
|
| 341 |
+
find=["verify distribution is valid"],
|
| 342 |
+
constraints=["A, B, C are mutually exclusive and exhaustive"],
|
| 343 |
+
)
|
| 344 |
+
result = verifier.verify(schema)
|
| 345 |
+
self.assertEqual(result.verification_status, "verified")
|
| 346 |
+
self.assertAlmostEqual(result.verified.get("sum"), 1.0, places=9)
|
| 347 |
+
self.assertEqual(result.verified.get("verification_method"), "sympy_rational_sum")
|
| 348 |
+
print(f" [probability] sum={result.verified.get('sum')} method={result.verified.get('verification_method')}")
|
| 349 |
+
|
| 350 |
+
# 🟡 STATISTICS: mean and std via NumPy
|
| 351 |
+
def test_statistics_exam_scores_and_method(self):
|
| 352 |
+
"""Scores [70, 80, 90, 100] → mean=85, std=11.18"""
|
| 353 |
+
schema = MathProblemSchema(
|
| 354 |
+
problem_type="statistics",
|
| 355 |
+
sub_type="descriptive",
|
| 356 |
+
given={"data": [70, 80, 90, 100]},
|
| 357 |
+
find=["mean", "standard deviation"],
|
| 358 |
+
constraints=[],
|
| 359 |
+
)
|
| 360 |
+
result = verifier.verify(schema)
|
| 361 |
+
self.assertEqual(result.verification_status, "verified")
|
| 362 |
+
self.assertAlmostEqual(result.verified.get("mean"), 85.0, places=4)
|
| 363 |
+
self.assertAlmostEqual(result.verified.get("std"), 11.1803, places=3)
|
| 364 |
+
self.assertEqual(result.verified.get("verification_method"), "numpy_population_stats")
|
| 365 |
+
print(f" [statistics] mean={result.verified.get('mean')} method={result.verified.get('verification_method')}")
|
| 366 |
+
|
| 367 |
+
# 🟢 CALCULUS: derivative at a point
|
| 368 |
+
def test_calculus_polynomial_derivative_and_method(self):
|
| 369 |
+
"""f(x) = x^3 - 4x → f'(2) = 3*4 - 4 = 8"""
|
| 370 |
+
schema = MathProblemSchema(
|
| 371 |
+
problem_type="calculus",
|
| 372 |
+
sub_type="differentiation",
|
| 373 |
+
given={"function": "x**3 - 4*x", "point": 2},
|
| 374 |
+
find=["derivative at x=2"],
|
| 375 |
+
constraints=[],
|
| 376 |
+
)
|
| 377 |
+
result = verifier.verify(schema)
|
| 378 |
+
self.assertEqual(result.verification_status, "verified")
|
| 379 |
+
self.assertAlmostEqual(result.verified.get("derivative_at_point"), 8.0, places=4)
|
| 380 |
+
self.assertEqual(result.verified.get("verification_method"), "sympy_differentiation")
|
| 381 |
+
print(f" [calculus] f'(2)={result.verified.get('derivative_at_point')} method={result.verified.get('verification_method')}")
|
| 382 |
+
|
| 383 |
+
# 🟢 TRIGONOMETRY: identity verification
|
| 384 |
+
def test_trigonometry_pythagorean_identity_and_method(self):
|
| 385 |
+
"""sin(x)**2 + cos(x)**2 = 1 → must simplify to 0 symbolically"""
|
| 386 |
+
schema = MathProblemSchema(
|
| 387 |
+
problem_type="trigonometry",
|
| 388 |
+
sub_type="identity",
|
| 389 |
+
given={"identity": "sin(x)**2 + cos(x)**2 = 1"},
|
| 390 |
+
find=["verify Pythagorean identity"],
|
| 391 |
+
constraints=[],
|
| 392 |
+
)
|
| 393 |
+
result = verifier.verify(schema)
|
| 394 |
+
self.assertEqual(result.verification_status, "verified")
|
| 395 |
+
self.assertTrue(result.verified.get("identity_valid"))
|
| 396 |
+
self.assertEqual(result.verified.get("verification_method"), "sympy_symbolic_simplification")
|
| 397 |
+
print(f" [trig] identity_valid={result.verified.get('identity_valid')} method={result.verified.get('verification_method')}")
|
backend/universal_verifier.py
CHANGED
|
@@ -122,8 +122,12 @@ class UniversalVerifier:
|
|
| 122 |
verified["M"] = (cx, cy)
|
| 123 |
verified["center"] = (cx, cy)
|
| 124 |
verified["center_method"] = "diameter_line_intersection"
|
|
|
|
| 125 |
schema.verified.update(verified)
|
| 126 |
-
logger.info(
|
|
|
|
|
|
|
|
|
|
| 127 |
|
| 128 |
# Verify radius from a known point
|
| 129 |
_compute_radius_from_center(g, center, verified, schema)
|
|
@@ -196,11 +200,12 @@ class UniversalVerifier:
|
|
| 196 |
verified["center"] = (float(cx), float(cy))
|
| 197 |
verified["M"] = (float(cx), float(cy))
|
| 198 |
verified["radius"] = float(computed_radius)
|
| 199 |
-
verified["center_method"] = "
|
|
|
|
| 200 |
schema.verified.update(verified)
|
| 201 |
|
| 202 |
logger.info(
|
| 203 |
-
"📐 [VERIFIER/circle/fallback] midpoint(%s,%s) → center=%s radius=%s",
|
| 204 |
p1_label, p2_label, center, computed_radius,
|
| 205 |
)
|
| 206 |
|
|
@@ -227,7 +232,11 @@ class UniversalVerifier:
|
|
| 227 |
)
|
| 228 |
|
| 229 |
schema.verified.update(verified)
|
| 230 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
|
| 232 |
def _verify_linear(self, schema: MathProblemSchema, g: dict, verified: dict) -> None:
|
| 233 |
schema.warnings.append("Linear geometry verifier: basic — extend as needed")
|
|
@@ -252,7 +261,8 @@ class UniversalVerifier:
|
|
| 252 |
expr = sp.sympify(str(equation_str))
|
| 253 |
solutions = sp.solve(expr, x)
|
| 254 |
verified["solutions"] = [str(s) for s in solutions]
|
| 255 |
-
|
|
|
|
| 256 |
given_solution = g.get("solution")
|
| 257 |
if given_solution is not None:
|
| 258 |
given_val = sp.Rational(given_solution)
|
|
@@ -279,6 +289,8 @@ class UniversalVerifier:
|
|
| 279 |
try:
|
| 280 |
total = sum(sp.Rational(str(p)) for p in probs)
|
| 281 |
verified["sum"] = float(total)
|
|
|
|
|
|
|
| 282 |
if total != 1:
|
| 283 |
schema.warnings.append(
|
| 284 |
f"CONTRADICTION: probabilities sum to {float(total):.4f}, not 1.0"
|
|
@@ -304,6 +316,11 @@ class UniversalVerifier:
|
|
| 304 |
arr = np.array([float(x) for x in data])
|
| 305 |
verified["mean"] = float(np.mean(arr))
|
| 306 |
verified["std"] = float(np.std(arr, ddof=0))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
for key in ("mean", "std"):
|
| 308 |
given_val = g.get(key)
|
| 309 |
if given_val is not None and abs(float(given_val) - verified[key]) > 1e-6:
|
|
@@ -335,6 +352,8 @@ class UniversalVerifier:
|
|
| 335 |
diff = sp.simplify(lhs - rhs)
|
| 336 |
if diff == 0:
|
| 337 |
verified["identity_valid"] = True
|
|
|
|
|
|
|
| 338 |
schema.verification_status = "verified"
|
| 339 |
elif angle_deg is not None:
|
| 340 |
angle_rad = sp.Rational(angle_deg) * sp.pi / 180
|
|
@@ -363,6 +382,8 @@ class UniversalVerifier:
|
|
| 363 |
f = sp.sympify(str(func_str))
|
| 364 |
deriv = sp.diff(f, x)
|
| 365 |
verified["derivative"] = str(deriv)
|
|
|
|
|
|
|
| 366 |
if point_raw is not None:
|
| 367 |
pt = _to_rational(point_raw)
|
| 368 |
val = float(deriv.subs(x, pt))
|
|
|
|
| 122 |
verified["M"] = (cx, cy)
|
| 123 |
verified["center"] = (cx, cy)
|
| 124 |
verified["center_method"] = "diameter_line_intersection"
|
| 125 |
+
verified["verification_method"] = "diameter_line_intersection"
|
| 126 |
schema.verified.update(verified)
|
| 127 |
+
logger.info(
|
| 128 |
+
"✅ [VERIFIER/circle] M = (%s, %s) via %s (method=diameter_line_intersection)",
|
| 129 |
+
cx, cy, method,
|
| 130 |
+
)
|
| 131 |
|
| 132 |
# Verify radius from a known point
|
| 133 |
_compute_radius_from_center(g, center, verified, schema)
|
|
|
|
| 200 |
verified["center"] = (float(cx), float(cy))
|
| 201 |
verified["M"] = (float(cx), float(cy))
|
| 202 |
verified["radius"] = float(computed_radius)
|
| 203 |
+
verified["center_method"] = "chord_midpoint_fallback"
|
| 204 |
+
verified["verification_method"] = "chord_midpoint_fallback"
|
| 205 |
schema.verified.update(verified)
|
| 206 |
|
| 207 |
logger.info(
|
| 208 |
+
"📐 [VERIFIER/circle/fallback] midpoint(%s,%s) → center=%s radius=%s [FALLBACK — status=partial]",
|
| 209 |
p1_label, p2_label, center, computed_radius,
|
| 210 |
)
|
| 211 |
|
|
|
|
| 232 |
)
|
| 233 |
|
| 234 |
schema.verified.update(verified)
|
| 235 |
+
# Chord midpoint fallback is always partial — center is unconfirmed without lines
|
| 236 |
+
if schema.verified.get("center_method") == "chord_midpoint_fallback":
|
| 237 |
+
schema.verification_status = "partial"
|
| 238 |
+
else:
|
| 239 |
+
schema.verification_status = "partial" if schema.null_fields() else "verified"
|
| 240 |
|
| 241 |
def _verify_linear(self, schema: MathProblemSchema, g: dict, verified: dict) -> None:
|
| 242 |
schema.warnings.append("Linear geometry verifier: basic — extend as needed")
|
|
|
|
| 261 |
expr = sp.sympify(str(equation_str))
|
| 262 |
solutions = sp.solve(expr, x)
|
| 263 |
verified["solutions"] = [str(s) for s in solutions]
|
| 264 |
+
verified["verification_method"] = "sympy_solve_substitution"
|
| 265 |
+
logger.info("📐 [VERIFIER/algebra] Solutions: %s (method=sympy_solve_substitution)", solutions)
|
| 266 |
given_solution = g.get("solution")
|
| 267 |
if given_solution is not None:
|
| 268 |
given_val = sp.Rational(given_solution)
|
|
|
|
| 289 |
try:
|
| 290 |
total = sum(sp.Rational(str(p)) for p in probs)
|
| 291 |
verified["sum"] = float(total)
|
| 292 |
+
verified["verification_method"] = "sympy_rational_sum"
|
| 293 |
+
logger.info("📐 [VERIFIER/probability] sum=%s (method=sympy_rational_sum)", float(total))
|
| 294 |
if total != 1:
|
| 295 |
schema.warnings.append(
|
| 296 |
f"CONTRADICTION: probabilities sum to {float(total):.4f}, not 1.0"
|
|
|
|
| 316 |
arr = np.array([float(x) for x in data])
|
| 317 |
verified["mean"] = float(np.mean(arr))
|
| 318 |
verified["std"] = float(np.std(arr, ddof=0))
|
| 319 |
+
verified["verification_method"] = "numpy_population_stats"
|
| 320 |
+
logger.info(
|
| 321 |
+
"📐 [VERIFIER/statistics] mean=%.4f std=%.4f (method=numpy_population_stats)",
|
| 322 |
+
verified["mean"], verified["std"],
|
| 323 |
+
)
|
| 324 |
for key in ("mean", "std"):
|
| 325 |
given_val = g.get(key)
|
| 326 |
if given_val is not None and abs(float(given_val) - verified[key]) > 1e-6:
|
|
|
|
| 352 |
diff = sp.simplify(lhs - rhs)
|
| 353 |
if diff == 0:
|
| 354 |
verified["identity_valid"] = True
|
| 355 |
+
verified["verification_method"] = "sympy_symbolic_simplification"
|
| 356 |
+
logger.info("✅ [VERIFIER/trig] Identity valid (method=sympy_symbolic_simplification)")
|
| 357 |
schema.verification_status = "verified"
|
| 358 |
elif angle_deg is not None:
|
| 359 |
angle_rad = sp.Rational(angle_deg) * sp.pi / 180
|
|
|
|
| 382 |
f = sp.sympify(str(func_str))
|
| 383 |
deriv = sp.diff(f, x)
|
| 384 |
verified["derivative"] = str(deriv)
|
| 385 |
+
verified["verification_method"] = "sympy_differentiation"
|
| 386 |
+
logger.info("📐 [VERIFIER/calculus] f'(x)=%s (method=sympy_differentiation)", deriv)
|
| 387 |
if point_raw is not None:
|
| 388 |
pt = _to_rational(point_raw)
|
| 389 |
val = float(deriv.subs(x, pt))
|