diff --git a/interfaces/cython/cantera/__init__.py b/interfaces/cython/cantera/__init__.py index 43165e049..bb017a83f 100644 --- a/interfaces/cython/cantera/__init__.py +++ b/interfaces/cython/cantera/__init__.py @@ -1,5 +1,6 @@ from ._cantera import * from ._cantera import __version__, __sundials_version__ +from .composite import * from .liquidvapor import * from .onedim import * from .utils import * diff --git a/interfaces/cython/cantera/composite.py b/interfaces/cython/cantera/composite.py new file mode 100644 index 000000000..acb8de8e4 --- /dev/null +++ b/interfaces/cython/cantera/composite.py @@ -0,0 +1,104 @@ +from ._cantera import * + +class Quantity(object): + """ + A class representing a specific quantity of a `Solution`. + """ + def __init__(self, phase, mass=None, moles=None, constant='UV'): + self.state = phase.TDY + self._phase = phase + + if mass is not None: + self.mass = mass + elif moles is not None: + self.moles = moles + else: + self.mass = 1.0 + + assert constant in ('TP','TV','HP','SP','SV','UV') + self.constant = constant + + @property + def phase(self): + self._phase.TDY = self.state + return self._phase + + @property + def moles(self): + return self.mass / self.phase.mean_molecular_weight + + @moles.setter + def moles(self, n): + self.mass = n * self.phase.mean_molecular_weight + + @property + def volume(self): + return self.mass * self.phase.volume_mass + + @property + def int_energy(self): + return self.mass * self.phase.int_energy_mass + + @property + def enthalpy(self): + return self.mass * self.phase.enthalpy_mass + + @property + def entropy(self): + return self.mass * self.phase.entropy_mass + + @property + def gibbs(self): + return self.mass * self.phase.gibbs_mass + + def __imul__(self, other): + self.mass *= other + return self + + def __mul__(self, other): + return Quantity(self.phase, mass=self.mass * other) + + def __rmul__(self, other): + return Quantity(self.phase, mass=self.mass * other) + + def __iadd__(self, other): + assert(self.constant == other.constant) + a1,b1 = getattr(self.phase, self.constant) + a2,b2 = getattr(other.phase, self.constant) + m = self.mass + other.mass + a = (a1 * self.mass + a2 * other.mass) / m + b = (b1 * self.mass + b2 * other.mass) / m + self._phase.Y = (self.Y * self.mass + other.Y * other.mass) / m + setattr(self._phase, self.constant, (a,b)) + self.state = self._phase.TDY + self.mass = m + return self + + def __add__(self, other): + newquantity = Quantity(self.phase, mass=self.mass, constant=self.constant) + newquantity += other + return newquantity + +# Synonyms for total properties +Quantity.V = Quantity.volume +Quantity.U = Quantity.int_energy +Quantity.H = Quantity.enthalpy +Quantity.S = Quantity.entropy +Quantity.G = Quantity.gibbs + +# Add properties to act as pass-throughs for attributes of class Solution +def _prop(attr): + def getter(self): + return getattr(self.phase, attr) + + def setter(self, value): + setattr(self.phase, attr, value) + self.state = self._phase.TDY + + return property(getter, setter, doc=getattr(Solution, attr).__doc__) + +for _attr in dir(Solution): + if _attr.startswith('_') or _attr in Quantity.__dict__: + continue + else: + setattr(Quantity, _attr, _prop(_attr)) diff --git a/interfaces/cython/cantera/test/test_thermo.py b/interfaces/cython/cantera/test/test_thermo.py index e2a79c978..a8f1cb0aa 100644 --- a/interfaces/cython/cantera/test/test_thermo.py +++ b/interfaces/cython/cantera/test/test_thermo.py @@ -910,3 +910,72 @@ class TestSpeciesThermo(utilities.CanteraTest): self.assertEqual(st.max_temp, 3500) self.assertEqual(st.reference_pressure, 101325) self.assertArrayNear(self.h2o_coeffs, st.coeffs) + + +class TestQuantity(utilities.CanteraTest): + @classmethod + def setUpClass(cls): + cls.gas = ct.Solution('gri30.xml') + + def setUp(self): + self.gas.TPX = 300, 101325, 'O2:1.0, N2:3.76' + + def test_mass_moles(self): + q1 = ct.Quantity(self.gas, mass=5) + self.assertNear(q1.mass, 5) + self.assertNear(q1.moles, 5 / q1.mean_molecular_weight) + + q1.mass = 7 + self.assertNear(q1.moles, 7 / q1.mean_molecular_weight) + + q1.moles = 9 + self.assertNear(q1.moles, 9) + self.assertNear(q1.mass, 9 * q1.mean_molecular_weight) + + def test_extensive(self): + q1 = ct.Quantity(self.gas, mass=5) + self.assertNear(q1.mass, 5) + + self.assertNear(q1.volume * q1.density, q1.mass) + self.assertNear(q1.V * q1.density, q1.mass) + self.assertNear(q1.int_energy, q1.moles * q1.int_energy_mole) + self.assertNear(q1.enthalpy, q1.moles * q1.enthalpy_mole) + self.assertNear(q1.entropy, q1.moles * q1.entropy_mole) + self.assertNear(q1.gibbs, q1.moles * q1.gibbs_mole) + self.assertNear(q1.int_energy, q1.U) + self.assertNear(q1.enthalpy, q1.H) + self.assertNear(q1.entropy, q1.S) + self.assertNear(q1.gibbs, q1.G) + + def test_multiply(self): + q1 = ct.Quantity(self.gas, mass=5) + q2 = q1 * 2.5 + self.assertNear(q1.mass * 2.5, q2.mass) + self.assertNear(q1.moles * 2.5, q2.moles) + self.assertNear(q1.entropy * 2.5, q2.entropy) + self.assertArrayNear(q1.X, q2.X) + + def test_iadd(self): + q0 = ct.Quantity(self.gas, mass=5) + q1 = ct.Quantity(self.gas, mass=5) + q2 = ct.Quantity(self.gas, mass=5) + q2.TPX = 500, 101325, 'CH4:1.0' + + q1 += q2 + self.assertNear(q0.mass + q2.mass, q1.mass) + # addition is at constant UV + self.assertNear(q0.U + q2.U, q1.U) + self.assertNear(q0.V + q2.V, q1.V) + self.assertArrayNear(q0.X*q0.moles + q2.X*q2.moles, q1.X*q1.moles) + + def test_add(self): + q1 = ct.Quantity(self.gas, mass=5) + q2 = ct.Quantity(self.gas, mass=5) + q2.TPX = 500, 101325, 'CH4:1.0' + + q3 = q1 + q2 + self.assertNear(q1.mass + q2.mass, q3.mass) + # addition is at constant UV + self.assertNear(q1.U + q2.U, q3.U) + self.assertNear(q1.V + q2.V, q3.V) + self.assertArrayNear(q1.X*q1.moles + q2.X*q2.moles, q3.X*q3.moles)