"""Dive report: everything about a computed dive, ready for presentation.
:class:`DiveReport` is pure data assembled from a :class:`~diveplan.dive.dive.Dive` — the
schedule rows plus derived figures (gas consumption, CNS/OTU, rock bottom,
TTS variations). Formatters (`diveplan.dive.formatters`) turn a report into
console text, JSON, or a Subsurface dive log; they never touch models or
profiles directly, so a new output format is a single class.
"""
from datetime import timedelta
from typing import NamedTuple
from diveplan.core.config import DiveConfig
from diveplan.core.gas import Gas
from diveplan.core.pressure import Pressure
from diveplan.dive.dive_profile import DiveProfile
from diveplan.dive.dive import Dive, TtsVariations
from diveplan.models.base import DecoState
from diveplan.planning.gas_plan import (
GasPlan,
cns_percent,
gas_consumption,
otu,
rock_bottom,
)
__all__ = ["DiveReport", "ReportRow"]
[docs]
class ReportRow(NamedTuple):
"""One schedule line: a segment with its cumulative runtime at the end."""
runtime: timedelta # at the END of the segment
start_depth_m: float
end_depth_m: float
duration: timedelta
gas: Gas
kind: str # SegmentKind member name
[docs]
class DiveReport:
"""Immutable summary of a computed dive.
Build via :meth:`from_dive`. Holds the schedule and the derived
numbers; formatting belongs to `diveplan.dive.formatters`.
"""
__slots__ = (
"profile",
"model_name",
"rows",
"runtime",
"max_depth",
"consumption_l",
"cns",
"otus",
"rock_bottom_l",
"sac_bottom",
"sac_deco",
"sac_factor",
"tts_variations",
)
profile: DiveProfile
model_name: str
rows: tuple[ReportRow, ...]
runtime: timedelta
max_depth: Pressure
consumption_l: tuple[tuple[Gas, float], ...]
cns: float
otus: float
rock_bottom_l: float
sac_bottom: float
sac_deco: float
sac_factor: float
tts_variations: TtsVariations | None
def __init__(
self,
*,
profile: DiveProfile,
model_name: str,
rows: tuple[ReportRow, ...],
runtime: timedelta,
max_depth: Pressure,
consumption_l: tuple[tuple[Gas, float], ...],
cns: float,
otus: float,
rock_bottom_l: float,
sac_bottom: float,
sac_deco: float,
sac_factor: float,
tts_variations: TtsVariations | None,
):
self.profile = profile
self.model_name = model_name
self.rows = rows
self.runtime = runtime
self.max_depth = max_depth
self.consumption_l = consumption_l
self.cns = cns
self.otus = otus
self.rock_bottom_l = rock_bottom_l
self.sac_bottom = sac_bottom
self.sac_deco = sac_deco
self.sac_factor = sac_factor
self.tts_variations = tts_variations
[docs]
@classmethod
def from_dive(
cls,
dive: Dive[DecoState],
*,
gas_plan: GasPlan | None = None,
tts_variations: TtsVariations | None = None,
) -> "DiveReport":
"""Assemble a report from a computed dive.
Args:
dive: The computed dive (normally the *full* dive, bottom plus
planned ascent — see :meth:`Dive.with_ascent`).
gas_plan: Unused for consumption (which follows the profile's
own gases) — reserved for future reserve summaries.
tts_variations: The "+1 m / +1 min" figures. They are meaningful
for a *bottom-phase* dive, so compute them on the bottom
dive (``bottom.tts_variations()``) and pass them here; a
report over a full dive cannot derive them itself.
"""
profile = dive.profile
segments = profile.segments
rows: list[ReportRow] = []
elapsed = timedelta(0)
for segment in segments:
elapsed += segment.duration
rows.append(
ReportRow(
runtime=elapsed,
start_depth_m=segment.start_pressure.depth_m,
end_depth_m=segment.end_pressure.depth_m,
duration=segment.duration,
gas=segment.gas,
kind=segment.kind.name,
)
)
max_pressure = max(
(p for s in segments for p in (s.start_pressure, s.end_pressure)),
default=Pressure.surface(),
)
# Consumption and rock bottom read the active config; capture the
# SAC assumptions here so the report stays consistent with them.
gas_cfg = DiveConfig.current().gas
return cls(
profile=profile,
model_name=dive.model_name,
rows=tuple(rows),
runtime=profile.runtime,
max_depth=max_pressure,
consumption_l=tuple(gas_consumption(segments).items()),
cns=cns_percent(segments),
otus=otu(segments),
rock_bottom_l=rock_bottom(max_pressure),
sac_bottom=gas_cfg.sac_bottom,
sac_deco=gas_cfg.sac_deco,
sac_factor=gas_cfg.sac_factor,
tts_variations=tts_variations,
)
def __repr__(self) -> str:
return (
f"DiveReport({self.model_name}, {len(self.rows)} rows, "
f"runtime={self.runtime}, max depth {self.max_depth.depth_m:.1f} m)"
)