diff --git a/doc/sphinx/cython/tutorial.rst b/doc/sphinx/cython/tutorial.rst index fe0117443..75257dd60 100644 --- a/doc/sphinx/cython/tutorial.rst +++ b/doc/sphinx/cython/tutorial.rst @@ -276,6 +276,16 @@ Or, to set all the mass fractions to equal values:: >>> gas1.Y = np.ones(53) +When setting the state, you can control what properties are held constant by +passing the special value `None` to the property setter. For example, to +change the specific volume to 2.1 m^3/kg while holding entropy constant:: + + >>> gas.SV = None, 2.1 + +Or to set the mass fractions while holding temperature and pressure constant:: + + >>> gas.TPX = None, None, 'CH4:1.0, O2:0.5' + Working With Mechanism Files ---------------------------- diff --git a/interfaces/cython/cantera/test/test_thermo.py b/interfaces/cython/cantera/test/test_thermo.py index f5205ca15..64df0e03f 100644 --- a/interfaces/cython/cantera/test/test_thermo.py +++ b/interfaces/cython/cantera/test/test_thermo.py @@ -168,6 +168,40 @@ class TestThermoPhase(utilities.CanteraTest): self.check_setters(T1 = 750.0, rho1 = 0.02, Y1 = [0.2, 0.1, 0.0, 0.3, 0.1, 0.0, 0.0, 0.2, 0.1]) + def test_setters_hold_constant(self): + props = ('T','P','s','h','u','v','X','Y') + pairs = [('TP', 'T', 'P'), ('SP', 's', 'P'), + ('UV', 'u', 'v')] + + self.phase.TDX = 1000, 1.5, 'H2O:0.1, O2:0.95, AR:3.0' + values = {} + for p in props: + values[p] = getattr(self.phase, p) + + for pair, first, second in pairs: + self.phase.TDX = 500, 2.5, 'H2:0.1, O2:1.0, AR:3.0' + first_val = getattr(self.phase, first) + second_val = getattr(self.phase, second) + + setattr(self.phase, pair, (values[first], None)) + self.assertNear(getattr(self.phase, first), values[first]) + self.assertNear(getattr(self.phase, second), second_val) + + self.phase.TDX = 500, 2.5, 'H2:0.1, O2:1.0, AR:3.0' + setattr(self.phase, pair, (None, values[second])) + self.assertNear(getattr(self.phase, first), first_val) + self.assertNear(getattr(self.phase, second), values[second]) + + self.phase.TDX = 500, 2.5, 'H2:0.1, O2:1.0, AR:3.0' + setattr(self.phase, pair + 'X', (None, None, values['X'])) + self.assertNear(getattr(self.phase, first), first_val) + self.assertNear(getattr(self.phase, second), second_val) + + self.phase.TDX = 500, 2.5, 'H2:0.1, O2:1.0, AR:3.0' + setattr(self.phase, pair + 'Y', (None, None, values['Y'])) + self.assertNear(getattr(self.phase, first), first_val) + self.assertNear(getattr(self.phase, second), second_val) + def test_setter_errors(self): with self.assertRaises(Exception): self.phase.TD = 400 diff --git a/interfaces/cython/cantera/thermo.pyx b/interfaces/cython/cantera/thermo.pyx index df889bb1d..39e0ffcce 100644 --- a/interfaces/cython/cantera/thermo.pyx +++ b/interfaces/cython/cantera/thermo.pyx @@ -440,7 +440,9 @@ cdef class ThermoPhase(_SolutionBase): return self.T, self.density def __set__(self, values): assert len(values) == 2 - self.thermo.setState_TR(values[0], values[1] * self._mass_factor()) + T = values[0] if values[0] is not None else self.T + D = values[1] if values[1] is not None else self.density + self.thermo.setState_TR(T, D * self._mass_factor()) property TDX: """ @@ -451,8 +453,10 @@ cdef class ThermoPhase(_SolutionBase): return self.T, self.density, self.X def __set__(self, values): assert len(values) == 3 + T = values[0] if values[0] is not None else self.T + D = values[1] if values[1] is not None else self.density self.X = values[2] - self.TD = values[:2] + self.thermo.setState_TR(T, D * self._mass_factor()) property TDY: """ @@ -463,8 +467,10 @@ cdef class ThermoPhase(_SolutionBase): return self.T, self.density, self.Y def __set__(self, values): assert len(values) == 3 + T = values[0] if values[0] is not None else self.T + D = values[1] if values[1] is not None else self.density self.Y = values[2] - self.TD = values[:2] + self.thermo.setState_TR(T, D * self._mass_factor()) property TP: """Get/Set temperature [K] and pressure [Pa].""" @@ -472,7 +478,9 @@ cdef class ThermoPhase(_SolutionBase): return self.T, self.P def __set__(self, values): assert len(values) == 2 - self.thermo.setState_TP(values[0], values[1]) + T = values[0] if values[0] is not None else self.T + P = values[1] if values[1] is not None else self.P + self.thermo.setState_TP(T, P) property TPX: """Get/Set temperature [K], pressure [Pa], and mole fractions.""" @@ -480,8 +488,10 @@ cdef class ThermoPhase(_SolutionBase): return self.T, self.P, self.X def __set__(self, values): assert len(values) == 3 + T = values[0] if values[0] is not None else self.T + P = values[1] if values[1] is not None else self.P self.X = values[2] - self.TP = values[:2] + self.thermo.setState_TP(T, P) property TPY: """Get/Set temperature [K], pressure [Pa], and mass fractions.""" @@ -489,8 +499,10 @@ cdef class ThermoPhase(_SolutionBase): return self.T, self.P, self.Y def __set__(self, values): assert len(values) == 3 + T = values[0] if values[0] is not None else self.T + P = values[1] if values[1] is not None else self.P self.Y = values[2] - self.TP = values[:2] + self.thermo.setState_TP(T, P) property UV: """ @@ -501,8 +513,10 @@ cdef class ThermoPhase(_SolutionBase): return self.u, self.v def __set__(self, values): assert len(values) == 2 - self.thermo.setState_UV(values[0] / self._mass_factor(), - values[1] / self._mass_factor()) + U = values[0] if values[0] is not None else self.u + V = values[1] if values[1] is not None else self.v + self.thermo.setState_UV(U / self._mass_factor(), + V / self._mass_factor()) property UVX: """ @@ -513,8 +527,11 @@ cdef class ThermoPhase(_SolutionBase): return self.u, self.v, self.X def __set__(self, values): assert len(values) == 3 + U = values[0] if values[0] is not None else self.u + V = values[1] if values[1] is not None else self.v self.X = values[2] - self.UV = values[:2] + self.thermo.setState_UV(U / self._mass_factor(), + V / self._mass_factor()) property UVY: """ @@ -525,8 +542,11 @@ cdef class ThermoPhase(_SolutionBase): return self.u, self.v, self.Y def __set__(self, values): assert len(values) == 3 + U = values[0] if values[0] is not None else self.u + V = values[1] if values[1] is not None else self.v self.Y = values[2] - self.UV = values[:2] + self.thermo.setState_UV(U / self._mass_factor(), + V / self._mass_factor()) property HP: """Get/Set enthalpy [J/kg or J/kmol] and pressure [Pa].""" @@ -534,7 +554,9 @@ cdef class ThermoPhase(_SolutionBase): return self.h, self.P def __set__(self, values): assert len(values) == 2 - self.thermo.setState_HP(values[0] / self._mass_factor(), values[1]) + H = values[0] if values[0] is not None else self.h + P = values[1] if values[1] is not None else self.P + self.thermo.setState_HP(H / self._mass_factor(), P) property HPX: """Get/Set enthalpy [J/kg or J/kmol], pressure [Pa] and mole fractions.""" @@ -542,8 +564,10 @@ cdef class ThermoPhase(_SolutionBase): return self.h, self.P, self.X def __set__(self, values): assert len(values) == 3 + H = values[0] if values[0] is not None else self.h + P = values[1] if values[1] is not None else self.P self.X = values[2] - self.HP = values[:2] + self.thermo.setState_HP(H / self._mass_factor(), P) property HPY: """Get/Set enthalpy [J/kg or J/kmol], pressure [Pa] and mass fractions.""" @@ -551,8 +575,10 @@ cdef class ThermoPhase(_SolutionBase): return self.h, self.P, self.Y def __set__(self, values): assert len(values) == 3 + H = values[0] if values[0] is not None else self.h + P = values[1] if values[1] is not None else self.P self.Y = values[2] - self.HP = values[:2] + self.thermo.setState_HP(H / self._mass_factor(), P) property SP: """Get/Set entropy [J/kg/K or J/kmol/K] and pressure [Pa].""" @@ -560,7 +586,9 @@ cdef class ThermoPhase(_SolutionBase): return self.s, self.P def __set__(self, values): assert len(values) == 2 - self.thermo.setState_SP(values[0] / self._mass_factor(), values[1]) + S = values[0] if values[0] is not None else self.s + P = values[1] if values[1] is not None else self.P + self.thermo.setState_SP(S / self._mass_factor(), P) property SPX: """Get/Set entropy [J/kg/K or J/kmol/K], pressure [Pa], and mole fractions.""" @@ -568,8 +596,10 @@ cdef class ThermoPhase(_SolutionBase): return self.s, self.P, self.X def __set__(self, values): assert len(values) == 3 + S = values[0] if values[0] is not None else self.s + P = values[1] if values[1] is not None else self.P self.X = values[2] - self.SP = values[:2] + self.thermo.setState_SP(S / self._mass_factor(), P) property SPY: """Get/Set entropy [J/kg/K or J/kmol/K], pressure [Pa], and mass fractions.""" @@ -577,8 +607,10 @@ cdef class ThermoPhase(_SolutionBase): return self.s, self.P, self.Y def __set__(self, values): assert len(values) == 3 + S = values[0] if values[0] is not None else self.s + P = values[1] if values[1] is not None else self.P self.Y = values[2] - self.SP = values[:2] + self.thermo.setState_SP(S / self._mass_factor(), P) property SV: """ @@ -589,8 +621,10 @@ cdef class ThermoPhase(_SolutionBase): return self.s, self.v def __set__(self, values): assert len(values) == 2 - self.thermo.setState_SV(values[0] / self._mass_factor(), - values[1] / self._mass_factor()) + S = values[0] if values[0] is not None else self.s + V = values[1] if values[1] is not None else self.v + self.thermo.setState_SV(S / self._mass_factor(), + V / self._mass_factor()) property SVX: """ @@ -601,8 +635,11 @@ cdef class ThermoPhase(_SolutionBase): return self.s, self.v, self.X def __set__(self, values): assert len(values) == 3 + S = values[0] if values[0] is not None else self.s + V = values[1] if values[1] is not None else self.v self.X = values[2] - self.SV = values[:2] + self.thermo.setState_SV(S / self._mass_factor(), + V / self._mass_factor()) property SVY: """ @@ -613,8 +650,11 @@ cdef class ThermoPhase(_SolutionBase): return self.s, self.v, self.Y def __set__(self, values): assert len(values) == 3 + S = values[0] if values[0] is not None else self.s + V = values[1] if values[1] is not None else self.v self.Y = values[2] - self.SV = values[:2] + self.thermo.setState_SV(S / self._mass_factor(), + V / self._mass_factor()) # partial molar / non-dimensional properties property partial_molar_enthalpies: