108 lines
5.8 KiB
Python
108 lines
5.8 KiB
Python
import json
|
|
import math
|
|
from pathlib import Path
|
|
import random
|
|
import subprocess
|
|
import sys
|
|
import unittest
|
|
|
|
from geometry import Measurement, solve, smallest_enclosing_circle, first_bearing_candidate_disk
|
|
from 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', '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()
|