Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

introducing class Quantity #290

Merged
merged 6 commits into from
Aug 24, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/sphinx/cython/thermo.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ Mixture

.. autoclass:: Mixture

Quantity
--------
.. autoclass:: Quantity

Species
-------

Expand Down
1 change: 1 addition & 0 deletions interfaces/cython/cantera/__init__.py
Original file line number Diff line number Diff line change
@@ -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 *
Expand Down
193 changes: 193 additions & 0 deletions interfaces/cython/cantera/composite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
from ._cantera import *

class Quantity(object):
"""
A class representing a specific quantity of a `Solution`. In addition to the
properties which can be computed for class `Solution`, class `Quantity`
provides several additional capabilities. A `Quantity` object is created
from a `Solution` with either the mass or number of moles specified::

>>> gas = ct.Solution('gri30.xml')
>>> gas.TPX = 300, 5e5, 'O2:1.0, N2:3.76'
>>> q1 = ct.Quantity(gas, mass=5) # 5 kg of air

The state of a `Quantity` can be changed in the same way as a `Solution`::

>>> q1.TP = 500, 101325

Quantities have properties which provide access to extensive properties::

>>> q1.volume
7.1105094
>>> q1.enthalpy
1032237.84

The size of a `Quantity` can be changed by setting the mass or number of
moles::

>>> q1.moles = 3
>>> q1.mass
86.552196
>>> q1.volume
123.086

or by multiplication::

>>> q1 *= 2
>>> q1.moles
6.0

Finally, Quantities can be added, providing an easy way of calculating the
state resulting from mixing two substances::

>>> q1.mass = 5
>>> q2 = ct.Quantity(gas)
>>> q2.TPX = 300, 101325, 'CH4:1.0'
>>> q2.mass = 1
>>> q3 = q1 + q2 # combine at constant UV
>>> q3.T
432.31234
>>> q3.P
97974.9871
>>> q3.mole_fraction_dict()
{'CH4': 0.26452900448117395,
'N2': 0.5809602821745349,
'O2': 0.1545107133442912}

If a different property pair should be held constant when combining, this
can be specified as follows::

>>> q1.constant = q2.constant = 'HP'
>>> q3 = q1 + q2 # combine at constant HP
>>> q3.T
436.03320
>>> q3.P
101325.0
"""
def __init__(self, phase, mass=None, moles=None, constant='UV'):
self.state = phase.TDY
self._phase = phase

# A unique key to prevent adding phases with different species
# definitions
self._id = hash((phase.name,) + tuple(phase.species_names))

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):
"""
Get the underlying `Solution` object, with the state set to match the
wrapping `Quantity` object.
"""
self._phase.TDY = self.state
return self._phase

@property
def moles(self):
""" Get/Set the number of moles [kmol] represented by the `Quantity`. """
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):
""" Get the total volume [m^3] represented by the `Quantity`. """
return self.mass * self.phase.volume_mass

@property
def int_energy(self):
""" Get the total internal energy [J] represented by the `Quantity`. """
return self.mass * self.phase.int_energy_mass

@property
def enthalpy(self):
""" Get the total enthalpy [J] represented by the `Quantity`. """
return self.mass * self.phase.enthalpy_mass

@property
def entropy(self):
""" Get the total entropy [J/K] represented by the `Quantity`. """
return self.mass * self.phase.entropy_mass

@property
def gibbs(self):
"""
Get the total Gibbs free energy [J] represented by the `Quantity`.
"""
return self.mass * self.phase.gibbs_mass

def equilibrate(self, XY=None, *args, **kwargs):
"""
Set the state to equilibrium. By default, the property pair
`self.constant` is held constant. See `ThermoPhase.equilibrate`.
"""
if XY is None:
XY = self.constant
self.phase.equilibrate(XY, *args, **kwargs)
self.state = self._phase.TDY

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):
if (self._id != other._id):
raise ValueError('Cannot add Quantities with different phase '
'definitions.')
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))
3 changes: 3 additions & 0 deletions interfaces/cython/cantera/examples/reactors/mix1.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
species that is not present in the downstream reaction mechanism, it will be
ignored. In general, reaction mechanisms for downstream reactors should
contain all species that might be present in any upstream reactor.

Compare this approach for the transient problem to the method used for the
steady-state problem in thermo/mixing.py.
"""

import cantera as ct
Expand Down
35 changes: 35 additions & 0 deletions interfaces/cython/cantera/examples/thermo/mixing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""
Mixing two streams using `Quantity` objects.

In this example, air and methane are mixed in stoichiometric proportions. This
is a simpler, steady-state version of the example ``reactors/mix1.py``.

Since the goal is to simulate a continuous flow system, the mixing takes place
at constant enthalpy and pressure.
"""

import cantera as ct

gas = ct.Solution('gri30.xml')

# Stream A (air)
A = ct.Quantity(gas, constant='HP')
A.TPX = 300.0, ct.one_atm, 'O2:0.21, N2:0.78, AR:0.01'

# Stream B (methane)
B = ct.Quantity(gas, constant='HP')
B.TPX = 300.0, ct.one_atm, 'CH4:1'

# Set the molar flow rates corresponding to stoichiometric reaction,
# CH4 + 2 O2 -> CO2 + 2 H2O
A.moles = 1
nO2 = A.X[A.species_index('O2')]
B.moles = nO2 * 0.5

# Compute the mixed state
M = A + B
print(M.report())

# Show that this state corresponds to stoichiometric combustion
M.equilibrate('TP')
print(M.report())
87 changes: 87 additions & 0 deletions interfaces/cython/cantera/test/test_thermo.py
Original file line number Diff line number Diff line change
Expand Up @@ -910,3 +910,90 @@ def test_coeffs(self):
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)

def test_equilibrate(self):
self.gas.TPX = 300, 101325, 'CH4:1.0, O2:0.2, N2:1.0'
q1 = ct.Quantity(self.gas)
self.gas.equilibrate('HP')
T2 = self.gas.T

self.assertNear(q1.T, 300)
q1.equilibrate('HP')
self.assertNear(q1.T, T2)

def test_incompatible(self):
gas2 = ct.Solution('h2o2.xml')
q1 = ct.Quantity(self.gas)
q2 = ct.Quantity(gas2)

with self.assertRaises(Exception):
q1+q2