import json import math from pathlib import Path import random import subprocess import sys import unittest from mcm26b.geometry import (Measurement, solve, smallest_enclosing_circle, first_bearing_candidate_disk) from mcm26b.geometry.localization import _enclosing_circle_reference, distance ROOT = Path(__file__).resolve().parents[1] class CertificateTests(unittest.TestCase): def test_incremental_circle_against_exhaustive_reference(self): rng = random.Random(271828) for _ in range(250): points = [(rng.uniform(-20, 20), rng.uniform(-20, 20)) for _ in range(rng.randrange(1, 20))] expected_c, expected_r = _enclosing_circle_reference(points) center, radius = smallest_enclosing_circle(points) self.assertAlmostEqual(radius, expected_r, places=7) self.assertLess(distance(center, expected_c), 1e-6) self.assertTrue(all(distance(center, p) <= radius for p in points)) def test_circle_large_cocircular_duplicate_and_collinear_sets(self): # More than Python's recursion limit, with analytically known answers. points = [(1e6+30*math.cos(i*math.tau/1500), -2e6+30*math.sin(i*math.tau/1500)) for i in range(1500)] center, radius = smallest_enclosing_circle(points+points[:100]) self.assertLess(distance(center, (1e6, -2e6)), 1e-7) self.assertAlmostEqual(radius, 30, places=7) c, r = smallest_enclosing_circle([(i, i*2) for i in range(1500)]) self.assertAlmostEqual(r, 1499*math.sqrt(5)/2) self.assertEqual(c, (749.5, 1499)) self.assertEqual(smallest_enclosing_circle([(3, 4)]*10), ((3, 4), 0)) def test_witness_and_clearance_margin_from_saved_observations(self): path = next((ROOT/'output/q1/practice').glob('*_observations.json')) rows = json.loads(path.read_text(encoding='utf-8'))['measurements'] result = solve([Measurement(**m) for m in rows]) circle = result['diameter_circle'] self.assertFalse(circle['covers']) self.assertIn(circle['witness_vertex'], result['vertices']) self.assertAlmostEqual(distance(circle['witness_vertex'], circle['center'])-circle['radius_m'], circle['max_excess_m']) for i in circle['outside_vertex_indices']: self.assertGreater(distance(result['vertices'][i], circle['center']), circle['radius_m']+circle['coverage_tolerance_m']) cert = result['clearance_certificate'] self.assertGreater(cert['remaining_margin_m'], 7.9) self.assertTrue(result['guaranteed_clearable_at_mec_center']) self.assertAlmostEqual(cert['remaining_margin_m']+cert['radius_with_margin_m'], 20) def test_clearance_at_threshold_is_conservative(self): for length, expected in [(39.999, True), (40, False), (40.001, False)]: r = solve([Measurement(0, 0, 0, 0), Measurement(length, 0, 180, 0)]) self.assertEqual(r['guaranteed_clearable_at_mec_center'], expected) self.assertTrue(r['diameter_circle']['covers']) self.assertIsNone(r['diameter_circle']['witness_vertex']) def test_first_sector_disk_covers_all_angles_and_distances(self): for angle in [0, 359.9, -180]: for delta in [0, 1, 1.005, 45]: m = Measurement(-800, 1200, angle, delta) cert = first_bearing_candidate_disk(m) enclosing = cert['sector_enclosing_circle'] candidate = cert['sufficient_candidate_disk'] for j in range(21): for k in range(11): phi = math.radians(angle-delta+2*delta*j/20) p = (m.x+150*k*math.cos(phi), m.y+150*k*math.sin(phi)) self.assertLessEqual(distance(p, enclosing['center']), enclosing['radius_m']+1e-9) if candidate: for t in range(8): q = (candidate['center'][0]+candidate['radius_m']*math.cos(t*math.tau/8), candidate['center'][1]+candidate['radius_m']*math.sin(t*math.tau/8)) self.assertLessEqual(distance(p, q), 1000+1e-9) if delta <= 1.005: self.assertTrue(cert['candidate_nonempty_certified']) self.assertGreater(candidate['radius_m'], 249.88) def test_candidate_equality_and_no_certificate(self): equality = first_bearing_candidate_disk(Measurement(0, 0, 0, 0), max_range_m=2000) self.assertTrue(equality['candidate_nonempty_certified']) self.assertEqual(equality['sufficient_candidate_disk']['radius_m'], 0) no_certificate = first_bearing_candidate_disk(Measurement(0, 0, 0), max_range_m=3000) self.assertIsNone(no_certificate['sufficient_candidate_disk']) self.assertNotIn('candidate_empty', no_certificate) self.assertTrue(no_certificate['distance_only']) with self.assertRaises(ValueError): first_bearing_candidate_disk(Measurement(0, 0, 0, 46)) def test_cli_explicit_rounding_margin(self): base = [sys.executable, '-m', 'mcm26b.geometry', 'examples/q1/triangle.json'] outputs = [json.loads(subprocess.check_output(base+args, cwd=ROOT, encoding='utf-8')) for args in [[], ['--rounding-margin-deg', '0.005']]] self.assertEqual(outputs[0]['error_policy']['effective_half_angles_deg'], [1, 1, 1]) self.assertEqual(outputs[1]['error_policy']['effective_half_angles_deg'], [1.005]*3) self.assertGreater(outputs[1]['diameter_m'], outputs[0]['diameter_m']) bad = subprocess.run(base+['--rounding-margin-deg', '-1'], cwd=ROOT, capture_output=True) self.assertNotEqual(bad.returncode, 0) if __name__ == '__main__': unittest.main()