Source code for exerpy.functions

import json
import logging
import math
import os

import CoolProp.CoolProp as CP

from exerpy import __datapath__


[docs] def mass_to_molar_fractions(mass_fractions): """ Convert mass fractions to molar fractions. Parameters: - mass_fractions: Dictionary with component names as keys and mass fractions as values. Returns: - molar_fractions: Dictionary with component names as keys and molar fractions as values. """ molar_fractions = {} if len(mass_fractions.values()) == 1: molar_fractions = {comp: 1.0 for comp in mass_fractions} else: molar_masses = {} # Step 1: Get the molar masses for each component for fraction in mass_fractions: try: molar_masses[fraction] = CP.PropsSI("M", fraction) except Exception: # print(f"Warning: Could not retrieve molar mass for {fraction} ({fraction}). Error: {e}") continue # Skip this fraction if there's an issue # Step 2: Check if we have valid molar masses if not molar_masses: raise ValueError(f"No valid molar masses were retrieved for substances {list(mass_fractions.keys())}") else: # Step 3: Calculate total moles in the mixture total_moles = sum(mass_fractions[comp] / molar_masses[comp] for comp in molar_masses) # Step 4: Calculate molar fractions for component in molar_masses: molar_fractions[component] = (mass_fractions[component] / molar_masses[component]) / total_moles # Step 5: Check if molar fractions sum to approximately 1 molar_sum = sum(molar_fractions.values()) if abs(molar_sum - 1.0) > 1e-6: raise ValueError(f"Error: Molar fractions do not sum to 1. Sum is {molar_sum}") return molar_fractions
[docs] def molar_to_mass_fractions(molar_fractions): """ Convert molar fractions to mass fractions. Parameters: - molar_fractions: Dictionary with component names as keys and molar fractions as values. Returns: - mass_fractions: Dictionary with component names as keys and mass fractions as values. """ molar_masses = {} mass_fractions = {} # Step 1: Get the molar masses for each component for fraction in molar_fractions: try: molar_masses[fraction] = CP.PropsSI("M", fraction) except Exception: # print(f"Warning: Could not retrieve molar mass for {fraction} ({fraction}). Error: {e}") continue # Skip this fraction if there's an issue # Step 2: Check if we have valid molar masses if not molar_masses: raise ValueError("No valid molar masses were retrieved. Exiting...") # Step 3: Calculate total mass in the mixture total_mass = sum(molar_fractions[comp] * molar_masses[comp] for comp in molar_masses) # Step 4: Calculate mass fractions for component in molar_masses: mass_fractions[component] = (molar_fractions[component] * molar_masses[component]) / total_mass # Step 5: Check if mass fractions sum to approximately 1 mass_sum = sum(mass_fractions.values()) if abs(mass_sum - 1.0) > 1e-6: raise ValueError(f"Error: Mass fractions do not sum to 1. Sum is {mass_sum}") return mass_fractions
[docs] def calc_chemical_exergy(stream_data, Tamb, pamb, chemExLib): """ Calculate the chemical exergy of a stream based on the molar fractions and chemical exergy data. There are three cases: - Case A: Handle pure substance. - Case B: If water condenses, handle the liquid and gas phases separately. - Case C: If water doesn't condense or if water is not present, handle the mixture using the standard approach (ideal mixture). Parameters: - stream_data: Dictionary containing 'mass_composition' of the stream. - Tamb: Ambient temperature in Celsius. - pamb: Ambient pressure in bar. Returns: - eCH: Chemical exergy in kJ/kg. """ logging.info( f"Starting chemical exergy of stream of composition {stream_data} calculation with Tamb={Tamb}, pamb={pamb}" ) try: # Check if molar fractions already exist if "molar_composition" in stream_data: molar_fractions = stream_data["molar_composition"] else: # If not, convert mass composition to molar fractions molar_fractions = mass_to_molar_fractions(stream_data["mass_composition"]) try: # Load chemical exergy data chem_ex_file = os.path.join(__datapath__, f"{chemExLib}.json") with open(chem_ex_file) as file: chem_ex_data = json.load(file) # data in J/kmol except FileNotFoundError: error_msg = f"Chemical exergy data file '{chemExLib}.json' not found. Please ensure the file exists or set chemExLib to 'Ahrendts'." logging.error(error_msg) raise FileNotFoundError(error_msg) R = 8.314 # Universal gas constant in J/(molK) aliases_water = CP.get_aliases("H2O") # Handle pure substance (Case A) molar_fractions = {key: value for key, value in molar_fractions.items() if value > 1e-6} if len(molar_fractions) == 1: logging.info("Handling pure substance case (Case A).") substance = next(iter(molar_fractions)) # Get the single key try: aliases = CP.get_aliases(substance) if set(aliases) & set(aliases_water): eCH = chem_ex_data["WATER"][2] / CP.PropsSI("M", "H2O") # liquid water, in J/kg logging.info(f"Pure water detected. Chemical exergy: {eCH} J/kg") else: for alias in aliases: if alias.upper() in chem_ex_data: eCH = chem_ex_data[alias.upper()][3] / CP.PropsSI("M", substance) # in J/kg logging.info(f"Found exergy data for {substance}. Chemical exergy: {eCH} J/kg") break else: logging.error(f"No matching alias found for {substance}") raise KeyError(f"No matching alias found for {substance}") except Exception: eCH = 0 # If no aliases found, set chemical exergy to 0 logging.warning(f"No CoolProp aliases found for {substance}. Setting chemical exergy to 0 J/kg.") # Handle mixtures (Case B or C) else: logging.info("Handling mixture case (Case B or C).") total_molar_mass = 0 # To compute the molar mass of the mixture eCH_gas_mol = 0 # Molar chemical exergy of the gas phase if condensation eCH_liquid_mol = 0 # Molar chemical exergy of the liquid phase if condensation molar_fractions_gas = {} # Molar fractions within the gas phase if condensation entropy_mixing = 0 # Entropy of mixing of ideal mixtures # Calculate the total molar mass of the mixture for substance, fraction in molar_fractions.items(): molar_mass = CP.PropsSI("M", substance) # Molar mass in kg/mol total_molar_mass += fraction * molar_mass # Weighted sum for molar mass in kg/mol logging.info(f"Total molar mass of the mixture: {total_molar_mass} kg/mol") water_present = any(alias in molar_fractions for alias in aliases_water) if water_present: water_alias = next(alias for alias in aliases_water if alias in molar_fractions) pH2O_sat = CP.PropsSI("P", "T", Tamb, "Q", 1, "Water") # Saturation pressure of water in bar pH2O = molar_fractions[water_alias] * pamb # Partial pressure of water if pH2O > pH2O_sat: # Case B: Water condenses logging.info("Condensation occurs in the mixture.") x_dry = sum(fraction for comp, fraction in molar_fractions.items() if comp != water_alias) x_H2O_gas = x_dry / (pamb / pH2O_sat - 1) # Vaporous water fraction in the total mixture x_H2O_liquid = molar_fractions[water_alias] - x_H2O_gas # Liquid water fraction x_total_gas = 1 - x_H2O_liquid # Total gas phase fraction eCH_liquid_mol = x_H2O_liquid * (chem_ex_data["WATER"][2]) # Liquid phase contribution, in J/mol for substance, fraction in molar_fractions.items(): if substance == water_alias: molar_fractions_gas[substance] = x_H2O_gas / x_total_gas else: molar_fractions_gas[substance] = molar_fractions[substance] / x_total_gas for substance, fraction in molar_fractions_gas.items(): aliases = CP.get_aliases(substance) for alias in aliases: if alias.upper() in chem_ex_data: eCH_gas_mol += fraction * (chem_ex_data[alias.upper()][3]) # Exergy is in J/mol break else: logging.error(f"No matching alias found for {substance}") raise KeyError(f"No matching alias found for {substance}") if fraction > 0: # Avoid log(0) entropy_mixing += fraction * math.log(fraction) eCH_gas_mol += R * Tamb * entropy_mixing eCH_mol = eCH_gas_mol + eCH_liquid_mol logging.info(f"Condensed phase chemical exergy: {eCH_mol} J/kmol") else: # Case C: Water doesn't condense logging.info("Water does not condense.") eCH_mol = 0 for substance, fraction in molar_fractions.items(): aliases = CP.get_aliases(substance) for alias in aliases: if alias.upper() in chem_ex_data: eCH_mol += fraction * (chem_ex_data[alias.upper()][3]) # Exergy in J/kmol break else: logging.error(f"No matching alias found for {substance}") raise KeyError(f"No matching alias found for {substance}") if fraction > 0: # Avoid log(0) entropy_mixing += fraction * math.log(fraction) eCH_mol += R * Tamb * entropy_mixing else: # Case C: No water present logging.info("No water present in the mixture.") eCH_mol = 0 for substance, fraction in molar_fractions.items(): aliases = CP.get_aliases(substance) for alias in aliases: if alias.upper() in chem_ex_data: eCH_mol += fraction * (chem_ex_data[alias.upper()][3]) # Exergy in J/kmol break else: logging.error(f"No matching alias found for {substance}") raise KeyError(f"No matching alias found for {substance}") if fraction > 0: # Avoid log(0) entropy_mixing += fraction * math.log(fraction) eCH_mol += R * Tamb * entropy_mixing eCH = eCH_mol / total_molar_mass # Divide molar exergy by molar mass of mixture logging.info(f"Chemical exergy: {eCH} kJ/kg") return eCH except Exception as e: logging.error(f"Error in calc_chemical_exergy: {e}") raise
[docs] def add_chemical_exergy(my_json, Tamb, pamb, chemExLib): """ Adds the chemical exergy to each connection in the JSON data, prioritizing molar composition if available. Parameters: - my_json: The JSON object containing the components and connections. - Tamb: Ambient temperature in Celsius. - pamb: Ambient pressure in bar. Returns: - The modified JSON object with added chemical exergy for each connection. """ # Check if Tamb and pamb are provided and not None if Tamb is None or pamb is None: raise ValueError( "Ambient temperature (Tamb) and pressure (pamb) are required for chemical exergy calculation. " "Please ensure they are included in the JSON or passed as arguments." ) # Iterate over each material connection with kind == 'material' for conn_name, conn_data in my_json["connections"].items(): if conn_data["kind"] == "material": # Prefer molar composition if available, otherwise use mass composition molar_composition = conn_data.get("molar_composition", {}) mass_composition = conn_data.get("mass_composition", {}) # Prepare stream data for exergy calculation, prioritizing molar composition if molar_composition: stream_data = {"molar_composition": molar_composition} logging.info(f"Using molar composition for connection {conn_name}") else: stream_data = {"mass_composition": mass_composition} logging.info(f"Using mass composition for connection {conn_name}") # Add the chemical exergy value conn_data["e_CH"] = calc_chemical_exergy(stream_data, Tamb, pamb, chemExLib) conn_data["e_CH_unit"] = fluid_property_data["e"]["SI_unit"] logging.info(f"Added chemical exergy to connection {conn_name}: {conn_data['e_CH']} kJ/kg") else: logging.info( f"Skipped chemical exergy calculation for non-material connection {conn_name} ({conn_data['kind']})" ) return my_json
[docs] def add_total_exergy_flow(my_json, split_physical_exergy): r""" Adds the total exergy flow to each connection in the JSON data based on its kind. - For 'material' connections, the exergy is calculated as before. - For 'power' connections, the energy flow value is used directly. - For 'heat' connections, if the associated component is of class SimpleHeatExchanger, the thermal exergy difference is computed as: .. math:: E = (e^\mathrm{T}_\mathrm{in} \cdot \dot m_\mathrm{in}) - (e^\mathrm{T}_\mathrm{out} \cdot \dot m_\mathrm{out}) Otherwise, a warning is logged and E is set to None. Parameters ---------- my_json : dict The JSON object containing the components and connections. split_physical_exergy : bool Split physical exergy in mechanical and thermal shares. Returns ------- dict The modified JSON object with added total exergy flow for each connection. """ for conn_name, conn_data in my_json["connections"].items(): try: if conn_data["kind"] == "material": # For material connections: E = m * (e^PH + e^CH) conn_data["E_PH"] = conn_data["m"] * conn_data["e_PH"] if conn_data.get("e_CH") is not None: conn_data["E_CH"] = conn_data["m"] * conn_data["e_CH"] conn_data["E"] = conn_data["E_PH"] + conn_data["E_CH"] else: conn_data["E"] = conn_data["E_PH"] logging.info(f"Missing chemical exergy for connection {conn_name}. Using only physical exergy.") if split_physical_exergy: if conn_data.get("e_T") is not None: conn_data["E_T"] = conn_data["m"] * conn_data["e_T"] else: msg = f"Missing thermal exergy for connection {conn_name}." logging.error(msg) raise KeyError(msg) if conn_data.get("e_M") is not None: conn_data["E_M"] = conn_data["m"] * conn_data["e_M"] else: msg = f"Missing mechanical exergy for connection {conn_name}." logging.error(msg) raise KeyError(msg) elif conn_data["kind"] == "power": # For power connections, use the energy flow value directly. conn_data["E"] = conn_data["energy_flow"] elif conn_data["kind"] == "heat": # For heat connections, find the heat exchanger component on either end. # The other end is typically a HeatSource/HeatSink which is not in our component dict. source_component = conn_data["source_component"] target_component = conn_data["target_component"] she_comps = my_json["components"].get("SimpleHeatExchanger", {}) comp_name = source_component if source_component in she_comps else target_component # Check if the component is either a SimpleHeatExchanger or a SteamGenerator. if ( "SimpleHeatExchanger" in my_json["components"] and comp_name in my_json["components"]["SimpleHeatExchanger"] ): # Retrieve the inlet material streams: those with this component as target. inlet_conns = [ c for c in my_json["connections"].values() if c.get("target_component") == comp_name and c.get("kind") == "material" ] # Retrieve the outlet material streams: those with this component as source. outlet_conns = [ c for c in my_json["connections"].values() if c.get("source_component") == comp_name and c.get("kind") == "material" ] # Determine which exergy key to use based on the flag. exergy_key = "e_T" if split_physical_exergy else "e_PH" if inlet_conns and outlet_conns: # For simplicity, take the first inlet and first outlet. inlet = inlet_conns[0] outlet = outlet_conns[0] # Calculate the heat exergy difference using the selected key: conn_data["E"] = abs( inlet.get(exergy_key, 0) * inlet.get("m", 0) - outlet.get(exergy_key, 0) * outlet.get("m", 0) ) else: conn_data["E"] = None logging.warning( f"Not enough material connections for heat exchanger {comp_name} for heat exergy calculation." ) elif "SteamGenerator" in my_json["components"] and comp_name in my_json["components"]["SteamGenerator"]: # Retrieve material connections for the steam generator. inlet_conns = [ c for c in my_json["connections"].values() if c.get("target_component") == comp_name and c.get("kind") == "material" ] outlet_conns = [ c for c in my_json["connections"].values() if c.get("source_component") == comp_name and c.get("kind") == "material" ] if inlet_conns and outlet_conns: # For the steam generator, group the material connections as follows: feed_water = inlet_conns[0] # inl[0]: Feed water inlet (HP) steam_inlet = inlet_conns[1] if len(inlet_conns) > 1 else {} # inl[1]: Steam inlet (IP) superheated_HP = outlet_conns[0] # outl[0]: Superheated steam outlet (HP) superheated_IP = ( outlet_conns[1] if len(outlet_conns) > 1 else {} ) # outl[1]: Superheated steam outlet (IP) water_inj_HP = inlet_conns[2] if len(inlet_conns) > 2 else {} # inl[2]: Water injection (HP) water_inj_IP = inlet_conns[3] if len(inlet_conns) > 3 else {} # inl[3]: Water injection (IP) exergy_type = "e_T" if split_physical_exergy else "e_PH" # Calculate the contributions based on the new E_F definition: E_F_HP = superheated_HP.get("m", 0) * superheated_HP.get(exergy_type, 0) - feed_water.get( "m", 0 ) * feed_water.get(exergy_type, 0) E_F_IP = superheated_IP.get("m", 0) * superheated_IP.get(exergy_type, 0) - steam_inlet.get( "m", 0 ) * steam_inlet.get(exergy_type, 0) E_F_w_inj = water_inj_HP.get("m", 0) * water_inj_HP.get(exergy_type, 0) + water_inj_IP.get( "m", 0 ) * water_inj_IP.get(exergy_type, 0) # Total exergy flow for the heat input (E_TOT) is taken as the exergy fuel E_F: E_TOT = E_F_HP + E_F_IP - E_F_w_inj conn_data["E"] = E_TOT else: conn_data["E"] = None logging.warning( f"Not enough material connections for steam generator {comp_name} for heat exergy calculation." ) else: conn_data["E"] = None logging.warning( f"Heat connection {conn_name} is not associated with a recognized heat exchanger component." ) elif conn_data["kind"] == "other": # No exergy flow calculation for 'other' kind. pass else: logging.warning( f"Unknown connection kind: {conn_data['kind']} for connection {conn_name}. Skipping exergy flow calculation." ) conn_data["E"] = None # Assign the exergy unit (assuming fluid_property_data is defined elsewhere) conn_data["E_unit"] = fluid_property_data["power"]["SI_unit"] except Exception as e: logging.error(f"Error calculating total exergy flow for connection {conn_name}: {e}") conn_data["E"] = None return my_json
[docs] def convert_to_SI(property, value, unit): r""" Convert a value to its SI value. Parameters ---------- property : str Fluid property to convert. value : float Value to convert. unit : str Unit of the value. Returns ------- SI_value : float Specified fluid property in SI value. Raises ------ ValueError: If the property or unit is invalid or conversion is not possible. """ # Check if value is None if value is None: logging.warning(f"Value is None for property '{property}', cannot convert.") return None # Check if the property is valid and exists in fluid_property_data if property not in fluid_property_data: logging.warning(f"Unrecognized property: '{property}'. Returning original value {value} {unit}.") return value # Check if the unit is valid if unit == "Unknown": logging.warning(f"Unrecognized unit {unit} for property '{property}'. Returning original value {value} {unit}.") return value try: # Handle temperature conversions separately if property == "T": if unit not in fluid_property_data["T"]["units"]: raise ValueError(f"Invalid unit '{unit}' for temperature. Unit not found.") converters = fluid_property_data["T"]["units"][unit] return (value + converters[0]) * converters[1] # Handle all other property conversions else: if unit not in fluid_property_data[property]["units"]: raise ValueError(f"Invalid unit '{unit}' for property '{property}'. Unit not found.") conversion_factor = fluid_property_data[property]["units"][unit] return value * conversion_factor except KeyError as e: raise ValueError(f"Conversion error: {e}") except Exception as e: raise ValueError(f"An error occurred during the unit conversion: {e}")
fluid_property_data = { "m": { "text": "mass flow", "SI_unit": "kg / s", "units": { "kg / s": 1, "kg / min": 1 / 60, "kg / h": 1 / 3.6e3, "kg/s": 1, "kg/min": 1 / 60, "kg/h": 1 / 3.6e3, "kg / sec": 1, "kg/sec": 1, "t / h": 1 / 3.6, "g / s": 1e-3, "t/h": 1 / 3.6, "g/s": 1e-3, "g / sec": 1e-3, "g/sec": 1e-3, }, "latex_eq": r"0 = \dot{m} - \dot{m}_\mathrm{spec}", "documentation": {"float_fmt": "{:,.3f}"}, }, "n": { "text": "molar flow", "SI_unit": "mol / s", "units": { "mol / s": 1, "mol / min": 1 / 60, "mol / h": 1 / 3.6e3, "mol/s": 1, "mol/min": 1 / 60, "mol/h": 1 / 3.6e3, "kmol / s": 1e3, "kmol / min": 1 / 60e3, "kmol / h": 1 / 3.6e6, "kmol/s": 1e3, "kmol/min": 1 / 60e3, "kmol/h": 1 / 3.6e6, "mol / sec": 1, "mol/sec": 1, "kmol / sec": 1e3, "kmol/sec": 1e3, }, "latex_eq": r"0 = \dot{n} - \dot{n}_\mathrm{spec}", "documentation": {"float_fmt": "{:,.3f}"}, }, "v": { "text": "volumetric flow", "SI_unit": "m3 / s", "units": { "m3 / s": 1, "m3 / min": 1 / 60, "m3 / h": 1 / 3.6e3, "l / s": 1 / 1e3, "l / min": 1 / 60e3, "l / h": 1 / 3.6e6, }, "latex_eq": (r"0 = \dot{m} \cdot v \left(p,h\right)- \dot{V}_\mathrm{spec}"), "documentation": {"float_fmt": "{:,.3f}"}, }, "p": { "text": "pressure", "SI_unit": "Pa", "units": {"Pa": 1, "kPa": 1e3, "psi": 6.8948e3, "bar": 1e5, "atm": 1.01325e5, "MPa": 1e6}, "latex_eq": r"0 = p - p_\mathrm{spec}", "documentation": {"float_fmt": "{:,.3f}"}, }, "h": { "text": "enthalpy", "SI_unit": "J / kg", "SI_unit_molar:": "J / mol", "units": { "J / kg": 1, "kJ / kg": 1e3, "MJ / kg": 1e6, "J/kg": 1, "kJ/kg": 1e3, "MJ/kg": 1e6, "cal / kg": 4.184, "kcal / kg": 4.184e3, "cal/kg": 4.184, "kcal/kg": 4.184e3, "Wh / kg": 3.6e3, "kWh / kg": 3.6e6, "Wh/kg": 3.6e3, "kWh kg": 3.6e6, "J / mol": 1, "kJ / mol": 1e3, "MJ / mol": 1e6, "J/mol": 1, "kJ/mol": 1e3, "MJ/mol": 1e6, "J / kmol": 1e-3, "kJ / kmol": 1, "MJ / kmol": 1e3, "J/kmol": 1e-3, "kJ/kmol": 1, "MJ/kmol": 1e3, }, "latex_eq": r"0 = h - h_\mathrm{spec}", "documentation": {"float_fmt": "{:,.3f}"}, }, "e": { "text": "exergy", "SI_unit": "J / kg", "SI_unit_molar:": "J / mol", "units": { "J / kg": 1, "kJ / kg": 1e3, "MJ / kg": 1e6, "J/kg": 1, "kJ/kg": 1e3, "MJ/kg": 1e6, "cal / kg": 4.184, "kcal / kg": 4.184e3, "cal/kg": 4.184, "kcal/kg": 4.184e3, "Wh / kg": 3.6e3, "kWh / kg": 3.6e6, "Wh/kg": 3.6e3, "kWh kg": 3.6e6, "J / mol": 1, "kJ / mol": 1e3, "MJ / mol": 1e6, "J/mol": 1, "kJ/mol": 1e3, "MJ/mol": 1e6, "J / kmol": 1e-3, "kJ / kmol": 1, "MJ / kmol": 1e3, "J/kmol": 1e-3, "kJ/kmol": 1, "MJ/kmol": 1e3, }, "latex_eq": r"0 = h - h_\mathrm{spec}", "documentation": {"float_fmt": "{:,.3f}"}, }, "T": { "text": "temperature", "SI_unit": "K", "units": {"K": [0, 1], "R": [0, 5 / 9], "C": [273.15, 1], "F": [459.67, 5 / 9]}, "latex_eq": r"0 = T \left(p, h \right) - T_\mathrm{spec}", "documentation": {"float_fmt": "{:,.1f}"}, }, "Td_bp": { "text": "temperature difference to boiling point", "SI_unit": "K", "units": {"K": 1, "R": 5 / 9, "C": 1, "F": 5 / 9}, "latex_eq": r"0 = \Delta T_\mathrm{spec}- T_\mathrm{sat}\left(p\right)", "documentation": {"float_fmt": "{:,.1f}"}, }, "vol": { "text": "specific volume", "SI_unit": "m3 / kg", "units": {"m3 / kg": 1, "l / kg": 1e-3}, "latex_eq": (r"0 = v\left(p,h\right) \cdot \dot{m} - \dot{V}_\mathrm{spec}"), "documentation": {"float_fmt": "{:,.3f}"}, }, "x": { "text": "vapor mass fraction", "SI_unit": "-", "units": {"1": 1, "-": 1, "%": 1e-2, "ppm": 1e-6}, "latex_eq": r"0 = h - h\left(p, x_\mathrm{spec}\right)", "documentation": {"float_fmt": "{:,.2f}"}, }, "s": { "text": "entropy", "SI_unit": "J / kgK", "SI_unit_molar:": "J / molK", "units": { "J / kgK": 1, "kJ / kgK": 1e3, "MJ / kgK": 1e6, "J/kgK": 1, "kJ/kgK": 1e3, "MJ/kgK": 1e6, "J / kg-K": 1, "kJ / kg-K": 1e3, "MJ / kg-K": 1e6, "J/kg-K": 1, "kJ/kg-K": 1e3, "MJ/kg-K": 1e6, "J / molK": 1, "kJ / molK": 1e3, "MJ / molK": 1e6, "J/molK": 1, "kJ/molK": 1e3, "MJ/molK": 1e6, "J / mol-K": 1, "kJ / mol-K": 1e3, "MJ / mol-K": 1e6, "J/mol-K": 1, "kJ/mol-K": 1e3, "MJ/mol-K": 1e6, "J / kmolK": 1e-3, "kJ / kmolK": 1, "MJ / kmolK": 1e3, "J/kmolK": 1e-3, "kJ/kmolK": 1, "MJ/kmolK": 1e3, "J / kmol-K": 1e-3, "kJ / kmol-K": 1, "MJ / kmol-K": 1e3, "J/kmol-K": 1e-3, "kJ/kmol-K": 1, "MJ/kmol-K": 1e3, }, "latex_eq": r"0 = s_\mathrm{spec} - s\left(p, h \right)", "documentation": {"float_fmt": "{:,.2f}"}, }, "power": { "text": "power", "SI_unit": "W", "units": {"W": 1, "kW": 1e3, "MW": 1e6}, }, "heat": { "text": "heat", "SI_unit": "W", "units": {"W": 1, "kW": 1e3, "MW": 1e6}, }, "kA": { "text": "kA", "SI_unit": "W / K", "units": {"W / K": 1, "kW / K": 1e3, "MW / K": 1e6, "W/K": 1, "kW/K": 1e3, "MW/K": 1e6}, }, "A": { "text": "area", "SI_unit": "m2", "units": {"m2": 1, "cm2": 1e-4, "mm2": 1e-6, "m²": 1, "cm²": 1e-4, "mm²": 1e-6}, }, "VM": { "text": "volume flow", "SI_unit": "m3 / s", "units": {"m3 / s": 1, "l / s": 1e-3, "l/s": 1e-3, "m³/s": 1, "l/min": 1 / 60e3, "l/h": 1 / 3.6e6}, }, }