[P2] VPM-B no-stop search checks only the current state and a 240-minute hold, so it reports no limit (>240) right before a heliox switch transient forces NDL to 0 #64

Open
opened 2026-10-31 19:26:42 +00:00 by pedro · 0 comments
Owner

Priority: P2
Type: bug
Source: full codebase review requested 2026-10-31 (independently reproduced against 2cac7cf)

Location: src/models/vpmb.ts:656, src/models/vpmb.ts:658, src/models/vpmb.ts:662, src/models/vpmb.ts:668, src/models/projection.ts:49

Problem and evidence

findNoStopLimit (src/models/vpmb.ts:601-670) returns 0 if the current state is not clear. Otherwise it returns null (no limit, shown as >240) when a 240-minute hold probe is clear. If that probe is not clear, it runs a 16-step bisection on [0, 240]. Both steps assume clearance is monotone in hold time. After switching from air to a helium-rich mix, helium counterdiffusion causes a temporary rise in the VPM-B unrelaxed bubble ceiling. Short holds therefore fail the clear() probe: planAscent returns "Current depth is shallower than the unrelaxed bubble ceiling", and clear() treats that as not clear for non-current states. Later holds pass again. The search skips that interval, so the projection shows NDL = null right before NDL drops to 0 within 0.25-1 min. The ZHL engines got an interval-certified chronological search for #48. docs/MODEL-ARCHITECTURE.md:15 says that work "do[es] not change VPM's separate no-stop planner", so VPM-B still has the old monotone assumption. Correction to the original claim: I could not reproduce its first example (air 25 m/15 min, then Heliox 21/79 at 12 m). Its VPM-B NDL fell steadily from 48.6 with no zero window. In every case I reproduced, the transient appears as a null (>240) NDL, not a finite NDL. The ndl=0 holds that follow mostly carry the "Current depth is shallower than the unrelaxed bubble ceiling" error, not a stop list. The bisection could in principle also hide a transient behind a finite NDL, but my scan found no such case.

I wrote scratch scripts in /tmp/divesim-verify-J13TVq (t.mts to t4.mts) and ran them with node --import tsx against the real source. They use createSimulation("VPM-B", c).advance with 20 m/min descent and 9 m/min ascent, then switchGas to gasFromFractions(21,79), then calculateProjection({model:"VPM-B", ascent:9, spacing:3, ...}) after holds at the switch depth.

  • Air 15 m/45 min, Heliox at 4.5 m, conservatism 3: NDL at the switch = null. At holds of 0.1 and 0.25 min it is still null; the unrelaxed ceiling is 4.00 m at the switch, 4.11 at 0.1 min and 4.28 at 0.25 min. From 0.4 to 6 min, ndl = 0 with plan.error "Current depth is shallower than the unrelaxed bubble ceiling..." and the ceiling peaks at 4.94 m, above the 4.5 m depth. At 7 min and later, ndl = null again.
  • Air 30 m/10 min, Heliox at 9 m, conservatism 3: NDL at the switch = null, but at 0.5 min ndl = 0. Holds of 0.5-3.75 min are all 0 before NDL returns to null. At conservatism 2: null at 0 and 0.5 min, 0 at 1-3 min, null again at 4 min.
  • Other reproducing profiles: 25 m/10 min to 6 m (0 for 0.25-3.75 min), 18 m/30 min to 6 m (0.25-5.25 min) and 15 m/30 min to 4.5 m (0.5-4.25 min). In all of them NDL at the switch was null.
    Heliox and trimix are user-configurable in the live app (App.tsx:2473, "Configure trimix/heliox, multiple gases, and ascent switches"), so these profiles can be reached through the UI.

Suggested correction

Replace the monotone bisection in findNoStopLimit with a time-ordered search, as the ZHL fix for #48 does. Return the first hold time at which clear() fails, scanning from 0 upward, and never return null or move the lower bound past an interval whose clearance has not been proven. Without an analytic VPM envelope, a bounded scan works. Probe a geometric or fixed grid of short holds (for example every 10 s for the first 10-15 min after a gas change, then coarser), stop at the first failure, then bisect only between the last clear probe and that first failure. Treat a clear 240-minute probe as "no limit" only when every grid probe before it is also clear. Keep the existing 24-hour-horizon handling, where a probe beyond the horizon counts as needing stops.

Acceptance criteria

Add VPM-B cases to tests/unit/ndl-transient.test.ts:
(a) Air 15 m/45 min, Heliox 21/79 at 4.5 m, conservatism 3: the NDL at the switch must be finite, between 0.25 and 0.4 min. Holding for that NDL must leave clear() true, and holding NDL + a small epsilon must be not clear.
(b) Air 30 m/10 min, Heliox at 9 m, conservatism 3: the NDL must be below 0.5 min and not null.
(c) After holding 0.5 min in (b), the remaining NDL must equal the original NDL minus 0.5, or 0 when that is negative.
(d) Single-gas air sweeps must give the same NDLs as before, within the bisection precision.
(e) The worst-case search cost must stay bounded, with a probe cap similar to the ZHL search's 256 projections. pnpm verify passes.

Scope: maintenance of the educational simulator. Preserve historical R1 acceptance and operational-validation limits.

Priority: P2 Type: bug Source: full codebase review requested 2026-10-31 (independently reproduced against `2cac7cf`) Location: `src/models/vpmb.ts:656`, `src/models/vpmb.ts:658`, `src/models/vpmb.ts:662`, `src/models/vpmb.ts:668`, `src/models/projection.ts:49` Problem and evidence findNoStopLimit (src/models/vpmb.ts:601-670) returns 0 if the current state is not clear. Otherwise it returns null (no limit, shown as >240) when a 240-minute hold probe is clear. If that probe is not clear, it runs a 16-step bisection on [0, 240]. Both steps assume clearance is monotone in hold time. After switching from air to a helium-rich mix, helium counterdiffusion causes a temporary rise in the VPM-B unrelaxed bubble ceiling. Short holds therefore fail the clear() probe: planAscent returns "Current depth is shallower than the unrelaxed bubble ceiling", and clear() treats that as not clear for non-current states. Later holds pass again. The search skips that interval, so the projection shows NDL = null right before NDL drops to 0 within 0.25-1 min. The ZHL engines got an interval-certified chronological search for #48. docs/MODEL-ARCHITECTURE.md:15 says that work "do[es] not change VPM's separate no-stop planner", so VPM-B still has the old monotone assumption. Correction to the original claim: I could not reproduce its first example (air 25 m/15 min, then Heliox 21/79 at 12 m). Its VPM-B NDL fell steadily from 48.6 with no zero window. In every case I reproduced, the transient appears as a null (>240) NDL, not a finite NDL. The ndl=0 holds that follow mostly carry the "Current depth is shallower than the unrelaxed bubble ceiling" error, not a stop list. The bisection could in principle also hide a transient behind a finite NDL, but my scan found no such case. I wrote scratch scripts in /tmp/divesim-verify-J13TVq (t.mts to t4.mts) and ran them with node --import tsx against the real source. They use createSimulation("VPM-B", c).advance with 20 m/min descent and 9 m/min ascent, then switchGas to gasFromFractions(21,79), then calculateProjection({model:"VPM-B", ascent:9, spacing:3, ...}) after holds at the switch depth. - Air 15 m/45 min, Heliox at 4.5 m, conservatism 3: NDL at the switch = null. At holds of 0.1 and 0.25 min it is still null; the unrelaxed ceiling is 4.00 m at the switch, 4.11 at 0.1 min and 4.28 at 0.25 min. From 0.4 to 6 min, ndl = 0 with plan.error "Current depth is shallower than the unrelaxed bubble ceiling..." and the ceiling peaks at 4.94 m, above the 4.5 m depth. At 7 min and later, ndl = null again. - Air 30 m/10 min, Heliox at 9 m, conservatism 3: NDL at the switch = null, but at 0.5 min ndl = 0. Holds of 0.5-3.75 min are all 0 before NDL returns to null. At conservatism 2: null at 0 and 0.5 min, 0 at 1-3 min, null again at 4 min. - Other reproducing profiles: 25 m/10 min to 6 m (0 for 0.25-3.75 min), 18 m/30 min to 6 m (0.25-5.25 min) and 15 m/30 min to 4.5 m (0.5-4.25 min). In all of them NDL at the switch was null. Heliox and trimix are user-configurable in the live app (App.tsx:2473, "Configure trimix/heliox, multiple gases, and ascent switches"), so these profiles can be reached through the UI. Suggested correction Replace the monotone bisection in findNoStopLimit with a time-ordered search, as the ZHL fix for #48 does. Return the first hold time at which clear() fails, scanning from 0 upward, and never return null or move the lower bound past an interval whose clearance has not been proven. Without an analytic VPM envelope, a bounded scan works. Probe a geometric or fixed grid of short holds (for example every 10 s for the first 10-15 min after a gas change, then coarser), stop at the first failure, then bisect only between the last clear probe and that first failure. Treat a clear 240-minute probe as "no limit" only when every grid probe before it is also clear. Keep the existing 24-hour-horizon handling, where a probe beyond the horizon counts as needing stops. Acceptance criteria Add VPM-B cases to tests/unit/ndl-transient.test.ts: (a) Air 15 m/45 min, Heliox 21/79 at 4.5 m, conservatism 3: the NDL at the switch must be finite, between 0.25 and 0.4 min. Holding for that NDL must leave clear() true, and holding NDL + a small epsilon must be not clear. (b) Air 30 m/10 min, Heliox at 9 m, conservatism 3: the NDL must be below 0.5 min and not null. (c) After holding 0.5 min in (b), the remaining NDL must equal the original NDL minus 0.5, or 0 when that is negative. (d) Single-gas air sweeps must give the same NDLs as before, within the bisection precision. (e) The worst-case search cost must stay bounded, with a probe cap similar to the ZHL search's 256 projections. pnpm verify passes. Scope: maintenance of the educational simulator. Preserve historical R1 acceptance and operational-validation limits.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: pedro/DiveSim#64
No description provided.