1"""Which fluids exist, declared as data.
3The species table lives in ``species.toml``, not in Python. Adding a propellant
4is an edit to a data file; it is never a new branch in a function. That is the
5difference between a simulator that supports four fluids and one that supports
6whatever you point it at, and it is cheap only if it is done on day one --
7retrofitting a fluid registry after fifty ``if fluid == "lox"`` sites have
8accumulated is the expensive version of this decision.
10Anything the equation of state already knows -- molar mass, critical point,
11triple point -- is deliberately *not* declared here. Duplicating it would create
12a second source of truth that silently disagrees with the first.
14Runtime registration is supported too, so a project can add a species without
17 from feedtwin.props import register_species, SpeciesSpec
19 register_species(SpeciesSpec(name="nitrous", backend_fluid="NitrousOxide"))
22from __future__
import annotations
25from dataclasses
import dataclass, field
26from pathlib
import Path
37DEFAULT_CHAIN: tuple[str, ...] = (
"bicubic",
"heos")
39_SPECIES_FILE = Path(__file__).with_name(
"species.toml")
42@dataclass(frozen=True, slots=True)
44 """One declared fluid.
47 name: Canonical lowercase identifier, e.g. ``"nitrogen"``.
48 backend_fluid: What the equation-of-state library calls it, e.g.
49 ``"Nitrogen"``. Separate from ``name`` because the library's
50 spelling is its business, not ours.
51 aliases: Other names that resolve here -- ``"lox"`` for oxygen, ``"gn2"``
52 for nitrogen. Matched case-insensitively.
53 roles: What the fluid is used as: ``oxidizer``, ``fuel``, ``pressurant``.
54 Advisory metadata for UIs and validation, not physics.
55 chain: Backends to try in order. Defaults to :data:`DEFAULT_CHAIN`.
56 options: Free-form settings passed through to backends, so a backend can
57 carry per-species configuration without this class growing a field
58 for every backend that ever exists.
63 aliases: tuple[str, ...] = ()
64 roles: tuple[str, ...] = ()
65 chain: tuple[str, ...] = DEFAULT_CHAIN
66 options: dict[str, Any] = field(default_factory=dict)
69_SPECIES: dict[str, SpeciesSpec] = {}
70_ALIASES: dict[str, str] = {}
82 """Add or replace a species. Aliases are indexed case-insensitively.
84 Registrations layer *on top of* the shipped table: it is loaded first if it
85 has not been already, so adding a species never removes one.
92 """Register without loading the shipped table -- used by the loader itself."""
93 _SPECIES[spec.name] = spec
94 _ALIASES[spec.name.lower()] = spec.name
95 for alias
in spec.aliases:
96 _ALIASES[alias.lower()] = spec.name
101 """Resolve a name or alias to its species. Case-insensitive."""
103 key = _ALIASES.get(name.strip().lower())
110 """Canonical names of every declared species, sorted."""
112 return sorted(_SPECIES)
127 """Load a species table from a TOML file and register everything in it.
129 Public so a project can layer its own table on top of the shipped one --
130 later definitions replace earlier ones by name.
132 with path.open(
"rb")
as handle:
133 raw = tomllib.load(handle)
135 loaded: list[SpeciesSpec] = []
136 for name, body
in raw.items():
137 if not isinstance(body, dict):
139 f
"{path}: [{name}] must be a table, got {type(body).__name__}"
146 backend_fluid = body.get(
"backend_fluid")
147 if not isinstance(backend_fluid, str):
148 raise ValueError(f
"species [{name}] needs a string 'backend_fluid'")
150 chain = body.get(
"chain", list(DEFAULT_CHAIN))
151 options = body.get(
"options", {})
152 if not isinstance(options, dict):
153 raise ValueError(f
"species [{name}]: 'options' must be a table")
157 backend_fluid=backend_fluid,
158 aliases=
_str_tuple(name,
"aliases", body.get(
"aliases", [])),
159 roles=
_str_tuple(name,
"roles", body.get(
"roles", [])),
165def _str_tuple(species: str, key: str, value: Any) -> tuple[str, ...]:
166 if not isinstance(value, list)
or not all(isinstance(v, str)
for v
in value):
167 raise ValueError(f
"species [{species}]: '{key}' must be a list of strings")
168 return tuple(str(v)
for v
in value)
list[str] registered_species()
SpeciesSpec _spec_from_toml(str name, dict[str, Any] body)
tuple[str,...] _str_tuple(str species, str key, Any value)
SpeciesSpec _register(SpeciesSpec spec)
SpeciesSpec get_species(str name)
list[SpeciesSpec] load_species_file(Path path)
SpeciesSpec register_species(SpeciesSpec spec)