laser.emission-witness: the head is placed back only by a sample that read the window open

rapid_place searched the samples up to the first disarmed one, that one
included, and sample_to_disarm read each sample's X after its armed
flag. So the first sample to read the window closed could place the
head back with an X read after the close: a window that closed on the
return leg, up to about one sampling loop before the head was back,
placed the rapid inside the window with a disarm time of 0, and the
`0 <=` on that time could not fail.

rapid_place now searches the samples before the first disarmed one
alone, and sample_to_disarm reads the controller's report (its state
and X) before the sample, so a sample that reads the window open read
its X with the window open. The head stands back for 1 s before the M2,
not 0.5 s: the placement needs a sample whose X and armed flag both
come between the head's return and the close, and a slow sampling loop
needs that time. The judge only gets stricter. No energy or motion path
changes: the longer dwell runs dark after the M5 with the window open,
as the shorter one did.

Proof: tests/test_laser_dwell.py, the Unit tests step of forgetest-ci.yml
(python -B -m unittest discover -s tests), 13 tests OK on Windows with
python 3.14 and on Ubuntu 24.04 with python 3.12:
test_a_window_closed_before_the_head_is_back_does_not_place_the_rapid
gains the close at the first sample with the head back (2, None, 6),
and test_the_sampling_runs_on_past_the_close_and_ends_where_it_stopped
holds the order (each sample's X read before its armed flag).
test_cooling_suite.py (3) passes. Negative controls on scratch copies:
the search with the first disarmed sample included fails the first
case with (2, 6, 6); the report read after the sample fails the second.
emission_witness run whole on a fake clock with only its I/O stubbed
(the review's sweep: the armed flag served mid-GET, X read mid status
report, 40 phases of the rapid against sampling loops of 0.20 to
0.64 s): a window closing 0 to 2 s before the head is back fails the
placement at every phase and every loop, and the normal close passes
at 40 of 40 phases at every loop. The code before this change passed
early closes at some phases up to about one loop early (0.20 s at a
0.20 s loop, 0.5 s at a 0.64 s loop). With the new order and the 0.5 s
dwell, the normal close passed 39 of 40 at a 0.38 s loop and 12 of 40
at a 0.64 s loop. pyflakes reports no undefined name.

Acceptance: laser.emission-witness's fingerprint moves (its own text and
laser.py's shared text), laser.armed-kill and
laser.pause-resume-lid-cancel move through the shared text, and
cooling.flow-under-load and cooling.fail-tier-stop move because they
import from .laser in their own functions; the other 38 do not move
(source_sha compared before and after). Against the campaign this
series already needs, that adds no run: 850498f moved all 43 once.
laser.emission-witness is always run, and the next campaign judges the
placement on the bench.
This commit is contained in:
ScottW514
2026-10-16 22:56:33 -04:00
parent d57421c71d
commit 7b0f065a3f
2 changed files with 33 additions and 20 deletions
+9 -1
View File
@@ -166,7 +166,8 @@ class RapidTailTests(unittest.TestCase):
class Grbl:
def status_report(self):
return {"state": "Idle", "MPos": (0.0, 0.0, 0.0)}
# the time of the read, as the head's X
return {"state": "Idle", "MPos": (clock.now, 0.0, 0.0)}
saved = laser.time, laser.sample
self.addCleanup(setattr, laser, "time", saved[0])
self.addCleanup(setattr, laser, "sample", saved[1])
@@ -177,6 +178,9 @@ class RapidTailTests(unittest.TestCase):
self.assertTrue(all(s["armed"] for s in tail[:-1]))
self.assertGreaterEqual(t_end - closed["t"], 2 * laser.EMISSION_WINDOW_S + laser.EMISSION_READ_GAP_S)
self.assertFalse(covered(laser.read_gaps(tail, "emission", t_end)))
# each sample's X is read before its armed flag: an X that places
# the head was read with the window open
self.assertTrue(all(s["x"] < s["t"] for s in tail))
def place_trail(xs, closes_at):
@@ -195,6 +199,10 @@ class RapidPlaceTests(unittest.TestCase):
# the window closes in the dwell at +20 mm and the head comes back
# after the close: the samples past the close do not place it
self.assertEqual(laser.rapid_place(place_trail(self.XS, 4), 0.0), (2, None, 4))
# the window reads closed at the first sample with the head back:
# that sample does not tell whether the head was back before the
# close, so it does not place it
self.assertEqual(laser.rapid_place(place_trail(self.XS, 6), 0.0), (2, None, 6))
if __name__ == "__main__":