[P2] Original replay applies planned gas switches that happen mid-tick only at the next recorded sample, so its peak ppO₂, cylinder pressures and tissues differ from the live dive and summary #112

Open
opened 2026-10-31 19:47:41 +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/replay/diveReplay.ts:280-306, src/App.tsx:887-905, src/App.tsx:197-211, src/models/zhl16c.ts:170-193, src/simulation/summaryRecording.ts:16-42

Problem and evidence

During a live ascent, the engine splits a tick at the exact planned-switch depth (zhl16c.ts gas travel). It loads tissues on the old gas down to the switch depth and on the new gas after it. It also records a gasEvent with the exact time and depth. chargeCylinders then bills each gas's usage to its own cylinder. The App recorder (App.tsx ~887-905) pushes only the end-of-tick DiveSample. That sample is already shallower than the switch depth and already carries the new breathingGas and cylinder. replayDive (diveReplay.ts ~284-306) integrates the whole a->b leg on a.breathingGas and charges it to a.activeCylinderId. The #19 "incoming gas" point is then evaluated only at b.depth. As a result, the reconstruction:

  • keeps breathing the old gas past the switch depth,
  • bills that gas to the old cylinder, and
  • calculates the new gas's peak ppO₂ at the shallower sample depth.
    The summary is correct, because recordedProfile adds a peak from dive.gasEvents at the exact switch depth. Replay ignores CompletedDive.gasSwitches and dive.history turn points. The larger the tick (high warp speed, +1:00 steps, background-capped ticks), the larger the divergence. This also breaks the PROJECT_SCOPE acceptance item "replay identical exposure at different speeds".
  1. I served the HEAD production build (/tmp/divesim-review-dist-SqbiNx) and ran Playwright (pw-switch.mjs, copied to /tmp/divesim-verify-2FkGpu). Steps: imperial units; add an Oxygen cylinder; add an enabled 20 ft O2 ascent switch; 98 ft with +1:00 x4; 32 ft with +1:00 x4; depth 10 ft with +1:00 x3; Surface. Output:
  • Summary "Peak ppO₂ 23.54 psi" (1.60 ATA, the O2 switch at 20 ft).
  • Compare & replay row: "Peak ppO₂ 1.30 ATA 1.30 ATA". That is O2 at 10 ft, the next recorded sample, so the replay understates the summary's peak with no warning.
  1. I ran the earlier reviewer's harness (/tmp/divesim-review-DYXkZe/plusone.ts). It mirrors the clock, chargeCylinders and the recorder against the real engine, replayDive and recordedProfile. Profile: Air + EAN50 + O2, switches at 21 m and 20 ft.
  • +1:00 clicks: tissue max abs diff 0.021 bar. Air cylinder 27.24 bar live vs 24.60 bar replay; EAN50 191.60 vs 195.58 bar.
  • 128x with 0.1 s ticks: summary peak 1.602 ATA vs replay peak 1.495 ATA.
  • 8x: differences shrink to noise (3.6e-4 bar), consistent with a tick-size-dependent mechanism.

Suggested correction

Pick one of these:
(a) In the App recorder, when a tick produced new dive.gasEvents (or new dive.history turn points), push a synthetic DiveSample at each event's time and depth before the end-of-tick sample. Each synthetic sample needs the breathingGas in effect and the activeCylinderId chosen by the chargeCylinders rule.
(b) In replayDive, split each a->b leg at the CompletedDive.gasSwitches (and profile turn points) that fall inside it. Integrate and charge the pre-switch part on a's gas and cylinder and the remainder on the switched gas. Evaluate the incoming-gas ppO₂ at the switch depth. This requires storing the cylinder id (and full gas) with each recorded switch.

Acceptance criteria

Regression test: simulate a live dive with an enabled ascent switch (e.g. EAN50 at 21 m or O2 at 6 m) where the switch falls inside a single large tick (e.g. a 1-minute +1:00 step or a 128x tick). Then run replayDive(originalReplay(completed)).

  • The original replay peak ppO₂ must equal the summary peakOxygen within 1e-6.
  • Per-cylinder ending pressures and final tissue loadings must match the live dive (within ~1e-3 bar).
  • Results must be the same at 8x, 128x and +1:00 stepping.
    Browser check: the Playwright scenario above must show the same Peak ppO₂ (1.60 ATA) in the summary and in the Original replay column. 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/replay/diveReplay.ts:280-306`, `src/App.tsx:887-905`, `src/App.tsx:197-211`, `src/models/zhl16c.ts:170-193`, `src/simulation/summaryRecording.ts:16-42` Problem and evidence During a live ascent, the engine splits a tick at the exact planned-switch depth (zhl16c.ts gas travel). It loads tissues on the old gas down to the switch depth and on the new gas after it. It also records a gasEvent with the exact time and depth. chargeCylinders then bills each gas's usage to its own cylinder. The App recorder (App.tsx ~887-905) pushes only the end-of-tick DiveSample. That sample is already shallower than the switch depth and already carries the new breathingGas and cylinder. replayDive (diveReplay.ts ~284-306) integrates the whole a->b leg on a.breathingGas and charges it to a.activeCylinderId. The #19 "incoming gas" point is then evaluated only at b.depth. As a result, the reconstruction: - keeps breathing the old gas past the switch depth, - bills that gas to the old cylinder, and - calculates the new gas's peak ppO₂ at the shallower sample depth. The summary is correct, because recordedProfile adds a peak from dive.gasEvents at the exact switch depth. Replay ignores CompletedDive.gasSwitches and dive.history turn points. The larger the tick (high warp speed, +1:00 steps, background-capped ticks), the larger the divergence. This also breaks the PROJECT_SCOPE acceptance item "replay identical exposure at different speeds". 1) I served the HEAD production build (/tmp/divesim-review-dist-SqbiNx) and ran Playwright (pw-switch.mjs, copied to /tmp/divesim-verify-2FkGpu). Steps: imperial units; add an Oxygen cylinder; add an enabled 20 ft O2 ascent switch; 98 ft with +1:00 x4; 32 ft with +1:00 x4; depth 10 ft with +1:00 x3; Surface. Output: - Summary "Peak ppO₂ 23.54 psi" (1.60 ATA, the O2 switch at 20 ft). - Compare & replay row: "Peak ppO₂ 1.30 ATA 1.30 ATA". That is O2 at 10 ft, the next recorded sample, so the replay understates the summary's peak with no warning. 2) I ran the earlier reviewer's harness (/tmp/divesim-review-DYXkZe/plusone.ts). It mirrors the clock, chargeCylinders and the recorder against the real engine, replayDive and recordedProfile. Profile: Air + EAN50 + O2, switches at 21 m and 20 ft. - +1:00 clicks: tissue max abs diff 0.021 bar. Air cylinder 27.24 bar live vs 24.60 bar replay; EAN50 191.60 vs 195.58 bar. - 128x with 0.1 s ticks: summary peak 1.602 ATA vs replay peak 1.495 ATA. - 8x: differences shrink to noise (3.6e-4 bar), consistent with a tick-size-dependent mechanism. Suggested correction Pick one of these: (a) In the App recorder, when a tick produced new dive.gasEvents (or new dive.history turn points), push a synthetic DiveSample at each event's time and depth before the end-of-tick sample. Each synthetic sample needs the breathingGas in effect and the activeCylinderId chosen by the chargeCylinders rule. (b) In replayDive, split each a->b leg at the CompletedDive.gasSwitches (and profile turn points) that fall inside it. Integrate and charge the pre-switch part on a's gas and cylinder and the remainder on the switched gas. Evaluate the incoming-gas ppO₂ at the switch depth. This requires storing the cylinder id (and full gas) with each recorded switch. Acceptance criteria Regression test: simulate a live dive with an enabled ascent switch (e.g. EAN50 at 21 m or O2 at 6 m) where the switch falls inside a single large tick (e.g. a 1-minute +1:00 step or a 128x tick). Then run replayDive(originalReplay(completed)). - The original replay peak ppO₂ must equal the summary peakOxygen within 1e-6. - Per-cylinder ending pressures and final tissue loadings must match the live dive (within ~1e-3 bar). - Results must be the same at 8x, 128x and +1:00 stepping. Browser check: the Playwright scenario above must show the same Peak ppO₂ (1.60 ATA) in the summary and in the Original replay column. 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#112
No description provided.