STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
backend.py
Go to the documentation of this file.
1"""The backend seam: what a property source must provide, and how to add one.
2
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.
7
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.
11
12Backends are registered by name, so a project can add one without editing this
13package::
14
15 from feedtwin.props.backend import register_backend
16
17 register_backend("my_eos", lambda species: MyBackend(species))
18
19and then name it in a fluid's chain.
20"""
21
22from __future__ import annotations
23
24from enum import Enum
25from typing import TYPE_CHECKING, Callable, Protocol, runtime_checkable
26
27from feedtwin.props.state import Phase
28
29if TYPE_CHECKING: # pragma: no cover - import cycle guard, types only
30 from feedtwin.props.species import SpeciesSpec
31
32
33class StatePair(Enum):
34 """Which two independent variables a state point is specified by.
35
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.
39
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.
43 """
44
45 PT = "PT"
46 PH = "PH"
47 PS = "PS"
48 PQ = "PQ"
49 TQ = "TQ"
50 PD = "PD"
51 HS = "HS"
52 DT = "DT"
53 """Density and temperature. What a vessel of known mass and volume has."""
54 DU = "DU"
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."""
59
60
61@runtime_checkable
62class PropertyBackend(Protocol):
63 """A source of thermophysical properties for one species.
64
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.
70 """
71
72 @property
73 def name(self) -> str:
74 """Short identifier, e.g. ``"bicubic"``. Appears in ThermoState."""
75 ...
76
77 def supports(self, prop: str) -> bool:
78 """Whether this backend can compute ``prop`` at all.
79
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.
82 """
83 ...
84
85 def update(self, pair: StatePair, v1: float, v2: float) -> None:
86 """Move to a state point.
87
88 Raises:
89 OutOfRange: the point is outside this backend's envelope. Routine --
90 the caller's chain tries the next backend.
91 """
92 ...
93
94 def value(self, prop: str) -> float:
95 """Read a property at the current state point.
96
97 Raises:
98 UnsupportedProperty: this backend cannot compute it.
99 """
100 ...
101
102 def phase(self) -> Phase:
103 """Phase at the current state point."""
104 ...
105
106 def quality(self) -> float | None:
107 """Vapour mass fraction, or ``None`` outside the two-phase dome."""
108 ...
109
110
111#: Builds a backend for one species. Registered under a name and named in a
112#: fluid's chain; taking the whole SpeciesSpec (not just a fluid name) is what
113#: lets a backend read its own settings out of the species table.
114BackendFactory = Callable[["SpeciesSpec"], PropertyBackend]
115
116_BACKENDS: dict[str, BackendFactory] = {}
117
118
119def register_backend(name: str, factory: BackendFactory) -> None:
120 """Register a backend factory under ``name``.
121
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.
124 """
125 _BACKENDS[name] = factory
126
127
128def get_backend_factory(name: str) -> BackendFactory:
129 if name not in _BACKENDS:
130 raise KeyError(
131 f"unknown property backend {name!r}; registered: "
132 f"{', '.join(sorted(_BACKENDS))}"
133 )
134 return _BACKENDS[name]
135
136
137def registered_backends() -> list[str]:
138 return sorted(_BACKENDS)
None update(self, StatePair pair, float v1, float v2)
Definition backend.py:85
bool supports(self, str prop)
Definition backend.py:77
uint8_t value
list[str] registered_backends()
Definition backend.py:137
None register_backend(str name, BackendFactory factory)
Definition backend.py:119
BackendFactory get_backend_factory(str name)
Definition backend.py:128