Planning

Ascent planner

Ascent planner: compute the decompression schedule from a model state.

plan_ascent is a pure function of (model state, position, gases, config) → list of DiveSegments. It clones the model and never mutates the caller’s — the property that makes TTS/counterfactual queries safe.

Algorithm (standard staged-decompression loop):

  1. From the current depth, find the shallowest reachable target — the surface, or the deepest required stop on the configured stop grid (stop_increment_m, cut off at last_stop_m).

  2. Ascend there at ascent_rate (integrating the ascent into the model).

  3. At a stop: switch to the best deco gas (richest ppO2-safe mix from the gas plan), then wait in min_stop_time_s increments until the next shallower target clears.

  4. Repeat until surfaced.

The planner is model-agnostic: the ceiling test delegates to BaseDecoModel.get_ascent_ceiling(target, first_stop), so any ascent-context behavior (Bühlmann’s gradient-factor interpolation, VPM-B’s future Boyle/CVA compensation) lives in the model, never here. VPM-B currently uses the default plain ceiling — its schedules use the conservative pre-CVA gradients.

diveplan.planning.ascent_plan.plan_ascent(model, start_pressure, gas, gas_plan=None, clock_offset=datetime.timedelta(0))[source]

Plan the decompression ascent from the given position and model state.

Parameters:
  • model (BaseDecoModel[Any]) – Deco model holding the tissue state at start_pressure. Cloned internally — the caller’s instance is not touched.

  • start_pressure (Pressure | str | float) – Current position — a Pressure, a “40 m”-style string, or bare metres.

  • gas (Gas | str) – Gas currently being breathed — a Gas or a name like “ean50”.

  • gas_plan (GasPlan | None) – Gases available for switches during the ascent. Defaults to just the current gas.

  • clock_offset (timedelta | float) – Dive runtime at which this ascent starts (minutes or timedelta). Stop departures are extended to whole multiples of min_stop_time_s on this clock — the dive-table convention (a stop ends at e.g. runtime 31:00, not 30:26), which keeps schedules comparable with planners like Subsurface. Pass the bottom runtime when planning a real dive; the default plans on an ascent-relative clock.

Returns:

deco ascents, stops, and gas switches. Empty if already at the surface.

Return type:

list[DiveSegment]

Raises:

AscentNotConvergingError – If stops fail to clear within the iteration cap (pathological state or misconfiguration).

exception diveplan.planning.ascent_plan.AscentNotConvergingError[source]

Bases: RuntimeError

The stop loop failed to clear the next target within the iteration cap.

Gas plan, consumption, reserves, and oxygen exposure

Gas plan: carried gases, selection, consumption, and reserve planning.

Selection delegates breathability to Gas.is_breathable() (the configured deco ppO2 window — the planner switches gases during ascent, where the deco limit applies). Among breathable carried gases the richest (highest fO2) wins: it off-gasses inert load fastest.

Everything about breathing gas over a dive lives here: GasPlan (selection), gas_consumption() (surface litres per gas), rock_bottom() (emergency reserve), and the oxygen-exposure trackers cns_percent() / otu(). The accounting functions all take any sequence of segments — a profile’s or an ascent plan’s. They are module functions, not GasPlan methods, deliberately: toxicity and consumption depend on what was breathed (the profile), not on what was carried.

class diveplan.planning.gas_plan.GasPlan(gases)[source]

Bases: object

An ordered collection of carried gases with depth-based selection.

Gases may be given as Gas objects or names — GasPlan(["air", "ean50"]) — parsed via Gas.from_name().

Parameters:

gases (Iterable[Gas | str])

property gases: tuple[Gas, ...]

The carried gases (duplicates removed, insertion order).

best_gas_at(pressure)[source]

Richest breathable gas at pressure, or None if none qualifies.

Breathability is Gas.is_breathable() — the configured deco ppO2 window.

Parameters:

pressure (Pressure)

Return type:

Gas | None

diveplan.planning.gas_plan.gas_consumption(segments)[source]

Surface litres of each gas consumed over segments.

Per segment: SAC × mean ambient pressure (atm) × minutes — exact, since the mean pressure of a linear traverse is its midpoint. Deco phases (deco ascents, stops, gas switches) are billed at gas.sac_deco, everything else at gas.sac_bottom. Works on a profile’s segments or on a plan from plan_ascent().

Parameters:

segments (Iterable[DiveSegment])

Return type:

dict[Gas, float]

diveplan.planning.gas_plan.rock_bottom(depth, *, divers=2)[source]

Minimum gas reserve (surface litres) at depth for an emergency.

Models the classic worst case: divers divers (out-of-gas buddy plus donor) breathe from one supply at a stressed rate (sac_bottom × sac_factor) while solving the problem at depth for problem_solving_minutes, then ascend directly to the surface at the configured ascent rate. Decompression stops are not included — this is a direct-ascent reserve; divide by cylinder size × working pressure to express it in bar.

Parameters:
  • depth (Pressure | str | float) – Depth as a Pressure, a “40 m”-style string, or bare metres.

  • divers (int) – Divers sharing the supply. Defaults to 2.

Raises:

ValueError – If divers is not strictly positive.

Return type:

float

diveplan.planning.gas_plan.cns_percent(segments, *, step=datetime.timedelta(seconds=10))[source]

CNS oxygen-toxicity clock over segments, in percent (100 = NOAA limit).

Parameters:
Return type:

float

diveplan.planning.gas_plan.otu(segments, *, step=datetime.timedelta(seconds=10))[source]

Pulmonary oxygen-toxicity units (REPEX) accumulated over segments.

Parameters:
Return type:

float