Skip to content

Commit

Permalink
Add write_hdf to SolutionArray objects
Browse files Browse the repository at this point in the history
 * The commit implements saving of data extracted from SolutionArrays
 to HDF containers using pandas infrastructure.
 * Two methods are introduced: `write_hdf` and `to_pandas`.
 * Both methods only work if the pandas module can be imported; an
 exception is raised only if the method is called without a working
 pandas installation.
  • Loading branch information
Ingmar Schoegl authored and ischoegl committed Aug 7, 2019
1 parent bbdc790 commit 5c9e348
Showing 1 changed file with 99 additions and 22 deletions.
121 changes: 99 additions & 22 deletions interfaces/cython/cantera/composite.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# This file is part of Cantera. See License.txt in the top-level directory or
# at http://www.cantera.org/license.txt for license and copyright information.
# at https://cantera.org/license.txt for license and copyright information.

from ._cantera import *
import numpy as np
import csv as _csv


class Solution(ThermoPhase, Kinetics, Transport):
"""
A class for chemically-reacting solutions. Instances can be created to
Expand Down Expand Up @@ -157,6 +158,7 @@ class Quantity:
>>> q3.P
101325.0
"""

def __init__(self, phase, mass=None, moles=None, constant='UV'):
self.state = phase.TDY
self._phase = phase
Expand All @@ -172,7 +174,7 @@ def __init__(self, phase, mass=None, moles=None, constant='UV'):
else:
self.mass = 1.0

assert constant in ('TP','TV','HP','SP','SV','UV')
assert constant in ('TP', 'TV', 'HP', 'SP', 'SV', 'UV')
self.constant = constant

@property
Expand Down Expand Up @@ -243,24 +245,26 @@ def __rmul__(self, other):
def __iadd__(self, other):
if (self._id != other._id):
raise ValueError('Cannot add Quantities with different phase '
'definitions.')
'definitions.')
assert(self.constant == other.constant)
a1,b1 = getattr(self.phase, self.constant)
a2,b2 = getattr(other.phase, self.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))
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 = 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
Expand All @@ -269,6 +273,8 @@ def __add__(self, other):
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)
Expand All @@ -279,6 +285,7 @@ def setter(self, value):

return property(getter, setter, doc=getattr(Solution, attr).__doc__)


for _attr in dir(Solution):
if _attr.startswith('_') or _attr in Quantity.__dict__ or _attr == 'state':
continue
Expand All @@ -294,7 +301,7 @@ class SolutionArray:
SolutionArray can represent both 1D and multi-dimensional arrays of states,
with shapes described in the same way as Numpy arrays. All of the states
can be set in a single call.
can be set in a single call::
>>> gas = ct.Solution('gri30.cti')
>>> states = ct.SolutionArray(gas, (6, 10))
Expand Down Expand Up @@ -347,7 +354,7 @@ class SolutionArray:
... # do something with mu[i,j]
Information about a subset of species may also be accessed, using
parentheses to specify the species:
parentheses to specify the species::
>>> states('CH4').Y # -> mass fraction of CH4 in each state
>>> states('H2','O2').partial_molar_cp # -> cp for H2 and O2
Expand All @@ -361,10 +368,20 @@ class SolutionArray:
>>> s.reaction_equation(10)
'CH4 + O <=> CH3 + OH'
Data represnted by a SolutionArray can be extracted and saved to a CSV file
using the `write_csv` method:
Data represented by a SolutionArray can be extracted and saved to a CSV file
using the `write_csv` method::
>>> states.write_csv('somefile.csv', cols=('T','P','X','net_rates_of_progress'))
>>> states.write_csv('somefile.csv', cols=('T', 'P', 'X', 'net_rates_of_progress'))
As an alternative, data extracted from SolutionArray objects can be saved
to a Pandas compatible HDF container file using the `write_hdf` method::
>>> states.write_hdf('somefile.h5', cols=('T', 'P', 'X'), key='some_key')
In this case, the (optional) key argument allows for saving and accessing
multiple solutions in a single container file. Note that `write_hdf` requires
working installations of Pandas and PyTable, i.e. use pip or conda to install
pandas and tables to enable this optional output method.
:param phase: The `Solution` object used to compute the thermodynamic,
kinetic, and transport properties
Expand Down Expand Up @@ -471,7 +488,8 @@ def __init__(self, phase, shape=(0,), states=None, extra=None):
for name, v in extra.items():
if not np.shape(v):
self._extra_lists[name] = [v]*self._shape[0]
self._extra_arrays[name] = np.array(self._extra_lists[name])
self._extra_arrays[name] = np.array(
self._extra_lists[name])
elif len(v) == self._shape[0]:
self._extra_lists[name] = list(v)
else:
Expand All @@ -486,8 +504,8 @@ def __init__(self, phase, shape=(0,), states=None, extra=None):

elif extra:
raise ValueError("Initial values for extra properties must be"
" supplied in a dict if the SolutionArray is not initially"
" empty")
" supplied in a dict if the SolutionArray is not initially"
" empty")

def __getitem__(self, index):
states = self._states[index]
Expand Down Expand Up @@ -543,15 +561,16 @@ def append(self, state=None, **kwargs):
elif len(kwargs) == 1:
attr, value = next(iter(kwargs.items()))
if frozenset(attr) not in self._phase._full_states:
raise KeyError("{} does not specify a full thermodynamic state")
raise KeyError(
"{} does not specify a full thermodynamic state")
setattr(self._phase, attr, value)

else:
try:
attr = self._phase._full_states[frozenset(kwargs)]
except KeyError:
raise KeyError("{} is not a valid combination of properties "
"for setting the thermodynamic state".format(tuple(kwargs)))
"for setting the thermodynamic state".format(tuple(kwargs)))
setattr(self._phase, attr, [kwargs[a] for a in attr])

self._states.append(self._phase.state)
Expand All @@ -565,7 +584,7 @@ def equilibrate(self, *args, **kwargs):
self._phase.equilibrate(*args, **kwargs)
self._states[index][:] = self._phase.state

def collect_data(self, cols=('extra','T','density','Y'), threshold=0,
def collect_data(self, cols=('extra', 'T', 'density', 'Y'), threshold=0,
species='Y'):
"""
Returns the data specified by *cols* in a single 2D Numpy array, along
Expand Down Expand Up @@ -637,7 +656,7 @@ def collect_data(self, cols=('extra','T','density','Y'), threshold=0,

return np.hstack(data), labels

def write_csv(self, filename, cols=('extra','T','density','Y'),
def write_csv(self, filename, cols=('extra', 'T', 'density', 'Y'),
*args, **kwargs):
"""
Write a CSV file named *filename* containing the data specified by
Expand All @@ -653,6 +672,61 @@ def write_csv(self, filename, cols=('extra','T','density','Y'),
for row in data:
writer.writerow(row)

def to_pandas(self, cols=('extra', 'T', 'density', 'Y'),
*args, **kwargs):
"""
Returns the data specified by *cols* in a single Pandas DataFrame.
Additional arguments are passed on to `collect_data`. This method works
only with 1D SolutionArray objects and requires a working Pandas
installation. Use pip or conda to install pandas to enable this method.
"""

# local import avoids explicit dependence of cantera on Pandas
import pandas as pd

data, labels = self.collect_data(cols, *args, **kwargs)
return pd.DataFrame(data=data, columns=labels)

def write_hdf(self, filename, cols=('extra', 'T', 'density', 'Y'),
key='df', mode=None, append=None, complevel=None,
*args, **kwargs):
"""
Write data specified by *cols* to a HDF container file named *filename*,
where *key* is used to label a group entry. Note that it is possible to
write multiple data sets to a single HDF container file, where unique
keys specify individual entries.
Internally, the HDF data entry is a `pandas.DataFrame` generated via
the `to_pandas` method.
:param filename: name of the HDF container file; typical file extensions
are `.hdf`, `.hdf5` or `.h5`.
:param cols: A list of any properties of the solution being exported.
:param key: Identifier for the group in the container file.
:param mode: Mode to open the file {None,'a','w','r+}.
:param append: use less efficient structure that makes HDF entries
appendable or append to existing appendable HDF entry {None,True,False}.
:param complevel: Specifies a compression level for data {None,0-9}.
A value of 0 disables compression.
Arguments *key*, *mode*, *append*, and *complevel* are used to set
parameters for `pandas.DataFrame.to_hdf`; the choice `None` for *mode*,
*append*, and *complevel* results in default values set by Pandas.
Additional arguments (i.e. *args* and *kwargs*) are passed on to
`collect_data` via `to_pandas`; see `collect_data` for further
information. This method works only with 1D SolutionArray objects
and requires working installations of Pandas and PyTables. Use
pip or conda to install pandas and tables to enable this method.
"""

# create Pandas DataFame and write to file
df = self.to_pandas(cols, *args, **kwargs)
pd_kwargs = {'mode': mode, 'append': append, 'complevel': complevel}
pd_kwargs = {k: v for k, v in pd_kwargs.items() if v is not None}
df.to_hdf(filename, key, **pd_kwargs)


def _make_functions():
# this is wrapped in a function to avoid polluting the module namespace
Expand Down Expand Up @@ -697,9 +771,10 @@ def getter(self):
return a, b, c

def setter(self, ABC):
assert len(ABC) == 3, "Expected 3 elements, got {}".format(len(ABC))
assert len(ABC) == 3, "Expected 3 elements, got {}".format(
len(ABC))
A, B, _ = np.broadcast_arrays(ABC[0], ABC[1], self._output_dummy)
XY = ABC[2] # composition
XY = ABC[2] # composition
if len(np.shape(XY)) < 2:
# composition is a single array (or string or dict)
for index in self._indices:
Expand Down Expand Up @@ -765,7 +840,8 @@ def getter(self):
setattr(SolutionArray, name, make_prop(name, empty_species2, Solution))

for name in SolutionArray._n_reactions:
setattr(SolutionArray, name, make_prop(name, empty_reactions, Solution))
setattr(SolutionArray, name, make_prop(
name, empty_reactions, Solution))

# Factory for creating wrappers for functions which return a value
def caller(name, get_container):
Expand Down Expand Up @@ -798,4 +874,5 @@ def setter(self, value):
for name in SolutionArray._interface_passthrough:
setattr(SolutionArray, name, passthrough_prop(name, Interface))


_make_functions()

0 comments on commit 5c9e348

Please sign in to comment.