1"""The backend seam: what a property source must provide, and how to add one.
3A backend answers "given two independent state variables, what is this
4property". That is the entire contract. CoolProp's Helmholtz equations of state
5are one implementation; its bicubic tables are a second; a table of numbers
6measured on a flow bench is a third, and the layer above cannot tell them apart.
8Keeping this seam narrow is what lets Phase 12 drop measured data in front of an
9equation of state without touching a solver, and what would let REFPROP or a
10Modelica media model in later without touching anything else.
12Backends are registered by name, so a project can add one without editing this
15 from feedtwin.props.backend import register_backend
17 register_backend("my_eos", lambda species: MyBackend(species))
19and then name it in a fluid's chain.
22from __future__
import annotations
25from typing
import TYPE_CHECKING, Callable, Protocol, runtime_checkable
34 """Which two independent variables a state point is specified by.
36 Backend-neutral on purpose: CoolProp spells these as integer constants,
37 another library would spell them differently, and the translation belongs
38 inside the backend rather than at every call site.
40 Two variables fix the state of a single-component fluid. ``PQ`` and ``TQ``
41 address the saturation dome, where pressure and temperature are not
42 independent and quality is the second variable instead.
53 """Density and temperature. What a vessel of known mass and volume has."""
55 """Density and specific internal energy -- the exact state variables of a
56 closed volume. A vessel's energy balance is written in ``u``, so tracking
57 ``(rho, u)`` keeps it exact for a real gas rather than approximating it
58 through a heat capacity."""
63 """A source of thermophysical properties for one species.
65 Implementations are **stateful and single-threaded**: :meth:`update` moves
66 the backend to a state point and :meth:`value` reads from wherever it was
67 last moved. That is CoolProp's model and it is fast for exactly the reason
68 it is dangerous -- one object, reused. :class:`~feedtwin.props.fluid.Fluid`
69 owns the thread-confinement that makes it safe; do not share an instance.
74 """Short identifier, e.g. ``"bicubic"``. Appears in ThermoState."""
78 """Whether this backend can compute ``prop`` at all.
80 A property of the backend, not of the state, so callers can answer it
81 once and route around a gap rather than probing per state point.
85 def update(self, pair: StatePair, v1: float, v2: float) ->
None:
86 """Move to a state point.
89 OutOfRange: the point is outside this backend's envelope. Routine --
90 the caller's chain tries the next backend.
94 def value(self, prop: str) -> float:
95 """Read a property at the current state point.
98 UnsupportedProperty: this backend cannot compute it.
103 """Phase at the current state point."""
107 """Vapour mass fraction, or ``None`` outside the two-phase dome."""
114BackendFactory = Callable[[
"SpeciesSpec"], PropertyBackend]
116_BACKENDS: dict[str, BackendFactory] = {}
120 """Register a backend factory under ``name``.
122 Re-registering the same name replaces it, which is deliberate: a project
123 overriding a shipped backend should not have to remove it first.
125 _BACKENDS[name] = factory
129 if name
not in _BACKENDS:
131 f
"unknown property backend {name!r}; registered: "
132 f
"{', '.join(sorted(_BACKENDS))}"
134 return _BACKENDS[name]
138 return sorted(_BACKENDS)
None update(self, StatePair pair, float v1, float v2)
bool supports(self, str prop)
list[str] registered_backends()
None register_backend(str name, BackendFactory factory)
BackendFactory get_backend_factory(str name)