From c2cb6645965eb3e65e9c82251308d95d6e5069ad Mon Sep 17 00:00:00 2001 From: niklasmarxer Date: Fri, 16 Jan 2026 11:54:06 -0800 Subject: [PATCH 01/18] Update constants and units for CCUS TEA integration --- pyproject.toml | 3 + .../Constants/ThermodynamicProperties.py | 390 ++++++++++++++++++ steer_core/Constants/Units.py | 310 +++++++++++++- steer_core/Constants/Universal.py | 41 +- steer_core/Constants/__init__.py | 134 ++++++ steer_core/__init__.py | 47 ++- 6 files changed, 908 insertions(+), 17 deletions(-) create mode 100644 steer_core/Constants/ThermodynamicProperties.py diff --git a/pyproject.toml b/pyproject.toml index d021b76..062a3d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,9 @@ dependencies = [ "datetime==5.5", "plotly==6.2.0", "scipy==1.15.3", + "shapely>=2.0.0", # Required by Mixins.Coordinates + "msgpack>=1.0.0", # Required by Mixins.Serializer + "msgpack-numpy>=0.4.0", # Required by Mixins.Serializer ] [project.urls] diff --git a/steer_core/Constants/ThermodynamicProperties.py b/steer_core/Constants/ThermodynamicProperties.py new file mode 100644 index 0000000..9e7e89e --- /dev/null +++ b/steer_core/Constants/ThermodynamicProperties.py @@ -0,0 +1,390 @@ +""" +Thermodynamic properties for gases and fluids. + +This module contains thermodynamic property classes for common gases and fluids +used in process engineering calculations. All values use SI units. + +Classes: + CO2: Carbon dioxide properties + N2: Nitrogen properties + FlueGas: Flue gas / air mixture properties + Water: Water properties + Steam: Steam properties at various pressures + NaturalGas: Natural gas properties for combustion calculations + MEA: Monoethanolamine solvent properties +""" + +import math +from typing import Callable + + +# ============================================================================= +# CO2 PROPERTIES +# ============================================================================= + +class CO2: + """Carbon dioxide thermodynamic properties. + + All molecular weights in kg/mol (SI) with kg/kmol aliases. + """ + + # Molecular weight + MW_KG_KMOL = 44.01 # kg/kmol (same as g/mol) + MW_KG_MOL = 0.04401 # kg/mol (SI for molar calculations) + MW_CO2 = 0.04401 # kg/mol (alias for compatibility) + + # Antoine equation coefficients for sublimation curve + # log10(P_mmHg) = A - B / (T_celsius + C) + # Valid range: approximately -120°C to -60°C + ANTOINE_A = 9.81 + ANTOINE_B = 1347.79 + ANTOINE_C = 273.0 + + # Phase change enthalpies + H_SUBLIMATION_KJ_KG = 571.0 # kJ/kg, solid → gas + H_FUSION_KJ_KG = 196.0 # kJ/kg, solid → liquid + + # Heat capacities + CP_SOLID_KJ_KG_K = 0.95 # kJ/(kg·K), solid phase + CP_LIQUID_KJ_KG_K = 2.5 # kJ/(kg·K), liquid phase + + # Triple point + T_TRIPLE_C = -56.6 # °C + P_TRIPLE_PA = 5.18e5 # Pa + + # Typical pipeline pressure + P_PIPELINE_PA = 150e5 # Pa (150 bar) + + # Liquid density + RHO_LIQUID_KG_M3 = 1100 # kg/m³ + + +# ============================================================================= +# NITROGEN PROPERTIES +# ============================================================================= + +class N2: + """Nitrogen properties.""" + + MW_KG_KMOL = 28.01 # kg/kmol + MW_KG_MOL = 0.02801 # kg/mol + MW_N2 = 0.02801 # kg/mol (alias for compatibility) + + +# ============================================================================= +# FLUE GAS / AIR PROPERTIES +# ============================================================================= + +class FlueGas: + """Flue gas and air properties. + + These are typical values for combustion flue gas. + Exact values depend on fuel composition. + """ + + # Average molecular weight (varies with composition) + MW_AVG_KG_KMOL = 29.0 # kg/kmol, approximate for air-like mixture + MW_FLUE_GAS = 0.029 # kg/mol (alias for compatibility) + MW_AIR = 0.02897 # kg/mol (dry air average, alias for compatibility) + + # Heat capacity (average for flue gas mixture) + CP_KJ_KG_K = 1.05 # kJ/(kg·K) + + # Density at ambient conditions (~25°C, 1 atm) + RHO_AMBIENT_KG_M3 = 1.2 # kg/m³ + + # Ratio of specific heats (Cp/Cv), for air-like mixtures + GAMMA = 1.4 + + +# ============================================================================= +# WATER PROPERTIES +# ============================================================================= + +class Water: + """Water thermodynamic properties.""" + + MW_KG_KMOL = 18.015 # kg/kmol + MW_KG_MOL = 0.018015 # kg/mol (SI) + MW_H2O = 0.01802 # kg/mol (alias for compatibility) + + # Latent heat of vaporization + LAMBDA_VAPORIZATION_KJ_KG = 2200.0 # kJ/kg at ~120°C (stripper conditions) + LAMBDA_H2O_KJ_KG = 2260.0 # kJ/kg at 100°C (latent heat of vaporization) + + # Heat capacity + CP_WATER_KJ_KG_K = 4.18 # kJ/(kg·K) - Specific heat capacity of water + + # Density + DENSITY_KG_M3 = 1000.0 # kg/m³ + RHO_LIQUID_KG_M3 = 1000.0 # kg/m³ (alias) + + +# Alias for Water +H2O = Water + + +# ============================================================================= +# STEAM PROPERTIES +# ============================================================================= + +class Steam: + """Steam thermodynamic properties at various pressures. + + Reference: Steam tables (IAPWS-IF97) + """ + + # Latent heat of vaporization at different pressures + # Used for reboiler steam calculations + LAMBDA_LP_KJ_KG = 2200.0 # kJ/kg at ~2-3 bar (LP steam, ~120-135°C) + LAMBDA_MP_KJ_KG = 2015.0 # kJ/kg at ~10 bar (MP steam, ~180°C) + LAMBDA_6BAR_KJ_KG = 2086.0 # kJ/kg at 6 bar saturated (~159°C) + + # For energy cost calculations (converting GJ thermal to tonnes steam) + LAMBDA_LP_GJ_TONNE = 2.2 # GJ/tonne for LP steam (2.2 GJ = 2200 kJ/kg × 1000 kg) + LAMBDA_6BAR_GJ_TONNE = 2.086 # GJ/tonne at 6 bar + + +# ============================================================================= +# NATURAL GAS PROPERTIES +# ============================================================================= + +class NaturalGas: + """Natural gas properties for boiler calculations. + + Values are for typical pipeline-quality natural gas (mainly methane). + """ + + # Lower Heating Value (net calorific value) + LHV_MJ_KG = 47.0 # MJ/kg - typical for natural gas + LHV_GJ_TONNE = 47.0 # GJ/tonne + LHV_MJ_NM3 = 36.0 # MJ/Nm³ - volumetric basis + + # Higher Heating Value (gross calorific value) + HHV_MJ_KG = 52.2 # MJ/kg + HHV_GJ_TONNE = 52.2 # GJ/tonne + + # For $/MMBTU to $/GJ conversion + GJ_PER_MMBTU = 1.055 # 1 MMBTU = 1.055 GJ + + +# ============================================================================= +# MEA SOLVENT PROPERTIES +# ============================================================================= + +class MEA: + """Monoethanolamine solvent properties. + + Properties for aqueous MEA solution (typically 30 wt%). + + References: + - Weiland et al. (1998): Density and viscosity correlations + - Amundsen et al. (2009): Physical properties of MEA solutions + """ + + MW_KG_KMOL = 61.08 # kg/kmol, pure MEA + MW_KG_MOL = 0.06108 # kg/mol (SI) + MW_MEA = 0.06108 # kg/mol (alias for compatibility) + + # Heat capacity of 30 wt% MEA solution (temperature-averaged) + CP_SOLUTION_KJ_KG_K = 3.8 # kJ/(kg·K) + CP_MEA_30_KJ_KG_K = 3.8 # kJ/(kg·K) (alias for compatibility) + + # Density of 30 wt% MEA solution + RHO_COLD_KG_M3 = 1020 # kg/m³, at absorber conditions (~45°C) + RHO_HOT_KG_M3 = 950 # kg/m³, at stripper conditions (~120°C) + RHO_MEA_30_KG_M3 = 1020.0 # kg/m³ at 40°C (alias for compatibility) + RHO_MEA_30_HOT_KG_M3 = 950.0 # kg/m³ at ~120°C (alias for compatibility) + + # Viscosity of 30 wt% MEA solution (Pa·s) + # Reference: Weiland et al. (1998) + MU_45C_PA_S = 0.003 # Pa·s at 45°C (absorber conditions) + MU_120C_PA_S = 0.001 # Pa·s at 120°C (stripper conditions) + MU_COLD_PA_S = 0.003 # Pa·s at absorber (alias) + MU_HOT_PA_S = 0.001 # Pa·s at stripper (alias) + + # Heat of absorption for CO2 (Kim & Svendsen) + HEAT_OF_ABSORPTION_J_MOL = 84000.0 # J/mol CO2 + + @staticmethod + def viscosity_pa_s(T_celsius: float) -> float: + """ + Calculate 30 wt% MEA solution viscosity as function of temperature. + + Simplified Arrhenius-type correlation fitted to Weiland et al. (1998) data. + Valid range: 25-130°C + + Args: + T_celsius: Temperature in Celsius + + Returns: + Dynamic viscosity in Pa·s + """ + # Arrhenius parameters (fitted to MEA 30 wt% data) + # μ = A × exp(B/T_K) + A = 2.0e-6 # Pa·s + B = 2500.0 # K + T_K = T_celsius + 273.15 + return A * math.exp(B / T_K) + + @staticmethod + def density_kg_m3(T_celsius: float) -> float: + """ + Calculate 30 wt% MEA solution density as function of temperature. + + Linear interpolation based on Weiland et al. (1998) data. + Valid range: 25-130°C + + Args: + T_celsius: Temperature in Celsius + + Returns: + Density in kg/m³ + """ + # Linear fit: ρ = ρ_ref - α × (T - T_ref) + rho_ref = 1030.0 # kg/m³ at 25°C + T_ref = 25.0 # °C + alpha = 0.7 # kg/m³/°C thermal expansion coefficient + return rho_ref - alpha * (T_celsius - T_ref) + + +# ============================================================================= +# THERMODYNAMIC FUNCTIONS +# ============================================================================= + +def antoine_pressure_mmhg(T_celsius: float) -> float: + """ + Calculate CO2 sublimation pressure using Antoine equation. + + Args: + T_celsius: Temperature in Celsius + + Returns: + Pressure in mmHg + """ + return 10 ** (CO2.ANTOINE_A - CO2.ANTOINE_B / (T_celsius + CO2.ANTOINE_C)) + + +def antoine_temperature_celsius(P_mmhg: float) -> float: + """ + Calculate CO2 sublimation temperature from pressure. + + Args: + P_mmhg: Pressure in mmHg + + Returns: + Temperature in Celsius + """ + log_P = math.log10(P_mmhg) + return CO2.ANTOINE_B / (CO2.ANTOINE_A - log_P) - CO2.ANTOINE_C + + +def water_vapor_pressure_pa(T_K: float) -> float: + """ + Calculate water vapor pressure using Antoine equation. + + Args: + T_K: Temperature in Kelvin + + Returns: + Pressure in Pa + """ + # NIST Antoine parameters for water + A, B, C = 5.19621, 1730.63, -39.724 + log10_P_bar = A - B / (T_K + C) + P_bar = 10**log10_P_bar + return P_bar * 1e5 # Convert bar to Pa + + +def mea_equilibrium_pressure_pa(loading: float, T_K: float) -> float: + """ + Calculate equilibrium partial pressure of CO2 over MEA solution. + + Correlation fitted to Hilliard (2008) for 30 wt% MEA data. + ln(P_CO2 [Pa]) = A + B/T + C*alpha + + Valid range: 40-120°C, α=0.1-0.55 + + Args: + loading: CO2 loading (mol CO2 / mol MEA) + T_K: Temperature (Kelvin) + + Returns: + P_star_CO2 (Pa) + """ + # Keep loading in physical bounds + alpha = max(0.001, min(0.60, loading)) + + # Calibrated coefficients (fitted to Hilliard 2008 data) + A = 24.48 + B = -6882.0 + C = 12.97 + + ln_p_pa = A + B/T_K + C*alpha + return math.exp(ln_p_pa) + + +def mea_equilibrium_loading(P_CO2_Pa: float, T_K: float) -> float: + """ + Inverse VLE: Calculate equilibrium loading given P_CO2 and T. + + Solves ln(P) = A + B/T + C*alpha for alpha. + + Args: + P_CO2_Pa: CO2 partial pressure (Pa) + T_K: Temperature (Kelvin) + + Returns: + Equilibrium loading (mol CO2 / mol MEA) + """ + A = 24.48 + B = -6882.0 + C = 12.97 + + target_ln_p = math.log(max(1.0, P_CO2_Pa)) + + # Rearrange: alpha = (ln_P - A - B/T) / C + numerator = target_ln_p - A - B/T_K + alpha = numerator / C + + # Clamp to physical limits + return max(0.01, min(0.60, alpha)) + + +def mea_bubble_point_temperature(loading: float, P_total_Pa: float) -> float: + """ + Calculate bubble point temperature for MEA solution. + + At bubble point: P_total = P*_CO2 + P*_H2O + + Args: + loading: CO2 loading (mol CO2 / mol MEA) + P_total_Pa: Total pressure (Pa) + + Returns: + Bubble point temperature (Kelvin) + """ + # Initial guess: 120°C (typical stripper temperature) + T_guess_K = 273.15 + 120.0 + + # Iterate to find bubble point + for _ in range(20): # Max 20 iterations + P_co2_star = mea_equilibrium_pressure_pa(loading, T_guess_K) + P_h2o_star = water_vapor_pressure_pa(T_guess_K) + P_total_calc = P_co2_star + P_h2o_star + + # Check convergence + error = abs(P_total_calc - P_total_Pa) / P_total_Pa + if error < 0.01: # 1% tolerance + return T_guess_K + + # Adjust temperature + if P_total_calc > P_total_Pa: + T_guess_K -= 2.0 + else: + T_guess_K += 2.0 + + # Clamp to reasonable range + T_guess_K = max(273.15 + 80.0, min(273.15 + 140.0, T_guess_K)) + + return T_guess_K diff --git a/steer_core/Constants/Units.py b/steer_core/Constants/Units.py index 29cf4d6..65139e7 100644 --- a/steer_core/Constants/Units.py +++ b/steer_core/Constants/Units.py @@ -1,7 +1,40 @@ -## Unit conversions +""" +Unit conversion constants and functions. + +This module provides consistent unit handling across the codebase. +All conversions are defined as constants or pure functions. + +IMPORTANT: Always use these functions/constants for unit conversions. + Never use raw multipliers like /1000 or *1000 directly. + +Categories: + - Length/Mass + - Time + - Power/Energy + - Pressure + - Temperature + - Volume + - Currency +""" + +import math +from typing import Union + + +# ============================================================================= +# LENGTH AND MASS UNITS +# ============================================================================= + # Length 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 (million tonnes) +MT_TO_T = 1e6 # megatonnes to tonnes + +KMOL_TO_MOL = 1e3 +MOL_TO_KMOL = 1e-3 M_TO_CM = 1e2 CM_TO_M = 1e-2 M_TO_MM = 1e3 @@ -20,11 +53,17 @@ CM_TO_UM = 1e4 UM_TO_CM = 1e-4 + + # 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 @@ -32,17 +71,270 @@ S_TO_Y = 1 / (3600 * 24 * 365) Y_TO_S = 3600 * 24 * 365 -# Energy units +SECONDS_PER_MINUTE = 60 +MINUTES_PER_HOUR = 60 +SECONDS_PER_HOUR = 3600 +HOURS_PER_DAY = 24 +DAYS_PER_YEAR = 365 +HOURS_PER_YEAR = 8760 +SECONDS_PER_YEAR = SECONDS_PER_HOUR * HOURS_PER_YEAR +SECONDS_PER_DAY = SECONDS_PER_HOUR * HOURS_PER_DAY + + +# ============================================================================= +# POWER UNITS +# ============================================================================= + W_TO_KW = 1e-3 +KW_TO_W = 1e3 +KW_TO_MW = 1e-3 +MW_TO_KW = 1e3 + + +def mw_to_kw(power_mw: float) -> float: + """Convert megawatts to kilowatts.""" + return power_mw * MW_TO_KW + + +def kw_to_mw(power_kw: float) -> float: + """Convert kilowatts to megawatts.""" + return power_kw * KW_TO_MW + + +def w_to_kw(power_w: float) -> float: + """Convert watts to kilowatts.""" + return power_w * W_TO_KW + + +def kw_to_w(power_kw: float) -> float: + """Convert kilowatts to watts.""" + return power_kw * KW_TO_W + + +def w_to_mw(power_w: float) -> float: + """Convert watts to megawatts.""" + return power_w * W_TO_KW * KW_TO_MW + + +# Horsepower conversions +KW_PER_HP = 0.7457 +HP_PER_KW = 1.0 / KW_PER_HP + + +def hp_to_kw(power_hp: float) -> float: + """Convert horsepower to kilowatts.""" + return power_hp * KW_PER_HP + + +def kw_to_hp(power_kw: float) -> float: + """Convert kilowatts to horsepower.""" + return power_kw * HP_PER_KW + + +# ============================================================================= +# ENERGY UNITS +# ============================================================================= + J_TO_WH = 1 / 3600 -# Angle units +# Energy conversion factors (MW·h to GJ) +MWH_TO_GJ = 3.6 # 1 MW·h = 3600 MJ = 3.6 GJ +GJ_TO_MWH = 1.0 / MWH_TO_GJ # 1 GJ = 1/3.6 MW·h + +# Energy conversion factors (kJ to GJ) +KJ_TO_GJ = 1e-6 # 1 kJ = 0.000001 GJ +GJ_TO_KJ = 1e6 # 1 GJ = 1,000,000 kJ +GJ_TO_MGJ = 1e-6 # GJ to million GJ (megagigajoules) +MGJ_TO_GJ = 1e6 # million GJ to GJ + +# Energy conversion factors (J to kJ) +J_TO_KJ = 1e-3 # 1 J = 0.001 kJ +KJ_TO_J = 1e3 # 1 kJ = 1000 J + +# Power-energy factors +KW_TO_MJ_PER_HOUR = 3.6 # kW·h to MJ conversion: 1 kW·h = 3600 kJ = 3.6 MJ +KW_PER_KG_TO_MJ_PER_KG = 3.6 # kW/kg = kJ/(kg·s) = 3600 kJ/(kg·h) = 3.6 MJ/kg + + +def gj_to_mwh(energy_gj: float) -> float: + """Convert gigajoules to megawatt-hours.""" + return energy_gj * GJ_TO_MWH + + +def mwh_to_gj(energy_mwh: float) -> float: + """Convert megawatt-hours to gigajoules.""" + return energy_mwh * MWH_TO_GJ + + +def gj_per_tonne_to_mwh_per_tonne(specific_energy: float) -> float: + """Convert GJ/tonne to MWh/tonne.""" + return specific_energy / 3.6 + + +def j_to_kj(energy_j: float) -> float: + """Convert joules to kilojoules.""" + return energy_j * J_TO_KJ + + +def kj_to_j(energy_kj: float) -> float: + """Convert kilojoules to joules.""" + return energy_kj * KJ_TO_J + + +def kg_kmol_to_kg_mol(mw_kg_kmol: float) -> float: + """Convert molecular weight from kg/kmol to kg/mol.""" + return mw_kg_kmol * MOL_TO_KMOL + + +def kg_mol_to_kg_kmol(mw_kg_mol: float) -> float: + """Convert molecular weight from kg/mol to kg/kmol.""" + return mw_kg_mol * KMOL_TO_MOL + + +# ============================================================================= +# PRESSURE UNITS +# ============================================================================= + +BAR_TO_PA = 1e5 # bar to Pa conversion factor +PA_TO_BAR = 1e-5 # Pa to bar conversion factor +KPA_TO_PA = 1e3 # kPa to Pa conversion factor +PA_TO_KPA = 1e-3 # Pa to kPa conversion factor + + +def bar_to_pa(pressure_bar: float) -> float: + """Convert bar to Pascal.""" + return pressure_bar * BAR_TO_PA + + +def pa_to_bar(pressure_pa: float) -> float: + """Convert Pascal to bar.""" + return pressure_pa * PA_TO_BAR + + +def atm_to_bar(pressure_atm: float) -> float: + """Convert atmospheres to bar.""" + return pressure_atm * 1.01325 + + +def bar_to_atm(pressure_bar: float) -> float: + """Convert bar to atmospheres.""" + return pressure_bar / 1.01325 + + +def psi_to_bar(pressure_psi: float) -> float: + """Convert psi to bar.""" + return pressure_psi * 0.0689476 + + +# ============================================================================= +# TEMPERATURE UNITS +# ============================================================================= + +KELVIN_OFFSET = 273.15 # Celsius to Kelvin offset + + +def celsius_to_kelvin(temp_c: float) -> float: + """Convert Celsius to Kelvin.""" + return temp_c + KELVIN_OFFSET + + +def kelvin_to_celsius(temp_k: float) -> float: + """Convert Kelvin to Celsius.""" + return temp_k - KELVIN_OFFSET + + +# ============================================================================= +# VOLUME UNITS +# ============================================================================= + +L_TO_M3 = 1e-3 +M3_TO_L = 1e3 + + +def m3_to_l(volume_m3: float) -> float: + """Convert cubic meters to liters.""" + return volume_m3 * M3_TO_L + + +def l_to_m3(volume_l: float) -> float: + """Convert liters to cubic meters.""" + return volume_l * L_TO_M3 + + +def m3_to_gal(volume_m3: float) -> float: + """Convert cubic meters to US gallons.""" + return volume_m3 * 264.172 + + +# ============================================================================= +# AREA UNITS +# ============================================================================= + +def m2_to_ft2(area_m2: float) -> float: + """Convert square meters to square feet.""" + return area_m2 * 10.7639 + + +def ft2_to_m2(area_ft2: float) -> float: + """Convert square feet to square meters.""" + return area_ft2 / 10.7639 + + +# ============================================================================= +# FLOW RATE UNITS +# ============================================================================= + +def m3_s_to_gpm(flow_m3_s: float) -> float: + """Convert m³/s to gallons per minute.""" + return flow_m3_s * 15850.3 + + +def gpm_to_m3_s(flow_gpm: float) -> float: + """Convert gallons per minute to m³/s.""" + return flow_gpm / 15850.3 + + +# ============================================================================= +# CURRENCY UNITS +# ============================================================================= + +USD_TO_KUSD = 1e-3 # $ to k$ (thousands) +KUSD_TO_USD = 1e3 # k$ to $ +USD_TO_MUSD = 1e-6 # $ to M$ (millions) +MUSD_TO_USD = 1e6 # M$ to $ + + +def usd_to_kusd(cost_usd: float) -> float: + """Convert dollars to thousands of dollars.""" + return cost_usd * USD_TO_KUSD + + +def kusd_to_usd(cost_kusd: float) -> float: + """Convert thousands of dollars to dollars.""" + return cost_kusd * KUSD_TO_USD + + +def usd_to_musd(cost_usd: float) -> float: + """Convert dollars to millions of dollars.""" + return cost_usd * USD_TO_MUSD + + +def musd_to_usd(cost_musd: float) -> float: + """Convert millions of dollars to dollars.""" + return cost_musd * MUSD_TO_USD + + +# ============================================================================= +# ANGLE UNITS +# ============================================================================= + DEG_TO_RAD = 0.017453292519943295 +RAD_TO_DEG = 57.29577951308232 + + +# ============================================================================= +# PERCENTAGE UNITS +# ============================================================================= -# Percentage units PERCENT_TO_FRACTION = 1e-2 FRACTION_TO_PERCENT = 1e2 - -# Volume units -L_TO_M3 = 1e-3 -M3_TO_L = 1e3 \ No newline at end of file diff --git a/steer_core/Constants/Universal.py b/steer_core/Constants/Universal.py index e38839f..f5dce25 100644 --- a/steer_core/Constants/Universal.py +++ b/steer_core/Constants/Universal.py @@ -1,2 +1,41 @@ -## Constants +""" +Universal physical constants. + +This module contains fundamental physical constants used across +engineering calculations. All values use SI units. +""" + + +# ============================================================================= +# MATHEMATICAL CONSTANTS +# ============================================================================= + PI = 3.14159265358979323846 + + +# ============================================================================= +# UNIVERSAL PHYSICAL CONSTANTS +# ============================================================================= + +R_GAS = 8.314 # J/(mol·K) Universal gas constant +R_GAS_J_MOL_K = 8.314 # J/(mol·K) - Universal gas constant (alias) +R_GAS_KJ_KMOL_K = 8.314 # kJ/(kmol·K) - For use with kmol units + +GRAVITY = 9.81 # m/s² - Standard acceleration due to gravity + + +# ============================================================================= +# STANDARD CONDITIONS +# ============================================================================= + +STANDARD_PRESSURE = 101325.0 # Pa (1 atm) +STANDARD_TEMPERATURE = 273.15 # K + + +# ============================================================================= +# CONVERSION FACTORS FOR SPECIAL UNITS +# ============================================================================= + +MMHG_PER_PA = 0.00750062 # mmHg per Pascal +PA_PER_BAR = 1e5 # Pascal per bar +GPU_TO_SI = 3.35e-10 # mol/(m²·s·Pa) per GPU (gas permeation unit) diff --git a/steer_core/Constants/__init__.py b/steer_core/Constants/__init__.py index e69de29..5531615 100644 --- a/steer_core/Constants/__init__.py +++ b/steer_core/Constants/__init__.py @@ -0,0 +1,134 @@ +""" +Constants module for steer-core. + +This module provides centralized access to: +- Universal physical constants (R_GAS, PI, GRAVITY, etc.) +- Unit conversion constants and functions +- Thermodynamic properties for gases and fluids + +Example usage: + from steer_core.Constants import R_GAS, CO2, celsius_to_kelvin + from steer_core.Constants import HOURS_PER_YEAR, MW_TO_KW +""" + +# Universal physical constants +from steer_core.Constants.Universal import ( + PI, + R_GAS, + R_GAS_J_MOL_K, + R_GAS_KJ_KMOL_K, + GRAVITY, + STANDARD_PRESSURE, + STANDARD_TEMPERATURE, + MMHG_PER_PA, + PA_PER_BAR, + GPU_TO_SI, +) + +# Unit conversion constants +from steer_core.Constants.Units import ( + # Length/Mass + KG_TO_G, G_TO_KG, + M_TO_CM, CM_TO_M, M_TO_MM, MM_TO_M, M_TO_UM, UM_TO_M, + KG_TO_T, T_TO_KG, T_TO_MT, MT_TO_T, + # Time + S_TO_H, H_TO_S, S_TO_MIN, MIN_TO_S, S_TO_Y, Y_TO_S, + SECONDS_PER_MINUTE, MINUTES_PER_HOUR, SECONDS_PER_HOUR, + HOURS_PER_DAY, DAYS_PER_YEAR, HOURS_PER_YEAR, + SECONDS_PER_YEAR, SECONDS_PER_DAY, + # Power + W_TO_KW, KW_TO_W, KW_TO_MW, MW_TO_KW, + KW_PER_HP, HP_PER_KW, + # Energy + J_TO_WH, MWH_TO_GJ, GJ_TO_MWH, KJ_TO_GJ, GJ_TO_KJ, J_TO_KJ, KJ_TO_J, + GJ_TO_MGJ, MGJ_TO_GJ, + KW_TO_MJ_PER_HOUR, KW_PER_KG_TO_MJ_PER_KG, + # Pressure + BAR_TO_PA, PA_TO_BAR, KPA_TO_PA, PA_TO_KPA, + # Temperature + KELVIN_OFFSET, + # Volume + L_TO_M3, M3_TO_L, + # Currency + USD_TO_KUSD, KUSD_TO_USD, USD_TO_MUSD, MUSD_TO_USD, + # Angle + DEG_TO_RAD, RAD_TO_DEG, + # Percentage + PERCENT_TO_FRACTION, FRACTION_TO_PERCENT, +) + +# Unit conversion functions +from steer_core.Constants.Units import ( + # Power + mw_to_kw, kw_to_mw, w_to_kw, kw_to_w, w_to_mw, + hp_to_kw, kw_to_hp, + # Energy + gj_to_mwh, mwh_to_gj, gj_per_tonne_to_mwh_per_tonne, + j_to_kj, kj_to_j, + kg_kmol_to_kg_mol, kg_mol_to_kg_kmol, + # Pressure + bar_to_pa, pa_to_bar, atm_to_bar, bar_to_atm, psi_to_bar, + # Temperature + celsius_to_kelvin, kelvin_to_celsius, + # Volume + m3_to_l, l_to_m3, m3_to_gal, + # Area + m2_to_ft2, ft2_to_m2, + # Flow rate + m3_s_to_gpm, gpm_to_m3_s, + # Currency + usd_to_kusd, kusd_to_usd, usd_to_musd, musd_to_usd, +) + +# Thermodynamic property classes +from steer_core.Constants.ThermodynamicProperties import ( + CO2, + N2, + FlueGas, + Water, H2O, # H2O is alias for Water + Steam, + NaturalGas, + MEA, +) + +# Thermodynamic functions +from steer_core.Constants.ThermodynamicProperties import ( + antoine_pressure_mmhg, + antoine_temperature_celsius, + water_vapor_pressure_pa, + mea_equilibrium_pressure_pa, + mea_equilibrium_loading, + mea_bubble_point_temperature, +) + + +# Aliases for backward compatibility +GAS_CONSTANT = R_GAS + + +__all__ = [ + # Universal constants + "PI", "R_GAS", "R_GAS_J_MOL_K", "R_GAS_KJ_KMOL_K", "GAS_CONSTANT", + "GRAVITY", "STANDARD_PRESSURE", "STANDARD_TEMPERATURE", + "MMHG_PER_PA", "PA_PER_BAR", "GPU_TO_SI", + # Unit conversion constants + "KG_TO_G", "G_TO_KG", "M_TO_CM", "CM_TO_M", "KG_TO_T", "T_TO_KG", "T_TO_MT", "MT_TO_T", + "S_TO_H", "H_TO_S", "SECONDS_PER_HOUR", "HOURS_PER_YEAR", "SECONDS_PER_YEAR", + "W_TO_KW", "KW_TO_W", "KW_TO_MW", "MW_TO_KW", + "MWH_TO_GJ", "GJ_TO_MWH", "KJ_TO_GJ", "GJ_TO_KJ", "GJ_TO_MGJ", "MGJ_TO_GJ", + "BAR_TO_PA", "PA_TO_BAR", "KELVIN_OFFSET", + "L_TO_M3", "M3_TO_L", + "USD_TO_KUSD", "KUSD_TO_USD", "USD_TO_MUSD", "MUSD_TO_USD", + # Unit conversion functions + "mw_to_kw", "kw_to_mw", "w_to_kw", "kw_to_w", "w_to_mw", + "gj_to_mwh", "mwh_to_gj", "j_to_kj", "kj_to_j", + "bar_to_pa", "pa_to_bar", + "celsius_to_kelvin", "kelvin_to_celsius", + "m3_to_l", "l_to_m3", + # Thermodynamic classes + "CO2", "N2", "FlueGas", "Water", "H2O", "Steam", "NaturalGas", "MEA", + # Thermodynamic functions + "antoine_pressure_mmhg", "antoine_temperature_celsius", + "water_vapor_pressure_pa", "mea_equilibrium_pressure_pa", + "mea_equilibrium_loading", "mea_bubble_point_temperature", +] diff --git a/steer_core/__init__.py b/steer_core/__init__.py index 59cb861..56b87bc 100644 --- a/steer_core/__init__.py +++ b/steer_core/__init__.py @@ -1,10 +1,43 @@ __version__ = "0.1.32" -from .Mixins.Colors import ColorMixin -from .Mixins.Coordinates import CoordinateMixin -from .Mixins.Plotter import PlotterMixin -from .Mixins.TypeChecker import ValidationMixin -from .Mixins.Dunder import DunderMixin -from .Mixins.Serializer import SerializerMixin -from .Mixins.Data import DataMixin +# Use lazy imports for Mixins to avoid triggering optional dependencies +# (like shapely for Coordinates) when only importing Constants. +# Users can still access Mixins via explicit import or attribute access. +def __getattr__(name): + """Lazy import handler for Mixins.""" + if name == "ColorMixin": + from .Mixins.Colors import ColorMixin + return ColorMixin + elif name == "CoordinateMixin": + from .Mixins.Coordinates import CoordinateMixin + return CoordinateMixin + elif name == "PlotterMixin": + from .Mixins.Plotter import PlotterMixin + return PlotterMixin + elif name == "ValidationMixin": + from .Mixins.TypeChecker import ValidationMixin + return ValidationMixin + elif name == "DunderMixin": + from .Mixins.Dunder import DunderMixin + return DunderMixin + elif name == "SerializerMixin": + from .Mixins.Serializer import SerializerMixin + return SerializerMixin + elif name == "DataMixin": + from .Mixins.Data import DataMixin + return DataMixin + raise AttributeError(f"module 'steer_core' has no attribute '{name}'") + + +# List of available Mixins (for discoverability) +__all__ = [ + "__version__", + "ColorMixin", + "CoordinateMixin", + "PlotterMixin", + "ValidationMixin", + "DunderMixin", + "SerializerMixin", + "DataMixin", +] From a198005d098d26379b510d287d3f712f6fc651fc Mon Sep 17 00:00:00 2001 From: niklasmarxer Date: Thu, 22 Jan 2026 14:58:23 -0800 Subject: [PATCH 02/18] Reset init file to original state --- steer_core/__init__.py | 48 ++++++------------------------------------ 1 file changed, 7 insertions(+), 41 deletions(-) diff --git a/steer_core/__init__.py b/steer_core/__init__.py index 2666373..5ef7236 100644 --- a/steer_core/__init__.py +++ b/steer_core/__init__.py @@ -1,43 +1,9 @@ __version__ = "0.1.33" -# Use lazy imports for Mixins to avoid triggering optional dependencies -# (like shapely for Coordinates) when only importing Constants. -# Users can still access Mixins via explicit import or attribute access. - -def __getattr__(name): - """Lazy import handler for Mixins.""" - if name == "ColorMixin": - from .Mixins.Colors import ColorMixin - return ColorMixin - elif name == "CoordinateMixin": - from .Mixins.Coordinates import CoordinateMixin - return CoordinateMixin - elif name == "PlotterMixin": - from .Mixins.Plotter import PlotterMixin - return PlotterMixin - elif name == "ValidationMixin": - from .Mixins.TypeChecker import ValidationMixin - return ValidationMixin - elif name == "DunderMixin": - from .Mixins.Dunder import DunderMixin - return DunderMixin - elif name == "SerializerMixin": - from .Mixins.Serializer import SerializerMixin - return SerializerMixin - elif name == "DataMixin": - from .Mixins.Data import DataMixin - return DataMixin - raise AttributeError(f"module 'steer_core' has no attribute '{name}'") - - -# List of available Mixins (for discoverability) -__all__ = [ - "__version__", - "ColorMixin", - "CoordinateMixin", - "PlotterMixin", - "ValidationMixin", - "DunderMixin", - "SerializerMixin", - "DataMixin", -] +from .Mixins.Colors import ColorMixin +from .Mixins.Coordinates import CoordinateMixin +from .Mixins.Plotter import PlotterMixin +from .Mixins.TypeChecker import ValidationMixin +from .Mixins.Dunder import DunderMixin +from .Mixins.Serializer import SerializerMixin +from .Mixins.Data import DataMixin \ No newline at end of file From caca3c2da9b92dcadb2141b291acefae6d4a2601 Mon Sep 17 00:00:00 2001 From: niklasmarxer Date: Mon, 26 Jan 2026 17:38:47 -0800 Subject: [PATCH 03/18] Refactor Constants module: simplify Units.py, remove redundant conversion functions - Replaced conversion functions with direct constant multipliers - Removed duplicate aliases (R_GAS_J_MOL_K, R_GAS_KJ_KMOL_K) - Renamed thermodynamic functions to simpler names (antoine_pressure, etc.) - Simplified __init__.py to use wildcard imports - Added cascaded conversion constants (W_TO_MW, MUSD_TO_KUSD, etc.) --- .../Constants/ThermodynamicProperties.py | 12 +- steer_core/Constants/Units.py | 317 +++--------------- steer_core/Constants/Universal.py | 2 - steer_core/Constants/__init__.py | 136 +------- 4 files changed, 67 insertions(+), 400 deletions(-) diff --git a/steer_core/Constants/ThermodynamicProperties.py b/steer_core/Constants/ThermodynamicProperties.py index 9e7e89e..9ff9fae 100644 --- a/steer_core/Constants/ThermodynamicProperties.py +++ b/steer_core/Constants/ThermodynamicProperties.py @@ -252,7 +252,7 @@ def density_kg_m3(T_celsius: float) -> float: # THERMODYNAMIC FUNCTIONS # ============================================================================= -def antoine_pressure_mmhg(T_celsius: float) -> float: +def antoine_pressure(T_celsius: float) -> float: """ Calculate CO2 sublimation pressure using Antoine equation. @@ -265,7 +265,7 @@ def antoine_pressure_mmhg(T_celsius: float) -> float: return 10 ** (CO2.ANTOINE_A - CO2.ANTOINE_B / (T_celsius + CO2.ANTOINE_C)) -def antoine_temperature_celsius(P_mmhg: float) -> float: +def antoine_temperature(P_mmhg: float) -> float: """ Calculate CO2 sublimation temperature from pressure. @@ -279,7 +279,7 @@ def antoine_temperature_celsius(P_mmhg: float) -> float: return CO2.ANTOINE_B / (CO2.ANTOINE_A - log_P) - CO2.ANTOINE_C -def water_vapor_pressure_pa(T_K: float) -> float: +def water_vapor_pressure(T_K: float) -> float: """ Calculate water vapor pressure using Antoine equation. @@ -296,7 +296,7 @@ def water_vapor_pressure_pa(T_K: float) -> float: return P_bar * 1e5 # Convert bar to Pa -def mea_equilibrium_pressure_pa(loading: float, T_K: float) -> float: +def mea_equilibrium_pressure(loading: float, T_K: float) -> float: """ Calculate equilibrium partial pressure of CO2 over MEA solution. @@ -369,8 +369,8 @@ def mea_bubble_point_temperature(loading: float, P_total_Pa: float) -> float: # Iterate to find bubble point for _ in range(20): # Max 20 iterations - P_co2_star = mea_equilibrium_pressure_pa(loading, T_guess_K) - P_h2o_star = water_vapor_pressure_pa(T_guess_K) + P_co2_star = mea_equilibrium_pressure(loading, T_guess_K) + P_h2o_star = water_vapor_pressure(T_guess_K) P_total_calc = P_co2_star + P_h2o_star # Check convergence diff --git a/steer_core/Constants/Units.py b/steer_core/Constants/Units.py index 65139e7..0c749bb 100644 --- a/steer_core/Constants/Units.py +++ b/steer_core/Constants/Units.py @@ -6,32 +6,22 @@ IMPORTANT: Always use these functions/constants for unit conversions. Never use raw multipliers like /1000 or *1000 directly. - -Categories: - - Length/Mass - - Time - - Power/Energy - - Pressure - - Temperature - - Volume - - Currency """ import math -from typing import Union - # ============================================================================= # LENGTH AND MASS UNITS # ============================================================================= - -# Length 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 (million tonnes) +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 KMOL_TO_MOL = 1e3 MOL_TO_KMOL = 1e-3 @@ -53,288 +43,91 @@ CM_TO_UM = 1e4 UM_TO_CM = 1e-4 - - -# Current units -A_TO_mA = 1e3 -mA_TO_A = 1e-3 - - # ============================================================================= # TIME UNITS # ============================================================================= +S_TO_H = 1.0 / 3600.0 +H_TO_S = 3600.0 +MIN_TO_H = 1.0 / 60.0 +H_TO_MIN = 60.0 +S_TO_MIN = 1.0 / 60.0 +MIN_TO_S = 60.0 +S_TO_Y = 1.0 / (3600.0 * 24.0 * 365) +Y_TO_S = 3600.0 * 24.0 * 365 -S_TO_H = 1 / 3600 -H_TO_S = 3600 -S_TO_MIN = 1 / 60 -MIN_TO_S = 60 -S_TO_Y = 1 / (3600 * 24 * 365) -Y_TO_S = 3600 * 24 * 365 - -SECONDS_PER_MINUTE = 60 -MINUTES_PER_HOUR = 60 -SECONDS_PER_HOUR = 3600 -HOURS_PER_DAY = 24 -DAYS_PER_YEAR = 365 -HOURS_PER_YEAR = 8760 -SECONDS_PER_YEAR = SECONDS_PER_HOUR * HOURS_PER_YEAR -SECONDS_PER_DAY = SECONDS_PER_HOUR * HOURS_PER_DAY - +D_TO_H = 24.0 +Y_TO_D = 365 +Y_TO_H = Y_TO_D * D_TO_H # Cascaded +D_TO_S = D_TO_H * H_TO_S # Cascaded # ============================================================================= -# POWER UNITS +# POWER & ENERGY UNITS # ============================================================================= - W_TO_KW = 1e-3 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 - -def mw_to_kw(power_mw: float) -> float: - """Convert megawatts to kilowatts.""" - return power_mw * MW_TO_KW - - -def kw_to_mw(power_kw: float) -> float: - """Convert kilowatts to megawatts.""" - return power_kw * KW_TO_MW - - -def w_to_kw(power_w: float) -> float: - """Convert watts to kilowatts.""" - return power_w * W_TO_KW - - -def kw_to_w(power_kw: float) -> float: - """Convert kilowatts to watts.""" - return power_kw * KW_TO_W - - -def w_to_mw(power_w: float) -> float: - """Convert watts to megawatts.""" - return power_w * W_TO_KW * KW_TO_MW - - -# Horsepower conversions 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 -def hp_to_kw(power_hp: float) -> float: - """Convert horsepower to kilowatts.""" - return power_hp * KW_PER_HP - - -def kw_to_hp(power_kw: float) -> float: - """Convert kilowatts to horsepower.""" - return power_kw * HP_PER_KW - - -# ============================================================================= -# ENERGY UNITS -# ============================================================================= - -J_TO_WH = 1 / 3600 - -# Energy conversion factors (MW·h to GJ) -MWH_TO_GJ = 3.6 # 1 MW·h = 3600 MJ = 3.6 GJ -GJ_TO_MWH = 1.0 / MWH_TO_GJ # 1 GJ = 1/3.6 MW·h - -# Energy conversion factors (kJ to GJ) -KJ_TO_GJ = 1e-6 # 1 kJ = 0.000001 GJ -GJ_TO_KJ = 1e6 # 1 GJ = 1,000,000 kJ -GJ_TO_MGJ = 1e-6 # GJ to million GJ (megagigajoules) -MGJ_TO_GJ = 1e6 # million GJ to GJ - -# Energy conversion factors (J to kJ) -J_TO_KJ = 1e-3 # 1 J = 0.001 kJ -KJ_TO_J = 1e3 # 1 kJ = 1000 J - -# Power-energy factors -KW_TO_MJ_PER_HOUR = 3.6 # kW·h to MJ conversion: 1 kW·h = 3600 kJ = 3.6 MJ -KW_PER_KG_TO_MJ_PER_KG = 3.6 # kW/kg = kJ/(kg·s) = 3600 kJ/(kg·h) = 3.6 MJ/kg - - -def gj_to_mwh(energy_gj: float) -> float: - """Convert gigajoules to megawatt-hours.""" - return energy_gj * GJ_TO_MWH - - -def mwh_to_gj(energy_mwh: float) -> float: - """Convert megawatt-hours to gigajoules.""" - return energy_mwh * MWH_TO_GJ - - -def gj_per_tonne_to_mwh_per_tonne(specific_energy: float) -> float: - """Convert GJ/tonne to MWh/tonne.""" - return specific_energy / 3.6 - - -def j_to_kj(energy_j: float) -> float: - """Convert joules to kilojoules.""" - return energy_j * J_TO_KJ - - -def kj_to_j(energy_kj: float) -> float: - """Convert kilojoules to joules.""" - return energy_kj * KJ_TO_J - - -def kg_kmol_to_kg_mol(mw_kg_kmol: float) -> float: - """Convert molecular weight from kg/kmol to kg/mol.""" - return mw_kg_kmol * MOL_TO_KMOL - - -def kg_mol_to_kg_kmol(mw_kg_mol: float) -> float: - """Convert molecular weight from kg/mol to kg/kmol.""" - return mw_kg_mol * KMOL_TO_MOL +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 # ============================================================================= # PRESSURE UNITS # ============================================================================= - -BAR_TO_PA = 1e5 # bar to Pa conversion factor -PA_TO_BAR = 1e-5 # Pa to bar conversion factor -KPA_TO_PA = 1e3 # kPa to Pa conversion factor -PA_TO_KPA = 1e-3 # Pa to kPa conversion factor - - -def bar_to_pa(pressure_bar: float) -> float: - """Convert bar to Pascal.""" - return pressure_bar * BAR_TO_PA - - -def pa_to_bar(pressure_pa: float) -> float: - """Convert Pascal to bar.""" - return pressure_pa * PA_TO_BAR - - -def atm_to_bar(pressure_atm: float) -> float: - """Convert atmospheres to bar.""" - return pressure_atm * 1.01325 - - -def bar_to_atm(pressure_bar: float) -> float: - """Convert bar to atmospheres.""" - return pressure_bar / 1.01325 - - -def psi_to_bar(pressure_psi: float) -> float: - """Convert psi to bar.""" - return pressure_psi * 0.0689476 - +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 # ============================================================================= # TEMPERATURE UNITS # ============================================================================= - -KELVIN_OFFSET = 273.15 # Celsius to Kelvin offset - - -def celsius_to_kelvin(temp_c: float) -> float: - """Convert Celsius to Kelvin.""" - return temp_c + KELVIN_OFFSET - - -def kelvin_to_celsius(temp_k: float) -> float: - """Convert Kelvin to Celsius.""" - return temp_k - KELVIN_OFFSET - - -# ============================================================================= -# VOLUME UNITS -# ============================================================================= - -L_TO_M3 = 1e-3 -M3_TO_L = 1e3 - - -def m3_to_l(volume_m3: float) -> float: - """Convert cubic meters to liters.""" - return volume_m3 * M3_TO_L - - -def l_to_m3(volume_l: float) -> float: - """Convert liters to cubic meters.""" - return volume_l * L_TO_M3 - - -def m3_to_gal(volume_m3: float) -> float: - """Convert cubic meters to US gallons.""" - return volume_m3 * 264.172 - - -# ============================================================================= -# AREA UNITS -# ============================================================================= - -def m2_to_ft2(area_m2: float) -> float: - """Convert square meters to square feet.""" - return area_m2 * 10.7639 - - -def ft2_to_m2(area_ft2: float) -> float: - """Convert square feet to square meters.""" - return area_ft2 / 10.7639 - +K_TO_C = -273.15 +C_TO_K = 273.15 # ============================================================================= -# FLOW RATE UNITS +# VISCOSITY UNITS # ============================================================================= - -def m3_s_to_gpm(flow_m3_s: float) -> float: - """Convert m³/s to gallons per minute.""" - return flow_m3_s * 15850.3 - - -def gpm_to_m3_s(flow_gpm: float) -> float: - """Convert gallons per minute to m³/s.""" - return flow_gpm / 15850.3 - - -# ============================================================================= -# CURRENCY UNITS -# ============================================================================= - -USD_TO_KUSD = 1e-3 # $ to k$ (thousands) -KUSD_TO_USD = 1e3 # k$ to $ -USD_TO_MUSD = 1e-6 # $ to M$ (millions) -MUSD_TO_USD = 1e6 # M$ to $ - - -def usd_to_kusd(cost_usd: float) -> float: - """Convert dollars to thousands of dollars.""" - return cost_usd * USD_TO_KUSD - - -def kusd_to_usd(cost_kusd: float) -> float: - """Convert thousands of dollars to dollars.""" - return cost_kusd * KUSD_TO_USD - - -def usd_to_musd(cost_usd: float) -> float: - """Convert dollars to millions of dollars.""" - return cost_usd * USD_TO_MUSD - - -def musd_to_usd(cost_musd: float) -> float: - """Convert millions of dollars to dollars.""" - return cost_musd * MUSD_TO_USD - +PA_S_TO_CP = 1e3 # Pascal-seconds to Centipoise +CP_TO_PA_S = 1e-3 # Centipoise to Pascal-seconds # ============================================================================= -# ANGLE UNITS +# 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 DEG_TO_RAD = 0.017453292519943295 RAD_TO_DEG = 57.29577951308232 - -# ============================================================================= -# PERCENTAGE UNITS -# ============================================================================= - PERCENT_TO_FRACTION = 1e-2 FRACTION_TO_PERCENT = 1e2 + +# Derived Volume +L_TO_M3 = (DM_TO_M)**3 +M3_TO_L = 1.0 / L_TO_M3 \ No newline at end of file diff --git a/steer_core/Constants/Universal.py b/steer_core/Constants/Universal.py index f5dce25..35f1fe5 100644 --- a/steer_core/Constants/Universal.py +++ b/steer_core/Constants/Universal.py @@ -18,8 +18,6 @@ # ============================================================================= R_GAS = 8.314 # J/(mol·K) Universal gas constant -R_GAS_J_MOL_K = 8.314 # J/(mol·K) - Universal gas constant (alias) -R_GAS_KJ_KMOL_K = 8.314 # kJ/(kmol·K) - For use with kmol units GRAVITY = 9.81 # m/s² - Standard acceleration due to gravity diff --git a/steer_core/Constants/__init__.py b/steer_core/Constants/__init__.py index 5531615..d751825 100644 --- a/steer_core/Constants/__init__.py +++ b/steer_core/Constants/__init__.py @@ -1,134 +1,10 @@ -""" -Constants module for steer-core. - -This module provides centralized access to: -- Universal physical constants (R_GAS, PI, GRAVITY, etc.) -- Unit conversion constants and functions -- Thermodynamic properties for gases and fluids +"""Convenience exports for steer_core.Constants. -Example usage: - from steer_core.Constants import R_GAS, CO2, celsius_to_kelvin - from steer_core.Constants import HOURS_PER_YEAR, MW_TO_KW +This facade keeps downstream imports short during development +by exposing all symbols from the core constant submodules. """ -# Universal physical constants -from steer_core.Constants.Universal import ( - PI, - R_GAS, - R_GAS_J_MOL_K, - R_GAS_KJ_KMOL_K, - GRAVITY, - STANDARD_PRESSURE, - STANDARD_TEMPERATURE, - MMHG_PER_PA, - PA_PER_BAR, - GPU_TO_SI, -) - -# Unit conversion constants -from steer_core.Constants.Units import ( - # Length/Mass - KG_TO_G, G_TO_KG, - M_TO_CM, CM_TO_M, M_TO_MM, MM_TO_M, M_TO_UM, UM_TO_M, - KG_TO_T, T_TO_KG, T_TO_MT, MT_TO_T, - # Time - S_TO_H, H_TO_S, S_TO_MIN, MIN_TO_S, S_TO_Y, Y_TO_S, - SECONDS_PER_MINUTE, MINUTES_PER_HOUR, SECONDS_PER_HOUR, - HOURS_PER_DAY, DAYS_PER_YEAR, HOURS_PER_YEAR, - SECONDS_PER_YEAR, SECONDS_PER_DAY, - # Power - W_TO_KW, KW_TO_W, KW_TO_MW, MW_TO_KW, - KW_PER_HP, HP_PER_KW, - # Energy - J_TO_WH, MWH_TO_GJ, GJ_TO_MWH, KJ_TO_GJ, GJ_TO_KJ, J_TO_KJ, KJ_TO_J, - GJ_TO_MGJ, MGJ_TO_GJ, - KW_TO_MJ_PER_HOUR, KW_PER_KG_TO_MJ_PER_KG, - # Pressure - BAR_TO_PA, PA_TO_BAR, KPA_TO_PA, PA_TO_KPA, - # Temperature - KELVIN_OFFSET, - # Volume - L_TO_M3, M3_TO_L, - # Currency - USD_TO_KUSD, KUSD_TO_USD, USD_TO_MUSD, MUSD_TO_USD, - # Angle - DEG_TO_RAD, RAD_TO_DEG, - # Percentage - PERCENT_TO_FRACTION, FRACTION_TO_PERCENT, -) - -# Unit conversion functions -from steer_core.Constants.Units import ( - # Power - mw_to_kw, kw_to_mw, w_to_kw, kw_to_w, w_to_mw, - hp_to_kw, kw_to_hp, - # Energy - gj_to_mwh, mwh_to_gj, gj_per_tonne_to_mwh_per_tonne, - j_to_kj, kj_to_j, - kg_kmol_to_kg_mol, kg_mol_to_kg_kmol, - # Pressure - bar_to_pa, pa_to_bar, atm_to_bar, bar_to_atm, psi_to_bar, - # Temperature - celsius_to_kelvin, kelvin_to_celsius, - # Volume - m3_to_l, l_to_m3, m3_to_gal, - # Area - m2_to_ft2, ft2_to_m2, - # Flow rate - m3_s_to_gpm, gpm_to_m3_s, - # Currency - usd_to_kusd, kusd_to_usd, usd_to_musd, musd_to_usd, -) - -# Thermodynamic property classes -from steer_core.Constants.ThermodynamicProperties import ( - CO2, - N2, - FlueGas, - Water, H2O, # H2O is alias for Water - Steam, - NaturalGas, - MEA, -) - -# Thermodynamic functions -from steer_core.Constants.ThermodynamicProperties import ( - antoine_pressure_mmhg, - antoine_temperature_celsius, - water_vapor_pressure_pa, - mea_equilibrium_pressure_pa, - mea_equilibrium_loading, - mea_bubble_point_temperature, -) - - -# Aliases for backward compatibility -GAS_CONSTANT = R_GAS - +from .Units import * # noqa: F401,F403 +from .ThermodynamicProperties import * # noqa: F401,F403 +from .Universal import * # noqa: F401,F403 -__all__ = [ - # Universal constants - "PI", "R_GAS", "R_GAS_J_MOL_K", "R_GAS_KJ_KMOL_K", "GAS_CONSTANT", - "GRAVITY", "STANDARD_PRESSURE", "STANDARD_TEMPERATURE", - "MMHG_PER_PA", "PA_PER_BAR", "GPU_TO_SI", - # Unit conversion constants - "KG_TO_G", "G_TO_KG", "M_TO_CM", "CM_TO_M", "KG_TO_T", "T_TO_KG", "T_TO_MT", "MT_TO_T", - "S_TO_H", "H_TO_S", "SECONDS_PER_HOUR", "HOURS_PER_YEAR", "SECONDS_PER_YEAR", - "W_TO_KW", "KW_TO_W", "KW_TO_MW", "MW_TO_KW", - "MWH_TO_GJ", "GJ_TO_MWH", "KJ_TO_GJ", "GJ_TO_KJ", "GJ_TO_MGJ", "MGJ_TO_GJ", - "BAR_TO_PA", "PA_TO_BAR", "KELVIN_OFFSET", - "L_TO_M3", "M3_TO_L", - "USD_TO_KUSD", "KUSD_TO_USD", "USD_TO_MUSD", "MUSD_TO_USD", - # Unit conversion functions - "mw_to_kw", "kw_to_mw", "w_to_kw", "kw_to_w", "w_to_mw", - "gj_to_mwh", "mwh_to_gj", "j_to_kj", "kj_to_j", - "bar_to_pa", "pa_to_bar", - "celsius_to_kelvin", "kelvin_to_celsius", - "m3_to_l", "l_to_m3", - # Thermodynamic classes - "CO2", "N2", "FlueGas", "Water", "H2O", "Steam", "NaturalGas", "MEA", - # Thermodynamic functions - "antoine_pressure_mmhg", "antoine_temperature_celsius", - "water_vapor_pressure_pa", "mea_equilibrium_pressure_pa", - "mea_equilibrium_loading", "mea_bubble_point_temperature", -] From 25385c074939c33934e0828f5fa9e24fcc59ebbb Mon Sep 17 00:00:00 2001 From: niklasmarxer Date: Mon, 26 Jan 2026 18:12:51 -0800 Subject: [PATCH 04/18] Add ThermodynamicsMixin with generic Antoine equation methods Provides reusable thermodynamic calculation methods that material classes can inherit. Part of cross-repo migration to split ThermodynamicProperties.py across steer-core, steer-materials, and TEA. --- steer_core/Mixins/Thermodynamics.py | 59 +++++++++++++++++++++++++++++ steer_core/Mixins/__init__.py | 5 +++ 2 files changed, 64 insertions(+) create mode 100644 steer_core/Mixins/Thermodynamics.py diff --git a/steer_core/Mixins/Thermodynamics.py b/steer_core/Mixins/Thermodynamics.py new file mode 100644 index 0000000..fa243cf --- /dev/null +++ b/steer_core/Mixins/Thermodynamics.py @@ -0,0 +1,59 @@ +""" +Thermodynamics mixin providing generic phase equilibrium calculations. + +This mixin provides reusable thermodynamic functions that can be +composed with material classes. +""" + +import math + + +class ThermodynamicsMixin: + """Mixin providing generic thermodynamic calculation methods.""" + + @staticmethod + def 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 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 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³ + """ + R = 8.314 # J/(mol·K) + return P_Pa * MW_kg_mol / (R * T_K) diff --git a/steer_core/Mixins/__init__.py b/steer_core/Mixins/__init__.py index e69de29..d2cdab4 100644 --- a/steer_core/Mixins/__init__.py +++ b/steer_core/Mixins/__init__.py @@ -0,0 +1,5 @@ +"""Mixins module for steer-core.""" + +from .Thermodynamics import ThermodynamicsMixin + +__all__ = ["ThermodynamicsMixin"] From d6edab1246c341ac4bc209cf29efd9791cba7efb Mon Sep 17 00:00:00 2001 From: niklasmarxer Date: Mon, 26 Jan 2026 18:32:18 -0800 Subject: [PATCH 05/18] Refactor: Start migration of thermodynamic properties - Rename ThermodynamicProperties.py to ThermodynamicProperties_DEPRECATED.py - Update Mixins/Thermodynamics.py to use central R_GAS constant - Update Constants/__init__.py to stop exporting deprecated properties --- .../ThermodynamicProperties_DEPRECATED.py | 390 ++++++++++++++++++ steer_core/Constants/__init__.py | 6 +- steer_core/Mixins/Thermodynamics.py | 5 +- 3 files changed, 397 insertions(+), 4 deletions(-) create mode 100644 steer_core/Constants/ThermodynamicProperties_DEPRECATED.py diff --git a/steer_core/Constants/ThermodynamicProperties_DEPRECATED.py b/steer_core/Constants/ThermodynamicProperties_DEPRECATED.py new file mode 100644 index 0000000..9ff9fae --- /dev/null +++ b/steer_core/Constants/ThermodynamicProperties_DEPRECATED.py @@ -0,0 +1,390 @@ +""" +Thermodynamic properties for gases and fluids. + +This module contains thermodynamic property classes for common gases and fluids +used in process engineering calculations. All values use SI units. + +Classes: + CO2: Carbon dioxide properties + N2: Nitrogen properties + FlueGas: Flue gas / air mixture properties + Water: Water properties + Steam: Steam properties at various pressures + NaturalGas: Natural gas properties for combustion calculations + MEA: Monoethanolamine solvent properties +""" + +import math +from typing import Callable + + +# ============================================================================= +# CO2 PROPERTIES +# ============================================================================= + +class CO2: + """Carbon dioxide thermodynamic properties. + + All molecular weights in kg/mol (SI) with kg/kmol aliases. + """ + + # Molecular weight + MW_KG_KMOL = 44.01 # kg/kmol (same as g/mol) + MW_KG_MOL = 0.04401 # kg/mol (SI for molar calculations) + MW_CO2 = 0.04401 # kg/mol (alias for compatibility) + + # Antoine equation coefficients for sublimation curve + # log10(P_mmHg) = A - B / (T_celsius + C) + # Valid range: approximately -120°C to -60°C + ANTOINE_A = 9.81 + ANTOINE_B = 1347.79 + ANTOINE_C = 273.0 + + # Phase change enthalpies + H_SUBLIMATION_KJ_KG = 571.0 # kJ/kg, solid → gas + H_FUSION_KJ_KG = 196.0 # kJ/kg, solid → liquid + + # Heat capacities + CP_SOLID_KJ_KG_K = 0.95 # kJ/(kg·K), solid phase + CP_LIQUID_KJ_KG_K = 2.5 # kJ/(kg·K), liquid phase + + # Triple point + T_TRIPLE_C = -56.6 # °C + P_TRIPLE_PA = 5.18e5 # Pa + + # Typical pipeline pressure + P_PIPELINE_PA = 150e5 # Pa (150 bar) + + # Liquid density + RHO_LIQUID_KG_M3 = 1100 # kg/m³ + + +# ============================================================================= +# NITROGEN PROPERTIES +# ============================================================================= + +class N2: + """Nitrogen properties.""" + + MW_KG_KMOL = 28.01 # kg/kmol + MW_KG_MOL = 0.02801 # kg/mol + MW_N2 = 0.02801 # kg/mol (alias for compatibility) + + +# ============================================================================= +# FLUE GAS / AIR PROPERTIES +# ============================================================================= + +class FlueGas: + """Flue gas and air properties. + + These are typical values for combustion flue gas. + Exact values depend on fuel composition. + """ + + # Average molecular weight (varies with composition) + MW_AVG_KG_KMOL = 29.0 # kg/kmol, approximate for air-like mixture + MW_FLUE_GAS = 0.029 # kg/mol (alias for compatibility) + MW_AIR = 0.02897 # kg/mol (dry air average, alias for compatibility) + + # Heat capacity (average for flue gas mixture) + CP_KJ_KG_K = 1.05 # kJ/(kg·K) + + # Density at ambient conditions (~25°C, 1 atm) + RHO_AMBIENT_KG_M3 = 1.2 # kg/m³ + + # Ratio of specific heats (Cp/Cv), for air-like mixtures + GAMMA = 1.4 + + +# ============================================================================= +# WATER PROPERTIES +# ============================================================================= + +class Water: + """Water thermodynamic properties.""" + + MW_KG_KMOL = 18.015 # kg/kmol + MW_KG_MOL = 0.018015 # kg/mol (SI) + MW_H2O = 0.01802 # kg/mol (alias for compatibility) + + # Latent heat of vaporization + LAMBDA_VAPORIZATION_KJ_KG = 2200.0 # kJ/kg at ~120°C (stripper conditions) + LAMBDA_H2O_KJ_KG = 2260.0 # kJ/kg at 100°C (latent heat of vaporization) + + # Heat capacity + CP_WATER_KJ_KG_K = 4.18 # kJ/(kg·K) - Specific heat capacity of water + + # Density + DENSITY_KG_M3 = 1000.0 # kg/m³ + RHO_LIQUID_KG_M3 = 1000.0 # kg/m³ (alias) + + +# Alias for Water +H2O = Water + + +# ============================================================================= +# STEAM PROPERTIES +# ============================================================================= + +class Steam: + """Steam thermodynamic properties at various pressures. + + Reference: Steam tables (IAPWS-IF97) + """ + + # Latent heat of vaporization at different pressures + # Used for reboiler steam calculations + LAMBDA_LP_KJ_KG = 2200.0 # kJ/kg at ~2-3 bar (LP steam, ~120-135°C) + LAMBDA_MP_KJ_KG = 2015.0 # kJ/kg at ~10 bar (MP steam, ~180°C) + LAMBDA_6BAR_KJ_KG = 2086.0 # kJ/kg at 6 bar saturated (~159°C) + + # For energy cost calculations (converting GJ thermal to tonnes steam) + LAMBDA_LP_GJ_TONNE = 2.2 # GJ/tonne for LP steam (2.2 GJ = 2200 kJ/kg × 1000 kg) + LAMBDA_6BAR_GJ_TONNE = 2.086 # GJ/tonne at 6 bar + + +# ============================================================================= +# NATURAL GAS PROPERTIES +# ============================================================================= + +class NaturalGas: + """Natural gas properties for boiler calculations. + + Values are for typical pipeline-quality natural gas (mainly methane). + """ + + # Lower Heating Value (net calorific value) + LHV_MJ_KG = 47.0 # MJ/kg - typical for natural gas + LHV_GJ_TONNE = 47.0 # GJ/tonne + LHV_MJ_NM3 = 36.0 # MJ/Nm³ - volumetric basis + + # Higher Heating Value (gross calorific value) + HHV_MJ_KG = 52.2 # MJ/kg + HHV_GJ_TONNE = 52.2 # GJ/tonne + + # For $/MMBTU to $/GJ conversion + GJ_PER_MMBTU = 1.055 # 1 MMBTU = 1.055 GJ + + +# ============================================================================= +# MEA SOLVENT PROPERTIES +# ============================================================================= + +class MEA: + """Monoethanolamine solvent properties. + + Properties for aqueous MEA solution (typically 30 wt%). + + References: + - Weiland et al. (1998): Density and viscosity correlations + - Amundsen et al. (2009): Physical properties of MEA solutions + """ + + MW_KG_KMOL = 61.08 # kg/kmol, pure MEA + MW_KG_MOL = 0.06108 # kg/mol (SI) + MW_MEA = 0.06108 # kg/mol (alias for compatibility) + + # Heat capacity of 30 wt% MEA solution (temperature-averaged) + CP_SOLUTION_KJ_KG_K = 3.8 # kJ/(kg·K) + CP_MEA_30_KJ_KG_K = 3.8 # kJ/(kg·K) (alias for compatibility) + + # Density of 30 wt% MEA solution + RHO_COLD_KG_M3 = 1020 # kg/m³, at absorber conditions (~45°C) + RHO_HOT_KG_M3 = 950 # kg/m³, at stripper conditions (~120°C) + RHO_MEA_30_KG_M3 = 1020.0 # kg/m³ at 40°C (alias for compatibility) + RHO_MEA_30_HOT_KG_M3 = 950.0 # kg/m³ at ~120°C (alias for compatibility) + + # Viscosity of 30 wt% MEA solution (Pa·s) + # Reference: Weiland et al. (1998) + MU_45C_PA_S = 0.003 # Pa·s at 45°C (absorber conditions) + MU_120C_PA_S = 0.001 # Pa·s at 120°C (stripper conditions) + MU_COLD_PA_S = 0.003 # Pa·s at absorber (alias) + MU_HOT_PA_S = 0.001 # Pa·s at stripper (alias) + + # Heat of absorption for CO2 (Kim & Svendsen) + HEAT_OF_ABSORPTION_J_MOL = 84000.0 # J/mol CO2 + + @staticmethod + def viscosity_pa_s(T_celsius: float) -> float: + """ + Calculate 30 wt% MEA solution viscosity as function of temperature. + + Simplified Arrhenius-type correlation fitted to Weiland et al. (1998) data. + Valid range: 25-130°C + + Args: + T_celsius: Temperature in Celsius + + Returns: + Dynamic viscosity in Pa·s + """ + # Arrhenius parameters (fitted to MEA 30 wt% data) + # μ = A × exp(B/T_K) + A = 2.0e-6 # Pa·s + B = 2500.0 # K + T_K = T_celsius + 273.15 + return A * math.exp(B / T_K) + + @staticmethod + def density_kg_m3(T_celsius: float) -> float: + """ + Calculate 30 wt% MEA solution density as function of temperature. + + Linear interpolation based on Weiland et al. (1998) data. + Valid range: 25-130°C + + Args: + T_celsius: Temperature in Celsius + + Returns: + Density in kg/m³ + """ + # Linear fit: ρ = ρ_ref - α × (T - T_ref) + rho_ref = 1030.0 # kg/m³ at 25°C + T_ref = 25.0 # °C + alpha = 0.7 # kg/m³/°C thermal expansion coefficient + return rho_ref - alpha * (T_celsius - T_ref) + + +# ============================================================================= +# THERMODYNAMIC FUNCTIONS +# ============================================================================= + +def antoine_pressure(T_celsius: float) -> float: + """ + Calculate CO2 sublimation pressure using Antoine equation. + + Args: + T_celsius: Temperature in Celsius + + Returns: + Pressure in mmHg + """ + return 10 ** (CO2.ANTOINE_A - CO2.ANTOINE_B / (T_celsius + CO2.ANTOINE_C)) + + +def antoine_temperature(P_mmhg: float) -> float: + """ + Calculate CO2 sublimation temperature from pressure. + + Args: + P_mmhg: Pressure in mmHg + + Returns: + Temperature in Celsius + """ + log_P = math.log10(P_mmhg) + return CO2.ANTOINE_B / (CO2.ANTOINE_A - log_P) - CO2.ANTOINE_C + + +def water_vapor_pressure(T_K: float) -> float: + """ + Calculate water vapor pressure using Antoine equation. + + Args: + T_K: Temperature in Kelvin + + Returns: + Pressure in Pa + """ + # NIST Antoine parameters for water + A, B, C = 5.19621, 1730.63, -39.724 + log10_P_bar = A - B / (T_K + C) + P_bar = 10**log10_P_bar + return P_bar * 1e5 # Convert bar to Pa + + +def mea_equilibrium_pressure(loading: float, T_K: float) -> float: + """ + Calculate equilibrium partial pressure of CO2 over MEA solution. + + Correlation fitted to Hilliard (2008) for 30 wt% MEA data. + ln(P_CO2 [Pa]) = A + B/T + C*alpha + + Valid range: 40-120°C, α=0.1-0.55 + + Args: + loading: CO2 loading (mol CO2 / mol MEA) + T_K: Temperature (Kelvin) + + Returns: + P_star_CO2 (Pa) + """ + # Keep loading in physical bounds + alpha = max(0.001, min(0.60, loading)) + + # Calibrated coefficients (fitted to Hilliard 2008 data) + A = 24.48 + B = -6882.0 + C = 12.97 + + ln_p_pa = A + B/T_K + C*alpha + return math.exp(ln_p_pa) + + +def mea_equilibrium_loading(P_CO2_Pa: float, T_K: float) -> float: + """ + Inverse VLE: Calculate equilibrium loading given P_CO2 and T. + + Solves ln(P) = A + B/T + C*alpha for alpha. + + Args: + P_CO2_Pa: CO2 partial pressure (Pa) + T_K: Temperature (Kelvin) + + Returns: + Equilibrium loading (mol CO2 / mol MEA) + """ + A = 24.48 + B = -6882.0 + C = 12.97 + + target_ln_p = math.log(max(1.0, P_CO2_Pa)) + + # Rearrange: alpha = (ln_P - A - B/T) / C + numerator = target_ln_p - A - B/T_K + alpha = numerator / C + + # Clamp to physical limits + return max(0.01, min(0.60, alpha)) + + +def mea_bubble_point_temperature(loading: float, P_total_Pa: float) -> float: + """ + Calculate bubble point temperature for MEA solution. + + At bubble point: P_total = P*_CO2 + P*_H2O + + Args: + loading: CO2 loading (mol CO2 / mol MEA) + P_total_Pa: Total pressure (Pa) + + Returns: + Bubble point temperature (Kelvin) + """ + # Initial guess: 120°C (typical stripper temperature) + T_guess_K = 273.15 + 120.0 + + # Iterate to find bubble point + for _ in range(20): # Max 20 iterations + P_co2_star = mea_equilibrium_pressure(loading, T_guess_K) + P_h2o_star = water_vapor_pressure(T_guess_K) + P_total_calc = P_co2_star + P_h2o_star + + # Check convergence + error = abs(P_total_calc - P_total_Pa) / P_total_Pa + if error < 0.01: # 1% tolerance + return T_guess_K + + # Adjust temperature + if P_total_calc > P_total_Pa: + T_guess_K -= 2.0 + else: + T_guess_K += 2.0 + + # Clamp to reasonable range + T_guess_K = max(273.15 + 80.0, min(273.15 + 140.0, T_guess_K)) + + return T_guess_K diff --git a/steer_core/Constants/__init__.py b/steer_core/Constants/__init__.py index d751825..abf0586 100644 --- a/steer_core/Constants/__init__.py +++ b/steer_core/Constants/__init__.py @@ -2,9 +2,11 @@ This facade keeps downstream imports short during development by exposing all symbols from the core constant submodules. + +NOTE: ThermodynamicProperties (CO2, Water, MEA, etc.) have been moved to: + - steer-materials: CO2, N2, FlueGas, Water, Steam, NaturalGas + - steer_ccus_tea: MEA and VLE functions """ from .Units import * # noqa: F401,F403 -from .ThermodynamicProperties import * # noqa: F401,F403 from .Universal import * # noqa: F401,F403 - diff --git a/steer_core/Mixins/Thermodynamics.py b/steer_core/Mixins/Thermodynamics.py index fa243cf..6b08ae3 100644 --- a/steer_core/Mixins/Thermodynamics.py +++ b/steer_core/Mixins/Thermodynamics.py @@ -55,5 +55,6 @@ def ideal_gas_density(P_Pa: float, T_K: float, MW_kg_mol: float) -> float: Returns: Density in kg/m³ """ - R = 8.314 # J/(mol·K) - return P_Pa * MW_kg_mol / (R * T_K) + + from steer_core.Constants.Universal import R_GAS + return P_Pa * MW_kg_mol / (R_GAS * T_K) From 0be0077ec02553495c959bd3797f85abc79ba161 Mon Sep 17 00:00:00 2001 From: niklasmarxer Date: Mon, 26 Jan 2026 20:08:29 -0800 Subject: [PATCH 06/18] Refactor: Remove legacy ThermodynamicProperties.py --- .../amine_improvement_potential.json | 952 ++++++++++++++++++ .../Constants/ThermodynamicProperties.py | 390 ------- .../ThermodynamicProperties_DEPRECATED.py | 390 ------- steer_core/Constants/Units.py | 25 +- steer_core/Constants/Universal.py | 7 - steer_core/Constants/__init__.py | 4 - steer_core/Mixins/Thermodynamics.py | 2 +- steer_core/Mixins/__init__.py | 5 - 8 files changed, 963 insertions(+), 812 deletions(-) create mode 100644 results/wmbt_analysis/amine_improvement_potential.json delete mode 100644 steer_core/Constants/ThermodynamicProperties.py delete mode 100644 steer_core/Constants/ThermodynamicProperties_DEPRECATED.py diff --git a/results/wmbt_analysis/amine_improvement_potential.json b/results/wmbt_analysis/amine_improvement_potential.json new file mode 100644 index 0000000..7e71c4e --- /dev/null +++ b/results/wmbt_analysis/amine_improvement_potential.json @@ -0,0 +1,952 @@ +{ + "technology": "amine", + "flow_rate": 100.0, + "capture_rate": 0.85, + "concentration": 0.12, + "timestamp": "2026-01-25T17:46:35.808660", + "baseline": { + "cost_per_tonne": 98.94613946313589, + "capex_per_tonne": 45.01440287593447, + "opex_per_tonne": 53.93173658720142 + }, + "optimal": { + "cost_per_tonne": 78.22189253743598, + "capex_per_tonne": 27.508801757515513, + "opex_per_tonne": 33.207489661501505 + }, + "total_reduction": { + "cost_per_tonne": 20.724246925699916, + "capex_per_tonne": 17.50560111841896, + "opex_per_tonne": 20.724246925699916 + }, + "summary": { + "total_parameters": 33, + "tunable_parameters": 22, + "beneficial_parameters": 19, + "filtered_parameters": 3 + }, + "improvements": [ + { + "parameter_name": "heat_of_absorption", + "display_name": "Heat of Absorption", + "unit": "kJ/mol", + "current_value": 85.0, + "target_optimal": 50.0, + "max_cost_reduction": -9.344679683489915, + "max_capex_reduction": -4.390835178413155, + "max_opex_reduction": -4.953844505076752, + "is_beneficial": true, + "priority_score": 0.5626933316811681, + "cost_impact": "opex", + "constraint_type": "material" + }, + { + "parameter_name": "mea_concentration", + "display_name": "MEA Concentration", + "unit": "wt fraction", + "current_value": 0.3, + "target_optimal": 0.4, + "max_cost_reduction": 1.3109960026025647, + "max_capex_reduction": -0.6642512506284888, + "max_opex_reduction": 1.9752472532310463, + "is_beneficial": false, + "priority_score": 0.2666666666666667, + "cost_impact": "both", + "constraint_type": "material" + }, + { + "parameter_name": "lrhe_effectiveness", + "display_name": "LRHE Effectiveness", + "unit": "fraction", + "current_value": 0.85, + "target_optimal": 0.92, + "max_cost_reduction": -1.3815972339235145, + "max_capex_reduction": 1.2907612899991818, + "max_opex_reduction": -2.6723585239227035, + "is_beneficial": true, + "priority_score": 0.2579185052529996, + "cost_impact": "both", + "constraint_type": "equipment" + }, + { + "parameter_name": "pump_efficiency", + "display_name": "Pump Efficiency", + "unit": "fraction", + "current_value": 0.75, + "target_optimal": 0.85, + "max_cost_reduction": 0.0, + "max_capex_reduction": 0.0, + "max_opex_reduction": 0.0, + "is_beneficial": false, + "priority_score": 0.08666666666666666, + "cost_impact": "opex", + "constraint_type": "equipment" + }, + { + "parameter_name": "blower_efficiency", + "display_name": "Blower Efficiency", + "unit": "fraction", + "current_value": 0.75, + "target_optimal": 0.85, + "max_cost_reduction": 0.0, + "max_capex_reduction": 0.0, + "max_opex_reduction": 0.0, + "is_beneficial": false, + "priority_score": 0.08666666666666666, + "cost_impact": "opex", + "constraint_type": "equipment" + }, + { + "parameter_name": "boiler_efficiency", + "display_name": "Boiler Efficiency", + "unit": "fraction", + "current_value": 0.85, + "target_optimal": 0.92, + "max_cost_reduction": 0.0, + "max_capex_reduction": 0.0, + "max_opex_reduction": 0.0, + "is_beneficial": false, + "priority_score": 0.07647058823529414, + "cost_impact": "opex", + "constraint_type": "equipment" + }, + { + "parameter_name": "rich_pump_head", + "display_name": "Rich Pump Head", + "unit": "m", + "current_value": 50.0, + "target_optimal": 40.0, + "max_cost_reduction": -0.09430701739456993, + "max_capex_reduction": -0.03976636325209171, + "max_opex_reduction": -0.05454065414247111, + "is_beneficial": true, + "priority_score": 0.24282921052183712, + "cost_impact": "opex", + "constraint_type": "design" + }, + { + "parameter_name": "lean_pump_head", + "display_name": "Lean Pump Head", + "unit": "m", + "current_value": 60.0, + "target_optimal": 50.0, + "max_cost_reduction": 0.017398281550811134, + "max_capex_reduction": 0.05244672425808261, + "max_opex_reduction": -0.03504844270727148, + "is_beneficial": false, + "priority_score": 0.23333333333333334, + "cost_impact": "opex", + "constraint_type": "design" + }, + { + "parameter_name": "absorber_temperature", + "display_name": "Absorber Temperature", + "unit": "\u00b0C", + "current_value": 45.0, + "target_optimal": 40.0, + "max_cost_reduction": -3.6534148809295885, + "max_capex_reduction": -0.9570411643340577, + "max_opex_reduction": -2.6963737165955237, + "is_beneficial": true, + "priority_score": 0.33182466865010984, + "cost_impact": "both", + "constraint_type": "physics" + }, + { + "parameter_name": "stripper_pressure", + "display_name": "Stripper Pressure", + "unit": "bar", + "current_value": 1.8, + "target_optimal": 2.0, + "max_cost_reduction": 0.0, + "max_capex_reduction": 0.0, + "max_opex_reduction": 0.0, + "is_beneficial": false, + "priority_score": 0.08222222222222221, + "cost_impact": "both", + "constraint_type": "physics" + }, + { + "parameter_name": "heat_loss_fraction", + "display_name": "Heat Loss Fraction", + "unit": "fraction", + "current_value": 0.05, + "target_optimal": 0.02, + "max_cost_reduction": -0.9969948354573717, + "max_capex_reduction": -0.08982534033115996, + "max_opex_reduction": -0.9071694951261975, + "is_beneficial": true, + "priority_score": 0.34990984506372114, + "cost_impact": "opex", + "constraint_type": "design" + }, + { + "parameter_name": "electricity_price", + "display_name": "Electricity Price", + "unit": "USD/MWh", + "current_value": 80.0, + "target_optimal": 50.0, + "max_cost_reduction": -0.18276219973245134, + "max_capex_reduction": 0.0, + "max_opex_reduction": -0.18276219973245844, + "is_beneficial": true, + "priority_score": 0.2804828659919736, + "cost_impact": "opex", + "constraint_type": "economic" + }, + { + "parameter_name": "steam_cost", + "display_name": "Steam Cost", + "unit": "USD/GJ", + "current_value": 8.0, + "target_optimal": 5.0, + "max_cost_reduction": -20.724246925699916, + "max_capex_reduction": 0.0, + "max_opex_reduction": -20.724246925699916, + "is_beneficial": true, + "priority_score": 0.875, + "cost_impact": "opex", + "constraint_type": "economic" + }, + { + "parameter_name": "solvent_makeup_cost", + "display_name": "Solvent Makeup Cost", + "unit": "USD/kg MEA", + "current_value": 2.0, + "target_optimal": 1.5, + "max_cost_reduction": -0.7874999999999943, + "max_capex_reduction": 0.0, + "max_opex_reduction": -0.7875000000000014, + "is_beneficial": true, + "priority_score": 0.27362499999999984, + "cost_impact": "opex", + "constraint_type": "economic" + }, + { + "parameter_name": "solvent_degradation_rate", + "display_name": "Solvent Degradation Rate", + "unit": "kg MEA/t CO2", + "current_value": 1.5, + "target_optimal": 0.8, + "max_cost_reduction": -1.470000000000013, + "max_capex_reduction": 0.0, + "max_opex_reduction": -1.470000000000006, + "is_beneficial": true, + "priority_score": 0.3374333333333337, + "cost_impact": "opex", + "constraint_type": "material" + }, + { + "parameter_name": "labor_cost_factor", + "display_name": "Labor Cost Factor", + "unit": "fraction of CAPEX/yr", + "current_value": 0.01, + "target_optimal": 0.008, + "max_cost_reduction": 0.0, + "max_capex_reduction": 0.0, + "max_opex_reduction": 0.0, + "is_beneficial": false, + "priority_score": 0.1, + "cost_impact": "opex", + "constraint_type": "economic" + }, + { + "parameter_name": "maintenance_factor", + "display_name": "Maintenance Factor", + "unit": "fraction of CAPEX/yr", + "current_value": 0.03, + "target_optimal": 0.025, + "max_cost_reduction": -2.378811625817491, + "max_capex_reduction": 0.0, + "max_opex_reduction": -2.378811625817484, + "is_beneficial": true, + "priority_score": 0.3046976821078581, + "cost_impact": "opex", + "constraint_type": "economic" + }, + { + "parameter_name": "taxes_insurance_factor", + "display_name": "Taxes & Insurance Factor", + "unit": "fraction of CAPEX/yr", + "current_value": 0.02, + "target_optimal": 0.01, + "max_cost_reduction": 0.0, + "max_capex_reduction": 0.0, + "max_opex_reduction": 0.0, + "is_beneficial": false, + "priority_score": 0.16, + "cost_impact": "opex", + "constraint_type": "economic" + }, + { + "parameter_name": "labor_scaling_exponent", + "display_name": "Labor Scaling Exponent", + "unit": "dimensionless", + "current_value": 0.25, + "target_optimal": 0.2, + "max_cost_reduction": 0.24450915091001946, + "max_capex_reduction": 0.0, + "max_opex_reduction": 0.24450915091001235, + "is_beneficial": false, + "priority_score": 0.24, + "cost_impact": "opex", + "constraint_type": "economic" + }, + { + "parameter_name": "supervision_factor", + "display_name": "Supervision Factor", + "unit": "fraction of labor", + "current_value": 0.2, + "target_optimal": 0.15, + "max_cost_reduction": 0.0, + "max_capex_reduction": 0.0, + "max_opex_reduction": 0.0, + "is_beneficial": false, + "priority_score": 0.11000000000000001, + "cost_impact": "opex", + "constraint_type": "economic" + }, + { + "parameter_name": "overhead_factor", + "display_name": "Overhead Factor", + "unit": "fraction of labor", + "current_value": 0.3, + "target_optimal": 0.25, + "max_cost_reduction": -0.34951583042814605, + "max_capex_reduction": 0.0, + "max_opex_reduction": -0.34951583042815315, + "is_beneficial": true, + "priority_score": 0.24381880824617772, + "cost_impact": "opex", + "constraint_type": "economic" + }, + { + "parameter_name": "delta_T_cooling_water", + "display_name": "Cooling Water Delta T", + "unit": "\u00b0C", + "current_value": 8.0, + "target_optimal": 10.0, + "max_cost_reduction": 0.0, + "max_capex_reduction": 0.0, + "max_opex_reduction": 0.0, + "is_beneficial": false, + "priority_score": 0.11, + "cost_impact": "opex", + "constraint_type": "design" + }, + { + "parameter_name": "equipment_manufacturing_cost_factor", + "display_name": "Equipment Manufacturing Cost", + "unit": "fraction", + "current_value": 1.0, + "target_optimal": 0.75, + "max_cost_reduction": -2.3182281568115712, + "max_capex_reduction": -1.9137048816023992, + "max_opex_reduction": -0.4045232752091721, + "is_beneficial": true, + "priority_score": 0.31954684470434713, + "cost_impact": "capex", + "constraint_type": "economic" + }, + { + "parameter_name": "interest_rate", + "display_name": "Interest Rate / WACC", + "unit": "fraction", + "current_value": 0.08, + "target_optimal": 0.05, + "max_cost_reduction": -12.048781843132645, + "max_capex_reduction": -12.048781843132637, + "max_opex_reduction": 0.0, + "is_beneficial": true, + "priority_score": 0.6364634552939794, + "cost_impact": "capex", + "constraint_type": "economic" + }, + { + "parameter_name": "project_lifetime", + "display_name": "Project Lifetime", + "unit": "years", + "current_value": 25.0, + "target_optimal": 30.0, + "max_cost_reduction": 0.0, + "max_capex_reduction": 0.0, + "max_opex_reduction": 0.0, + "is_beneficial": false, + "priority_score": 0.1, + "cost_impact": "capex", + "constraint_type": "design" + }, + { + "parameter_name": "epc_cost_factor", + "display_name": "EPC Cost Factor", + "unit": "fraction", + "current_value": 0.12, + "target_optimal": 0.08, + "max_cost_reduction": -1.6076572455690723, + "max_capex_reduction": -1.6076572455690723, + "max_opex_reduction": 0.0, + "is_beneficial": true, + "priority_score": 0.3148963840337388, + "cost_impact": "capex", + "constraint_type": "economic" + }, + { + "parameter_name": "process_contingency_factor", + "display_name": "Process Contingency", + "unit": "fraction", + "current_value": 0.3, + "target_optimal": 0.1, + "max_cost_reduction": -6.925292750143768, + "max_capex_reduction": -6.925292750143761, + "max_opex_reduction": 0.0, + "is_beneficial": true, + "priority_score": 0.5410921158376464, + "cost_impact": "capex", + "constraint_type": "economic" + }, + { + "parameter_name": "project_contingency_factor", + "display_name": "Project Contingency", + "unit": "fraction", + "current_value": 0.2, + "target_optimal": 0.1, + "max_cost_reduction": -17.50560111841895, + "max_capex_reduction": -17.50560111841896, + "max_opex_reduction": 0.0, + "is_beneficial": true, + "priority_score": 0.8251680335525685, + "cost_impact": "capex", + "constraint_type": "economic" + }, + { + "parameter_name": "owner_cost_factor", + "display_name": "Owner's Costs", + "unit": "fraction", + "current_value": 0.15, + "target_optimal": 0.12, + "max_cost_reduction": 0.0, + "max_capex_reduction": 0.0, + "max_opex_reduction": 0.0, + "is_beneficial": false, + "priority_score": 0.1, + "cost_impact": "capex", + "constraint_type": "economic" + }, + { + "parameter_name": "absorber_pressure", + "display_name": "Absorber Pressure", + "unit": "bar", + "current_value": 1.1, + "target_optimal": 1.2, + "max_cost_reduction": -3.9440060599758056, + "max_capex_reduction": -2.6916261173346427, + "max_opex_reduction": -1.2523799426411557, + "is_beneficial": true, + "priority_score": 0.3365019999810923, + "cost_impact": "both", + "constraint_type": "equipment" + }, + { + "parameter_name": "base_operators", + "display_name": "Base Operators", + "unit": "FTEs", + "current_value": 8.0, + "target_optimal": 5.0, + "max_cost_reduction": -1.3456359471483665, + "max_capex_reduction": 0.0, + "max_opex_reduction": -1.3456359471483523, + "is_beneficial": true, + "priority_score": 0.315369078414451, + "cost_impact": "opex", + "constraint_type": "design" + }, + { + "parameter_name": "labor_rate", + "display_name": "Labor Rate", + "unit": "USD/FTE/year", + "current_value": 80000.0, + "target_optimal": 60000.0, + "max_cost_reduction": -0.5767011202064367, + "max_capex_reduction": 0.0, + "max_opex_reduction": -0.5767011202064367, + "is_beneficial": true, + "priority_score": 0.2673010336061931, + "cost_impact": "opex", + "constraint_type": "economic" + }, + { + "parameter_name": "construction_duration", + "display_name": "Construction Duration", + "unit": "years", + "current_value": 3.0, + "target_optimal": 2.0, + "max_cost_reduction": 0.0, + "max_capex_reduction": 0.0, + "max_opex_reduction": 0.0, + "is_beneficial": false, + "priority_score": 0.12666666666666665, + "cost_impact": "capex", + "constraint_type": "design" + } + ], + "ranked_beneficial": [ + { + "parameter_name": "steam_cost", + "display_name": "Steam Cost", + "max_cost_reduction": -20.724246925699916, + "priority_score": 0.875 + }, + { + "parameter_name": "project_contingency_factor", + "display_name": "Project Contingency", + "max_cost_reduction": -17.50560111841895, + "priority_score": 0.8251680335525685 + }, + { + "parameter_name": "interest_rate", + "display_name": "Interest Rate / WACC", + "max_cost_reduction": -12.048781843132645, + "priority_score": 0.6364634552939794 + }, + { + "parameter_name": "heat_of_absorption", + "display_name": "Heat of Absorption", + "max_cost_reduction": -9.344679683489915, + "priority_score": 0.5626933316811681 + }, + { + "parameter_name": "process_contingency_factor", + "display_name": "Process Contingency", + "max_cost_reduction": -6.925292750143768, + "priority_score": 0.5410921158376464 + }, + { + "parameter_name": "heat_loss_fraction", + "display_name": "Heat Loss Fraction", + "max_cost_reduction": -0.9969948354573717, + "priority_score": 0.34990984506372114 + }, + { + "parameter_name": "solvent_degradation_rate", + "display_name": "Solvent Degradation Rate", + "max_cost_reduction": -1.470000000000013, + "priority_score": 0.3374333333333337 + }, + { + "parameter_name": "absorber_pressure", + "display_name": "Absorber Pressure", + "max_cost_reduction": -3.9440060599758056, + "priority_score": 0.3365019999810923 + }, + { + "parameter_name": "absorber_temperature", + "display_name": "Absorber Temperature", + "max_cost_reduction": -3.6534148809295885, + "priority_score": 0.33182466865010984 + }, + { + "parameter_name": "equipment_manufacturing_cost_factor", + "display_name": "Equipment Manufacturing Cost", + "max_cost_reduction": -2.3182281568115712, + "priority_score": 0.31954684470434713 + }, + { + "parameter_name": "base_operators", + "display_name": "Base Operators", + "max_cost_reduction": -1.3456359471483665, + "priority_score": 0.315369078414451 + }, + { + "parameter_name": "epc_cost_factor", + "display_name": "EPC Cost Factor", + "max_cost_reduction": -1.6076572455690723, + "priority_score": 0.3148963840337388 + }, + { + "parameter_name": "maintenance_factor", + "display_name": "Maintenance Factor", + "max_cost_reduction": -2.378811625817491, + "priority_score": 0.3046976821078581 + }, + { + "parameter_name": "electricity_price", + "display_name": "Electricity Price", + "max_cost_reduction": -0.18276219973245134, + "priority_score": 0.2804828659919736 + }, + { + "parameter_name": "solvent_makeup_cost", + "display_name": "Solvent Makeup Cost", + "max_cost_reduction": -0.7874999999999943, + "priority_score": 0.27362499999999984 + }, + { + "parameter_name": "labor_rate", + "display_name": "Labor Rate", + "max_cost_reduction": -0.5767011202064367, + "priority_score": 0.2673010336061931 + }, + { + "parameter_name": "lrhe_effectiveness", + "display_name": "LRHE Effectiveness", + "max_cost_reduction": -1.3815972339235145, + "priority_score": 0.2579185052529996 + }, + { + "parameter_name": "overhead_factor", + "display_name": "Overhead Factor", + "max_cost_reduction": -0.34951583042814605, + "priority_score": 0.24381880824617772 + }, + { + "parameter_name": "rich_pump_head", + "display_name": "Rich Pump Head", + "max_cost_reduction": -0.09430701739456993, + "priority_score": 0.24282921052183712 + } + ], + "interactions": { + "heat_of_absorption": { + "trades_off_with": [ + "co2_capacity" + ], + "coupled_to": [], + "synergistic_with": [] + }, + "mea_concentration": { + "trades_off_with": [ + "corrosion_rate", + "degradation_rate" + ], + "coupled_to": [], + "synergistic_with": [] + }, + "lrhe_effectiveness": { + "trades_off_with": [ + "heat_exchanger_cost" + ], + "coupled_to": [], + "synergistic_with": [ + "reboiler_duty" + ] + }, + "pump_efficiency": { + "trades_off_with": [], + "coupled_to": [], + "synergistic_with": [] + }, + "blower_efficiency": { + "trades_off_with": [], + "coupled_to": [], + "synergistic_with": [] + }, + "boiler_efficiency": { + "trades_off_with": [], + "coupled_to": [], + "synergistic_with": [] + }, + "rich_pump_head": { + "trades_off_with": [], + "coupled_to": [], + "synergistic_with": [] + }, + "lean_pump_head": { + "trades_off_with": [], + "coupled_to": [], + "synergistic_with": [] + }, + "absorber_temperature": { + "trades_off_with": [ + "reaction_kinetics" + ], + "coupled_to": [], + "synergistic_with": [] + }, + "stripper_pressure": { + "trades_off_with": [ + "reboiler_temperature", + "co2_compression_work" + ], + "coupled_to": [], + "synergistic_with": [] + }, + "heat_loss_fraction": { + "trades_off_with": [], + "coupled_to": [], + "synergistic_with": [] + }, + "electricity_price": { + "trades_off_with": [], + "coupled_to": [], + "synergistic_with": [] + }, + "steam_cost": { + "trades_off_with": [], + "coupled_to": [], + "synergistic_with": [] + }, + "solvent_makeup_cost": { + "trades_off_with": [], + "coupled_to": [], + "synergistic_with": [] + }, + "solvent_degradation_rate": { + "trades_off_with": [ + "mea_concentration" + ], + "coupled_to": [], + "synergistic_with": [] + }, + "labor_cost_factor": { + "trades_off_with": [], + "coupled_to": [], + "synergistic_with": [] + }, + "maintenance_factor": { + "trades_off_with": [], + "coupled_to": [], + "synergistic_with": [] + }, + "taxes_insurance_factor": { + "trades_off_with": [], + "coupled_to": [], + "synergistic_with": [] + }, + "labor_scaling_exponent": { + "trades_off_with": [], + "coupled_to": [], + "synergistic_with": [] + }, + "supervision_factor": { + "trades_off_with": [], + "coupled_to": [], + "synergistic_with": [] + }, + "overhead_factor": { + "trades_off_with": [], + "coupled_to": [], + "synergistic_with": [] + }, + "delta_T_cooling_water": { + "trades_off_with": [], + "coupled_to": [], + "synergistic_with": [] + }, + "equipment_manufacturing_cost_factor": { + "trades_off_with": [], + "coupled_to": [], + "synergistic_with": [] + }, + "interest_rate": { + "trades_off_with": [], + "coupled_to": [], + "synergistic_with": [] + }, + "project_lifetime": { + "trades_off_with": [], + "coupled_to": [], + "synergistic_with": [] + }, + "epc_cost_factor": { + "trades_off_with": [], + "coupled_to": [], + "synergistic_with": [] + }, + "process_contingency_factor": { + "trades_off_with": [], + "coupled_to": [], + "synergistic_with": [] + }, + "project_contingency_factor": { + "trades_off_with": [], + "coupled_to": [], + "synergistic_with": [] + }, + "owner_cost_factor": { + "trades_off_with": [], + "coupled_to": [], + "synergistic_with": [] + }, + "absorber_pressure": { + "trades_off_with": [ + "blower_power", + "absorber_size" + ], + "coupled_to": [], + "synergistic_with": [] + }, + "base_operators": { + "trades_off_with": [ + "automation_capex" + ], + "coupled_to": [], + "synergistic_with": [] + }, + "labor_rate": { + "trades_off_with": [], + "coupled_to": [], + "synergistic_with": [] + }, + "construction_duration": { + "trades_off_with": [ + "afudc_cost" + ], + "coupled_to": [], + "synergistic_with": [] + } + }, + "tradeoffs": { + "heat_of_absorption": { + "total_reduction": 9.344679683489915, + "capex_contribution": 0.4698754079469239, + "opex_contribution": 0.5301245920530754, + "capex_change": -4.390835178413155, + "opex_change": -4.953844505076752, + "net_benefit": -9.344679683489915 + }, + "lrhe_effectiveness": { + "total_reduction": 1.3815972339235145, + "capex_contribution": 0.9342529489101732, + "opex_contribution": 1.9342529489101783, + "capex_change": 1.2907612899991818, + "opex_change": -2.6723585239227035, + "net_benefit": -1.3815972339235145 + }, + "rich_pump_head": { + "total_reduction": 0.09430701739456993, + "capex_contribution": 0.42166918592827224, + "opex_contribution": 0.5783308140716524, + "capex_change": -0.03976636325209171, + "opex_change": -0.05454065414247111, + "net_benefit": -0.09430701739456993 + }, + "absorber_temperature": { + "total_reduction": 3.6534148809295885, + "capex_contribution": 0.2619579750796177, + "opex_contribution": 0.7380420249203803, + "capex_change": -0.9570411643340577, + "opex_change": -2.6963737165955237, + "net_benefit": -3.6534148809295885 + }, + "heat_loss_fraction": { + "total_reduction": 0.9969948354573717, + "capex_contribution": 0.09009609391803175, + "opex_contribution": 0.909903906081954, + "capex_change": -0.08982534033115996, + "opex_change": -0.9071694951261975, + "net_benefit": -0.9969948354573717 + }, + "electricity_price": { + "total_reduction": 0.18276219973245134, + "capex_contribution": 0.0, + "opex_contribution": 1.0000000000000389, + "capex_change": 0.0, + "opex_change": -0.18276219973245844, + "net_benefit": -0.18276219973245134 + }, + "steam_cost": { + "total_reduction": 20.724246925699916, + "capex_contribution": 0.0, + "opex_contribution": 1.0, + "capex_change": 0.0, + "opex_change": -20.724246925699916, + "net_benefit": -20.724246925699916 + }, + "solvent_makeup_cost": { + "total_reduction": 0.7874999999999943, + "capex_contribution": 0.0, + "opex_contribution": 1.000000000000009, + "capex_change": 0.0, + "opex_change": -0.7875000000000014, + "net_benefit": -0.7874999999999943 + }, + "solvent_degradation_rate": { + "total_reduction": 1.470000000000013, + "capex_contribution": 0.0, + "opex_contribution": 0.9999999999999951, + "capex_change": 0.0, + "opex_change": -1.470000000000006, + "net_benefit": -1.470000000000013 + }, + "maintenance_factor": { + "total_reduction": 2.378811625817491, + "capex_contribution": 0.0, + "opex_contribution": 0.999999999999997, + "capex_change": 0.0, + "opex_change": -2.378811625817484, + "net_benefit": -2.378811625817491 + }, + "overhead_factor": { + "total_reduction": 0.34951583042814605, + "capex_contribution": 0.0, + "opex_contribution": 1.0000000000000204, + "capex_change": 0.0, + "opex_change": -0.34951583042815315, + "net_benefit": -0.34951583042814605 + }, + "equipment_manufacturing_cost_factor": { + "total_reduction": 2.3182281568115712, + "capex_contribution": 0.8255032516879001, + "opex_contribution": 0.17449674831209994, + "capex_change": -1.9137048816023992, + "opex_change": -0.4045232752091721, + "net_benefit": -2.3182281568115712 + }, + "interest_rate": { + "total_reduction": 12.048781843132645, + "capex_contribution": 0.9999999999999994, + "opex_contribution": 0.0, + "capex_change": -12.048781843132637, + "opex_change": 0.0, + "net_benefit": -12.048781843132645 + }, + "epc_cost_factor": { + "total_reduction": 1.6076572455690723, + "capex_contribution": 1.0, + "opex_contribution": 0.0, + "capex_change": -1.6076572455690723, + "opex_change": 0.0, + "net_benefit": -1.6076572455690723 + }, + "process_contingency_factor": { + "total_reduction": 6.925292750143768, + "capex_contribution": 0.999999999999999, + "opex_contribution": 0.0, + "capex_change": -6.925292750143761, + "opex_change": 0.0, + "net_benefit": -6.925292750143768 + }, + "project_contingency_factor": { + "total_reduction": 17.50560111841895, + "capex_contribution": 1.0000000000000004, + "opex_contribution": 0.0, + "capex_change": -17.50560111841896, + "opex_change": 0.0, + "net_benefit": -17.50560111841895 + }, + "absorber_pressure": { + "total_reduction": 3.9440060599758056, + "capex_contribution": 0.6824599344939025, + "opex_contribution": 0.3175400655060957, + "capex_change": -2.6916261173346427, + "opex_change": -1.2523799426411557, + "net_benefit": -3.9440060599758056 + }, + "base_operators": { + "total_reduction": 1.3456359471483665, + "capex_contribution": 0.0, + "opex_contribution": 0.9999999999999895, + "capex_change": 0.0, + "opex_change": -1.3456359471483523, + "net_benefit": -1.3456359471483665 + }, + "labor_rate": { + "total_reduction": 0.5767011202064367, + "capex_contribution": 0.0, + "opex_contribution": 1.0, + "capex_change": 0.0, + "opex_change": -0.5767011202064367, + "net_benefit": -0.5767011202064367 + } + } +} \ No newline at end of file diff --git a/steer_core/Constants/ThermodynamicProperties.py b/steer_core/Constants/ThermodynamicProperties.py deleted file mode 100644 index 9ff9fae..0000000 --- a/steer_core/Constants/ThermodynamicProperties.py +++ /dev/null @@ -1,390 +0,0 @@ -""" -Thermodynamic properties for gases and fluids. - -This module contains thermodynamic property classes for common gases and fluids -used in process engineering calculations. All values use SI units. - -Classes: - CO2: Carbon dioxide properties - N2: Nitrogen properties - FlueGas: Flue gas / air mixture properties - Water: Water properties - Steam: Steam properties at various pressures - NaturalGas: Natural gas properties for combustion calculations - MEA: Monoethanolamine solvent properties -""" - -import math -from typing import Callable - - -# ============================================================================= -# CO2 PROPERTIES -# ============================================================================= - -class CO2: - """Carbon dioxide thermodynamic properties. - - All molecular weights in kg/mol (SI) with kg/kmol aliases. - """ - - # Molecular weight - MW_KG_KMOL = 44.01 # kg/kmol (same as g/mol) - MW_KG_MOL = 0.04401 # kg/mol (SI for molar calculations) - MW_CO2 = 0.04401 # kg/mol (alias for compatibility) - - # Antoine equation coefficients for sublimation curve - # log10(P_mmHg) = A - B / (T_celsius + C) - # Valid range: approximately -120°C to -60°C - ANTOINE_A = 9.81 - ANTOINE_B = 1347.79 - ANTOINE_C = 273.0 - - # Phase change enthalpies - H_SUBLIMATION_KJ_KG = 571.0 # kJ/kg, solid → gas - H_FUSION_KJ_KG = 196.0 # kJ/kg, solid → liquid - - # Heat capacities - CP_SOLID_KJ_KG_K = 0.95 # kJ/(kg·K), solid phase - CP_LIQUID_KJ_KG_K = 2.5 # kJ/(kg·K), liquid phase - - # Triple point - T_TRIPLE_C = -56.6 # °C - P_TRIPLE_PA = 5.18e5 # Pa - - # Typical pipeline pressure - P_PIPELINE_PA = 150e5 # Pa (150 bar) - - # Liquid density - RHO_LIQUID_KG_M3 = 1100 # kg/m³ - - -# ============================================================================= -# NITROGEN PROPERTIES -# ============================================================================= - -class N2: - """Nitrogen properties.""" - - MW_KG_KMOL = 28.01 # kg/kmol - MW_KG_MOL = 0.02801 # kg/mol - MW_N2 = 0.02801 # kg/mol (alias for compatibility) - - -# ============================================================================= -# FLUE GAS / AIR PROPERTIES -# ============================================================================= - -class FlueGas: - """Flue gas and air properties. - - These are typical values for combustion flue gas. - Exact values depend on fuel composition. - """ - - # Average molecular weight (varies with composition) - MW_AVG_KG_KMOL = 29.0 # kg/kmol, approximate for air-like mixture - MW_FLUE_GAS = 0.029 # kg/mol (alias for compatibility) - MW_AIR = 0.02897 # kg/mol (dry air average, alias for compatibility) - - # Heat capacity (average for flue gas mixture) - CP_KJ_KG_K = 1.05 # kJ/(kg·K) - - # Density at ambient conditions (~25°C, 1 atm) - RHO_AMBIENT_KG_M3 = 1.2 # kg/m³ - - # Ratio of specific heats (Cp/Cv), for air-like mixtures - GAMMA = 1.4 - - -# ============================================================================= -# WATER PROPERTIES -# ============================================================================= - -class Water: - """Water thermodynamic properties.""" - - MW_KG_KMOL = 18.015 # kg/kmol - MW_KG_MOL = 0.018015 # kg/mol (SI) - MW_H2O = 0.01802 # kg/mol (alias for compatibility) - - # Latent heat of vaporization - LAMBDA_VAPORIZATION_KJ_KG = 2200.0 # kJ/kg at ~120°C (stripper conditions) - LAMBDA_H2O_KJ_KG = 2260.0 # kJ/kg at 100°C (latent heat of vaporization) - - # Heat capacity - CP_WATER_KJ_KG_K = 4.18 # kJ/(kg·K) - Specific heat capacity of water - - # Density - DENSITY_KG_M3 = 1000.0 # kg/m³ - RHO_LIQUID_KG_M3 = 1000.0 # kg/m³ (alias) - - -# Alias for Water -H2O = Water - - -# ============================================================================= -# STEAM PROPERTIES -# ============================================================================= - -class Steam: - """Steam thermodynamic properties at various pressures. - - Reference: Steam tables (IAPWS-IF97) - """ - - # Latent heat of vaporization at different pressures - # Used for reboiler steam calculations - LAMBDA_LP_KJ_KG = 2200.0 # kJ/kg at ~2-3 bar (LP steam, ~120-135°C) - LAMBDA_MP_KJ_KG = 2015.0 # kJ/kg at ~10 bar (MP steam, ~180°C) - LAMBDA_6BAR_KJ_KG = 2086.0 # kJ/kg at 6 bar saturated (~159°C) - - # For energy cost calculations (converting GJ thermal to tonnes steam) - LAMBDA_LP_GJ_TONNE = 2.2 # GJ/tonne for LP steam (2.2 GJ = 2200 kJ/kg × 1000 kg) - LAMBDA_6BAR_GJ_TONNE = 2.086 # GJ/tonne at 6 bar - - -# ============================================================================= -# NATURAL GAS PROPERTIES -# ============================================================================= - -class NaturalGas: - """Natural gas properties for boiler calculations. - - Values are for typical pipeline-quality natural gas (mainly methane). - """ - - # Lower Heating Value (net calorific value) - LHV_MJ_KG = 47.0 # MJ/kg - typical for natural gas - LHV_GJ_TONNE = 47.0 # GJ/tonne - LHV_MJ_NM3 = 36.0 # MJ/Nm³ - volumetric basis - - # Higher Heating Value (gross calorific value) - HHV_MJ_KG = 52.2 # MJ/kg - HHV_GJ_TONNE = 52.2 # GJ/tonne - - # For $/MMBTU to $/GJ conversion - GJ_PER_MMBTU = 1.055 # 1 MMBTU = 1.055 GJ - - -# ============================================================================= -# MEA SOLVENT PROPERTIES -# ============================================================================= - -class MEA: - """Monoethanolamine solvent properties. - - Properties for aqueous MEA solution (typically 30 wt%). - - References: - - Weiland et al. (1998): Density and viscosity correlations - - Amundsen et al. (2009): Physical properties of MEA solutions - """ - - MW_KG_KMOL = 61.08 # kg/kmol, pure MEA - MW_KG_MOL = 0.06108 # kg/mol (SI) - MW_MEA = 0.06108 # kg/mol (alias for compatibility) - - # Heat capacity of 30 wt% MEA solution (temperature-averaged) - CP_SOLUTION_KJ_KG_K = 3.8 # kJ/(kg·K) - CP_MEA_30_KJ_KG_K = 3.8 # kJ/(kg·K) (alias for compatibility) - - # Density of 30 wt% MEA solution - RHO_COLD_KG_M3 = 1020 # kg/m³, at absorber conditions (~45°C) - RHO_HOT_KG_M3 = 950 # kg/m³, at stripper conditions (~120°C) - RHO_MEA_30_KG_M3 = 1020.0 # kg/m³ at 40°C (alias for compatibility) - RHO_MEA_30_HOT_KG_M3 = 950.0 # kg/m³ at ~120°C (alias for compatibility) - - # Viscosity of 30 wt% MEA solution (Pa·s) - # Reference: Weiland et al. (1998) - MU_45C_PA_S = 0.003 # Pa·s at 45°C (absorber conditions) - MU_120C_PA_S = 0.001 # Pa·s at 120°C (stripper conditions) - MU_COLD_PA_S = 0.003 # Pa·s at absorber (alias) - MU_HOT_PA_S = 0.001 # Pa·s at stripper (alias) - - # Heat of absorption for CO2 (Kim & Svendsen) - HEAT_OF_ABSORPTION_J_MOL = 84000.0 # J/mol CO2 - - @staticmethod - def viscosity_pa_s(T_celsius: float) -> float: - """ - Calculate 30 wt% MEA solution viscosity as function of temperature. - - Simplified Arrhenius-type correlation fitted to Weiland et al. (1998) data. - Valid range: 25-130°C - - Args: - T_celsius: Temperature in Celsius - - Returns: - Dynamic viscosity in Pa·s - """ - # Arrhenius parameters (fitted to MEA 30 wt% data) - # μ = A × exp(B/T_K) - A = 2.0e-6 # Pa·s - B = 2500.0 # K - T_K = T_celsius + 273.15 - return A * math.exp(B / T_K) - - @staticmethod - def density_kg_m3(T_celsius: float) -> float: - """ - Calculate 30 wt% MEA solution density as function of temperature. - - Linear interpolation based on Weiland et al. (1998) data. - Valid range: 25-130°C - - Args: - T_celsius: Temperature in Celsius - - Returns: - Density in kg/m³ - """ - # Linear fit: ρ = ρ_ref - α × (T - T_ref) - rho_ref = 1030.0 # kg/m³ at 25°C - T_ref = 25.0 # °C - alpha = 0.7 # kg/m³/°C thermal expansion coefficient - return rho_ref - alpha * (T_celsius - T_ref) - - -# ============================================================================= -# THERMODYNAMIC FUNCTIONS -# ============================================================================= - -def antoine_pressure(T_celsius: float) -> float: - """ - Calculate CO2 sublimation pressure using Antoine equation. - - Args: - T_celsius: Temperature in Celsius - - Returns: - Pressure in mmHg - """ - return 10 ** (CO2.ANTOINE_A - CO2.ANTOINE_B / (T_celsius + CO2.ANTOINE_C)) - - -def antoine_temperature(P_mmhg: float) -> float: - """ - Calculate CO2 sublimation temperature from pressure. - - Args: - P_mmhg: Pressure in mmHg - - Returns: - Temperature in Celsius - """ - log_P = math.log10(P_mmhg) - return CO2.ANTOINE_B / (CO2.ANTOINE_A - log_P) - CO2.ANTOINE_C - - -def water_vapor_pressure(T_K: float) -> float: - """ - Calculate water vapor pressure using Antoine equation. - - Args: - T_K: Temperature in Kelvin - - Returns: - Pressure in Pa - """ - # NIST Antoine parameters for water - A, B, C = 5.19621, 1730.63, -39.724 - log10_P_bar = A - B / (T_K + C) - P_bar = 10**log10_P_bar - return P_bar * 1e5 # Convert bar to Pa - - -def mea_equilibrium_pressure(loading: float, T_K: float) -> float: - """ - Calculate equilibrium partial pressure of CO2 over MEA solution. - - Correlation fitted to Hilliard (2008) for 30 wt% MEA data. - ln(P_CO2 [Pa]) = A + B/T + C*alpha - - Valid range: 40-120°C, α=0.1-0.55 - - Args: - loading: CO2 loading (mol CO2 / mol MEA) - T_K: Temperature (Kelvin) - - Returns: - P_star_CO2 (Pa) - """ - # Keep loading in physical bounds - alpha = max(0.001, min(0.60, loading)) - - # Calibrated coefficients (fitted to Hilliard 2008 data) - A = 24.48 - B = -6882.0 - C = 12.97 - - ln_p_pa = A + B/T_K + C*alpha - return math.exp(ln_p_pa) - - -def mea_equilibrium_loading(P_CO2_Pa: float, T_K: float) -> float: - """ - Inverse VLE: Calculate equilibrium loading given P_CO2 and T. - - Solves ln(P) = A + B/T + C*alpha for alpha. - - Args: - P_CO2_Pa: CO2 partial pressure (Pa) - T_K: Temperature (Kelvin) - - Returns: - Equilibrium loading (mol CO2 / mol MEA) - """ - A = 24.48 - B = -6882.0 - C = 12.97 - - target_ln_p = math.log(max(1.0, P_CO2_Pa)) - - # Rearrange: alpha = (ln_P - A - B/T) / C - numerator = target_ln_p - A - B/T_K - alpha = numerator / C - - # Clamp to physical limits - return max(0.01, min(0.60, alpha)) - - -def mea_bubble_point_temperature(loading: float, P_total_Pa: float) -> float: - """ - Calculate bubble point temperature for MEA solution. - - At bubble point: P_total = P*_CO2 + P*_H2O - - Args: - loading: CO2 loading (mol CO2 / mol MEA) - P_total_Pa: Total pressure (Pa) - - Returns: - Bubble point temperature (Kelvin) - """ - # Initial guess: 120°C (typical stripper temperature) - T_guess_K = 273.15 + 120.0 - - # Iterate to find bubble point - for _ in range(20): # Max 20 iterations - P_co2_star = mea_equilibrium_pressure(loading, T_guess_K) - P_h2o_star = water_vapor_pressure(T_guess_K) - P_total_calc = P_co2_star + P_h2o_star - - # Check convergence - error = abs(P_total_calc - P_total_Pa) / P_total_Pa - if error < 0.01: # 1% tolerance - return T_guess_K - - # Adjust temperature - if P_total_calc > P_total_Pa: - T_guess_K -= 2.0 - else: - T_guess_K += 2.0 - - # Clamp to reasonable range - T_guess_K = max(273.15 + 80.0, min(273.15 + 140.0, T_guess_K)) - - return T_guess_K diff --git a/steer_core/Constants/ThermodynamicProperties_DEPRECATED.py b/steer_core/Constants/ThermodynamicProperties_DEPRECATED.py deleted file mode 100644 index 9ff9fae..0000000 --- a/steer_core/Constants/ThermodynamicProperties_DEPRECATED.py +++ /dev/null @@ -1,390 +0,0 @@ -""" -Thermodynamic properties for gases and fluids. - -This module contains thermodynamic property classes for common gases and fluids -used in process engineering calculations. All values use SI units. - -Classes: - CO2: Carbon dioxide properties - N2: Nitrogen properties - FlueGas: Flue gas / air mixture properties - Water: Water properties - Steam: Steam properties at various pressures - NaturalGas: Natural gas properties for combustion calculations - MEA: Monoethanolamine solvent properties -""" - -import math -from typing import Callable - - -# ============================================================================= -# CO2 PROPERTIES -# ============================================================================= - -class CO2: - """Carbon dioxide thermodynamic properties. - - All molecular weights in kg/mol (SI) with kg/kmol aliases. - """ - - # Molecular weight - MW_KG_KMOL = 44.01 # kg/kmol (same as g/mol) - MW_KG_MOL = 0.04401 # kg/mol (SI for molar calculations) - MW_CO2 = 0.04401 # kg/mol (alias for compatibility) - - # Antoine equation coefficients for sublimation curve - # log10(P_mmHg) = A - B / (T_celsius + C) - # Valid range: approximately -120°C to -60°C - ANTOINE_A = 9.81 - ANTOINE_B = 1347.79 - ANTOINE_C = 273.0 - - # Phase change enthalpies - H_SUBLIMATION_KJ_KG = 571.0 # kJ/kg, solid → gas - H_FUSION_KJ_KG = 196.0 # kJ/kg, solid → liquid - - # Heat capacities - CP_SOLID_KJ_KG_K = 0.95 # kJ/(kg·K), solid phase - CP_LIQUID_KJ_KG_K = 2.5 # kJ/(kg·K), liquid phase - - # Triple point - T_TRIPLE_C = -56.6 # °C - P_TRIPLE_PA = 5.18e5 # Pa - - # Typical pipeline pressure - P_PIPELINE_PA = 150e5 # Pa (150 bar) - - # Liquid density - RHO_LIQUID_KG_M3 = 1100 # kg/m³ - - -# ============================================================================= -# NITROGEN PROPERTIES -# ============================================================================= - -class N2: - """Nitrogen properties.""" - - MW_KG_KMOL = 28.01 # kg/kmol - MW_KG_MOL = 0.02801 # kg/mol - MW_N2 = 0.02801 # kg/mol (alias for compatibility) - - -# ============================================================================= -# FLUE GAS / AIR PROPERTIES -# ============================================================================= - -class FlueGas: - """Flue gas and air properties. - - These are typical values for combustion flue gas. - Exact values depend on fuel composition. - """ - - # Average molecular weight (varies with composition) - MW_AVG_KG_KMOL = 29.0 # kg/kmol, approximate for air-like mixture - MW_FLUE_GAS = 0.029 # kg/mol (alias for compatibility) - MW_AIR = 0.02897 # kg/mol (dry air average, alias for compatibility) - - # Heat capacity (average for flue gas mixture) - CP_KJ_KG_K = 1.05 # kJ/(kg·K) - - # Density at ambient conditions (~25°C, 1 atm) - RHO_AMBIENT_KG_M3 = 1.2 # kg/m³ - - # Ratio of specific heats (Cp/Cv), for air-like mixtures - GAMMA = 1.4 - - -# ============================================================================= -# WATER PROPERTIES -# ============================================================================= - -class Water: - """Water thermodynamic properties.""" - - MW_KG_KMOL = 18.015 # kg/kmol - MW_KG_MOL = 0.018015 # kg/mol (SI) - MW_H2O = 0.01802 # kg/mol (alias for compatibility) - - # Latent heat of vaporization - LAMBDA_VAPORIZATION_KJ_KG = 2200.0 # kJ/kg at ~120°C (stripper conditions) - LAMBDA_H2O_KJ_KG = 2260.0 # kJ/kg at 100°C (latent heat of vaporization) - - # Heat capacity - CP_WATER_KJ_KG_K = 4.18 # kJ/(kg·K) - Specific heat capacity of water - - # Density - DENSITY_KG_M3 = 1000.0 # kg/m³ - RHO_LIQUID_KG_M3 = 1000.0 # kg/m³ (alias) - - -# Alias for Water -H2O = Water - - -# ============================================================================= -# STEAM PROPERTIES -# ============================================================================= - -class Steam: - """Steam thermodynamic properties at various pressures. - - Reference: Steam tables (IAPWS-IF97) - """ - - # Latent heat of vaporization at different pressures - # Used for reboiler steam calculations - LAMBDA_LP_KJ_KG = 2200.0 # kJ/kg at ~2-3 bar (LP steam, ~120-135°C) - LAMBDA_MP_KJ_KG = 2015.0 # kJ/kg at ~10 bar (MP steam, ~180°C) - LAMBDA_6BAR_KJ_KG = 2086.0 # kJ/kg at 6 bar saturated (~159°C) - - # For energy cost calculations (converting GJ thermal to tonnes steam) - LAMBDA_LP_GJ_TONNE = 2.2 # GJ/tonne for LP steam (2.2 GJ = 2200 kJ/kg × 1000 kg) - LAMBDA_6BAR_GJ_TONNE = 2.086 # GJ/tonne at 6 bar - - -# ============================================================================= -# NATURAL GAS PROPERTIES -# ============================================================================= - -class NaturalGas: - """Natural gas properties for boiler calculations. - - Values are for typical pipeline-quality natural gas (mainly methane). - """ - - # Lower Heating Value (net calorific value) - LHV_MJ_KG = 47.0 # MJ/kg - typical for natural gas - LHV_GJ_TONNE = 47.0 # GJ/tonne - LHV_MJ_NM3 = 36.0 # MJ/Nm³ - volumetric basis - - # Higher Heating Value (gross calorific value) - HHV_MJ_KG = 52.2 # MJ/kg - HHV_GJ_TONNE = 52.2 # GJ/tonne - - # For $/MMBTU to $/GJ conversion - GJ_PER_MMBTU = 1.055 # 1 MMBTU = 1.055 GJ - - -# ============================================================================= -# MEA SOLVENT PROPERTIES -# ============================================================================= - -class MEA: - """Monoethanolamine solvent properties. - - Properties for aqueous MEA solution (typically 30 wt%). - - References: - - Weiland et al. (1998): Density and viscosity correlations - - Amundsen et al. (2009): Physical properties of MEA solutions - """ - - MW_KG_KMOL = 61.08 # kg/kmol, pure MEA - MW_KG_MOL = 0.06108 # kg/mol (SI) - MW_MEA = 0.06108 # kg/mol (alias for compatibility) - - # Heat capacity of 30 wt% MEA solution (temperature-averaged) - CP_SOLUTION_KJ_KG_K = 3.8 # kJ/(kg·K) - CP_MEA_30_KJ_KG_K = 3.8 # kJ/(kg·K) (alias for compatibility) - - # Density of 30 wt% MEA solution - RHO_COLD_KG_M3 = 1020 # kg/m³, at absorber conditions (~45°C) - RHO_HOT_KG_M3 = 950 # kg/m³, at stripper conditions (~120°C) - RHO_MEA_30_KG_M3 = 1020.0 # kg/m³ at 40°C (alias for compatibility) - RHO_MEA_30_HOT_KG_M3 = 950.0 # kg/m³ at ~120°C (alias for compatibility) - - # Viscosity of 30 wt% MEA solution (Pa·s) - # Reference: Weiland et al. (1998) - MU_45C_PA_S = 0.003 # Pa·s at 45°C (absorber conditions) - MU_120C_PA_S = 0.001 # Pa·s at 120°C (stripper conditions) - MU_COLD_PA_S = 0.003 # Pa·s at absorber (alias) - MU_HOT_PA_S = 0.001 # Pa·s at stripper (alias) - - # Heat of absorption for CO2 (Kim & Svendsen) - HEAT_OF_ABSORPTION_J_MOL = 84000.0 # J/mol CO2 - - @staticmethod - def viscosity_pa_s(T_celsius: float) -> float: - """ - Calculate 30 wt% MEA solution viscosity as function of temperature. - - Simplified Arrhenius-type correlation fitted to Weiland et al. (1998) data. - Valid range: 25-130°C - - Args: - T_celsius: Temperature in Celsius - - Returns: - Dynamic viscosity in Pa·s - """ - # Arrhenius parameters (fitted to MEA 30 wt% data) - # μ = A × exp(B/T_K) - A = 2.0e-6 # Pa·s - B = 2500.0 # K - T_K = T_celsius + 273.15 - return A * math.exp(B / T_K) - - @staticmethod - def density_kg_m3(T_celsius: float) -> float: - """ - Calculate 30 wt% MEA solution density as function of temperature. - - Linear interpolation based on Weiland et al. (1998) data. - Valid range: 25-130°C - - Args: - T_celsius: Temperature in Celsius - - Returns: - Density in kg/m³ - """ - # Linear fit: ρ = ρ_ref - α × (T - T_ref) - rho_ref = 1030.0 # kg/m³ at 25°C - T_ref = 25.0 # °C - alpha = 0.7 # kg/m³/°C thermal expansion coefficient - return rho_ref - alpha * (T_celsius - T_ref) - - -# ============================================================================= -# THERMODYNAMIC FUNCTIONS -# ============================================================================= - -def antoine_pressure(T_celsius: float) -> float: - """ - Calculate CO2 sublimation pressure using Antoine equation. - - Args: - T_celsius: Temperature in Celsius - - Returns: - Pressure in mmHg - """ - return 10 ** (CO2.ANTOINE_A - CO2.ANTOINE_B / (T_celsius + CO2.ANTOINE_C)) - - -def antoine_temperature(P_mmhg: float) -> float: - """ - Calculate CO2 sublimation temperature from pressure. - - Args: - P_mmhg: Pressure in mmHg - - Returns: - Temperature in Celsius - """ - log_P = math.log10(P_mmhg) - return CO2.ANTOINE_B / (CO2.ANTOINE_A - log_P) - CO2.ANTOINE_C - - -def water_vapor_pressure(T_K: float) -> float: - """ - Calculate water vapor pressure using Antoine equation. - - Args: - T_K: Temperature in Kelvin - - Returns: - Pressure in Pa - """ - # NIST Antoine parameters for water - A, B, C = 5.19621, 1730.63, -39.724 - log10_P_bar = A - B / (T_K + C) - P_bar = 10**log10_P_bar - return P_bar * 1e5 # Convert bar to Pa - - -def mea_equilibrium_pressure(loading: float, T_K: float) -> float: - """ - Calculate equilibrium partial pressure of CO2 over MEA solution. - - Correlation fitted to Hilliard (2008) for 30 wt% MEA data. - ln(P_CO2 [Pa]) = A + B/T + C*alpha - - Valid range: 40-120°C, α=0.1-0.55 - - Args: - loading: CO2 loading (mol CO2 / mol MEA) - T_K: Temperature (Kelvin) - - Returns: - P_star_CO2 (Pa) - """ - # Keep loading in physical bounds - alpha = max(0.001, min(0.60, loading)) - - # Calibrated coefficients (fitted to Hilliard 2008 data) - A = 24.48 - B = -6882.0 - C = 12.97 - - ln_p_pa = A + B/T_K + C*alpha - return math.exp(ln_p_pa) - - -def mea_equilibrium_loading(P_CO2_Pa: float, T_K: float) -> float: - """ - Inverse VLE: Calculate equilibrium loading given P_CO2 and T. - - Solves ln(P) = A + B/T + C*alpha for alpha. - - Args: - P_CO2_Pa: CO2 partial pressure (Pa) - T_K: Temperature (Kelvin) - - Returns: - Equilibrium loading (mol CO2 / mol MEA) - """ - A = 24.48 - B = -6882.0 - C = 12.97 - - target_ln_p = math.log(max(1.0, P_CO2_Pa)) - - # Rearrange: alpha = (ln_P - A - B/T) / C - numerator = target_ln_p - A - B/T_K - alpha = numerator / C - - # Clamp to physical limits - return max(0.01, min(0.60, alpha)) - - -def mea_bubble_point_temperature(loading: float, P_total_Pa: float) -> float: - """ - Calculate bubble point temperature for MEA solution. - - At bubble point: P_total = P*_CO2 + P*_H2O - - Args: - loading: CO2 loading (mol CO2 / mol MEA) - P_total_Pa: Total pressure (Pa) - - Returns: - Bubble point temperature (Kelvin) - """ - # Initial guess: 120°C (typical stripper temperature) - T_guess_K = 273.15 + 120.0 - - # Iterate to find bubble point - for _ in range(20): # Max 20 iterations - P_co2_star = mea_equilibrium_pressure(loading, T_guess_K) - P_h2o_star = water_vapor_pressure(T_guess_K) - P_total_calc = P_co2_star + P_h2o_star - - # Check convergence - error = abs(P_total_calc - P_total_Pa) / P_total_Pa - if error < 0.01: # 1% tolerance - return T_guess_K - - # Adjust temperature - if P_total_calc > P_total_Pa: - T_guess_K -= 2.0 - else: - T_guess_K += 2.0 - - # Clamp to reasonable range - T_guess_K = max(273.15 + 80.0, min(273.15 + 140.0, T_guess_K)) - - return T_guess_K diff --git a/steer_core/Constants/Units.py b/steer_core/Constants/Units.py index 6a3b876..ae825fd 100644 --- a/steer_core/Constants/Units.py +++ b/steer_core/Constants/Units.py @@ -63,21 +63,16 @@ S_TO_Y = 1.0 / (3600.0 * 24.0 * 365) Y_TO_S = 3600.0 * 24.0 * 365 -D_TO_H = 24.0 -Y_TO_D = 365 -Y_TO_H = Y_TO_D * D_TO_H # 8760 - Cascaded -H_TO_Y = 1.0 / Y_TO_H # From origin/dev -D_TO_S = D_TO_H * H_TO_S # Cascaded - -# Legacy aliases for compatibility -SECONDS_PER_MINUTE = MIN_TO_S -MINUTES_PER_HOUR = H_TO_MIN -SECONDS_PER_HOUR = H_TO_S -HOURS_PER_DAY = D_TO_H -DAYS_PER_YEAR = Y_TO_D -HOURS_PER_YEAR = Y_TO_H -SECONDS_PER_YEAR = Y_TO_S -SECONDS_PER_DAY = D_TO_S +# Time units +S_TO_H = 1 / 3600 +H_TO_S = 3600 +S_TO_MIN = 1 / 60 +MIN_TO_S = 60 +S_TO_Y = 1 / (3600 * 24 * 365) +Y_TO_S = 3600 * 24 * 365 +H_TO_Y = 1 / 8760 +Y_TO_H = 8760 + # ============================================================================= # POWER & ENERGY UNITS diff --git a/steer_core/Constants/Universal.py b/steer_core/Constants/Universal.py index d99ca38..dbc3b05 100644 --- a/steer_core/Constants/Universal.py +++ b/steer_core/Constants/Universal.py @@ -39,10 +39,3 @@ GPU_TO_SI = 3.35e-10 # mol/(m²·s·Pa) per GPU (gas permeation unit) -# ============================================================================= -# MOLAR MASSES (from origin/dev) -# ============================================================================= - -M_G_PER_MOL_NO2 = 46.0055 -M_G_PER_MOL_SO2 = 64.0638 -MW_G_PER_MOL_CO2 = 44.0095 diff --git a/steer_core/Constants/__init__.py b/steer_core/Constants/__init__.py index abf0586..a479d91 100644 --- a/steer_core/Constants/__init__.py +++ b/steer_core/Constants/__init__.py @@ -2,10 +2,6 @@ This facade keeps downstream imports short during development by exposing all symbols from the core constant submodules. - -NOTE: ThermodynamicProperties (CO2, Water, MEA, etc.) have been moved to: - - steer-materials: CO2, N2, FlueGas, Water, Steam, NaturalGas - - steer_ccus_tea: MEA and VLE functions """ from .Units import * # noqa: F401,F403 diff --git a/steer_core/Mixins/Thermodynamics.py b/steer_core/Mixins/Thermodynamics.py index 6b08ae3..fdab546 100644 --- a/steer_core/Mixins/Thermodynamics.py +++ b/steer_core/Mixins/Thermodynamics.py @@ -6,6 +6,7 @@ """ import math +from steer_core.Constants.Universal import R_GAS class ThermodynamicsMixin: @@ -56,5 +57,4 @@ def ideal_gas_density(P_Pa: float, T_K: float, MW_kg_mol: float) -> float: Density in kg/m³ """ - from steer_core.Constants.Universal import R_GAS return P_Pa * MW_kg_mol / (R_GAS * T_K) diff --git a/steer_core/Mixins/__init__.py b/steer_core/Mixins/__init__.py index d2cdab4..e69de29 100644 --- a/steer_core/Mixins/__init__.py +++ b/steer_core/Mixins/__init__.py @@ -1,5 +0,0 @@ -"""Mixins module for steer-core.""" - -from .Thermodynamics import ThermodynamicsMixin - -__all__ = ["ThermodynamicsMixin"] From e897d29e600e2f8bc69979500147187b0234dd69 Mon Sep 17 00:00:00 2001 From: niklasmarxer Date: Wed, 28 Jan 2026 11:01:44 -0800 Subject: [PATCH 07/18] removed unnecessary file --- .../amine_improvement_potential.json | 952 ------------------ 1 file changed, 952 deletions(-) delete mode 100644 results/wmbt_analysis/amine_improvement_potential.json diff --git a/results/wmbt_analysis/amine_improvement_potential.json b/results/wmbt_analysis/amine_improvement_potential.json deleted file mode 100644 index 7e71c4e..0000000 --- a/results/wmbt_analysis/amine_improvement_potential.json +++ /dev/null @@ -1,952 +0,0 @@ -{ - "technology": "amine", - "flow_rate": 100.0, - "capture_rate": 0.85, - "concentration": 0.12, - "timestamp": "2026-01-25T17:46:35.808660", - "baseline": { - "cost_per_tonne": 98.94613946313589, - "capex_per_tonne": 45.01440287593447, - "opex_per_tonne": 53.93173658720142 - }, - "optimal": { - "cost_per_tonne": 78.22189253743598, - "capex_per_tonne": 27.508801757515513, - "opex_per_tonne": 33.207489661501505 - }, - "total_reduction": { - "cost_per_tonne": 20.724246925699916, - "capex_per_tonne": 17.50560111841896, - "opex_per_tonne": 20.724246925699916 - }, - "summary": { - "total_parameters": 33, - "tunable_parameters": 22, - "beneficial_parameters": 19, - "filtered_parameters": 3 - }, - "improvements": [ - { - "parameter_name": "heat_of_absorption", - "display_name": "Heat of Absorption", - "unit": "kJ/mol", - "current_value": 85.0, - "target_optimal": 50.0, - "max_cost_reduction": -9.344679683489915, - "max_capex_reduction": -4.390835178413155, - "max_opex_reduction": -4.953844505076752, - "is_beneficial": true, - "priority_score": 0.5626933316811681, - "cost_impact": "opex", - "constraint_type": "material" - }, - { - "parameter_name": "mea_concentration", - "display_name": "MEA Concentration", - "unit": "wt fraction", - "current_value": 0.3, - "target_optimal": 0.4, - "max_cost_reduction": 1.3109960026025647, - "max_capex_reduction": -0.6642512506284888, - "max_opex_reduction": 1.9752472532310463, - "is_beneficial": false, - "priority_score": 0.2666666666666667, - "cost_impact": "both", - "constraint_type": "material" - }, - { - "parameter_name": "lrhe_effectiveness", - "display_name": "LRHE Effectiveness", - "unit": "fraction", - "current_value": 0.85, - "target_optimal": 0.92, - "max_cost_reduction": -1.3815972339235145, - "max_capex_reduction": 1.2907612899991818, - "max_opex_reduction": -2.6723585239227035, - "is_beneficial": true, - "priority_score": 0.2579185052529996, - "cost_impact": "both", - "constraint_type": "equipment" - }, - { - "parameter_name": "pump_efficiency", - "display_name": "Pump Efficiency", - "unit": "fraction", - "current_value": 0.75, - "target_optimal": 0.85, - "max_cost_reduction": 0.0, - "max_capex_reduction": 0.0, - "max_opex_reduction": 0.0, - "is_beneficial": false, - "priority_score": 0.08666666666666666, - "cost_impact": "opex", - "constraint_type": "equipment" - }, - { - "parameter_name": "blower_efficiency", - "display_name": "Blower Efficiency", - "unit": "fraction", - "current_value": 0.75, - "target_optimal": 0.85, - "max_cost_reduction": 0.0, - "max_capex_reduction": 0.0, - "max_opex_reduction": 0.0, - "is_beneficial": false, - "priority_score": 0.08666666666666666, - "cost_impact": "opex", - "constraint_type": "equipment" - }, - { - "parameter_name": "boiler_efficiency", - "display_name": "Boiler Efficiency", - "unit": "fraction", - "current_value": 0.85, - "target_optimal": 0.92, - "max_cost_reduction": 0.0, - "max_capex_reduction": 0.0, - "max_opex_reduction": 0.0, - "is_beneficial": false, - "priority_score": 0.07647058823529414, - "cost_impact": "opex", - "constraint_type": "equipment" - }, - { - "parameter_name": "rich_pump_head", - "display_name": "Rich Pump Head", - "unit": "m", - "current_value": 50.0, - "target_optimal": 40.0, - "max_cost_reduction": -0.09430701739456993, - "max_capex_reduction": -0.03976636325209171, - "max_opex_reduction": -0.05454065414247111, - "is_beneficial": true, - "priority_score": 0.24282921052183712, - "cost_impact": "opex", - "constraint_type": "design" - }, - { - "parameter_name": "lean_pump_head", - "display_name": "Lean Pump Head", - "unit": "m", - "current_value": 60.0, - "target_optimal": 50.0, - "max_cost_reduction": 0.017398281550811134, - "max_capex_reduction": 0.05244672425808261, - "max_opex_reduction": -0.03504844270727148, - "is_beneficial": false, - "priority_score": 0.23333333333333334, - "cost_impact": "opex", - "constraint_type": "design" - }, - { - "parameter_name": "absorber_temperature", - "display_name": "Absorber Temperature", - "unit": "\u00b0C", - "current_value": 45.0, - "target_optimal": 40.0, - "max_cost_reduction": -3.6534148809295885, - "max_capex_reduction": -0.9570411643340577, - "max_opex_reduction": -2.6963737165955237, - "is_beneficial": true, - "priority_score": 0.33182466865010984, - "cost_impact": "both", - "constraint_type": "physics" - }, - { - "parameter_name": "stripper_pressure", - "display_name": "Stripper Pressure", - "unit": "bar", - "current_value": 1.8, - "target_optimal": 2.0, - "max_cost_reduction": 0.0, - "max_capex_reduction": 0.0, - "max_opex_reduction": 0.0, - "is_beneficial": false, - "priority_score": 0.08222222222222221, - "cost_impact": "both", - "constraint_type": "physics" - }, - { - "parameter_name": "heat_loss_fraction", - "display_name": "Heat Loss Fraction", - "unit": "fraction", - "current_value": 0.05, - "target_optimal": 0.02, - "max_cost_reduction": -0.9969948354573717, - "max_capex_reduction": -0.08982534033115996, - "max_opex_reduction": -0.9071694951261975, - "is_beneficial": true, - "priority_score": 0.34990984506372114, - "cost_impact": "opex", - "constraint_type": "design" - }, - { - "parameter_name": "electricity_price", - "display_name": "Electricity Price", - "unit": "USD/MWh", - "current_value": 80.0, - "target_optimal": 50.0, - "max_cost_reduction": -0.18276219973245134, - "max_capex_reduction": 0.0, - "max_opex_reduction": -0.18276219973245844, - "is_beneficial": true, - "priority_score": 0.2804828659919736, - "cost_impact": "opex", - "constraint_type": "economic" - }, - { - "parameter_name": "steam_cost", - "display_name": "Steam Cost", - "unit": "USD/GJ", - "current_value": 8.0, - "target_optimal": 5.0, - "max_cost_reduction": -20.724246925699916, - "max_capex_reduction": 0.0, - "max_opex_reduction": -20.724246925699916, - "is_beneficial": true, - "priority_score": 0.875, - "cost_impact": "opex", - "constraint_type": "economic" - }, - { - "parameter_name": "solvent_makeup_cost", - "display_name": "Solvent Makeup Cost", - "unit": "USD/kg MEA", - "current_value": 2.0, - "target_optimal": 1.5, - "max_cost_reduction": -0.7874999999999943, - "max_capex_reduction": 0.0, - "max_opex_reduction": -0.7875000000000014, - "is_beneficial": true, - "priority_score": 0.27362499999999984, - "cost_impact": "opex", - "constraint_type": "economic" - }, - { - "parameter_name": "solvent_degradation_rate", - "display_name": "Solvent Degradation Rate", - "unit": "kg MEA/t CO2", - "current_value": 1.5, - "target_optimal": 0.8, - "max_cost_reduction": -1.470000000000013, - "max_capex_reduction": 0.0, - "max_opex_reduction": -1.470000000000006, - "is_beneficial": true, - "priority_score": 0.3374333333333337, - "cost_impact": "opex", - "constraint_type": "material" - }, - { - "parameter_name": "labor_cost_factor", - "display_name": "Labor Cost Factor", - "unit": "fraction of CAPEX/yr", - "current_value": 0.01, - "target_optimal": 0.008, - "max_cost_reduction": 0.0, - "max_capex_reduction": 0.0, - "max_opex_reduction": 0.0, - "is_beneficial": false, - "priority_score": 0.1, - "cost_impact": "opex", - "constraint_type": "economic" - }, - { - "parameter_name": "maintenance_factor", - "display_name": "Maintenance Factor", - "unit": "fraction of CAPEX/yr", - "current_value": 0.03, - "target_optimal": 0.025, - "max_cost_reduction": -2.378811625817491, - "max_capex_reduction": 0.0, - "max_opex_reduction": -2.378811625817484, - "is_beneficial": true, - "priority_score": 0.3046976821078581, - "cost_impact": "opex", - "constraint_type": "economic" - }, - { - "parameter_name": "taxes_insurance_factor", - "display_name": "Taxes & Insurance Factor", - "unit": "fraction of CAPEX/yr", - "current_value": 0.02, - "target_optimal": 0.01, - "max_cost_reduction": 0.0, - "max_capex_reduction": 0.0, - "max_opex_reduction": 0.0, - "is_beneficial": false, - "priority_score": 0.16, - "cost_impact": "opex", - "constraint_type": "economic" - }, - { - "parameter_name": "labor_scaling_exponent", - "display_name": "Labor Scaling Exponent", - "unit": "dimensionless", - "current_value": 0.25, - "target_optimal": 0.2, - "max_cost_reduction": 0.24450915091001946, - "max_capex_reduction": 0.0, - "max_opex_reduction": 0.24450915091001235, - "is_beneficial": false, - "priority_score": 0.24, - "cost_impact": "opex", - "constraint_type": "economic" - }, - { - "parameter_name": "supervision_factor", - "display_name": "Supervision Factor", - "unit": "fraction of labor", - "current_value": 0.2, - "target_optimal": 0.15, - "max_cost_reduction": 0.0, - "max_capex_reduction": 0.0, - "max_opex_reduction": 0.0, - "is_beneficial": false, - "priority_score": 0.11000000000000001, - "cost_impact": "opex", - "constraint_type": "economic" - }, - { - "parameter_name": "overhead_factor", - "display_name": "Overhead Factor", - "unit": "fraction of labor", - "current_value": 0.3, - "target_optimal": 0.25, - "max_cost_reduction": -0.34951583042814605, - "max_capex_reduction": 0.0, - "max_opex_reduction": -0.34951583042815315, - "is_beneficial": true, - "priority_score": 0.24381880824617772, - "cost_impact": "opex", - "constraint_type": "economic" - }, - { - "parameter_name": "delta_T_cooling_water", - "display_name": "Cooling Water Delta T", - "unit": "\u00b0C", - "current_value": 8.0, - "target_optimal": 10.0, - "max_cost_reduction": 0.0, - "max_capex_reduction": 0.0, - "max_opex_reduction": 0.0, - "is_beneficial": false, - "priority_score": 0.11, - "cost_impact": "opex", - "constraint_type": "design" - }, - { - "parameter_name": "equipment_manufacturing_cost_factor", - "display_name": "Equipment Manufacturing Cost", - "unit": "fraction", - "current_value": 1.0, - "target_optimal": 0.75, - "max_cost_reduction": -2.3182281568115712, - "max_capex_reduction": -1.9137048816023992, - "max_opex_reduction": -0.4045232752091721, - "is_beneficial": true, - "priority_score": 0.31954684470434713, - "cost_impact": "capex", - "constraint_type": "economic" - }, - { - "parameter_name": "interest_rate", - "display_name": "Interest Rate / WACC", - "unit": "fraction", - "current_value": 0.08, - "target_optimal": 0.05, - "max_cost_reduction": -12.048781843132645, - "max_capex_reduction": -12.048781843132637, - "max_opex_reduction": 0.0, - "is_beneficial": true, - "priority_score": 0.6364634552939794, - "cost_impact": "capex", - "constraint_type": "economic" - }, - { - "parameter_name": "project_lifetime", - "display_name": "Project Lifetime", - "unit": "years", - "current_value": 25.0, - "target_optimal": 30.0, - "max_cost_reduction": 0.0, - "max_capex_reduction": 0.0, - "max_opex_reduction": 0.0, - "is_beneficial": false, - "priority_score": 0.1, - "cost_impact": "capex", - "constraint_type": "design" - }, - { - "parameter_name": "epc_cost_factor", - "display_name": "EPC Cost Factor", - "unit": "fraction", - "current_value": 0.12, - "target_optimal": 0.08, - "max_cost_reduction": -1.6076572455690723, - "max_capex_reduction": -1.6076572455690723, - "max_opex_reduction": 0.0, - "is_beneficial": true, - "priority_score": 0.3148963840337388, - "cost_impact": "capex", - "constraint_type": "economic" - }, - { - "parameter_name": "process_contingency_factor", - "display_name": "Process Contingency", - "unit": "fraction", - "current_value": 0.3, - "target_optimal": 0.1, - "max_cost_reduction": -6.925292750143768, - "max_capex_reduction": -6.925292750143761, - "max_opex_reduction": 0.0, - "is_beneficial": true, - "priority_score": 0.5410921158376464, - "cost_impact": "capex", - "constraint_type": "economic" - }, - { - "parameter_name": "project_contingency_factor", - "display_name": "Project Contingency", - "unit": "fraction", - "current_value": 0.2, - "target_optimal": 0.1, - "max_cost_reduction": -17.50560111841895, - "max_capex_reduction": -17.50560111841896, - "max_opex_reduction": 0.0, - "is_beneficial": true, - "priority_score": 0.8251680335525685, - "cost_impact": "capex", - "constraint_type": "economic" - }, - { - "parameter_name": "owner_cost_factor", - "display_name": "Owner's Costs", - "unit": "fraction", - "current_value": 0.15, - "target_optimal": 0.12, - "max_cost_reduction": 0.0, - "max_capex_reduction": 0.0, - "max_opex_reduction": 0.0, - "is_beneficial": false, - "priority_score": 0.1, - "cost_impact": "capex", - "constraint_type": "economic" - }, - { - "parameter_name": "absorber_pressure", - "display_name": "Absorber Pressure", - "unit": "bar", - "current_value": 1.1, - "target_optimal": 1.2, - "max_cost_reduction": -3.9440060599758056, - "max_capex_reduction": -2.6916261173346427, - "max_opex_reduction": -1.2523799426411557, - "is_beneficial": true, - "priority_score": 0.3365019999810923, - "cost_impact": "both", - "constraint_type": "equipment" - }, - { - "parameter_name": "base_operators", - "display_name": "Base Operators", - "unit": "FTEs", - "current_value": 8.0, - "target_optimal": 5.0, - "max_cost_reduction": -1.3456359471483665, - "max_capex_reduction": 0.0, - "max_opex_reduction": -1.3456359471483523, - "is_beneficial": true, - "priority_score": 0.315369078414451, - "cost_impact": "opex", - "constraint_type": "design" - }, - { - "parameter_name": "labor_rate", - "display_name": "Labor Rate", - "unit": "USD/FTE/year", - "current_value": 80000.0, - "target_optimal": 60000.0, - "max_cost_reduction": -0.5767011202064367, - "max_capex_reduction": 0.0, - "max_opex_reduction": -0.5767011202064367, - "is_beneficial": true, - "priority_score": 0.2673010336061931, - "cost_impact": "opex", - "constraint_type": "economic" - }, - { - "parameter_name": "construction_duration", - "display_name": "Construction Duration", - "unit": "years", - "current_value": 3.0, - "target_optimal": 2.0, - "max_cost_reduction": 0.0, - "max_capex_reduction": 0.0, - "max_opex_reduction": 0.0, - "is_beneficial": false, - "priority_score": 0.12666666666666665, - "cost_impact": "capex", - "constraint_type": "design" - } - ], - "ranked_beneficial": [ - { - "parameter_name": "steam_cost", - "display_name": "Steam Cost", - "max_cost_reduction": -20.724246925699916, - "priority_score": 0.875 - }, - { - "parameter_name": "project_contingency_factor", - "display_name": "Project Contingency", - "max_cost_reduction": -17.50560111841895, - "priority_score": 0.8251680335525685 - }, - { - "parameter_name": "interest_rate", - "display_name": "Interest Rate / WACC", - "max_cost_reduction": -12.048781843132645, - "priority_score": 0.6364634552939794 - }, - { - "parameter_name": "heat_of_absorption", - "display_name": "Heat of Absorption", - "max_cost_reduction": -9.344679683489915, - "priority_score": 0.5626933316811681 - }, - { - "parameter_name": "process_contingency_factor", - "display_name": "Process Contingency", - "max_cost_reduction": -6.925292750143768, - "priority_score": 0.5410921158376464 - }, - { - "parameter_name": "heat_loss_fraction", - "display_name": "Heat Loss Fraction", - "max_cost_reduction": -0.9969948354573717, - "priority_score": 0.34990984506372114 - }, - { - "parameter_name": "solvent_degradation_rate", - "display_name": "Solvent Degradation Rate", - "max_cost_reduction": -1.470000000000013, - "priority_score": 0.3374333333333337 - }, - { - "parameter_name": "absorber_pressure", - "display_name": "Absorber Pressure", - "max_cost_reduction": -3.9440060599758056, - "priority_score": 0.3365019999810923 - }, - { - "parameter_name": "absorber_temperature", - "display_name": "Absorber Temperature", - "max_cost_reduction": -3.6534148809295885, - "priority_score": 0.33182466865010984 - }, - { - "parameter_name": "equipment_manufacturing_cost_factor", - "display_name": "Equipment Manufacturing Cost", - "max_cost_reduction": -2.3182281568115712, - "priority_score": 0.31954684470434713 - }, - { - "parameter_name": "base_operators", - "display_name": "Base Operators", - "max_cost_reduction": -1.3456359471483665, - "priority_score": 0.315369078414451 - }, - { - "parameter_name": "epc_cost_factor", - "display_name": "EPC Cost Factor", - "max_cost_reduction": -1.6076572455690723, - "priority_score": 0.3148963840337388 - }, - { - "parameter_name": "maintenance_factor", - "display_name": "Maintenance Factor", - "max_cost_reduction": -2.378811625817491, - "priority_score": 0.3046976821078581 - }, - { - "parameter_name": "electricity_price", - "display_name": "Electricity Price", - "max_cost_reduction": -0.18276219973245134, - "priority_score": 0.2804828659919736 - }, - { - "parameter_name": "solvent_makeup_cost", - "display_name": "Solvent Makeup Cost", - "max_cost_reduction": -0.7874999999999943, - "priority_score": 0.27362499999999984 - }, - { - "parameter_name": "labor_rate", - "display_name": "Labor Rate", - "max_cost_reduction": -0.5767011202064367, - "priority_score": 0.2673010336061931 - }, - { - "parameter_name": "lrhe_effectiveness", - "display_name": "LRHE Effectiveness", - "max_cost_reduction": -1.3815972339235145, - "priority_score": 0.2579185052529996 - }, - { - "parameter_name": "overhead_factor", - "display_name": "Overhead Factor", - "max_cost_reduction": -0.34951583042814605, - "priority_score": 0.24381880824617772 - }, - { - "parameter_name": "rich_pump_head", - "display_name": "Rich Pump Head", - "max_cost_reduction": -0.09430701739456993, - "priority_score": 0.24282921052183712 - } - ], - "interactions": { - "heat_of_absorption": { - "trades_off_with": [ - "co2_capacity" - ], - "coupled_to": [], - "synergistic_with": [] - }, - "mea_concentration": { - "trades_off_with": [ - "corrosion_rate", - "degradation_rate" - ], - "coupled_to": [], - "synergistic_with": [] - }, - "lrhe_effectiveness": { - "trades_off_with": [ - "heat_exchanger_cost" - ], - "coupled_to": [], - "synergistic_with": [ - "reboiler_duty" - ] - }, - "pump_efficiency": { - "trades_off_with": [], - "coupled_to": [], - "synergistic_with": [] - }, - "blower_efficiency": { - "trades_off_with": [], - "coupled_to": [], - "synergistic_with": [] - }, - "boiler_efficiency": { - "trades_off_with": [], - "coupled_to": [], - "synergistic_with": [] - }, - "rich_pump_head": { - "trades_off_with": [], - "coupled_to": [], - "synergistic_with": [] - }, - "lean_pump_head": { - "trades_off_with": [], - "coupled_to": [], - "synergistic_with": [] - }, - "absorber_temperature": { - "trades_off_with": [ - "reaction_kinetics" - ], - "coupled_to": [], - "synergistic_with": [] - }, - "stripper_pressure": { - "trades_off_with": [ - "reboiler_temperature", - "co2_compression_work" - ], - "coupled_to": [], - "synergistic_with": [] - }, - "heat_loss_fraction": { - "trades_off_with": [], - "coupled_to": [], - "synergistic_with": [] - }, - "electricity_price": { - "trades_off_with": [], - "coupled_to": [], - "synergistic_with": [] - }, - "steam_cost": { - "trades_off_with": [], - "coupled_to": [], - "synergistic_with": [] - }, - "solvent_makeup_cost": { - "trades_off_with": [], - "coupled_to": [], - "synergistic_with": [] - }, - "solvent_degradation_rate": { - "trades_off_with": [ - "mea_concentration" - ], - "coupled_to": [], - "synergistic_with": [] - }, - "labor_cost_factor": { - "trades_off_with": [], - "coupled_to": [], - "synergistic_with": [] - }, - "maintenance_factor": { - "trades_off_with": [], - "coupled_to": [], - "synergistic_with": [] - }, - "taxes_insurance_factor": { - "trades_off_with": [], - "coupled_to": [], - "synergistic_with": [] - }, - "labor_scaling_exponent": { - "trades_off_with": [], - "coupled_to": [], - "synergistic_with": [] - }, - "supervision_factor": { - "trades_off_with": [], - "coupled_to": [], - "synergistic_with": [] - }, - "overhead_factor": { - "trades_off_with": [], - "coupled_to": [], - "synergistic_with": [] - }, - "delta_T_cooling_water": { - "trades_off_with": [], - "coupled_to": [], - "synergistic_with": [] - }, - "equipment_manufacturing_cost_factor": { - "trades_off_with": [], - "coupled_to": [], - "synergistic_with": [] - }, - "interest_rate": { - "trades_off_with": [], - "coupled_to": [], - "synergistic_with": [] - }, - "project_lifetime": { - "trades_off_with": [], - "coupled_to": [], - "synergistic_with": [] - }, - "epc_cost_factor": { - "trades_off_with": [], - "coupled_to": [], - "synergistic_with": [] - }, - "process_contingency_factor": { - "trades_off_with": [], - "coupled_to": [], - "synergistic_with": [] - }, - "project_contingency_factor": { - "trades_off_with": [], - "coupled_to": [], - "synergistic_with": [] - }, - "owner_cost_factor": { - "trades_off_with": [], - "coupled_to": [], - "synergistic_with": [] - }, - "absorber_pressure": { - "trades_off_with": [ - "blower_power", - "absorber_size" - ], - "coupled_to": [], - "synergistic_with": [] - }, - "base_operators": { - "trades_off_with": [ - "automation_capex" - ], - "coupled_to": [], - "synergistic_with": [] - }, - "labor_rate": { - "trades_off_with": [], - "coupled_to": [], - "synergistic_with": [] - }, - "construction_duration": { - "trades_off_with": [ - "afudc_cost" - ], - "coupled_to": [], - "synergistic_with": [] - } - }, - "tradeoffs": { - "heat_of_absorption": { - "total_reduction": 9.344679683489915, - "capex_contribution": 0.4698754079469239, - "opex_contribution": 0.5301245920530754, - "capex_change": -4.390835178413155, - "opex_change": -4.953844505076752, - "net_benefit": -9.344679683489915 - }, - "lrhe_effectiveness": { - "total_reduction": 1.3815972339235145, - "capex_contribution": 0.9342529489101732, - "opex_contribution": 1.9342529489101783, - "capex_change": 1.2907612899991818, - "opex_change": -2.6723585239227035, - "net_benefit": -1.3815972339235145 - }, - "rich_pump_head": { - "total_reduction": 0.09430701739456993, - "capex_contribution": 0.42166918592827224, - "opex_contribution": 0.5783308140716524, - "capex_change": -0.03976636325209171, - "opex_change": -0.05454065414247111, - "net_benefit": -0.09430701739456993 - }, - "absorber_temperature": { - "total_reduction": 3.6534148809295885, - "capex_contribution": 0.2619579750796177, - "opex_contribution": 0.7380420249203803, - "capex_change": -0.9570411643340577, - "opex_change": -2.6963737165955237, - "net_benefit": -3.6534148809295885 - }, - "heat_loss_fraction": { - "total_reduction": 0.9969948354573717, - "capex_contribution": 0.09009609391803175, - "opex_contribution": 0.909903906081954, - "capex_change": -0.08982534033115996, - "opex_change": -0.9071694951261975, - "net_benefit": -0.9969948354573717 - }, - "electricity_price": { - "total_reduction": 0.18276219973245134, - "capex_contribution": 0.0, - "opex_contribution": 1.0000000000000389, - "capex_change": 0.0, - "opex_change": -0.18276219973245844, - "net_benefit": -0.18276219973245134 - }, - "steam_cost": { - "total_reduction": 20.724246925699916, - "capex_contribution": 0.0, - "opex_contribution": 1.0, - "capex_change": 0.0, - "opex_change": -20.724246925699916, - "net_benefit": -20.724246925699916 - }, - "solvent_makeup_cost": { - "total_reduction": 0.7874999999999943, - "capex_contribution": 0.0, - "opex_contribution": 1.000000000000009, - "capex_change": 0.0, - "opex_change": -0.7875000000000014, - "net_benefit": -0.7874999999999943 - }, - "solvent_degradation_rate": { - "total_reduction": 1.470000000000013, - "capex_contribution": 0.0, - "opex_contribution": 0.9999999999999951, - "capex_change": 0.0, - "opex_change": -1.470000000000006, - "net_benefit": -1.470000000000013 - }, - "maintenance_factor": { - "total_reduction": 2.378811625817491, - "capex_contribution": 0.0, - "opex_contribution": 0.999999999999997, - "capex_change": 0.0, - "opex_change": -2.378811625817484, - "net_benefit": -2.378811625817491 - }, - "overhead_factor": { - "total_reduction": 0.34951583042814605, - "capex_contribution": 0.0, - "opex_contribution": 1.0000000000000204, - "capex_change": 0.0, - "opex_change": -0.34951583042815315, - "net_benefit": -0.34951583042814605 - }, - "equipment_manufacturing_cost_factor": { - "total_reduction": 2.3182281568115712, - "capex_contribution": 0.8255032516879001, - "opex_contribution": 0.17449674831209994, - "capex_change": -1.9137048816023992, - "opex_change": -0.4045232752091721, - "net_benefit": -2.3182281568115712 - }, - "interest_rate": { - "total_reduction": 12.048781843132645, - "capex_contribution": 0.9999999999999994, - "opex_contribution": 0.0, - "capex_change": -12.048781843132637, - "opex_change": 0.0, - "net_benefit": -12.048781843132645 - }, - "epc_cost_factor": { - "total_reduction": 1.6076572455690723, - "capex_contribution": 1.0, - "opex_contribution": 0.0, - "capex_change": -1.6076572455690723, - "opex_change": 0.0, - "net_benefit": -1.6076572455690723 - }, - "process_contingency_factor": { - "total_reduction": 6.925292750143768, - "capex_contribution": 0.999999999999999, - "opex_contribution": 0.0, - "capex_change": -6.925292750143761, - "opex_change": 0.0, - "net_benefit": -6.925292750143768 - }, - "project_contingency_factor": { - "total_reduction": 17.50560111841895, - "capex_contribution": 1.0000000000000004, - "opex_contribution": 0.0, - "capex_change": -17.50560111841896, - "opex_change": 0.0, - "net_benefit": -17.50560111841895 - }, - "absorber_pressure": { - "total_reduction": 3.9440060599758056, - "capex_contribution": 0.6824599344939025, - "opex_contribution": 0.3175400655060957, - "capex_change": -2.6916261173346427, - "opex_change": -1.2523799426411557, - "net_benefit": -3.9440060599758056 - }, - "base_operators": { - "total_reduction": 1.3456359471483665, - "capex_contribution": 0.0, - "opex_contribution": 0.9999999999999895, - "capex_change": 0.0, - "opex_change": -1.3456359471483523, - "net_benefit": -1.3456359471483665 - }, - "labor_rate": { - "total_reduction": 0.5767011202064367, - "capex_contribution": 0.0, - "opex_contribution": 1.0, - "capex_change": 0.0, - "opex_change": -0.5767011202064367, - "net_benefit": -0.5767011202064367 - } - } -} \ No newline at end of file From 7ffeabfda8b0416d954bbb9deae0704e33eeb92c Mon Sep 17 00:00:00 2001 From: niklasmarxer Date: Wed, 28 Jan 2026 15:16:21 -0800 Subject: [PATCH 08/18] init update --- steer_core/Constants/Units.py | 15 ++++++--------- steer_core/Constants/__init__.py | 8 -------- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/steer_core/Constants/Units.py b/steer_core/Constants/Units.py index ae825fd..eba5fca 100644 --- a/steer_core/Constants/Units.py +++ b/steer_core/Constants/Units.py @@ -2,10 +2,7 @@ Unit conversion constants and functions. This module provides consistent unit handling across the codebase. -All conversions are defined as constants or pure functions. -IMPORTANT: Always use these functions/constants for unit conversions. - Never use raw multipliers like /1000 or *1000 directly. """ import math @@ -56,18 +53,18 @@ # ============================================================================= S_TO_H = 1.0 / 3600.0 H_TO_S = 3600.0 -MIN_TO_H = 1.0 / 60.0 -H_TO_MIN = 60.0 -S_TO_MIN = 1.0 / 60.0 -MIN_TO_S = 60.0 +M_TO_H = 1.0 / 60.0 +H_TO_M = 60.0 +S_TO_M = 1.0 / 60.0 +M_TO_S = 60.0 S_TO_Y = 1.0 / (3600.0 * 24.0 * 365) Y_TO_S = 3600.0 * 24.0 * 365 # Time units S_TO_H = 1 / 3600 H_TO_S = 3600 -S_TO_MIN = 1 / 60 -MIN_TO_S = 60 +S_TO_M = 1 / 60 +M_TO_S = 60 S_TO_Y = 1 / (3600 * 24 * 365) Y_TO_S = 3600 * 24 * 365 H_TO_Y = 1 / 8760 diff --git a/steer_core/Constants/__init__.py b/steer_core/Constants/__init__.py index a479d91..e69de29 100644 --- a/steer_core/Constants/__init__.py +++ b/steer_core/Constants/__init__.py @@ -1,8 +0,0 @@ -"""Convenience exports for steer_core.Constants. - -This facade keeps downstream imports short during development -by exposing all symbols from the core constant submodules. -""" - -from .Units import * # noqa: F401,F403 -from .Universal import * # noqa: F401,F403 From 2247e2cc49666428b3a7682664e273f745397d0d Mon Sep 17 00:00:00 2001 From: fschweden <148897399+fschweden@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:51:35 -0800 Subject: [PATCH 09/18] added molar masses --- steer_core/Constants/Universal.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/steer_core/Constants/Universal.py b/steer_core/Constants/Universal.py index dbc3b05..444490a 100644 --- a/steer_core/Constants/Universal.py +++ b/steer_core/Constants/Universal.py @@ -38,4 +38,9 @@ PA_PER_BAR = 1e5 # Pascal per bar GPU_TO_SI = 3.35e-10 # mol/(m²·s·Pa) per GPU (gas permeation unit) - +# ============================================================================= +# MOLAR MASSES +# ============================================================================= +M_G_PER_MOL_NO2 = 46.0055 +M_G_PER_MOL_SO2 = 64.0638 +MW_G_PER_MOL_CO2 = 44.0095 From 3c5c650ea8a9ec8a214d5f258f6b550f049dc9fe Mon Sep 17 00:00:00 2001 From: fschweden <148897399+fschweden@users.noreply.github.com> Date: Thu, 29 Jan 2026 09:55:54 -0800 Subject: [PATCH 10/18] added Molar Masses --- steer_core/Constants/Universal.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/steer_core/Constants/Universal.py b/steer_core/Constants/Universal.py index 444490a..519e6b3 100644 --- a/steer_core/Constants/Universal.py +++ b/steer_core/Constants/Universal.py @@ -41,6 +41,16 @@ # ============================================================================= # MOLAR MASSES # ============================================================================= -M_G_PER_MOL_NO2 = 46.0055 -M_G_PER_MOL_SO2 = 64.0638 -MW_G_PER_MOL_CO2 = 44.0095 +G_PER_MOL_NO2 = 46.0055 +G_PER_MOL_SO2 = 64.0638 +G_PER_MOL_N2 = 28.0134 +G_PER_MOL_O2 = 31.9988 +G_PER_MOL_H2O = 18.0152 +G_PER_MOL_CO2 = 44.0095 +G_PER_MOL_AR = 39.9480 +G_PER_MOL_H2 = 2.0158 +G_PER_MOL_CH4 = 16.0425 +G_PER_MOL_C2H6 = 30.0690 +G_PER_MOL_C3H8 = 44.0956 +G_PER_MOL_CO = 28.0101 +G_PER_MOL_C4H10 = 58.1222 From 2f20c887885f107d501449a983183aead4e8c4f9 Mon Sep 17 00:00:00 2001 From: fschweden <148897399+fschweden@users.noreply.github.com> Date: Wed, 4 Mar 2026 09:47:33 -0800 Subject: [PATCH 11/18] units added and small mistake fixed --- steer_core/Constants/Units.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/steer_core/Constants/Units.py b/steer_core/Constants/Units.py index eba5fca..4f35ec5 100644 --- a/steer_core/Constants/Units.py +++ b/steer_core/Constants/Units.py @@ -99,6 +99,12 @@ 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 # ============================================================================= @@ -148,5 +154,5 @@ M3_TO_L = 1.0 / L_TO_M3 GAL_TO_L = 3.78541 L_TO_GAL = 1 / 3.78541 -MMGAL_TO_GAL = 1e-6 -GAL_TO_MMGAL = 1e6 +MMGAL_TO_GAL = 1e6 +GAL_TO_MMGAL = 1e-6 From 771eb42dceb1fdf9e114d519f82b1ee747f9d808 Mon Sep 17 00:00:00 2001 From: Tom-Eliot Jullien Date: Mon, 27 Apr 2026 22:59:35 -0700 Subject: [PATCH 12/18] Clean version of dev_niklas --- docs/api/mixins.md | 4 +++ steer_core/Constants/Units.py | 55 ++++++++++++++++++++----------- steer_core/Constants/Universal.py | 4 +-- 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/docs/api/mixins.md b/docs/api/mixins.md index 39131a7..5064f85 100644 --- a/docs/api/mixins.md +++ b/docs/api/mixins.md @@ -41,4 +41,8 @@ geometry, datetime handling, change propagation, and more. --- +::: steer_core.Mixins.Thermodynamics + +--- + ::: steer_core.Mixins.TypeChecker diff --git a/steer_core/Constants/Units.py b/steer_core/Constants/Units.py index add8e37..a8187bc 100644 --- a/steer_core/Constants/Units.py +++ b/steer_core/Constants/Units.py @@ -1,8 +1,9 @@ # 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 @@ -13,8 +14,15 @@ G_TO_T = G_TO_KG * KG_TO_T # 1e-6 T_TO_G = T_TO_KG * KG_TO_G # 1e6 -KMOL_TO_MOL = 1e3 +# ============================================================================= +# 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 @@ -39,14 +47,18 @@ 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 @@ -60,24 +72,30 @@ 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 KW_TO_W = 1e3 KW_TO_MW = 1e-3 @@ -151,15 +169,12 @@ FRACTION_TO_PPM = 1e6 PPM_TO_FRACTION = 1e-6 -# Unitless -UNIT_TO_MILLION: float = 1e-6 -MILLION_TO_UNIT: float = 1e6 - -# Unitless -UNIT_TO_MILLION: float = 1e-6 -MILLION_TO_UNIT: float = 1e6 +UNIT_TO_MILLION = 1e-6 +MILLION_TO_UNIT = 1e6 -# Volume units +# ============================================================================= +# 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 diff --git a/steer_core/Constants/Universal.py b/steer_core/Constants/Universal.py index 2cdc20d..594797f 100644 --- a/steer_core/Constants/Universal.py +++ b/steer_core/Constants/Universal.py @@ -7,9 +7,9 @@ # Universal Physical Constants -R_GAS = 8.314 # J/(mol·K) Universal gas constant +R_GAS = 8.314462618 # J/(mol·K) Universal gas constant (CODATA 2018) -GRAVITY = 9.81 # m/s² - Standard acceleration due to gravity +GRAVITY = 9.80665 # m/s² — Standard acceleration due to gravity (exact by definition) # Standard Conditions From 028b9ef754d1220111fe2a47354a2dac82afc7ad Mon Sep 17 00:00:00 2001 From: Tom-Eliot Jullien Date: Wed, 29 Apr 2026 11:25:11 -0700 Subject: [PATCH 13/18] typecheckers implemented for frequency types (basis, display, compounding) --- steer_core/Mixins/TypeChecker.py | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/steer_core/Mixins/TypeChecker.py b/steer_core/Mixins/TypeChecker.py index c59680c..4513425 100644 --- a/steer_core/Mixins/TypeChecker.py +++ b/steer_core/Mixins/TypeChecker.py @@ -337,3 +337,43 @@ 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: + 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: + 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: + 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: + 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}'." + ) + From b423f4b49617ffd681bd86be446b184c7541f350 Mon Sep 17 00:00:00 2001 From: Tom-Eliot Jullien Date: Wed, 29 Apr 2026 11:27:53 -0700 Subject: [PATCH 14/18] updated validation frequency methods --- steer_core/Mixins/TypeChecker.py | 67 +++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/steer_core/Mixins/TypeChecker.py b/steer_core/Mixins/TypeChecker.py index 4513425..b5ab0a3 100644 --- a/steer_core/Mixins/TypeChecker.py +++ b/steer_core/Mixins/TypeChecker.py @@ -343,6 +343,20 @@ def validate_number(value, name: str) -> None: @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( @@ -351,6 +365,22 @@ def validate_base_binning_frequency(value, name: str = "base_binning_frequency") @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: @@ -361,6 +391,26 @@ def validate_display_frequency(display_freq, base_freq, name: str = "display_fre @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}': " @@ -369,6 +419,22 @@ def validate_base_binning_frequency_not_increased(current_freq, new_freq, name: @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: @@ -376,4 +442,3 @@ def validate_compounding_frequency(base_freq, compounding_freq, name: str = "com f"base_binning_frequency '{base_freq.value}' is coarser than " f"{name} '{compounding_freq.value}'." ) - From a22be2d4a4c22b4dc3b1fcbf353778a422b39435 Mon Sep 17 00:00:00 2001 From: Tom-Eliot Jullien Date: Wed, 29 Apr 2026 19:16:30 -0700 Subject: [PATCH 15/18] pressure units updated Co-authored-by: Copilot --- steer_core/Constants/Units.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/steer_core/Constants/Units.py b/steer_core/Constants/Units.py index a8187bc..3ce6a4b 100644 --- a/steer_core/Constants/Units.py +++ b/steer_core/Constants/Units.py @@ -137,6 +137,9 @@ # 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 From 8ffc566d635feb17791f1a343fea252ad19b35bb Mon Sep 17 00:00:00 2001 From: Tom-Eliot Jullien Date: Wed, 29 Apr 2026 19:16:59 -0700 Subject: [PATCH 16/18] rogue pressure units removed and MW removed Co-authored-by: Copilot --- steer_core/Constants/Universal.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/steer_core/Constants/Universal.py b/steer_core/Constants/Universal.py index 594797f..722f23b 100644 --- a/steer_core/Constants/Universal.py +++ b/steer_core/Constants/Universal.py @@ -16,26 +16,3 @@ STANDARD_PRESSURE = 101325.0 # Pa (1 atm) STANDARD_TEMPERATURE = 273.15 # K - - - -# Conversion Factor for Special Units - -MMHG_PER_PA = 0.00750062 # mmHg per Pascal -PA_PER_BAR = 1e5 # Pascal per bar -GPU_TO_SI = 3.35e-10 # mol/(m²·s·Pa) per GPU (gas permeation unit) - -# Molar Masses -MW_G_PER_MOL_NO2 = 46.0055 -MW_G_PER_MOL_SO2 = 64.0638 -MW_G_PER_MOL_N2 = 28.0134 -MW_G_PER_MOL_O2 = 31.9988 -MW_G_PER_MOL_H2O = 18.0152 -MW_G_PER_MOL_CO2 = 44.0095 -MW_G_PER_MOL_AR = 39.9480 -MW_G_PER_MOL_H2 = 2.0158 -MW_G_PER_MOL_CH4 = 16.0425 -MW_G_PER_MOL_C2H6 = 30.0690 -MW_G_PER_MOL_C3H8 = 44.0956 -MW_G_PER_MOL_CO = 28.0101 -MW_G_PER_MOL_C4H10 = 58.1222 From 50e6353ac87f4f9a776692420dd29b1d449dcd17 Mon Sep 17 00:00:00 2001 From: Tom-Eliot Jullien Date: Wed, 29 Apr 2026 19:17:12 -0700 Subject: [PATCH 17/18] test updated --- test/test_constants.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/test_constants.py b/test/test_constants.py index 1499ff6..b35eb58 100644 --- a/test/test_constants.py +++ b/test/test_constants.py @@ -67,11 +67,6 @@ def test_pi(self): def test_two_pi(self): assert Universal.TWO_PI == pytest.approx(2 * math.pi) - def test_molar_masses_positive(self): - assert Universal.MW_G_PER_MOL_NO2 > 0 - assert Universal.MW_G_PER_MOL_SO2 > 0 - assert Universal.MW_G_PER_MOL_CO2 > 0 - # Need pytest.approx at module level for parametrized tests import pytest From 3f6d7e0ff20d3fea66deb84a544dbba262691067 Mon Sep 17 00:00:00 2001 From: Tom-Eliot Jullien Date: Wed, 29 Apr 2026 19:17:26 -0700 Subject: [PATCH 18/18] consistant naming for the methods --- steer_core/Mixins/Thermodynamics.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/steer_core/Mixins/Thermodynamics.py b/steer_core/Mixins/Thermodynamics.py index fdab546..131c42e 100644 --- a/steer_core/Mixins/Thermodynamics.py +++ b/steer_core/Mixins/Thermodynamics.py @@ -13,7 +13,7 @@ class ThermodynamicsMixin: """Mixin providing generic thermodynamic calculation methods.""" @staticmethod - def antoine_pressure(T: float, A: float, B: float, C: float) -> float: + def calculate_antoine_pressure(T: float, A: float, B: float, C: float) -> float: """ Calculate vapor pressure using Antoine equation. @@ -29,7 +29,7 @@ def antoine_pressure(T: float, A: float, B: float, C: float) -> float: return 10 ** (A - B / (T + C)) @staticmethod - def antoine_temperature(P: float, A: float, B: float, C: float) -> float: + def calculate_antoine_temperature(P: float, A: float, B: float, C: float) -> float: """ Calculate temperature from vapor pressure using inverse Antoine equation. @@ -44,7 +44,7 @@ def antoine_temperature(P: float, A: float, B: float, C: float) -> float: return B / (A - log_P) - C @staticmethod - def ideal_gas_density(P_Pa: float, T_K: float, MW_kg_mol: float) -> float: + def calculate_ideal_gas_density(P_Pa: float, T_K: float, MW_kg_mol: float) -> float: """ Calculate ideal gas density using ideal gas law.