Skip to content

Commit a9d72b3

Browse files
authored
added enthalpy_units argument (#66)
1 parent 944a9fd commit a9d72b3

File tree

6 files changed

+268
-24
lines changed

6 files changed

+268
-24
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,16 @@ All notable user-visible changes to this project are documented here.
55
## [Unreleased]
66

77
### Changed
8+
- Python `cea.Reactant` now supports explicit `enthalpy_units` for custom reactant enthalpy input (`J/kg`, `kJ/kg`, `cal/kg`, `kcal/kg`, `J/mol`, `kJ/mol`, `cal/mol`, `kcal/mol`; `/mol` and `/mole` spellings accepted).
9+
- Backward-compatible behavior is preserved when `Reactant.enthalpy` is provided without `enthalpy_units`: the legacy default (`J/kg`) is still used, but this now emits a `FutureWarning` directing users to pass `enthalpy_units` explicitly.
10+
- Weight-based enthalpy units (`*/kg`) now require `Reactant.molecular_weight` for conversion, while molar units (`*/mol`) do not require molecular weight for enthalpy interpretation.
11+
- Python docs and examples were updated to pass explicit `enthalpy_units` for custom reactants (including RP-1311 Example 5) instead of relying on implicit defaults.
12+
- **Compatibility notice:** a future **minor** version increment will require `enthalpy_units` whenever `Reactant.enthalpy` is defined. Code that currently relies on implicit enthalpy units may break until updated.
813

914
### Fixed
1015

1116
### Added
17+
- Added Python regression coverage for `Reactant` enthalpy-unit handling, including omitted-units warning behavior, explicit molar/weight units, molecular-weight requirements for weight units, and accepted unit spellings.
1218

1319
## [3.1.2] - 2026-03-23
1420

docs/source/examples/equilibrium/example5.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,22 @@ Define the pressure schedule and reactant composition by weight fraction:
2121
weights = np.array([0.7206, 0.1858, 0.09, 0.002, 0.0016], dtype=np.float64)
2222
T_reac = np.array([298.15, 298.15, 298.15, 298.15, 298.15], dtype=np.float64)
2323
24-
Create a :class:`~cea.Reactant` for ``CHOS-Binder`` using SI values.
25-
In the Python API, custom reactant ``enthalpy`` is in J/kg and ``temperature`` is in K.
26-
Use :mod:`cea.units` helpers for pre-conversion as needed.
24+
Create a :class:`~cea.Reactant` for ``CHOS-Binder`` using explicit enthalpy units.
25+
For backward compatibility, omitting ``enthalpy_units`` still defaults to ``J/kg`` for now,
26+
but that implicit behavior is deprecated and emits a warning.
2727

2828
.. code-block:: python
2929
30-
chos_binder_mw_kg_per_mol = 14.6652984484e-3
31-
chos_binder_h_si = cea.units.cal_to_joule(-2999.082) / chos_binder_mw_kg_per_mol
30+
chos_binder_h_cal_per_mol = -2999.082
3231
3332
reactants = [
3433
"NH4CLO4(I)",
3534
cea.Reactant(
3635
name="CHOS-Binder",
3736
formula={"C": 1.0, "H": 1.86955, "O": 0.031256, "S": 0.008415},
3837
molecular_weight=14.6652984484,
39-
enthalpy=chos_binder_h_si,
38+
enthalpy=chos_binder_h_cal_per_mol,
39+
enthalpy_units="cal/mol",
4040
temperature=298.15,
4141
),
4242
"AL(cr)",

docs/source/interfaces/python_api.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ Mixture
1010
The :class:`~cea.Mixture` class is used to define a mixture of product or reactant species. It allows the user to specify the composition of the mixture and provides methods to compute thermodynamic curve fit properties.
1111
The instances of this class are then passed as inputs to the available solver classes (e.g., :class:`~cea.EqSolver`, :class:`~cea.RocketSolver`, :class:`~cea.ShockSolver`, or :class:`~cea.DetonationSolver`).
1212
Custom reactants can be provided through :class:`~cea.Reactant` objects (including mixed lists of strings and Reactant objects).
13-
For :class:`~cea.Reactant`, ``enthalpy`` and ``temperature`` are SI-only in the Python API (J/kg and K, respectively); use :mod:`cea.units` helpers for pre-conversion.
13+
For :class:`~cea.Reactant`, ``temperature`` is in K, and ``enthalpy`` must be paired with explicit
14+
``enthalpy_units`` (for example ``"J/kg"`` or ``"kJ/mol"``). Omitting ``enthalpy_units`` while
15+
providing ``enthalpy`` preserves the legacy default (``J/kg``) for backward compatibility, but this
16+
is deprecated and emits a ``FutureWarning``. A future minor version will require explicit
17+
``enthalpy_units``.
1418

1519
.. autoclass:: cea.Reactant
1620
:members:

source/bind/python/CEA.pyx

Lines changed: 102 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -530,22 +530,45 @@ cdef class Reactant:
530530
molecular_weight : float, optional
531531
Molecular weight in kg/kmol (numerically equivalent to g/mol).
532532
enthalpy : float, optional
533-
Reference enthalpy in SI units (J/kg).
533+
Reference enthalpy value interpreted according to ``enthalpy_units``.
534+
enthalpy_units : str, optional
535+
Units for ``enthalpy``. Supported values (case-insensitive):
536+
``J/kg``, ``kJ/kg``, ``cal/kg``, ``kcal/kg``, ``J/mol``, ``kJ/mol``,
537+
``cal/mol``, ``kcal/mol`` (``/mole`` spellings are also accepted).
538+
If omitted while ``enthalpy`` is provided, the legacy default ``J/kg``
539+
is used for backward compatibility and a ``FutureWarning`` is emitted.
540+
Future minor versions will require explicit ``enthalpy_units``.
534541
temperature : float, optional
535542
Reference temperature in SI units (K).
536543
537544
Notes
538545
-----
539-
Python Reactant inputs are SI-only. Use :mod:`cea.units` helpers to pre-convert
540-
non-SI values before constructing a Reactant.
546+
Weight-based enthalpy inputs (``*/kg``) require ``molecular_weight`` to
547+
convert to the core molar convention. Molar enthalpy inputs (``*/mol``)
548+
do not require ``molecular_weight`` for enthalpy interpretation.
541549
"""
542550
cdef public object name
543551
cdef public object formula
544552
cdef public object molecular_weight
545553
cdef public object enthalpy
554+
cdef public object enthalpy_units
546555
cdef public object temperature
556+
cdef object _enthalpy_core_units
557+
cdef object _enthalpy_is_weight_units
558+
559+
def __init__(
560+
self,
561+
name,
562+
formula=None,
563+
molecular_weight=None,
564+
enthalpy=None,
565+
enthalpy_units=None,
566+
temperature=None,
567+
):
568+
cdef object normalized_enthalpy_units = None
569+
cdef object enthalpy_core_units = None
570+
cdef bint enthalpy_is_weight_units = False
547571

548-
def __init__(self, name, formula=None, molecular_weight=None, enthalpy=None, temperature=None):
549572
if not isinstance(name, str) or len(name.strip()) == 0:
550573
raise TypeError("Reactant name must be a non-empty str")
551574

@@ -575,14 +598,80 @@ cdef class Reactant:
575598
raise TypeError(f"Reactant {field_name} must be numeric")
576599
if not np.isfinite(fval):
577600
raise ValueError(f"Reactant {field_name} must be finite")
578-
if enthalpy is not None and molecular_weight is None:
579-
raise ValueError("Reactant molecular_weight is required when enthalpy is specified")
601+
602+
if enthalpy is not None:
603+
if enthalpy_units is None:
604+
warnings.warn(
605+
"Reactant enthalpy without explicit enthalpy_units is deprecated; "
606+
"using legacy default J/kg for backward compatibility. "
607+
"Future minor versions will require explicit enthalpy_units. "
608+
"Pass enthalpy_units='J/kg' or enthalpy_units='kJ/mol'.",
609+
FutureWarning,
610+
)
611+
normalized_enthalpy_units = "j/kg"
612+
enthalpy_core_units = "j/mole"
613+
enthalpy_is_weight_units = True
614+
else:
615+
(
616+
normalized_enthalpy_units,
617+
enthalpy_core_units,
618+
enthalpy_is_weight_units,
619+
) = _normalize_enthalpy_units(enthalpy_units)
620+
if enthalpy_is_weight_units and molecular_weight is None:
621+
raise ValueError(
622+
"Reactant molecular_weight is required when enthalpy_units is weight-based "
623+
"(J/kg, kJ/kg, cal/kg, or kcal/kg)"
624+
)
625+
elif enthalpy_units is not None:
626+
normalized_enthalpy_units, _, _ = _normalize_enthalpy_units(enthalpy_units)
580627

581628
self.name = name
582629
self.formula = formula
583630
self.molecular_weight = molecular_weight
584631
self.enthalpy = enthalpy
632+
self.enthalpy_units = normalized_enthalpy_units
585633
self.temperature = temperature
634+
self._enthalpy_core_units = enthalpy_core_units
635+
self._enthalpy_is_weight_units = bool(enthalpy_is_weight_units)
636+
637+
638+
cdef tuple _normalize_enthalpy_units(object enthalpy_units):
639+
cdef str units_text
640+
cdef dict canonical_map
641+
cdef tuple normalized
642+
cdef object key
643+
644+
if isinstance(enthalpy_units, bytes):
645+
units_text = (<bytes>enthalpy_units).decode("utf-8", "strict")
646+
elif isinstance(enthalpy_units, str):
647+
units_text = <str>enthalpy_units
648+
else:
649+
raise TypeError("Reactant enthalpy_units must be str when provided")
650+
651+
units_text = "".join(units_text.split()).lower()
652+
653+
canonical_map = {
654+
"j/kg": ("j/kg", "j/mole", True),
655+
"kj/kg": ("kj/kg", "kj/mole", True),
656+
"cal/kg": ("cal/kg", "cal/mole", True),
657+
"kcal/kg": ("kcal/kg", "kcal/mole", True),
658+
"j/mol": ("j/mole", "j/mole", False),
659+
"j/mole": ("j/mole", "j/mole", False),
660+
"kj/mol": ("kj/mole", "kj/mole", False),
661+
"kj/mole": ("kj/mole", "kj/mole", False),
662+
"cal/mol": ("cal/mole", "cal/mole", False),
663+
"cal/mole": ("cal/mole", "cal/mole", False),
664+
"kcal/mol": ("kcal/mole", "kcal/mole", False),
665+
"kcal/mole": ("kcal/mole", "kcal/mole", False),
666+
}
667+
668+
if units_text not in canonical_map:
669+
raise ValueError(
670+
"Reactant enthalpy_units must be one of: "
671+
"J/kg, kJ/kg, cal/kg, kcal/kg, J/mol, kJ/mol, cal/mol, or kcal/mol"
672+
)
673+
normalized = canonical_map[units_text]
674+
return normalized
586675

587676

588677
cdef class Mixture:
@@ -638,7 +727,6 @@ cdef class Mixture:
638727
cdef list _reactant_keepalive = []
639728
cdef list _element_ptr_buffers = []
640729
cdef list _coeff_ptr_buffers = []
641-
cdef _CString _enthalpy_units
642730
cdef _CString _temperature_units
643731

644732
if species is None:
@@ -649,7 +737,6 @@ cdef class Mixture:
649737
raise TypeError("Mixture species must be a list")
650738
if type(omit) is not list:
651739
raise TypeError("Mixture omit must be a list")
652-
_enthalpy_units = _CString("j/mole", "Reactant enthalpy units")
653740
_temperature_units = _CString("k", "Reactant temperature units")
654741

655742
self.num_species = <int>len(species)
@@ -737,9 +824,14 @@ cdef class Mixture:
737824
cea_reactants[i].molecular_weight = float(reactant.molecular_weight)
738825

739826
if reactant.enthalpy is not None:
827+
_encoded = _CString(reactant._enthalpy_core_units, "Reactant enthalpy units")
828+
_reactant_keepalive.append(_encoded)
740829
cea_reactants[i].has_enthalpy = 1
741-
cea_reactants[i].enthalpy = float(reactant.enthalpy) * float(reactant.molecular_weight) / 1000.0
742-
cea_reactants[i].enthalpy_units = _enthalpy_units.ptr
830+
if reactant._enthalpy_is_weight_units:
831+
cea_reactants[i].enthalpy = float(reactant.enthalpy) * float(reactant.molecular_weight) / 1000.0
832+
else:
833+
cea_reactants[i].enthalpy = float(reactant.enthalpy)
834+
cea_reactants[i].enthalpy_units = _encoded.ptr
743835

744836
if reactant.temperature is not None:
745837
cea_reactants[i].has_temperature = 1

source/bind/python/cea/samples/rp1311/example5.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,17 @@
3434
"(HCOOH)2", "C6H6(L)", "C7H8(L)", "C8H18(L),n-octa", "Jet-A(L)", "H2O(s)", "H2O(L)",
3535
]
3636

37-
# Convert h,cal = -2999.082 cal/mol to SI J/kg for Python Reactant input.
38-
chos_binder_mw_kg_per_mol = 14.6652984484e-3
39-
chos_binder_h_si = cea.units.cal_to_joule(-2999.082) / chos_binder_mw_kg_per_mol
37+
# CHOS-Binder reference enthalpy from RP-1311 example input.
38+
chos_binder_h_cal_per_mol = -2999.082
4039

4140
reactants = [
4241
"NH4CLO4(I)",
4342
cea.Reactant(
4443
name="CHOS-Binder",
4544
formula={"C": 1.0, "H": 1.86955, "O": 0.031256, "S": 0.008415},
4645
molecular_weight=14.6652984484,
47-
enthalpy=chos_binder_h_si,
46+
enthalpy=chos_binder_h_cal_per_mol,
47+
enthalpy_units="cal/mol",
4848
temperature=298.15,
4949
),
5050
"AL(cr)",

0 commit comments

Comments
 (0)