From 23733b5384904d4419ea465be63e8598c88db73a Mon Sep 17 00:00:00 2001 From: Devin Kees <6895754+djkees@users.noreply.github.com> Date: Tue, 15 Sep 2026 11:54:37 -0600 Subject: [PATCH 1/4] Fix Mixture.calc_property inconsistent errors for invalid prop_type (#290) prop_type was typed as the Cython enum cea_property_type, so negative values raised OverflowError at the function boundary before the method's own ValueError allowlist check could run. Typed as plain int instead, with explicit casts at the four downstream C-call sites. Co-authored-by: Claude Sonnet 5 --- source/bind/python/CEA.pyx | 10 +++++----- source/bind/python/tests/test_mixture.py | 5 +++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/source/bind/python/CEA.pyx b/source/bind/python/CEA.pyx index dc52f0c..c0392b8 100644 --- a/source/bind/python/CEA.pyx +++ b/source/bind/python/CEA.pyx @@ -1224,7 +1224,7 @@ cdef class Mixture: return weights - def calc_property(self, cea_property_type prop_type, np.ndarray weights, + def calc_property(self, int prop_type, np.ndarray weights, temperature: float | list | np.ndarray, pressure: Optional[float] = None): """ Calculate thermodynamic property for mixture at specified conditions. @@ -1276,20 +1276,20 @@ cdef class Mixture: try: if prop_type in [ENTROPY, GIBBS_ENERGY, VOLUME, DENSITY]: if isinstance(temperature, float): - ierr = cea_mixture_calc_property_tp(self.ptr, prop_type, nspecies, reac_weights, temperature, pressure, &value) + ierr = cea_mixture_calc_property_tp(self.ptr, prop_type, nspecies, reac_weights, temperature, pressure, &value) elif type(temperature) in [list, np.ndarray]: for i in range(nspecies): reac_temps[i] = temperature[i] - ierr = cea_mixture_calc_property_tp_multitemp(self.ptr, prop_type, nspecies, reac_weights, nspecies, reac_temps, pressure, &value) + ierr = cea_mixture_calc_property_tp_multitemp(self.ptr, prop_type, nspecies, reac_weights, nspecies, reac_temps, pressure, &value) else: raise ValueError("Mixiture.calc_property: temperature must be a float, list, or np.ndarray") else: if isinstance(temperature, float): - ierr = cea_mixture_calc_property(self.ptr, prop_type, nspecies, reac_weights, temperature, &value) + ierr = cea_mixture_calc_property(self.ptr, prop_type, nspecies, reac_weights, temperature, &value) elif type(temperature) in [list, np.ndarray]: for i in range(nspecies): reac_temps[i] = temperature[i] - ierr = cea_mixture_calc_property_multitemp(self.ptr, prop_type, nspecies, reac_weights, nspecies, reac_temps, &value) + ierr = cea_mixture_calc_property_multitemp(self.ptr, prop_type, nspecies, reac_weights, nspecies, reac_temps, &value) else: raise ValueError("Mixiture.calc_property: temperature must be a float, list, or np.ndarray") _check_ierr(ierr, "Mixture.calc_property") diff --git a/source/bind/python/tests/test_mixture.py b/source/bind/python/tests/test_mixture.py index 7f448d3..a9c64a9 100644 --- a/source/bind/python/tests/test_mixture.py +++ b/source/bind/python/tests/test_mixture.py @@ -27,11 +27,12 @@ def test_calc_property_requires_pressure(cea_module): mix.calc_property(cea_module.ENTROPY, weights, temperature=3000.0) -def test_calc_property_rejects_unknown_type(cea_module): +@pytest.mark.parametrize("prop_type", [999999, -5]) +def test_calc_property_rejects_unknown_type(cea_module, prop_type): mix = cea_module.Mixture(["H2", "O2"]) weights = np.array([0.4, 0.6], dtype=np.float64) with pytest.raises(ValueError): - mix.calc_property(999999, weights, temperature=3000.0) + mix.calc_property(prop_type, weights, temperature=3000.0) @pytest.mark.parametrize( From 2578b9a01e1702d77768451fed2ff2e0be29886f Mon Sep 17 00:00:00 2001 From: Devin Kees <6895754+djkees@users.noreply.github.com> Date: Tue, 15 Sep 2026 12:35:23 -0600 Subject: [PATCH 2/4] Accept numpy/int scalar temperatures in Mixture.calc_property (#294) Widens the temperature-type dispatch to accept np.floating scalars and plain Python int, not just Python float, so values like arr[0] from a float32/float64 array no longer raise a spurious ValueError. Co-authored-by: Claude Sonnet 5 --- source/bind/python/CEA.pyx | 4 +- source/bind/python/tests/test_mixture.py | 50 ++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/source/bind/python/CEA.pyx b/source/bind/python/CEA.pyx index c0392b8..b078b13 100644 --- a/source/bind/python/CEA.pyx +++ b/source/bind/python/CEA.pyx @@ -1275,7 +1275,7 @@ cdef class Mixture: try: if prop_type in [ENTROPY, GIBBS_ENERGY, VOLUME, DENSITY]: - if isinstance(temperature, float): + if isinstance(temperature, (float, int, np.floating)): ierr = cea_mixture_calc_property_tp(self.ptr, prop_type, nspecies, reac_weights, temperature, pressure, &value) elif type(temperature) in [list, np.ndarray]: for i in range(nspecies): @@ -1284,7 +1284,7 @@ cdef class Mixture: else: raise ValueError("Mixiture.calc_property: temperature must be a float, list, or np.ndarray") else: - if isinstance(temperature, float): + if isinstance(temperature, (float, int, np.floating)): ierr = cea_mixture_calc_property(self.ptr, prop_type, nspecies, reac_weights, temperature, &value) elif type(temperature) in [list, np.ndarray]: for i in range(nspecies): diff --git a/source/bind/python/tests/test_mixture.py b/source/bind/python/tests/test_mixture.py index a9c64a9..c2d66ff 100644 --- a/source/bind/python/tests/test_mixture.py +++ b/source/bind/python/tests/test_mixture.py @@ -77,3 +77,53 @@ def test_multitemperature_density_matches_single_temperature(cea_module): ) assert density_multi == pytest.approx(density_single) + + +@pytest.mark.parametrize("temperature_dtype", [np.float32, np.float64]) +def test_calc_property_accepts_numpy_scalar_temperature(cea_module, temperature_dtype): + mix = cea_module.Mixture(["O2(L)"]) + weights = np.array([1.0]) + + density_python_float = mix.calc_property( + cea_module.DENSITY, weights, temperature=90.0, pressure=68.0 + ) + numpy_scalar_temperature = np.array([90.0], dtype=temperature_dtype)[0] + density_numpy_scalar = mix.calc_property( + cea_module.DENSITY, weights, temperature=numpy_scalar_temperature, pressure=68.0 + ) + + assert density_numpy_scalar == pytest.approx(density_python_float) + + +def test_calc_property_rejects_invalid_temperature_type(cea_module): + mix = cea_module.Mixture(["O2(L)"]) + weights = np.array([1.0]) + + with pytest.raises(ValueError): + mix.calc_property( + cea_module.DENSITY, weights, temperature="not a number", pressure=68.0 + ) + + +def test_calc_property_accepts_plain_int_temperature(cea_module): + mix = cea_module.Mixture(["O2(L)"]) + weights = np.array([1.0]) + + density_python_float = mix.calc_property( + cea_module.DENSITY, weights, temperature=90.0, pressure=68.0 + ) + density_python_int = mix.calc_property( + cea_module.DENSITY, weights, temperature=90, pressure=68.0 + ) + + assert density_python_int == pytest.approx(density_python_float) + + +def test_calc_property_rejects_numpy_integer_temperature(cea_module): + mix = cea_module.Mixture(["O2(L)"]) + weights = np.array([1.0]) + + with pytest.raises(ValueError): + mix.calc_property( + cea_module.DENSITY, weights, temperature=np.int64(90), pressure=68.0 + ) From 5b2ab15815e74899f35e2fc7d7af78e028bf1610 Mon Sep 17 00:00:00 2001 From: Devin Kees <6895754+djkees@users.noreply.github.com> Date: Thu, 17 Sep 2026 13:46:22 -0600 Subject: [PATCH 3/4] Validate multi-temperature length in Mixture.calc_property (#295) Raises a clear ValueError on a temperature array/list length mismatch instead of a raw IndexError (too few) or silent truncation (too many). Co-authored-by: Claude Sonnet 5 --- source/bind/python/CEA.pyx | 10 +++ source/bind/python/tests/test_mixture.py | 83 ++++++++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/source/bind/python/CEA.pyx b/source/bind/python/CEA.pyx index b078b13..bf09d40 100644 --- a/source/bind/python/CEA.pyx +++ b/source/bind/python/CEA.pyx @@ -1278,6 +1278,11 @@ cdef class Mixture: if isinstance(temperature, (float, int, np.floating)): ierr = cea_mixture_calc_property_tp(self.ptr, prop_type, nspecies, reac_weights, temperature, pressure, &value) elif type(temperature) in [list, np.ndarray]: + if len(temperature) != nspecies: + raise ValueError( + f"Mixture.calc_property: temperature has {len(temperature)} elements, " + f"expected {nspecies} (one per species in weights)" + ) for i in range(nspecies): reac_temps[i] = temperature[i] ierr = cea_mixture_calc_property_tp_multitemp(self.ptr, prop_type, nspecies, reac_weights, nspecies, reac_temps, pressure, &value) @@ -1287,6 +1292,11 @@ cdef class Mixture: if isinstance(temperature, (float, int, np.floating)): ierr = cea_mixture_calc_property(self.ptr, prop_type, nspecies, reac_weights, temperature, &value) elif type(temperature) in [list, np.ndarray]: + if len(temperature) != nspecies: + raise ValueError( + f"Mixture.calc_property: temperature has {len(temperature)} elements, " + f"expected {nspecies} (one per species in weights)" + ) for i in range(nspecies): reac_temps[i] = temperature[i] ierr = cea_mixture_calc_property_multitemp(self.ptr, prop_type, nspecies, reac_weights, nspecies, reac_temps, &value) diff --git a/source/bind/python/tests/test_mixture.py b/source/bind/python/tests/test_mixture.py index c2d66ff..ffdaeac 100644 --- a/source/bind/python/tests/test_mixture.py +++ b/source/bind/python/tests/test_mixture.py @@ -79,6 +79,21 @@ def test_multitemperature_density_matches_single_temperature(cea_module): assert density_multi == pytest.approx(density_single) +def test_multitemperature_density_matches_single_temperature_list(cea_module): + """Same as above, but passing temperature as a plain Python list instead of an ndarray.""" + mix = cea_module.Mixture(["O2(L)"]) + weights = np.array([1.0]) + + density_single = mix.calc_property( + cea_module.DENSITY, weights, temperature=90.0, pressure=68.0 + ) + density_multi = mix.calc_property( + cea_module.DENSITY, weights, temperature=[90.0], pressure=68.0 + ) + + assert density_multi == pytest.approx(density_single) + + @pytest.mark.parametrize("temperature_dtype", [np.float32, np.float64]) def test_calc_property_accepts_numpy_scalar_temperature(cea_module, temperature_dtype): mix = cea_module.Mixture(["O2(L)"]) @@ -127,3 +142,71 @@ def test_calc_property_rejects_numpy_integer_temperature(cea_module): mix.calc_property( cea_module.DENSITY, weights, temperature=np.int64(90), pressure=68.0 ) + + +@pytest.mark.parametrize( + "temperatures", + [np.array([90.0]), [90.0]], + ids=["ndarray", "list"], +) +def test_calc_property_rejects_too_few_multitemperatures_pressure_dependent( + cea_module, temperatures +): + """2 species but only 1 temperature given (DENSITY, which needs pressure) -> should error, not crash or guess.""" + mix = cea_module.Mixture(["O2(L)", "CH4(L)"]) + weights = np.array([0.5, 0.5]) + + with pytest.raises(ValueError): + mix.calc_property( + cea_module.DENSITY, weights, temperature=temperatures, pressure=68.0 + ) + + +@pytest.mark.parametrize( + "temperatures", + [np.array([90.0, 200.0, 300.0]), [90.0, 200.0, 300.0]], + ids=["ndarray", "list"], +) +def test_calc_property_rejects_too_many_multitemperatures_pressure_dependent( + cea_module, temperatures +): + """1 species but 3 temperatures given (DENSITY, which needs pressure) -> should error, not silently ignore the extras.""" + mix = cea_module.Mixture(["O2(L)"]) + weights = np.array([1.0]) + + with pytest.raises(ValueError): + mix.calc_property( + cea_module.DENSITY, weights, temperature=temperatures, pressure=68.0 + ) + + +@pytest.mark.parametrize( + "temperatures", + [np.array([90.0]), [90.0]], + ids=["ndarray", "list"], +) +def test_calc_property_rejects_too_few_multitemperatures_pressure_independent( + cea_module, temperatures +): + """Same too-few-temperatures check, but for FROZEN_CP (a property type that does NOT need pressure) -> should also error.""" + mix = cea_module.Mixture(["O2(L)", "CH4(L)"]) + weights = np.array([0.5, 0.5]) + + with pytest.raises(ValueError): + mix.calc_property(cea_module.FROZEN_CP, weights, temperature=temperatures) + + +@pytest.mark.parametrize( + "temperatures", + [np.array([90.0, 200.0, 300.0]), [90.0, 200.0, 300.0]], + ids=["ndarray", "list"], +) +def test_calc_property_rejects_too_many_multitemperatures_pressure_independent( + cea_module, temperatures +): + """Same too-many-temperatures check, but for FROZEN_CP (a property type that does NOT need pressure) -> should also error.""" + mix = cea_module.Mixture(["O2(L)"]) + weights = np.array([1.0]) + + with pytest.raises(ValueError): + mix.calc_property(cea_module.FROZEN_CP, weights, temperature=temperatures) From 078e3ba69d23f07cb4d99958defe4a8a6efd1d06 Mon Sep 17 00:00:00 2001 From: Devin Kees <6895754+djkees@users.noreply.github.com> Date: Fri, 18 Sep 2026 13:25:26 -0600 Subject: [PATCH 4/4] Fix reac_weights leak on early validation exit in calc_property Move the prop_type and pressure checks before the malloc so neither early-exit path can leak the buffer. Co-Authored-By: Claude Sonnet 5 --- source/bind/python/CEA.pyx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/source/bind/python/CEA.pyx b/source/bind/python/CEA.pyx index bf09d40..72f429d 100644 --- a/source/bind/python/CEA.pyx +++ b/source/bind/python/CEA.pyx @@ -1253,12 +1253,6 @@ cdef class Mixture: cdef cea_err ierr cdef cea_real value cdef int nspecies = len(weights) - cdef cea_real *reac_weights = malloc(nspecies * sizeof(cea_real)) - if reac_weights == NULL: - raise MemoryError("Failed to allocate weights buffer") - - for i in range(nspecies): - reac_weights[i] = weights[i] if prop_type not in [VOLUME, DENSITY, ENTHALPY, ENERGY, FROZEN_CP, FROZEN_CV, ENTROPY, GIBBS_ENERGY]: raise ValueError("Property type not supported for mixture calculations") @@ -1267,6 +1261,13 @@ cdef class Mixture: if pressure is None: raise ValueError("Pressure must be provided for enthalpy and Gibbs energy calculations") + cdef cea_real *reac_weights = malloc(nspecies * sizeof(cea_real)) + if reac_weights == NULL: + raise MemoryError("Failed to allocate weights buffer") + + for i in range(nspecies): + reac_weights[i] = weights[i] + # Handle the case where temperature is a list of numpy array cdef cea_real *reac_temps = malloc(nspecies * sizeof(cea_real)) if reac_temps == NULL: