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

SolutionArray/ flamebase importers allow unnormalized and/or negative mass/mole fractions #1037

Merged
merged 14 commits into from
Jul 9, 2021
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
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ years. If you've been left off, please report the omission on the Github issue
tracker.

Rounak Agarwal (@agarwalrounak)
David Akinpelu (@DavidAkinpelu), Louisiana State University
Emil Atz (@EmilAtz)
Philip Berndt
Wolfgang Bessler (@wbessler), Offenburg University of Applied Science
Expand Down
80 changes: 66 additions & 14 deletions interfaces/cython/cantera/composite.py
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,7 @@ def __call__(self, *species):
return SolutionArray(self._phase[species], states=self._states,
extra=self._extra)

def append(self, state=None, **kwargs):
def append(self, state=None, normalize=True, **kwargs):
"""
Append an element to the array with the specified state. Elements can
only be appended in cases where the array of states is one-dimensional.
Expand All @@ -649,6 +649,10 @@ def append(self, state=None, **kwargs):
the full-state setters::

mystates.append(T=300, P=101325, X={'O2':1.0, 'N2':3.76})

By default, the mass or mole fractions will be normalized i.e negative values
are truncated and the mass or mole fractions sum up to 1.0. If this
is not desired, the ``normalize`` argument can be set to ``False``.
"""
if len(self._shape) != 1:
raise IndexError("Can only append to 1D SolutionArray")
Expand Down Expand Up @@ -676,12 +680,21 @@ def append(self, state=None, **kwargs):
self._phase.state = state

elif len(kwargs) == 1:
attr, value = next(iter(kwargs.items()))
attr, value = kwargs.popitem()
if frozenset(attr) not in self._phase._full_states:
raise KeyError(
"'{}' does not specify a full thermodynamic state".format(attr)
)
setattr(self._phase, attr, value)
if normalize or attr.endswith("Q"):
setattr(self._phase, attr, value)
else:
if attr.endswith("X"):
self._phase.set_unnormalized_mole_fractions(value[-1])
elif attr.endswith("Y"):
self._phase.set_unnormalized_mass_fractions(value[-1])
attr = attr[:-1]
value = value[:-1]
setattr(self._phase, attr, value)

else:
try:
Expand All @@ -691,7 +704,15 @@ def append(self, state=None, **kwargs):
"{} is not a valid combination of properties for setting "
"the thermodynamic state".format(tuple(kwargs))
) from None
setattr(self._phase, attr, [kwargs[a] for a in attr])
if normalize or attr.endswith("Q"):
setattr(self._phase, attr, list(kwargs.values()))
else:
if attr.endswith("X"):
self._phase.set_unnormalized_mole_fractions(kwargs.pop("X"))
elif attr.endswith("Y"):
self._phase.set_unnormalized_mass_fractions(kwargs.pop("Y"))
attr = attr[:-1]
setattr(self._phase, attr, list(kwargs.values()))

for name, value in self._extra.items():
new = extra_temp[name]
Expand Down Expand Up @@ -750,7 +771,7 @@ def equilibrate(self, *args, **kwargs):
self._phase.equilibrate(*args, **kwargs)
self._states[index][:] = self._phase.state

def restore_data(self, data):
def restore_data(self, data, normalize=True):
"""
Restores a `SolutionArray` based on *data* specified in an ordered
dictionary. Thus, this method allows to restore data exported by
Expand All @@ -759,6 +780,9 @@ def restore_data(self, data):
:param data: Dictionary holding data to be restored, where keys
refer to thermodynamic states (e.g. ``T``, ``density``) or extra
entries, and values contain corresponding data.
:param normalize: If True, mole or mass fractions are normalized
so that they sum up to 1.0. If False, mole or mass fractions
are not normalized.

The receiving `SolutionArray` either has to be empty or should have
matching dimensions. Essential state properties and extra entries
Expand Down Expand Up @@ -904,9 +928,28 @@ def join(species):
self._shape = (rows,)

# restore data
for i in self._indices:
setattr(self._phase, mode, [st[i, ...] for st in state_data])
self._states[i] = self._phase.state
if normalize or mode.endswith("Q"):
for i in self._indices:
setattr(self._phase, mode, [st[i, ...] for st in state_data])
self._states[i] = self._phase.state
else:
for i in self._indices:
if mode.endswith("X"):
self._phase.set_unnormalized_mole_fractions(
[st[i, ...] for st in state_data][2]
)
setattr(self._phase, mode[:2],
[st[i, ...] for st in state_data[:2]])
elif mode.endswith("Y"):
self._phase.set_unnormalized_mass_fractions(
[st[i, ...] for st in state_data][2]
)
setattr(self._phase, mode[:2],
[st[i, ...] for st in state_data[:2]])
else:
setattr(self._phase, mode, [st[i, ...] for st in state_data])
self._states[i] = self._phase.state

self._extra = extra_lists

def set_equivalence_ratio(self, phi, *args, **kwargs):
Expand Down Expand Up @@ -1038,11 +1081,14 @@ def write_csv(self, filename, cols=None, *args, **kwargs):
for row in data:
writer.writerow(row)

def read_csv(self, filename):
def read_csv(self, filename, normalize=True):
"""
Read a CSV file named *filename* and restore data to the `SolutionArray`
using `restore_data`. This method allows for recreation of data
previously exported by `write_csv`.

The ``normalize`` argument is passed on to `restore_data` to normalize
mole or mass fractions. By default, ``normalize`` is ``True``.
"""
if np.lib.NumpyVersion(np.__version__) < "1.14.0":
# bytestring needs to be converted for columns containing strings
Expand All @@ -1060,7 +1106,7 @@ def read_csv(self, filename):
dtype=None, names=True, encoding=None)
data_dict = OrderedDict({label: data[label]
for label in data.dtype.names})
self.restore_data(data_dict)
self.restore_data(data_dict, normalize)

def to_pandas(self, cols=None, *args, **kwargs):
"""
Expand All @@ -1079,13 +1125,16 @@ def to_pandas(self, cols=None, *args, **kwargs):
labels = list(data_dict.keys())
return _pandas.DataFrame(data=data, columns=labels)

def from_pandas(self, df):
def from_pandas(self, df, normalize=True):
"""
Restores `SolutionArray` data from a pandas DataFrame *df*.

This method is intendend for loading of data that were previously
exported by `to_pandas`. The method requires a working pandas
installation. The package 'pandas' can be installed using pip or conda.

The ``normalize`` argument is passed on to `restore_data` to normalize
mole or mass fractions. By default, ``normalize`` is ``True``.
"""

data = df.to_numpy(dtype=float)
Expand All @@ -1094,7 +1143,7 @@ def from_pandas(self, df):
data_dict = OrderedDict()
for i, label in enumerate(labels):
data_dict[label] = data[:, i]
self.restore_data(data_dict)
self.restore_data(data_dict, normalize)

def write_hdf(self, filename, *args, cols=None, group=None, subgroup=None,
attrs={}, mode='a', append=False,
Expand Down Expand Up @@ -1221,7 +1270,7 @@ def write_hdf(self, filename, *args, cols=None, group=None, subgroup=None,

return group

def read_hdf(self, filename, group=None, subgroup=None, force=False):
def read_hdf(self, filename, group=None, subgroup=None, force=False, normalize=True):
"""
Read a dataset from a HDF container file and restore data to the
`SolutionArray` object. This method allows for recreation of data
Expand All @@ -1239,6 +1288,9 @@ def read_hdf(self, filename, group=None, subgroup=None, force=False):
`Solution` object), with an error being raised if the current source
does not match the original source. If True, the error is
suppressed.
:param normalize: Passed on to `restore_data`. If True, mole or mass
fractions are normalized so that they sum up to 1.0. If False, mole
or mass fractions are not normalized.
:return: User-defined attributes provided to describe the group holding
the `SolutionArray` information.

Expand Down Expand Up @@ -1312,7 +1364,7 @@ def strip_ext(source):
else:
data[name] = np.array(value)

self.restore_data(data)
self.restore_data(data, normalize)

return root_attrs

Expand Down
45 changes: 35 additions & 10 deletions interfaces/cython/cantera/onedim.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ def set_gas_state(self, point):
self.gas.set_unnormalized_mass_fractions(Y)
self.gas.TP = self.value(self.flame, 'T', point), self.P

def write_csv(self, filename, species='X', quiet=True):
def write_csv(self, filename, species='X', quiet=True, normalize=True):
"""
Write the velocity, temperature, density, and species profiles
to a CSV file.
Expand All @@ -387,20 +387,27 @@ def write_csv(self, filename, species='X', quiet=True):
:param species:
Attribute to use obtaining species profiles, e.g. ``X`` for
mole fractions or ``Y`` for mass fractions.
:param normalize:
Boolean flag to indicate whether the mole/mass fractions should
be normalized.
"""

# save data
cols = ('extra', 'T', 'D', species)
self.to_solution_array().write_csv(filename, cols=cols)
self.to_solution_array(normalize=normalize).write_csv(filename, cols=cols)

if not quiet:
print("Solution saved to '{0}'.".format(filename))

def to_solution_array(self, domain=None):
def to_solution_array(self, domain=None, normalize=True):
"""
Return the solution vector as a `SolutionArray` object.

Derived classes define default values for *other*.

By default, the mass or mole fractions will be normalized i.e they
sum up to 1.0. If this is not desired, the ``normalize`` argument
can be set to ``False``.
"""
if domain is None:
domain = self.flame
Expand All @@ -413,7 +420,16 @@ def to_solution_array(self, domain=None):
if n_points:
arr = SolutionArray(self.phase(domain), n_points,
extra=other_cols, meta=meta)
arr.TPY = states
if normalize:
arr.TPY = states
else:
if len(states) == 3:
for i in range(n_points):
arr._phase.set_unnormalized_mass_fractions(states[2][i])
arr._phase.TP = np.atleast_1d(states[0])[i], states[1]
arr._states[i] = arr._phase.state
else:
arr.TP = states
return arr
else:
return SolutionArray(self.phase(domain), meta=meta)
Expand All @@ -436,20 +452,23 @@ def from_solution_array(self, arr, domain=None):
meta = arr.meta
super().restore_data(domain, states, other_cols, meta)

def to_pandas(self, species='X'):
def to_pandas(self, species='X', normalize=True):
"""
Return the solution vector as a `pandas.DataFrame`.

:param species:
Attribute to use obtaining species profiles, e.g. ``X`` for
mole fractions or ``Y`` for mass fractions.
:param normalize:
Boolean flag to indicate whether the mole/mass fractions should
be normalized (default is ``True``)

This method uses `to_solution_array` and requires a working pandas
installation. Use pip or conda to install `pandas` to enable this
method.
"""
cols = ('extra', 'T', 'D', species)
return self.to_solution_array().to_pandas(cols=cols)
return self.to_solution_array(normalize=normalize).to_pandas(cols=cols)

def from_pandas(self, df, restore_boundaries=True, settings=None):
"""
Expand All @@ -476,7 +495,7 @@ def from_pandas(self, df, restore_boundaries=True, settings=None):

def write_hdf(self, filename, *args, group=None, species='X', mode='a',
description=None, compression=None, compression_opts=None,
quiet=True, **kwargs):
quiet=True, normalize=True, **kwargs):
"""
Write the solution vector to a HDF container file.

Expand Down Expand Up @@ -537,6 +556,9 @@ def write_hdf(self, filename, *args, group=None, species='X', mode='a',
corresponds to the compression level {None, 0-9}.
:param quiet:
Suppress message confirming successful file output.
:param normalize:
Boolean flag to indicate whether the mole/mass fractions should
be normalized (default is ``True``)

Additional arguments (i.e. *args* and *kwargs*) are passed on to
`SolutionArray.collect_data`. The method exports data using
Expand All @@ -551,7 +573,7 @@ def write_hdf(self, filename, *args, group=None, species='X', mode='a',
if description is not None:
meta['description'] = description
for i in range(3):
arr = self.to_solution_array(domain=self.domains[i])
arr = self.to_solution_array(domain=self.domains[i], normalize=normalize)
group = arr.write_hdf(filename, *args, group=group, cols=cols,
subgroup=self.domains[i].name,
attrs=meta, mode=mode, append=(i > 0),
Expand All @@ -565,7 +587,7 @@ def write_hdf(self, filename, *args, group=None, species='X', mode='a',
msg = "Solution saved to '{0}' as group '{1}'."
print(msg.format(filename, group))

def read_hdf(self, filename, group=None, restore_boundaries=True):
def read_hdf(self, filename, group=None, restore_boundaries=True, normalize=True):
"""
Restore the solution vector from a HDF container file.

Expand All @@ -576,6 +598,9 @@ def read_hdf(self, filename, group=None, restore_boundaries=True):
:param restore_boundaries:
Boolean flag to indicate whether boundaries should be restored
(default is ``True``)
:param normalize:
Boolean flag to indicate whether the mole/mass fractions should
be normalized (default is ``True``)

The method imports data using `SolutionArray.read_hdf` via
`from_solution_array` and requires a working installation of h5py
Expand All @@ -589,7 +614,7 @@ def read_hdf(self, filename, group=None, restore_boundaries=True):
for d in domains:
arr = SolutionArray(self.phase(d), extra=self.other_components(d))
meta = arr.read_hdf(filename, group=group,
subgroup=self.domains[d].name)
subgroup=self.domains[d].name, normalize=normalize)
self.from_solution_array(arr, domain=d)

self.settings = meta
Expand Down
Loading