[P3] VPM-B NDL throws on current-state schedule errors (e.g. the 24-hour limit), so replay of very long-deco VPM dives fails entirely while ZHL reports NDL 0 #82

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

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

Location: src/models/vpmb.ts:626-634, src/models/vpmb.ts:657, src/replay/diveReplay.ts:425-428, src/calculation/calculation.worker.ts:23-30, src/models/projection.ts:43-55, src/App.tsx:1720-1734

Problem and evidence

The clear() helper inside findNoStopLimit (vpmb.ts:626-634) throws new Error(plan.error) whenever the current state's own ascent plan has an error other than "Current depth is shallower". That covers the "Projection exceeded the 24-hour limit" error and the critical-volume non-convergence error. The if (!clear(state)) return 0 call at line 657 therefore throws instead of returning 0. ZHL's noDecoLimit returns 0 for the same kind of state and leaves the error on the schedule.

replayDive (diveReplay.ts:425-428) calls sim.noDecoLimit to get bottomNdl and does not catch errors. Its VPM tail-plan branch already handles plan.error by setting decompressionRequired = true, but the uncaught NDL call still throws afterwards. The worker (calculation.worker.ts:23) runs replayDive for both the original and the comparison inside one try. As a result, the whole VPM-B replay/comparison for such a dive becomes an error message and shows no profile. The same ZHL-16C dive replays normally with bottomNdl 0 and decompressionRequired true.

In the live view, VPM-B calculateProjection throws through projectVpm. App shows NDL "Unavailable" plus an alert with the error text. ZHL instead shows "Modeled decompression required" with NDL 0 and the schedule error. That is a less severe inconsistency because the message is still shown. This only affects extreme exposures whose modeled ascent exceeds 24 hours, such as long deep dives on a single hypoxic mix.

I wrote scratch scripts in /tmp/divesim-verify-Ra8a0h, run with node --import tsx from the worktree. They build imported Shearwater profiles with the same normalizeShearwater/prepareImportedReplay path as tests/unit/replay-ascent-seed.test.ts, then call replayDive(dive, originalReplay).

Results:

  • Trimix 10/70, 0 to 120 m at t=8, hold to t=98, surface at t=110: ZHL-16C returns bottomNdl 0 and decoReq true. VPM-B throws "Projection exceeded the 24-hour limit. No complete schedule is available."
  • Tx10/70 at 150 m for 60 min: same result (ZHL OK, VPM-B throws).
  • Air at 60 m for 60 min and air at 40 m for 300 min: both models OK, so this only happens when deco exceeds 24 hours.

Stack trace: clear (vpmb.ts:633), then findNoStopLimit (vpmb.ts:657), then runtime noDecoLimit (runtime.ts:52), then replayDive (diveReplay.ts:427).

Direct model sweep at 120 m on tx10/70 with sim.noDecoLimit:

  • VPM-B returns ndl 0 for holds of 10/20/30/45 min (total ascent 721/1004/1189/1402 min). At a 60 min hold it THROWS.
  • ZHL-16C returns ndl 0 for every hold, and from 30 min onward its plan carries the 24-hour error.

Suggested correction

In findNoStopLimit, when the current state is not cleared, return 0 instead of throwing. Simplest version: drop the throw in clear() for loaded === state and keep return !plan.error && !plan.stops.length. Input validation errors (depth, gas, level, state mismatch) are thrown separately and should stay as they are. projectVpm already returns the first plan (with its error) when ndl === 0, so the live UI will match ZHL: NDL 0 with the schedule error.

If the convergence-failure case should stay distinguishable, return 0 and let the plan carry its error. As defense in depth, replayDive could also treat a noDecoLimit failure as bottomNdl 0 when decompressionRequired is already true.

Acceptance criteria

Add a regression test: an imported/synthetic tx10/70 profile (0 to 120 m by t=8, hold to t=98, surface at t=110). replayDive with VPM-B and originalReplay must not throw, and it must return bottomNdl 0 and decompressionRequired true, the same as ZHL-16C.

Also add a check that calculateProjection with model "VPM-B" for the 120 m / 60-min tx10/70 state returns ndl 0 with plan.error containing "24-hour", the same as ZHL. Existing #31 probe-horizon tests and the vpmb/model unit tests must still pass. pnpm verify passes.

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

Priority: P3 Type: bug Source: full codebase review requested 2026-10-31 (independently reproduced against `2cac7cf`) Location: `src/models/vpmb.ts:626-634`, `src/models/vpmb.ts:657`, `src/replay/diveReplay.ts:425-428`, `src/calculation/calculation.worker.ts:23-30`, `src/models/projection.ts:43-55`, `src/App.tsx:1720-1734` Problem and evidence The `clear()` helper inside `findNoStopLimit` (vpmb.ts:626-634) throws `new Error(plan.error)` whenever the current state's own ascent plan has an error other than "Current depth is shallower". That covers the "Projection exceeded the 24-hour limit" error and the critical-volume non-convergence error. The `if (!clear(state)) return 0` call at line 657 therefore throws instead of returning 0. ZHL's `noDecoLimit` returns 0 for the same kind of state and leaves the error on the schedule. `replayDive` (diveReplay.ts:425-428) calls `sim.noDecoLimit` to get `bottomNdl` and does not catch errors. Its VPM tail-plan branch already handles `plan.error` by setting `decompressionRequired = true`, but the uncaught NDL call still throws afterwards. The worker (calculation.worker.ts:23) runs replayDive for both the original and the comparison inside one try. As a result, the whole VPM-B replay/comparison for such a dive becomes an error message and shows no profile. The same ZHL-16C dive replays normally with bottomNdl 0 and decompressionRequired true. In the live view, VPM-B `calculateProjection` throws through `projectVpm`. App shows NDL "Unavailable" plus an alert with the error text. ZHL instead shows "Modeled decompression required" with NDL 0 and the schedule error. That is a less severe inconsistency because the message is still shown. This only affects extreme exposures whose modeled ascent exceeds 24 hours, such as long deep dives on a single hypoxic mix. I wrote scratch scripts in /tmp/divesim-verify-Ra8a0h, run with `node --import tsx` from the worktree. They build imported Shearwater profiles with the same normalizeShearwater/prepareImportedReplay path as tests/unit/replay-ascent-seed.test.ts, then call replayDive(dive, originalReplay). Results: - Trimix 10/70, 0 to 120 m at t=8, hold to t=98, surface at t=110: ZHL-16C returns bottomNdl 0 and decoReq true. VPM-B throws "Projection exceeded the 24-hour limit. No complete schedule is available." - Tx10/70 at 150 m for 60 min: same result (ZHL OK, VPM-B throws). - Air at 60 m for 60 min and air at 40 m for 300 min: both models OK, so this only happens when deco exceeds 24 hours. Stack trace: clear (vpmb.ts:633), then findNoStopLimit (vpmb.ts:657), then runtime noDecoLimit (runtime.ts:52), then replayDive (diveReplay.ts:427). Direct model sweep at 120 m on tx10/70 with sim.noDecoLimit: - VPM-B returns ndl 0 for holds of 10/20/30/45 min (total ascent 721/1004/1189/1402 min). At a 60 min hold it THROWS. - ZHL-16C returns ndl 0 for every hold, and from 30 min onward its plan carries the 24-hour error. Suggested correction In `findNoStopLimit`, when the current state is not cleared, return 0 instead of throwing. Simplest version: drop the throw in `clear()` for `loaded === state` and keep `return !plan.error && !plan.stops.length`. Input validation errors (depth, gas, level, state mismatch) are thrown separately and should stay as they are. `projectVpm` already returns the first plan (with its `error`) when ndl === 0, so the live UI will match ZHL: NDL 0 with the schedule error. If the convergence-failure case should stay distinguishable, return 0 and let the plan carry its error. As defense in depth, replayDive could also treat a noDecoLimit failure as bottomNdl 0 when decompressionRequired is already true. Acceptance criteria Add a regression test: an imported/synthetic tx10/70 profile (0 to 120 m by t=8, hold to t=98, surface at t=110). replayDive with VPM-B and originalReplay must not throw, and it must return bottomNdl 0 and decompressionRequired true, the same as ZHL-16C. Also add a check that calculateProjection with model "VPM-B" for the 120 m / 60-min tx10/70 state returns ndl 0 with plan.error containing "24-hour", the same as ZHL. Existing #31 probe-horizon tests and the vpmb/model unit tests must still pass. 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#82
No description provided.