Retain aircraft through native landing and qualify bounded input diagnostics

This commit is contained in:
2026-09-19 20:34:18 +02:00
parent f870443b9b
commit 2fba12f987
129 changed files with 50369 additions and 88 deletions
+104 -3
View File
@@ -21,7 +21,7 @@ class Scene:
self.position=[98.5,0.,98.5];self.angle=0.;self.speed=0.;self.held=False
self.key_events=[];self.turns=[];self.notifications=[];self.focus=True
self.gain=.085;self.acceleration=8.;self.braking=8.;self.move_scale=1.
self.guard_cost=.18;self.ignore_turn=False;self.wrong_turn=False
self.guard_cost=.22;self.ignore_turn=False;self.wrong_turn=False
self.turn_delay=0.;self.pending_turns=[]
self.stale=False;self.no_movement=False;self.changed_geometry=False
self.context_lost=False;self.fail_down=False;self.coast_events=[]
@@ -149,7 +149,7 @@ class ApproachTests(unittest.TestCase):
scene=Scene();original=scene.send
def slow(key,down):
original(key,down)
if down:scene.guard_cost=.4
if down:scene.guard_cost=.41
scene.send=slow
with self.assertRaisesRegex(policy.Refused,'400ms'):scene.run()
self.assertFalse(scene.held);self.assertEqual([v[1] for v in scene.key_events],[True,False])
@@ -163,7 +163,11 @@ class ApproachTests(unittest.TestCase):
geometry['gate'][0]+=1
with self.assertRaisesRegex(policy.Refused,'placement'):policy.target_for(geometry)
def test_delayed_native_rotation_is_not_confused_with_unchanged_settling(self):
scene=Scene();scene.turn_delay=.4
scene=Scene();scene.turn_delay=.4;scene.angle=110.
scene.sample=scene.snapshot();scene.history=[copy.deepcopy(scene.sample)]
# A shorter initial heading isolates delayed native turn acknowledgement.
scene.position=[100.,0.,97.5]
scene.sample=scene.snapshot();scene.history=[copy.deepcopy(scene.sample)]
result=scene.run()
self.assertTrue(result['completed'])
self.assertTrue(all(m['response_sample'] > m['after_sample'] for m in result['motions']))
@@ -224,5 +228,102 @@ class ApproachTests(unittest.TestCase):
with self.assertRaises(policy.Refused):scene.run(calibration=c)
self.assertEqual(scene.turns,[])
def first_pulse_with_guard_cost(self, cost):
scene=Scene();scene.no_movement=True;original=scene.send
def send(key,down):
original(key,down)
if down:scene.guard_cost=cost
scene.send=send
with self.assertRaises(policy.Refused):scene.run()
self.assertFalse(scene.held)
self.assertEqual([event[1] for event in scene.key_events],[True,False])
return scene
def test_slow_release_guard_uses_initial_hold_without_extra_sleep(self):
scene=self.first_pulse_with_guard_cost(.20)
down,up=scene.key_events
self.assertAlmostEqual(up[2]-down[2],.20,places=6)
timing=scene.report['approach_proof']['release_timings'][0]
self.assertFalse(timing['cleanup']);self.assertEqual(len(timing['guards']),1)
self.assertAlmostEqual(timing['guards'][0]['entered_monotonic'],down[2])
self.assertAlmostEqual(timing['guards'][0]['returned_monotonic'],up[2])
self.assertAlmostEqual(timing['up_attempted_monotonic'],up[2])
self.assertAlmostEqual(timing['up_returned_monotonic'],up[2])
def test_fast_guard_waits_only_remainder_then_full_fresh_guard(self):
scene=self.first_pulse_with_guard_cost(.01)
down,up=scene.key_events
self.assertAlmostEqual(up[2]-down[2],.06,places=6)
timing=scene.report['approach_proof']['release_timings'][0]
first,second=timing['guards']
self.assertAlmostEqual(first['entered_monotonic'],down[2])
self.assertAlmostEqual(first['returned_monotonic']-down[2],.01,places=6)
self.assertAlmostEqual(second['entered_monotonic']-down[2],.05,places=6)
self.assertAlmostEqual(second['returned_monotonic'],timing['up_attempted_monotonic'])
def test_guard_refusal_records_attempt_then_finally_rechecks_before_release(self):
scene=Scene();original=scene.check;held_checks=0
def check():
nonlocal held_checks
if scene.held:
held_checks+=1
if held_checks==1:
scene.sleep(.02)
raise policy.Refused('first owned release guard refused')
original()
scene.check=check
with self.assertRaisesRegex(policy.Refused,'release guard refused'):scene.run()
proof=scene.report['approach_proof'];first,cleanup=proof['release_timings']
self.assertIn('refused',first['guards'][0]['error'])
self.assertIsNone(first['up_attempted_monotonic'])
self.assertTrue(cleanup['cleanup']);self.assertEqual(len(cleanup['guards']),1)
self.assertIsNotNone(cleanup['up_returned_monotonic'])
self.assertFalse(scene.held);self.assertIsNone(proof['unconfirmed_held_key'])
self.assertEqual([event[1] for event in scene.key_events],[True,False])
def test_fast_guard_cannot_authorize_release_after_wait_and_focus_loss(self):
scene=Scene();original_send=scene.send;original_check=scene.check;checks=0
def send(key,down):
original_send(key,down)
if down:scene.guard_cost=.01
def check():
nonlocal checks
if scene.held:
checks+=1
if checks>=2:scene.focus=False
original_check()
scene.send=send;scene.check=check
with self.assertRaisesRegex(policy.Refused,'focus lost'):scene.run()
proof=scene.report['approach_proof'];first,cleanup=proof['release_timings']
self.assertEqual(len(first['guards']),2)
self.assertIn('focus lost',first['guards'][1]['error'])
self.assertIsNone(first['up_attempted_monotonic'])
self.assertIsNone(cleanup['up_attempted_monotonic'])
self.assertEqual([event[1] for event in scene.key_events],[True])
self.assertEqual(proof['unconfirmed_held_key'],'W')
self.assertTrue(proof['paired_release_errors'])
def test_shorter_pulses_do_not_expand_original_pulse_budget(self):
scene=Scene();scene.guard_cost=.18 # Original synthetic scene now takes smaller steps.
with self.assertRaisesRegex(policy.Refused,'pulse budget'):scene.run()
proof=scene.report['approach_proof']
self.assertEqual(len(scene.key_events),24)
self.assertEqual(len(proof['release_timings']),12)
self.assertTrue(all(len(timing['guards'])==1 for timing in proof['release_timings']))
self.assertFalse(proof['completed']);self.assertFalse(scene.held)
self.assertEqual(proof['deadline_monotonic'],35)
def test_measured_stopping_deadzone_is_still_refused(self):
scene=Scene();scene.position=[102.7,0.,97.3];scene.angle=180.;scene.guard_cost=.33
scene.sample=scene.snapshot();scene.history=[copy.deepcopy(scene.sample)]
with self.assertRaisesRegex(policy.Refused,'measured stopping reserve'):scene.run()
proof=scene.report['approach_proof'];self.assertEqual(len(proof['stops']),1)
target,_=policy.target_for(scene.geometry)
remaining=policy.horizontal(scene.position,target)
self.assertGreater(remaining,.75)
self.assertLessEqual(remaining,proof['stops'][0]['distance']+.25)
self.assertEqual([event[1] for event in scene.key_events],[True,False])
self.assertFalse(proof['completed']);self.assertFalse(scene.held)
if __name__=='__main__':unittest.main()