From fbf6f1d1224f19a42dae4630a74c6aad845134a3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 02:59:09 +0000 Subject: [PATCH] Reserve a pressure condition on Jet, velocity remains the only implemented one Jet gains a last field condition='velocity'. 'pressure' raises NotImplementedError in __post_init__ before profile/amplitude validation, so a pressure jet never reaches surface_link_velocity, tables or kernels; unknown values raise ValueError. Amplitude stays a lattice velocity (not a flow rate); no pressure units, field reuse, tables or kernel changes are introduced. Co-Authored-By: Claude Opus 5.5 --- src/CelerisLab/body/jets.py | 17 ++++++++++++++++- tests/unit/test_jet_condition.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_jet_condition.py diff --git a/src/CelerisLab/body/jets.py b/src/CelerisLab/body/jets.py index 067c1c3..6e93f6d 100644 --- a/src/CelerisLab/body/jets.py +++ b/src/CelerisLab/body/jets.py @@ -9,6 +9,11 @@ whose links are evaluated; callers convert frames/units before evaluation. One physical link (receiver, direction) may be driven by at most one jet, and one jet may drive the links of only one body (checked at assignment, any amplitude). +``condition`` names the prescribed boundary type. Only ``"velocity"`` (the +default: amplitude is a signed lattice velocity, not a flow rate) is +implemented; ``"pressure"`` is reserved and rejected at construction, before +any amplitude validation, so it never reaches assignment or a kernel. + Edge rule: the lateral bound |s| < 1 is exclusive (the profile vanishes there for parabolic/quartic); Sector r_min/r_max and Tube 0 <= along <= length are inclusive. Jets are immutable; an amplitude change replaces the Jet. @@ -25,6 +30,9 @@ from dataclasses import dataclass, replace import numpy as np +CONDITIONS = ("velocity",) +RESERVED_CONDITIONS = ("pressure",) + PROFILES = { "uniform": lambda s: np.ones_like(s), "parabolic": lambda s: 1 - s*s, @@ -122,7 +130,8 @@ class Parallel: @dataclass(frozen=True) class Jet: - """Region (Sector|Tube), field (Radial|Parallel), profile name, signed amplitude. + """Region (Sector|Tube), field (Radial|Parallel), profile name, signed amplitude, + prescribed boundary condition (velocity only; pressure reserved). Frozen: change amplitude with ``with_amplitude`` (validated, new object); the cached per-link shapes stay valid because region/field/profile cannot change. @@ -131,8 +140,14 @@ class Jet: field: object profile: str = "uniform" amplitude: float = 0.0 + condition: str = "velocity" def __post_init__(self): + # The condition decides what amplitude and field mean: check it first. + if self.condition in RESERVED_CONDITIONS: + raise NotImplementedError(f"{self.condition} jets are reserved but not implemented") + if self.condition not in CONDITIONS: + raise ValueError(f"Jet condition must be one of {CONDITIONS + RESERVED_CONDITIONS}") if self.profile not in PROFILES: raise ValueError(f"Jet profile must be one of {sorted(PROFILES)}") _finite(self.amplitude) diff --git a/tests/unit/test_jet_condition.py b/tests/unit/test_jet_condition.py new file mode 100644 index 0000000..fe2b305 --- /dev/null +++ b/tests/unit/test_jet_condition.py @@ -0,0 +1,32 @@ +"""Jet boundary-condition hook: velocity implemented, pressure reserved (host only, no CUDA).""" +import pytest + +from CelerisLab.body.jets import Jet, Parallel, Tube + + +def region(): + return Tube((16., 12.), 0., 3., .7) + + +def test_default_is_velocity_and_survives_amplitude_change(): + jet = Jet(region(), Parallel(0.), amplitude=.01) + assert jet.condition == "velocity" + assert jet.with_amplitude(-.02).condition == "velocity" + # positional calls keep their meaning; condition is the new last field + assert (Jet(region(), Parallel(0.), "parabolic", .01, "velocity") + == Jet(region(), Parallel(0.), "parabolic", .01)) + + +def test_pressure_is_rejected_before_amplitude_validation(): + # An amplitude that the velocity check would reject must not decide the error. + for amplitude in (0., 5., float("nan")): + with pytest.raises(NotImplementedError, match="pressure jets are reserved"): + Jet(region(), Parallel(0.), amplitude=amplitude, condition="pressure") + with pytest.raises(NotImplementedError): + Jet(region(), None, profile="no-such-profile", condition="pressure") + + +def test_unknown_condition_is_a_value_error(): + for condition in ("Velocity", "flow_rate", "", None): + with pytest.raises(ValueError, match="condition"): + Jet(region(), Parallel(0.), amplitude=.01, condition=condition)