Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b643c4d
Merge pull request #5 from stanford-developers/dev
nicholas9182 Dec 13, 2025
f529284
Merge pull request #7 from stanford-developers/dev
nicholas9182 Dec 13, 2025
c2cb664
Update constants and units for CCUS TEA integration
niklasmarxer Jan 16, 2026
f75d8fa
Merge pull request #10 from stanford-developers/dev
nicholas9182 Jan 20, 2026
9b7ab39
Merge origin/main into dev_niklas
niklasmarxer Jan 22, 2026
a198005
Reset init file to original state
niklasmarxer Jan 22, 2026
caca3c2
Refactor Constants module: simplify Units.py, remove redundant conver…
niklasmarxer Jan 27, 2026
4e4b828
Merge origin/dev into dev_niklasResolved conflicts in Units.py and Un…
niklasmarxer Jan 27, 2026
25385c0
Add ThermodynamicsMixin with generic Antoine equation methods
niklasmarxer Jan 27, 2026
d6edab1
Refactor: Start migration of thermodynamic properties
niklasmarxer Jan 27, 2026
0be0077
Refactor: Remove legacy ThermodynamicProperties.py
niklasmarxer Jan 27, 2026
e897d29
removed unnecessary file
niklasmarxer Jan 28, 2026
7ffeabf
init update
niklasmarxer Jan 28, 2026
2247e2c
added molar masses
fschweden Jan 29, 2026
0f996b6
Merge branch 'dev_niklas' of https://github.com/stanford-developers/s…
fschweden Jan 29, 2026
3c5c650
added Molar Masses
fschweden Jan 29, 2026
2f20c88
units added and small mistake fixed
fschweden Mar 4, 2026
2bc3538
Merge remote-tracking branch 'origin/dev_niklas' into fix/dev-niklas-…
tomeliotjullien Apr 28, 2026
771eb42
Clean version of dev_niklas
tomeliotjullien Apr 28, 2026
028b9ef
typecheckers implemented for frequency types (basis, display, compoun…
tomeliotjullien Apr 29, 2026
b423f4b
updated validation frequency methods
tomeliotjullien Apr 29, 2026
a22be2d
pressure units updated
tomeliotjullien Apr 30, 2026
8ffc566
rogue pressure units removed and MW removed
tomeliotjullien Apr 30, 2026
50e6353
test updated
tomeliotjullien Apr 30, 2026
3f6d7e0
consistant naming for the methods
tomeliotjullien Apr 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/api/mixins.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,8 @@ geometry, datetime handling, change propagation, and more.

---

::: steer_core.Mixins.Thermodynamics

---

::: steer_core.Mixins.TypeChecker
138 changes: 114 additions & 24 deletions steer_core/Constants/Units.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
# SPDX-FileCopyrightText: 2024-2026 Stanford University
# SPDX-License-Identifier: AGPL-3.0-or-later

## Unit conversions
# Length units
# =============================================================================
# MASS UNITS
# =============================================================================
KG_TO_G = 1e3
KG_TO_T = 1e-3
T_TO_KG = 1e3
G_TO_KG = 1e-3
T_TO_MT = 1e-6 # tonnes to megatonnes
MT_TO_T = 1e6 # megatonnes to tonnes
# Cascaded mass conversions
G_TO_T = G_TO_KG * KG_TO_T # 1e-6
T_TO_G = T_TO_KG * KG_TO_G # 1e6

# =============================================================================
# MOLAR UNITS
# =============================================================================
KMOL_TO_MOL = 1e3
MOL_TO_KMOL = 1e-3

# =============================================================================
# LENGTH UNITS
# =============================================================================
M_TO_CM = 1e2
CM_TO_M = 1e-2
M_TO_MM = 1e3
Expand All @@ -23,73 +41,145 @@
G_TO_mG = 1e3
CM_TO_UM = 1e4
UM_TO_CM = 1e-4
KG_TO_T = 1e-3
T_TO_KG = 1e3
T_TO_MT = 1e-6
MT_TO_T = 1e6
LB_TO_KG = 0.453592
KG_TO_LB = 1 / 0.453592
T_TO_SHORT_TON = 1.10231
SHORT_TON_TO_T = 1 / 1.10231
LB_TO_SHORT_TON = 1 / 2000
SHORT_TON_TO_LB = 2000
LB_TO_T = 1 / 2000 * 1/1.10231

# Cascaded imperial-to-metric mass conversions
LB_TO_T = LB_TO_KG * KG_TO_T

# Current units
# =============================================================================
# CURRENT UNITS
# =============================================================================
A_TO_mA = 1e3
mA_TO_A = 1e-3

# Time units
# =============================================================================
# TIME UNITS
# =============================================================================
S_TO_H = 1 / 3600
H_TO_S = 3600
S_TO_MIN = 1 / 60
MIN_TO_S = 60
H_TO_MIN = 60
MIN_TO_H = 1 / 60
S_TO_Y = 1 / (3600 * 24 * 365)
Y_TO_S = 3600 * 24 * 365
H_TO_Y = 1 / 8760
Y_TO_H = 8760
S_TO_D = 1 / (3600 * 24)
D_TO_S = 3600 * 24
H_TO_D = 1 / 24
D_TO_H = 24
H_TO_W = 1 / (24 * 7)
D_TO_W = 1 / 7
W_TO_D = 7
Y_TO_M = 12
H_TO_US = 3600000
D_TO_H = 24
W_TO_D = 7
S_TO_US = 1000
M_TO_Y = 1 / 12

# Millisecond conversions (used for Plotly datetime axis bin sizing)
S_TO_MS = 1e3
H_TO_MS = H_TO_S * S_TO_MS # 3_600_000
D_TO_MS = D_TO_S * S_TO_MS # 86_400_000

D_TO_W = 1 / 7
# Average-based time conversions
AVG_D_TO_MONTH = 12 / 365.25
AVG_D_TO_Y = 1 / 365.25
Y_TO_AVG_D = 365.25
AVG_W_TO_Y = 1 / (365.25 / 7)
AVG_H_TO_Y = 1 / (365.25 * 24)


# Energy units
# =============================================================================
# ENERGY UNITS
# =============================================================================
W_TO_KW = 1e-3
J_TO_WH = 1 / 3600
KW_TO_W = 1e3
KW_TO_MW = 1e-3
MW_TO_KW = 1e3
# Cascaded power conversions
W_TO_MW = W_TO_KW * KW_TO_MW # 1e-6
MW_TO_W = MW_TO_KW * KW_TO_W # 1e6

KW_PER_HP = 0.7457
HP_PER_KW = 1.0 / KW_PER_HP

J_TO_WH = 1.0 / 3600.0
MWH_TO_GJ = 3.6
GJ_TO_MWH = 1.0 / MWH_TO_GJ

KJ_TO_GJ = 1e-6
GJ_TO_KJ = 1e6
GJ_TO_MGJ = 1e-6
MGJ_TO_GJ = 1e6

J_TO_KJ = 1e-3
KJ_TO_J = 1e3
J_TO_GJ = J_TO_KJ * KJ_TO_GJ
GJ_TO_J = GJ_TO_KJ * KJ_TO_J

BTU_TO_J = 1055.05585262
J_TO_BTU = 1 / 1055.05585262
BTU_TO_MMBTU = 1e-6
MMBTU_TO_BTU = 1e6


# =============================================================================
# PRESSURE UNITS
# =============================================================================
BAR_TO_PA = 1e5
PA_TO_BAR = 1e-5
KPA_TO_PA = 1e3
PA_TO_KPA = 1e-3
# Cascaded pressure conversions
BAR_TO_KPA = BAR_TO_PA * PA_TO_KPA # 1e2
KPA_TO_BAR = KPA_TO_PA * PA_TO_BAR # 1e-2
MMHG_TO_PA = 133.322 # mmHg to Pascal
PA_TO_MMHG = 1.0 / MMHG_TO_PA # Pascal to mmHg
GPU_TO_SI = 3.35e-10 # mol/(m²·s·Pa) per GPU (gas permeation unit)

# =============================================================================
# TEMPERATURE UNITS
# =============================================================================
K_TO_C = -273.15
C_TO_K = 273.15

# =============================================================================
# VISCOSITY UNITS
# =============================================================================
PA_S_TO_CP = 1e3 # Pascal-seconds to Centipoise
CP_TO_PA_S = 1e-3 # Centipoise to Pascal-seconds

# =============================================================================
# CURRENCY & MISC
# =============================================================================
USD_TO_KUSD = 1e-3
KUSD_TO_USD = 1e3
USD_TO_MUSD = 1e-6
MUSD_TO_USD = 1e6
# Cascaded currency conversions
MUSD_TO_KUSD = MUSD_TO_USD * USD_TO_KUSD # 1e3
KUSD_TO_MUSD = KUSD_TO_USD * USD_TO_MUSD # 1e-3

# Angle units
DEG_TO_RAD = 0.017453292519943295
RAD_TO_DEG = 57.29577951308232

# Percentage units
PERCENT_TO_FRACTION = 1e-2
FRACTION_TO_PERCENT = 1e2
FRACTION_TO_PPM = 1e6
PPM_TO_FRACTION = 1e-6

# Unitless
UNIT_TO_MILLION: float = 1e-6
MILLION_TO_UNIT: float = 1e6
UNIT_TO_MILLION = 1e-6
MILLION_TO_UNIT = 1e6

# Volume units
L_TO_M3 = 1e-3
M3_TO_L = 1e3
# =============================================================================
# VOLUME UNITS
# =============================================================================
L_TO_M3 = DM_TO_M**3 # Derived from length (1e-3)
M3_TO_L = 1.0 / L_TO_M3
GAL_TO_L = 3.78541
L_TO_GAL = 1 / 3.78541
MMGAL_TO_GAL = 1e-6
Expand Down
15 changes: 11 additions & 4 deletions steer_core/Constants/Universal.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@
PI = 3.14159265358979323846
TWO_PI = 2.0 * PI

# Molar Masses
MW_G_PER_MOL_NO2 = 46.0055
MW_G_PER_MOL_SO2 = 64.0638
MW_G_PER_MOL_CO2 = 44.0095
# Universal Physical Constants

R_GAS = 8.314462618 # J/(mol·K) Universal gas constant (CODATA 2018)

GRAVITY = 9.80665 # m/s² — Standard acceleration due to gravity (exact by definition)


# Standard Conditions

STANDARD_PRESSURE = 101325.0 # Pa (1 atm)
STANDARD_TEMPERATURE = 273.15 # K
60 changes: 60 additions & 0 deletions steer_core/Mixins/Thermodynamics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""
Thermodynamics mixin providing generic phase equilibrium calculations.

This mixin provides reusable thermodynamic functions that can be
composed with material classes.
"""

import math
from steer_core.Constants.Universal import R_GAS


class ThermodynamicsMixin:
"""Mixin providing generic thermodynamic calculation methods."""

@staticmethod
def calculate_antoine_pressure(T: float, A: float, B: float, C: float) -> float:
"""
Calculate vapor pressure using Antoine equation.

Equation: log10(P) = A - B / (T + C)

Args:
T: Temperature (units depend on Antoine coefficients, typically °C or K)
A, B, C: Antoine coefficients for the substance

Returns:
Vapor pressure (units depend on Antoine coefficients, typically mmHg or bar)
"""
return 10 ** (A - B / (T + C))

@staticmethod
def calculate_antoine_temperature(P: float, A: float, B: float, C: float) -> float:
"""
Calculate temperature from vapor pressure using inverse Antoine equation.

Args:
P: Vapor pressure (same units as Antoine coefficients)
A, B, C: Antoine coefficients for the substance

Returns:
Temperature (same units as Antoine coefficients)
"""
log_P = math.log10(P)
return B / (A - log_P) - C

@staticmethod
def calculate_ideal_gas_density(P_Pa: float, T_K: float, MW_kg_mol: float) -> float:
"""
Calculate ideal gas density using ideal gas law.

Args:
P_Pa: Pressure in Pascals
T_K: Temperature in Kelvin
MW_kg_mol: Molecular weight in kg/mol

Returns:
Density in kg/m³
"""

return P_Pa * MW_kg_mol / (R_GAS * T_K)
105 changes: 105 additions & 0 deletions steer_core/Mixins/TypeChecker.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,3 +337,108 @@ def validate_number(value, name: str) -> None:
if not isinstance(value, (int, float, np.int64)):
raise TypeError(f"{name} must be a number (int or float). Provided: {type(value).__name__}.")

# ------------------------------------------------------------------
# Frequency-architecture validators
# ------------------------------------------------------------------

@staticmethod
def validate_base_binning_frequency(value, name: str = "base_binning_frequency") -> None:
"""Validate that *value* is an allowed base binning frequency ('hourly' or 'daily').

Parameters
----------
value : Frequency enum or str
The value to validate.
name : str
Parameter name used in the error message.

Raises
------
ValueError
If the value is not 'hourly' or 'daily'.
"""
val = value.value if hasattr(value, "value") else str(value).lower()
if val not in ("hourly", "daily"):
raise ValueError(
f"{name} must be 'hourly' or 'daily'. Provided: '{val}'."
)

@staticmethod
def validate_display_frequency(display_freq, base_freq, name: str = "display_frequency") -> None:
"""Validate that *display_freq* is equal to or coarser than *base_freq*.

Parameters
----------
display_freq : Frequency enum
The display frequency to validate.
base_freq : Frequency enum
The base binning frequency.
name : str
Parameter name used in the error message.

Raises
------
ValueError
If display_freq is finer than base_freq.
"""
d_rank = display_freq.rank if hasattr(display_freq, "rank") else 0
b_rank = base_freq.rank if hasattr(base_freq, "rank") else 0
if d_rank < b_rank:
raise ValueError(
f"{name} '{display_freq.value}' is finer than "
f"base_binning_frequency '{base_freq.value}'."
)

@staticmethod
def validate_base_binning_frequency_not_increased(current_freq, new_freq, name: str = "base_binning_frequency") -> None:
"""Validate that *new_freq* is not finer than *current_freq*.

Prevents increasing the resolution of an already-set base binning frequency
(e.g., daily → hourly), which would fabricate precision that was never there.
Going coarser (hourly → daily) is always allowed.

Parameters
----------
current_freq : Frequency enum
The currently set base binning frequency.
new_freq : Frequency enum
The proposed new base binning frequency.
name : str
Parameter name used in the error message.

Raises
------
ValueError
If new_freq is finer than current_freq.
"""
if new_freq.rank < current_freq.rank:
raise ValueError(
f"Cannot change {name} from '{current_freq.value}' to '{new_freq.value}': "
f"cannot increase resolution on an existing instance."
)

@staticmethod
def validate_compounding_frequency(base_freq, compounding_freq, name: str = "compounding_frequency") -> None:
"""Validate that *base_freq* is equal to or finer than *compounding_freq*.

Parameters
----------
base_freq : Frequency enum
The base binning frequency.
compounding_freq : Frequency enum
The compounding frequency.
name : str
Parameter name used in the error message.

Raises
------
ValueError
If base_freq is coarser than compounding_freq.
"""
b_rank = base_freq.rank if hasattr(base_freq, "rank") else 0
c_rank = compounding_freq.rank if hasattr(compounding_freq, "rank") else 0
if b_rank > c_rank:
raise ValueError(
f"base_binning_frequency '{base_freq.value}' is coarser than "
f"{name} '{compounding_freq.value}'."
)
Loading
Loading