Source code for energyunits.substance
"""Substance properties for energy system modeling."""
import json
from pathlib import Path
[docs]
class SubstanceRegistry:
"""Registry of substances and their properties."""
def __init__(self):
self._substances = {}
self._load_defaults()
def _load_defaults(self):
"""Load default substance data from JSON."""
data_path = Path(__file__).parent / "data" / "substances.json"
with open(data_path) as f:
data = json.load(f)
# Filter out metadata fields
self._substances = {k: v for k, v in data.items() if not k.startswith("_")}
[docs]
def load_substances(self, file_path: str):
"""Load custom substances from JSON file."""
with open(file_path) as f:
data = json.load(f)
# Filter out metadata fields
self._substances.update({k: v for k, v in data.items() if not k.startswith("_")})
def __getitem__(self, substance_id):
"""Dict-like access to substance data."""
if substance_id not in self._substances:
import difflib
close = difflib.get_close_matches(
substance_id, self._substances.keys(), n=3
)
msg = f"Unknown substance: '{substance_id}'."
if close:
msg += f" Did you mean: {', '.join(close)}?"
else:
msg += (
f" Available substances: {', '.join(sorted(self._substances.keys()))}"
)
raise ValueError(msg)
return self._substances[substance_id]
def __contains__(self, substance_id):
"""Check if substance exists."""
return substance_id in self._substances
[docs]
def hhv(self, substance_id):
"""Get higher heating value in MJ/kg."""
substance = self[substance_id]
hhv = substance["hhv"]
if hhv is None:
raise ValueError(f"Substance '{substance_id}' has no heating value")
return hhv
[docs]
def lhv(self, substance_id):
"""Get lower heating value in MJ/kg."""
substance = self[substance_id]
lhv = substance["lhv"]
if lhv is None:
raise ValueError(f"Substance '{substance_id}' has no heating value")
return lhv
[docs]
def density(self, substance_id):
"""Get density in kg/m3."""
substance = self[substance_id]
density = substance["density"]
if density is None:
raise ValueError(f"Substance '{substance_id}' has no defined density")
return density
[docs]
def list_substances(self, has_property=None):
"""List all available substances, optionally filtered by property.
Args:
has_property: Filter to substances that have this property defined
(e.g., "hhv", "density", "carbon_content").
Returns:
Sorted list of substance names.
"""
if has_property is None:
return sorted(self._substances.keys())
return sorted(
name
for name, props in self._substances.items()
if props.get(has_property) is not None
)
[docs]
def get_properties(self, substance_id):
"""Get all properties for a substance as a dict.
Args:
substance_id: Substance identifier.
Returns:
Dict of property name to value.
"""
return dict(self[substance_id])
[docs]
def lhv_hhv_ratio(self, substance_id):
"""Get the ratio of LHV to HHV."""
return self.lhv(substance_id) / self.hhv(substance_id)
[docs]
def calculate_combustion_product(self, fuel_quantity, target_substance):
"""Calculate combustion products from fuel based on stoichiometry."""
from .quantity import Quantity
if fuel_quantity.substance is None:
raise ValueError("Fuel substance must be specified")
fuel_t = fuel_quantity.to("t")
fuel_mass = fuel_t.value
substance = self[fuel_quantity.substance]
if target_substance == "CO2":
carbon_mass = fuel_mass * substance["carbon_content"]
co2_mass = carbon_mass * (44 / 12)
return Quantity(co2_mass, "t", "CO2")
elif target_substance == "H2O":
hydrogen_mass = fuel_mass * substance["hydrogen_content"]
water_mass = hydrogen_mass * (18 / 2)
return Quantity(water_mass, "t", "H2O")
elif target_substance == "ash":
ash_mass = fuel_mass * substance["ash_content"]
return Quantity(ash_mass, "t", "ash")
else:
raise ValueError(f"Unknown combustion product: {target_substance}")
substance_registry = SubstanceRegistry()