from __future__ import annotations
"""
thermo_props.core
Facade module expected by `thermo_props.__init__`.
This file provides:
- coolprop_available() -> bool
- State dataclass alias (public name) + ThermoState import
- Props service -> build thermodynamic states + query thermodynamic properties (CoolProp-backed)
Latest facts (your codebase):
- `thermo_props.state` defines `ThermoState` (NOT `State`).
- `thermo_props.__init__` historically imported:
from .core import Props, State, coolprop_available
So this module must export `State`.
We do: State = ThermoState (alias) and also export ThermoState.
Design notes:
- Keep imports safe if CoolProp isn't installed (coolprop_backend guards).
- Support two usage modes:
(A) EES-ish mapping inputs with unit-suffixed keys (T_C, P_bar, h_kJkg, v_m3kg, ...)
-> state_from_mapping(...) from state.py
(B) Low-level direct calls with CoolProp-native keys (T,P,Q,Hmass,Smass,Umass,Dmass)
-> prop_from_state(...) and/or state_from_pair(...)
"""
from typing import Any, Dict, Iterable, Mapping, Optional, Sequence, Tuple
from .coolprop_backend import CoolPropCallError, CoolPropNotInstalled, coolprop_available
from .state import (
DEFAULT_OUTPUTS,
ThermoState,
prop_from_state,
state_from_mapping,
state_from_pair,
)
# Public alias expected by thermo_props/__init__.py (back-compat)
State = ThermoState
# ------------------------- output aliasing (EES-ish) -------------------------
# The ThermoState.props dict stores mostly CoolProp-native keys:
# T, P, Q, Hmass, Smass, Umass, Dmass, Cpmass, Cvmass, A, V, L, Prandtl
# plus derived:
# v, gamma, R_eff
#
# Allow requesting either EES-ish names ("h", "s", "rho", "cp", ...) or the stored keys.
_OUT_ALIASES: Dict[str, str] = {
# core thermo
"T": "T",
"P": "P",
"x": "Q",
"Q": "Q",
"h": "Hmass",
"H": "Hmass",
"s": "Smass",
"S": "Smass",
"u": "Umass",
"U": "Umass",
"rho": "Dmass",
"D": "Dmass",
# convenience / derived
"v": "v", # derived m^3/kg
"gamma": "gamma",
"R_eff": "R_eff",
# caloric / acoustic / transport
"cp": "Cpmass",
"cv": "Cvmass",
"a": "A",
"mu": "V", # viscosity key in DEFAULT_OUTPUTS is "V"
"k": "L", # thermal conductivity key in DEFAULT_OUTPUTS is "L"
"Pr": "Prandtl",
}
_DEFAULT_OUTPUTS: Tuple[str, ...] = tuple(DEFAULT_OUTPUTS)
def _norm_out_key(k: str) -> str:
kk = str(k).strip()
if not kk:
raise ValueError("Empty output key.")
if kk in _OUT_ALIASES:
return _OUT_ALIASES[kk]
lk = kk.lower()
if lk in _OUT_ALIASES:
return _OUT_ALIASES[lk]
# Allow requesting a stored CoolProp key directly, e.g. "Hmass", "Cpmass", "Prandtl"
return kk
def _require_coolprop(where: str) -> None:
"""
Raise a consistent, user-friendly error if CoolProp isn't available.
We intentionally raise CoolPropNotInstalled (subclass of ImportError) so callers
can catch it as ImportError in CLI/GUI flows.
"""
if coolprop_available():
return
raise CoolPropNotInstalled(
f"CoolProp is not available, but {where} was called. "
"Install it with: pip install CoolProp"
)
# ------------------------- Props service -------------------------
[docs]
class Props:
"""
Convenience thermo property service (CoolProp-backed).
This class delegates "state building" to `state.py` to avoid duplicating
normalization/unit-conversion logic.
Typical use:
props = Props()
st = props.state("R134a", {"T_C": -10, "x": 1.0})
print(st.h) # J/kg
print(st.props["Hmass"]) # same
print(props.get("cp", "R134a", {"T_C": -10, "x": 1.0}))
Notes:
- `inputs` must specify exactly TWO independent thermodynamic_properties (state.py enforces this).
- Larger sets of equations belong in `equations`.
"""
def __init__(
self,
default_outputs: Sequence[str] = _DEFAULT_OUTPUTS,
*,
include_phase: bool = True,
) -> None:
self.default_outputs = tuple(str(x) for x in default_outputs)
self.include_phase = bool(include_phase)
# ---- state builders ----
[docs]
def state(
self,
fluid: str,
inputs: Mapping[str, Any],
*,
outputs: Optional[Sequence[str]] = None,
include_phase: Optional[bool] = None,
) -> ThermoState:
"""
Build a ThermoState from a mapping containing exactly two independent inputs.
`inputs` may contain unit-suffixed keys such as:
- T_C, T_K
- P_bar, P_kPa, P_MPa, P_atm
- h_kJkg, s_kJkgK, u_kJkg
- rho_kgm3, v_m3kg
- x or Q (quality)
"""
_require_coolprop("thermo_props.Props.state(...)")
outs = tuple(outputs) if outputs is not None else self.default_outputs
inc_phase = self.include_phase if include_phase is None else bool(include_phase)
return state_from_mapping(
dict(inputs),
fluid=str(fluid),
outputs=outs,
include_phase=inc_phase,
)
[docs]
def state_from_pair(
self,
fluid: str,
in1: str,
v1: float,
in2: str,
v2: float,
*,
outputs: Optional[Sequence[str]] = None,
include_phase: Optional[bool] = None,
) -> ThermoState:
"""
Build a ThermoState from two CoolProp-native inputs (already SI).
Example:
props.state_from_pair("R134a", "T", 263.15, "Q", 1.0)
"""
_require_coolprop("thermo_props.Props.state_from_pair(...)")
outs = tuple(outputs) if outputs is not None else self.default_outputs
inc_phase = self.include_phase if include_phase is None else bool(include_phase)
return state_from_pair(
fluid=str(fluid),
in1=str(in1),
v1=float(v1),
in2=str(in2),
v2=float(v2),
outputs=outs,
include_phase=inc_phase,
)
# ---- property getters ----
[docs]
def get(
self,
out_key: str,
fluid: str,
inputs: Mapping[str, Any],
*,
default: Optional[float] = None,
) -> Optional[float]:
"""
Get one property from a two-input mapping.
`out_key` can be:
- EES-ish: h, s, u, rho, v, cp, cv, a, mu, k, Pr, x
- or stored key: Hmass, Smass, Umass, Dmass, Cpmass, Cvmass, A, V, L, Prandtl, T, P, Q
"""
st = self.state(fluid, inputs, outputs=self.default_outputs, include_phase=False)
k = _norm_out_key(out_key)
return st.props.get(k, default)
[docs]
def get_many(
self,
out_keys: Iterable[str],
fluid: str,
inputs: Mapping[str, Any],
*,
default: Optional[float] = None,
) -> Dict[str, Optional[float]]:
"""
Get multiple thermodynamic_properties from a two-input mapping.
Returns dict keyed by the ORIGINAL requested keys.
"""
st = self.state(fluid, inputs, outputs=self.default_outputs, include_phase=False)
out: Dict[str, Optional[float]] = {}
for k_req in out_keys:
k_req_s = str(k_req)
k = _norm_out_key(k_req_s)
out[k_req_s] = st.props.get(k, default)
return out
# ---- fast single-call helper (CoolProp-native only) ----
[docs]
def get_si_from_pair(
self,
out: str,
fluid: str,
in1: str,
v1: float,
in2: str,
v2: float,
) -> float:
"""
Fast path: one property from two SI inputs using CoolProp-native tokens.
Example:
props.get_si_from_pair("Hmass", "R134a", "T", 263.15, "Q", 1.0)
"""
_require_coolprop("thermo_props.Props.get_si_from_pair(...)")
return float(prop_from_state(str(fluid), str(out), str(in1), float(v1), str(in2), float(v2)))
__all__ = [
"Props",
"State",
"ThermoState",
"DEFAULT_OUTPUTS",
"CoolPropCallError",
"CoolPropNotInstalled",
"coolprop_available",
]