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

Differentiate quality from mole fraction in Python #719

Merged
merged 5 commits into from
Dec 27, 2019
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
6 changes: 6 additions & 0 deletions include/cantera/thermo/Phase.h
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,11 @@ class Phase
return false;
}

//! Return whether phase represents a substance with phase transitions
virtual bool hasPhaseTransition() const {
return false;
}

//! Return whether phase represents a compressible substance
virtual bool isCompressible() const {
return true;
Expand Down Expand Up @@ -325,6 +330,7 @@ class Phase
//! "V": specific volume
//! "H": specific enthalpy
//! "S": specific entropy
//! "Q": vapor fraction
virtual std::vector<std::string> fullStates() const;

//! Return a vector of settable partial property sets within a phase.
Expand Down
4 changes: 4 additions & 0 deletions include/cantera/thermo/PureFluidPhase.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ class PureFluidPhase : public ThermoPhase
return true;
}

virtual bool hasPhaseTransition() const {
return true;
}

virtual std::vector<std::string> fullStates() const;
virtual std::vector<std::string> partialStates() const;

Expand Down
1 change: 1 addition & 0 deletions interfaces/cython/cantera/_cantera.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ cdef extern from "cantera/thermo/ThermoPhase.h" namespace "Cantera":
# miscellaneous
string type()
string report(cbool, double) except +translate_exception
cbool hasPhaseTransition()
cbool isPure()
cbool isCompressible()
stdmap[string, size_t] nativeState() except +translate_exception
Expand Down
97 changes: 45 additions & 52 deletions interfaces/cython/cantera/composite.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,20 +481,12 @@ class SolutionArray:
'transport_model',
]

_all_states = [
# all setter/getter combos defined by the ThermoPhase base class
'TD', 'TDX', 'TDY', 'TP', 'TPX', 'TPY', 'UV', 'UVX', 'UVY',
'DP', 'DPX', 'DPY', 'HP', 'HPX', 'HPY', 'SP', 'SPX', 'SPY',
'SV', 'SVX', 'SVY'
]

_interface_passthrough = ['site_density']
_interface_n_species = ['coverages']

_purefluid_scalar = ['Q']

def __init__(self, phase, shape=(0,), states=None, extra=None):
super().__setattr__('_setters', {})
super().__setattr__('_getters', {})
super().__setattr__('_extra', {})
self._phase = phase

if isinstance(shape, int):
Expand All @@ -519,6 +511,7 @@ def __init__(self, phase, shape=(0,), states=None, extra=None):
self._indices = list(np.ndindex(self._shape))
self._output_dummy = self._states[..., 0]

self._extra = {}
if isinstance(extra, dict):
for name, v in extra.items():
if not np.shape(v):
Expand All @@ -538,41 +531,18 @@ def __init__(self, phase, shape=(0,), states=None, extra=None):
" supplied in a dict if the SolutionArray is not initially"
" empty")

# add properties dynamically for states not defined by ThermoPhase
state_sets = set(phase._full_states.values()) | set(phase._partial_states.values())
state_sets = state_sets - set(self._all_states)
for name in state_sets:
ph = type(phase)
if len(name) == 2:
getter, setter = _state2_prop(name, ph)
elif len(name) == 3:
getter, setter = _state3_prop(name, ph)
else:
raise NotImplementedError("Failed adding property '{}' for "
"phase '{}'".format(name, phase))
self._getters[name] = getter
self._setters[name] = setter

def __getitem__(self, index):
states = self._states[index]
shape = states.shape[:-1]
return SolutionArray(self._phase, shape, states)

def __getattr__(self, name):
if name in self._getters:
return self._getters[name](self)
elif name in self._extra:
if name in self._extra:
return np.array(self._extra[name])
else:
raise AttributeError("'{}' object has no attribute '{}'".format(
self.__class__.__name__, name))

def __setattr__(self, name, value):
if name in self._setters:
self._setters[name](self, value)
else:
super().__setattr__(name, value)

def __call__(self, *species):
return SolutionArray(self._phase[species], states=self._states,
extra=self._extra)
Expand Down Expand Up @@ -686,18 +656,15 @@ def restore_data(self, data, labels):
states = list(self._phase._full_states.values())

# add partial and/or potentially non-unique state definitions
if isinstance(self._phase, PureFluid):
states += ['TPX']
states += list(self._phase._partial_states.values())

# determine whether complete concentration is available (mass or mole)
# assumes that `X` or `Y` is always in last place
# assumes that 'X' or 'Y' is always in last place
mode = ''
valid_species = {}
for prefix in ['X_', 'Y_']:
if not any([prefix[0] in s for s in states]):
continue

spc = ['{}{}'.format(prefix, s) for s in self.species_names]
# solution species names also found in labels
valid_species = {s[2:]: labels.index(s) for s in spc
Expand All @@ -710,16 +677,16 @@ def restore_data(self, data, labels):
states = [v[:-1] for v in states if mode in v]
break
if mode == '':
# concentration specifier ('X' or 'Y') is not used
states = [s for s in states if 'X' not in s and 'Y' not in s]
# concentration/quality specifier ('X' or 'Y') is not used
states = [st.rstrip('XY') for st in states]
elif len(valid_species) != len(all_species):
incompatible = list(set(valid_species) ^ set(all_species))
raise ValueError('incompatible species information for '
'{}'.format(incompatible))

# determine suitable thermo properties for reconstruction
basis = 'mass' if self.basis == 'mass' else 'mole'
prop = {'T': ('T'), 'P': ('P'),
prop = {'T': ('T'), 'P': ('P'), 'Q': ('Q'),
'D': ('density', 'density_{}'.format(basis)),
'U': ('u', 'int_energy_{}'.format(basis)),
'V': ('v', 'volume_{}'.format(basis)),
Expand Down Expand Up @@ -843,6 +810,8 @@ def collect_data(self, cols=None, threshold=0, species='Y'):
elif c in species_names:
single_species = True
collabels = ['{}_{}'.format(species, c)]
elif c in self._purefluid_scalar:
collabels = [c]
else:
raise CanteraError('property "{}" not supported'.format(c))

Expand Down Expand Up @@ -1002,13 +971,16 @@ def setter(self, AB):
return getter, setter


def _state3_prop(name, doc_source):
def _state3_prop(name, doc_source, scalar=False):
# Factory for creating properties which consist of a tuple of three
# variables, e.g. 'TPY' or 'UVX'
def getter(self):
a = np.empty(self._shape)
b = np.empty(self._shape)
c = np.empty(self._shape + (self._phase.n_selected_species,))
if scalar:
c = np.empty(self._shape)
else:
c = np.empty(self._shape + (self._phase.n_selected_species,))
for index in self._indices:
self._phase.state = self._states[index]
a[index], b[index], c[index] = getattr(self._phase, name)
Expand Down Expand Up @@ -1038,15 +1010,33 @@ def setter(self, ABC):
def _make_functions():
# this is wrapped in a function to avoid polluting the module namespace

# state setters
for name in SolutionArray._all_states:
if len(name) == 2:
getter, setter = _state2_prop(name, Solution)
elif len(name) == 3:
getter, setter = _state3_prop(name, Solution)
doc = getattr(Solution, name).__doc__
prop = property(getter, setter, doc=doc)
setattr(SolutionArray, name, prop)
names = []
for ph, ext in [(ThermoPhase, 'XY'), (PureFluid, 'Q')]:

# all state setters/getters are combination of letters
setters = 'TDPUVHS' + ext
scalar = ext == 'Q'

# add deprecated setters for PureFluid (e.g. PX/TX)
# @todo: remove .. deprecated:: 2.5
setters = setters.replace('Q', 'QX')

# obtain setters/getters from thermo objects
all_states = [k for k in ph.__dict__
if not set(k) - set(setters) and len(k)>1]

# state setters (copy from ThermoPhase objects)
for name in all_states:
if name in names:
continue
names.append(name)
if len(name) == 2:
getter, setter = _state2_prop(name, ph)
elif len(name) == 3:
getter, setter = _state3_prop(name, ph, scalar)
doc = getattr(ph, name).__doc__
prop = property(getter, setter, doc=doc)
setattr(SolutionArray, name, prop)

# Functions which define empty output arrays of an appropriate size for
# different properties
Expand Down Expand Up @@ -1085,6 +1075,9 @@ def getter(self):
for name in SolutionArray._interface_n_species:
setattr(SolutionArray, name, make_prop(name, empty_species, Interface))

for name in SolutionArray._purefluid_scalar:
setattr(SolutionArray, name, make_prop(name, empty_scalar, PureFluid))

for name in SolutionArray._n_total_species:
setattr(SolutionArray, name,
make_prop(name, empty_total_species, Solution))
Expand Down
6 changes: 4 additions & 2 deletions interfaces/cython/cantera/examples/thermo/rankine.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""
A Rankine vapor power cycle

Requires: Cantera >= 2.5.0
"""

import cantera as ct
Expand Down Expand Up @@ -48,7 +50,7 @@ def printState(n, fluid):
w = ct.Water()

# start with saturated liquid water at 300 K
w.TX = 300.0, 0.0
w.TQ = 300.0, 0.0
h1 = w.h
p1 = w.P
printState(1, w)
Expand All @@ -60,7 +62,7 @@ def printState(n, fluid):

# heat it at constant pressure until it reaches the saturated vapor state
# at this pressure
w.PX = p_max, 1.0
w.PQ = p_max, 1.0
h3 = w.h
heat_added = h3 - h2
printState(3, w)
Expand Down
15 changes: 11 additions & 4 deletions interfaces/cython/cantera/test/test_composite.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ def test_load_thermo_models(self):
self.assertEqual(sol.T, T0)
self.assertEqual(sol.P, p0)

if sol.thermo_model in ('PureFluid',):
self.assertTrue(sol.has_phase_transition)
else:
self.assertFalse(sol.has_phase_transition)

if not sol.is_compressible:
with self.assertRaisesRegex(ct.CanteraError,
'Density is not an independent'):
Expand Down Expand Up @@ -239,21 +244,23 @@ def test_restore_water(self):
def check(a, b):
self.assertArrayNear(a.T, b.T)
self.assertArrayNear(a.P, b.P)
self.assertArrayNear(a.X, b.X)
self.assertArrayNear(a.Q, b.Q)

self.assertTrue(self.water.has_phase_transition)

# benchmark
a = ct.SolutionArray(self.water, 10)
a.TX = 373.15, np.linspace(0., 1., 10)
a.TQ = 373.15, np.linspace(0., 1., 10)

# complete data
cols = ('T', 'P', 'X')
cols = ('T', 'P', 'Q')
data, labels = a.collect_data(cols=cols)
b = ct.SolutionArray(self.water)
b.restore_data(data, labels)
check(a, b)

# partial data
cols = ('T', 'X')
cols = ('T', 'Q')
data, labels = a.collect_data(cols=cols)
b = ct.SolutionArray(self.water)
b.restore_data(data, labels)
Expand Down
Loading