diff --git a/interfaces/cython/cantera/composite.py b/interfaces/cython/cantera/composite.py index 938a3eb6c8f..83e9809039c 100644 --- a/interfaces/cython/cantera/composite.py +++ b/interfaces/cython/cantera/composite.py @@ -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 @@ -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 @@ -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 @@ -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 @@ -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) @@ -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 @@ -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)) @@ -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 @@ -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 @@ -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: @@ -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] @@ -543,7 +561,8 @@ 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: @@ -551,7 +570,7 @@ def append(self, state=None, **kwargs): 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) @@ -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 @@ -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 @@ -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 @@ -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: @@ -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): @@ -798,4 +874,5 @@ def setter(self, value): for name in SolutionArray._interface_passthrough: setattr(SolutionArray, name, passthrough_prop(name, Interface)) + _make_functions()