"""
core/pressure.py — Pressure value type.
Ground truth is always integer millibar. Pressure represents absolute pressures
only (ambient, partial pressures, tissue loadings) — always >= 0.
Arithmetic:
Pressure + Pressure -> Pressure (e.g. surface + hydrostatic)
Pressure - Pressure -> int (signed mbar delta — NOT a Pressure)
Pressure * float -> Pressure (e.g. ppO2 = ambient * o2_fraction)
float * Pressure -> Pressure (commutative)
Pressure / Pressure -> float (dimensionless ratio)
Pressure / float -> Pressure (scaling)
Depth conversion:
Pressure.from_depth_m(depth_m) reads DiveConfig.current().physics
pressure.depth_m reads DiveConfig.current().physics
Dependency: Pressure -> DiveConfig (one way only, never reversed).
"""
from typing import Literal, overload
from diveplan.core.config import DiveConfig
__all__ = ["Pressure", "PressureUnit"]
PressureUnit = Literal["bar", "mbar", "atm", "psi", "m", "ft"]
"""Unit names accepted by :meth:`Pressure.to_str`."""
[docs]
class Pressure:
"""Absolute pressure stored as integer millibar.
Immutable. __slots__ for memory efficiency in batch simulation.
Examples:
>>> Pressure(1013)
Pressure(1013)
>>> Pressure.from_bar(4.013).depth_m # at standard surface pressure, salt water
30.0
"""
__slots__ = ("_mbar",)
_mbar: int # For type checker only — actual storage is in __slots__ for immutability and memory efficiency.
# ------------------------------------------------------------------
# Construction
# ------------------------------------------------------------------
def __init__(self, mbar: int) -> None:
if mbar < 0:
raise ValueError(
f"Pressure cannot be negative (got {mbar} mbar). Use plain int mbar for signed deltas."
)
object.__setattr__(self, "_mbar", int(mbar))
[docs]
@classmethod
def from_bar(cls, bar: float) -> Pressure:
"""Construct from bar. Rounds to nearest mbar."""
return cls(round(bar * 1000))
[docs]
@classmethod
def from_mbar(cls, mbar: float) -> Pressure:
"""Construct from float mbar. Rounds to nearest integer mbar."""
if mbar < 0:
raise ValueError(f"Pressure cannot be negative (got {mbar} mbar).")
return cls(round(mbar))
[docs]
@classmethod
def from_depth_m(cls, depth_m: float) -> Pressure:
"""Construct from depth in metres using the current DiveConfig environment.
Reads DiveConfig.current().physics — context manager overrides apply:
with DiveConfig(physics=_PhysicsConfig(surface_pressure_mbar=800)):
Pressure.from_depth_m(40) # altitude dive
"""
physics = DiveConfig.current().physics
mbar = physics.surface_pressure_mbar + depth_m * physics.pressure_per_meter_mbar
return cls(round(mbar))
[docs]
@classmethod
def from_atm(cls, atm: float) -> Pressure:
"""Construct from atmospheres. Rounds to nearest mbar."""
physics = DiveConfig.current().physics
return cls.from_mbar(atm * physics.surface_pressure_mbar)
# psi ↔ mbar: 1 mbar = 0.0145038 psi ⇒ mbar = psi / 0.0145038
_PSI_PER_MBAR = 0.0145038
[docs]
@classmethod
def from_psi(cls, psi: float) -> Pressure:
"""Construct from psi. Rounds to nearest mbar."""
return cls.from_mbar(psi / cls._PSI_PER_MBAR)
[docs]
@classmethod
def from_depth_ft(cls, depth_ft: float) -> Pressure:
"""Construct from depth in feet using the current DiveConfig environment.
Reads DiveConfig.current().physics — context manager overrides apply:
with DiveConfig(physics=_PhysicsConfig(surface_pressure_mbar=800)):
Pressure.from_depth_ft(130) # altitude dive
"""
physics = DiveConfig.current().physics
mbar = (
physics.surface_pressure_mbar
+ depth_ft * 0.3048 * physics.pressure_per_meter_mbar
)
return cls(round(mbar))
[docs]
@classmethod
def surface(cls) -> Pressure:
"""Convenience constructor for surface pressure in the current DiveConfig environment."""
return cls.from_atm(1.0)
[docs]
@classmethod
def from_str(cls, s: str) -> Pressure:
"""Parse a pressure from a string with unit suffix. Supported formats:
"4.013 bar"
"4013 mbar"
"1 atm"
"14.7 psi"
"30 m"
"98 ft"
Args:
s: Input string to parse.
Raises:
ValueError: If the input string has an invalid format or unit.
ValueError: If the parsed value is negative.
ValueError: If the parsed value is not a valid number.
Returns:
Pressure: The parsed pressure instance.
"""
s = s.strip().lower()
# NB: order matters — "mbar" ends with "bar", and "atm" ends with "m",
# so the more specific suffixes must be tested first.
if s.endswith("mbar"):
try:
mbar = float(s[:-4].strip())
return cls.from_mbar(mbar)
except ValueError:
raise ValueError(f"Invalid pressure string: '{s}'")
elif s.endswith("bar"):
try:
bar = float(s[:-3].strip())
return cls.from_bar(bar)
except ValueError:
raise ValueError(f"Invalid pressure string: '{s}'")
elif s.endswith("atm"):
try:
atm = float(s[:-3].strip())
return cls.from_atm(atm)
except ValueError:
raise ValueError(f"Invalid pressure string: '{s}'")
elif s.endswith("psi"):
try:
psi = float(s[:-3].strip())
return cls.from_psi(psi)
except ValueError:
raise ValueError(f"Invalid pressure string: '{s}'")
elif s.endswith("ft"):
try:
depth_ft = float(s[:-2].strip())
return cls.from_depth_ft(depth_ft)
except ValueError:
raise ValueError(f"Invalid pressure string: '{s}'")
elif s.endswith("m"):
try:
depth_m = float(s[:-1].strip())
return cls.from_depth_m(depth_m)
except ValueError:
raise ValueError(f"Invalid pressure string: '{s}'")
else:
raise ValueError(
f"Invalid pressure string: '{s}' (must end with 'bar', 'mbar', 'atm', 'psi', 'ft', or 'm')"
)
# ------------------------------------------------------------------
# Properties
# ------------------------------------------------------------------
@property
def mbar(self) -> int:
"""Absolute pressure in integer millibar (the ground-truth value)."""
return self._mbar
@property
def bar(self) -> float:
"""Absolute pressure in bar."""
return self._mbar / 1000.0
@property
def depth_m(self) -> float:
"""Depth in metres in the context of the current DiveConfig environment.
Reads DiveConfig.current().physics — context manager overrides apply.
Returns a negative value if pressure is below surface pressure
(e.g. altitude surface, though that shouldn't arise in normal use).
"""
physics = DiveConfig.current().physics
return (
self._mbar - physics.surface_pressure_mbar
) / physics.pressure_per_meter_mbar
@property
def depth_ft(self) -> float:
"""Depth in feet in the context of the current DiveConfig environment.
Reads DiveConfig.current().physics — context manager overrides apply.
Returns a negative value if pressure is below surface pressure
(e.g. altitude surface, though that shouldn't arise in normal use).
"""
physics = DiveConfig.current().physics
return (self._mbar - physics.surface_pressure_mbar) / (
0.3048 * physics.pressure_per_meter_mbar
)
@property
def atm(self) -> float:
"""Absolute pressure in atmospheres, relative to the current surface pressure."""
physics = DiveConfig.current().physics
return self._mbar / physics.surface_pressure_mbar
@property
def psi(self) -> float:
"""Absolute pressure in pounds per square inch."""
return self._mbar * self._PSI_PER_MBAR
@property
def is_surface(self) -> bool:
"""Whether this pressure is at (or above) the configured surface."""
physics = DiveConfig.current().physics
return self._mbar <= physics.surface_pressure_mbar
# ------------------------------------------------------------------
# Immutability guard
# ------------------------------------------------------------------
def __setattr__(self, name: str, value: object) -> None:
raise AttributeError("Pressure is immutable — create a new instance instead.")
def __delattr__(self, name: str) -> None:
raise AttributeError("Pressure is immutable.")
# ------------------------------------------------------------------
# Arithmetic
# ------------------------------------------------------------------
def __add__(self, other: object) -> Pressure:
if isinstance(other, Pressure):
return Pressure(self._mbar + other._mbar)
return NotImplemented
def __sub__(self, other: object) -> int:
"""Returns a signed int delta in mbar — NOT a Pressure.
Subtraction of two absolute pressures is a delta (dimensionally
different). Returning int prevents accidental use as an absolute pressure.
"""
if isinstance(other, Pressure):
return self._mbar - other._mbar
return NotImplemented
def __mul__(self, scalar: object) -> Pressure:
if isinstance(scalar, (int, float)):
result = round(self._mbar * scalar)
if result < 0:
raise ValueError(
f"Pressure * {scalar} yields negative result ({result} mbar)."
)
return Pressure(result)
return NotImplemented
def __rmul__(self, scalar: object) -> Pressure:
return self.__mul__(scalar)
@overload
def __truediv__(self, other: Pressure) -> float: ...
@overload
def __truediv__(self, other: int | float) -> Pressure: ...
def __truediv__(self, other: object) -> float | Pressure:
if isinstance(other, Pressure):
if other._mbar == 0:
raise ZeroDivisionError("Cannot divide Pressure by zero Pressure.")
return self._mbar / other._mbar
if isinstance(other, (int, float)):
if other == 0:
raise ZeroDivisionError("Cannot divide Pressure by zero.")
result = round(self._mbar / other)
if result < 0:
raise ValueError(
f"Pressure / {other} yields negative result ({result} mbar)."
)
return Pressure(result)
return NotImplemented
# ------------------------------------------------------------------
# Ordering
# ------------------------------------------------------------------
def __eq__(self, other: object) -> bool:
if isinstance(other, Pressure):
return self._mbar == other._mbar
return NotImplemented
def __lt__(self, other: object) -> bool:
if isinstance(other, Pressure):
return self._mbar < other._mbar
return NotImplemented
def __le__(self, other: object) -> bool:
if isinstance(other, Pressure):
return self._mbar <= other._mbar
return NotImplemented
def __gt__(self, other: object) -> bool:
if isinstance(other, Pressure):
return self._mbar > other._mbar
return NotImplemented
def __ge__(self, other: object) -> bool:
if isinstance(other, Pressure):
return self._mbar >= other._mbar
return NotImplemented
def __hash__(self) -> int:
return hash(self._mbar)
# ------------------------------------------------------------------
# Display
# ------------------------------------------------------------------
[docs]
def to_str(self, unit: PressureUnit = "bar") -> str:
"""Format pressure as a string in the specified unit.
Supported units: 'bar', 'mbar', 'atm', 'psi', 'm' (depth in metres), 'ft' (depth in feet).
"""
u = unit.lower()
if u == "bar":
return f"{self.bar:.3f} bar"
elif u == "mbar":
return f"{self.mbar} mbar"
elif u == "atm":
return f"{self.atm:.3f} atm"
elif u == "psi":
return f"{self.psi:.2f} psi"
elif u == "m":
return f"{self.depth_m:.1f} m"
elif u == "ft":
return f"{self.depth_ft:.1f} ft"
else:
raise ValueError(
f"Unsupported unit '{unit}' for Pressure.to_str (must be 'bar', 'mbar', 'atm', 'psi', 'm', or 'ft')."
)
def __repr__(self) -> str:
return f"Pressure({self._mbar})"
def __str__(self) -> str:
return f"{self.bar:.3f} bar ({self._mbar} mbar)"