laser_stream_test: the rolloff reference pins the bench-default curve's rendering of S300 end to end

Rule 19 names laser_arm_test.c case P as the proof of the dose curve's
mapping. Case P calls curveComputeValue directly and reads its counts;
it does not reach the wiring from the S word to the stream:
spindleConfig installing the curve as the PWM compute hook, and the
level the hook returns riding the dither into FIRE density. A driver
that left the hook uninstalled passes case P and renders every cut
through the identity mapping, a different delivered dose at every S.

The rolloff reference session (rule 23) already runs one M4 line at
S300 under the shipped curve and reads its cruise density, to compare
against the two-cut run. It now also holds that density to the curve's
own point: 30 percent light through the bench-default curve lands
between its (45 percent density, 21 percent light) and (60, 37)
points, 53.44 percent density, level 68 of 127, density 0.535, held
to 0.02. The expectation is mirrored next to the curved configuration
the way the floor is, so a change to the curve changes it. The
header's rule 19 names the point. No session is added, and no session's
configuration changes.

Proof: the host test grblHAL-glowforge CI runs (the step "Laser stream
emission rules", python3 forgefirm/scripts/bench/laser_stream_test.py
build/grblHAL_glowforge) against the native null-sink build of the
driver at badc832 (-DGFMCODE_WAIT_S=5.0): the rolloff reference cruise
renders 0.5355 against the expected 0.5354, and the whole harness
passes, every session. Negative control, through a scratch copy of the
harness: the same cut with laser_dose_curve = off renders 0.3625 (the
identity mapping, level 46 through the 10 percent floor), 0.17 from
the point, and fails the assertion.

Acceptance: none; the harness is grblHAL's CI's, and src/** of
grblhal-glowforge keeps its covers.
This commit is contained in:
ScottW514
2026-10-15 22:42:49 -04:00
parent f47373a20d
commit 68e9d3fb8e
+17 -3
View File
@@ -82,9 +82,10 @@ over TCP, then checks the dumps against the kernel feeder contract:
is 0 (the floor, not $31, sets the bottom of the S range) and $35 is 0 (the floor, not $31, sets the bottom of the S range) and $35
is the board's floor, landing at PWMSAR_FLOOR_MIN or above is the board's floor, landing at PWMSAR_FLOOR_MIN or above
19. the dose curve's mapping of S onto delivered light is 19. the dose curve's mapping of S onto delivered light is
laser_arm_test.c case P's; every session here but the rolloff ones laser_arm_test.c case P's, and the rolloff reference run (rule 23)
runs with laser_dose_curve = off so its S-to-level arithmetic stays pins one point of it end to end: the S300 cruise renders the density
exact the curve maps 30 % light onto; every other session runs with
laser_dose_curve = off so its S-to-level arithmetic stays exact
20. the corner rolloff starves the slow spots: under M4 with the curve 20. the corner rolloff starves the slow spots: under M4 with the curve
in force, the accelerate-in head of a line renders less density at in force, the accelerate-in head of a line renders less density at
the default gamma of 2 than at gamma 1, and the cruise middle the default gamma of 2 than at gamma 1, and the cruise middle
@@ -309,6 +310,13 @@ DENSITY_CONF_FLOORED = ("laser_power_model = density\n"
"laser_dose_curve = off\n" + DENSITY_CONF_BASE) "laser_dose_curve = off\n" + DENSITY_CONF_BASE)
# The shipped default: the bench curve in force (no keys at all). # The shipped default: the bench curve in force (no keys at all).
DENSITY_CONF_CURVED = "laser_power_model = density\n" + DENSITY_CONF_BASE DENSITY_CONF_CURVED = "laser_power_model = density\n" + DENSITY_CONF_BASE
# Rule 19's end-to-end point: 30 % light (S300) through the bench-default
# curve (glowforge_laser.c curve_default) lands between its (45 % density,
# 21 % light) and (60 %, 37 %) points, 45 + 9/16 x 15 = 53.44 % density,
# level 68 of 127. Mirrored here the way the floor is: changing the curve
# changes this expectation.
CURVE_S300_LEVEL = 68
CURVE_S300_DENSITY = CURVE_S300_LEVEL / float(PWM_PERIOD)
DENSITY_LEVEL = tuple(int(x * PWM_PERIOD / RPM_MAX) for x in LADDER_S) DENSITY_LEVEL = tuple(int(x * PWM_PERIOD / RPM_MAX) for x in LADDER_S)
# A $35 typed ahead of the job: rule 18 says the arm overwrites it. # A $35 typed ahead of the job: rule 18 says the arm overwrites it.
@@ -1531,6 +1539,12 @@ def main():
a, b = rs[0] a, b = rs[0]
mid = rt[(a + b) // 2 - 1000:(a + b) // 2 + 1000] mid = rt[(a + b) // 2 - 1000:(a + b) // 2 + 1000]
dens_ref = sum(1 for t in mid if t & 0x10) / float(len(mid)) dens_ref = sum(1 for t in mid if t & 0x10) / float(len(mid))
# Rule 19: the reference cut is one M4 line at a known S under the
# shipped curve, so its cruise (velocity ratio 1, no rolloff) pins the
# curve's rendering end to end.
if abs(dens_ref - CURVE_S300_DENSITY) > 0.02:
fail("[rolloff-ref] the S300 cruise renders %.3f, expected %.3f through the "
"bench-default curve" % (dens_ref, CURVE_S300_DENSITY))
tt = tick_bytes(two) tt = tick_bytes(two)
ts = fire_spans(tt) ts = fire_spans(tt)
if len(ts) != 1: if len(ts) != 1: