dotandru commited on
Commit
5e14f93
·
1 Parent(s): 9e5ea2d

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 CHANGED
@@ -61,12 +61,14 @@ class TestVerifierRobustness(unittest.TestCase):
61
  constraints=["AD is a diameter"],
62
  )
63
  result = verifier.verify(schema)
64
- self.assertEqual(result.verification_status, "verified")
 
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("✅ [VERIFIER/circle] M = (%s, %s) via %s", cx, cy, method)
 
 
 
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"] = "diameter_chord_midpoint"
 
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
- schema.verification_status = "partial" if schema.null_fields() else "verified"
 
 
 
 
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
- logger.info("📐 [VERIFIER/algebra] Solutions: %s", solutions)
 
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))