From 9588f3e96ffc3c165d4d8f16515a1f348dfe02b3 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Mon, 19 Jun 2023 16:26:31 -0600 Subject: [PATCH 01/19] [base] Create framework for SolutionArray::save CSV output --- include/cantera/base/SolutionArray.h | 25 +++++++++++++++++-------- src/base/SolutionArray.cpp | 13 ++++++++++++- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/include/cantera/base/SolutionArray.h b/include/cantera/base/SolutionArray.h index 6a55d0ede8..0c356d4784 100644 --- a/include/cantera/base/SolutionArray.h +++ b/include/cantera/base/SolutionArray.h @@ -210,6 +210,14 @@ class SolutionArray static void writeHeader(AnyMap& root, const string& id, const string& desc, bool overwrite=false); + /*! + * Write SolutionArray data to comma-separated data file. + * + * @param fname Name of CSV file + * @param overwrite Force overwrite if file exists; optional (default=false) + */ + void writeEntry(const string& fname, bool overwrite=false); + /*! * Write SolutionArray data to container file. * @@ -234,17 +242,18 @@ class SolutionArray bool overwrite=false); /*! - * Save current SolutionArray and header to a container file. + * Save current SolutionArray and header to a data file. * - * @param fname Name of output container file (YAML or HDF) - * @param id Identifier of root location within the container file - * @param sub Name identifier for the subgroup holding actual data - * @param desc Custom comment describing the dataset to be stored - * @param overwrite Force overwrite if sub exists; optional (default=false) + * @param fname Name of output container file (CSV, YAML or HDF) + * @param id Identifier of root location within container file (YAML/HDF only) + * @param sub Name identifier for subgroup holding actual data (YAML/HDF only) + * @param desc Custom comment describing dataset to be stored (YAML/HDF only) + * @param overwrite Force overwrite if file and/or data entry exists; optional + * (default=false) * @param compression Compression level; optional (default=0; HDF only) */ - void save(const string& fname, const string& id, const string& sub, - const string& desc, bool overwrite=false, int compression=0); + void save(const string& fname, const string& id="", const string& sub="", + const string& desc="", bool overwrite=false, int compression=0); /*! * Read header data from container file. diff --git a/src/base/SolutionArray.cpp b/src/base/SolutionArray.cpp index c51066351a..5e2a51893c 100644 --- a/src/base/SolutionArray.cpp +++ b/src/base/SolutionArray.cpp @@ -966,6 +966,11 @@ void SolutionArray::writeHeader(AnyMap& root, const string& id, data.update(preamble(desc)); } +void SolutionArray::writeEntry(const string& fname, bool overwrite) +{ + throw NotImplementedError("SolutionArray::writeEntry"); +} + void SolutionArray::writeEntry(const string& fname, const string& id, const string& sub, bool overwrite, int compression) { @@ -1139,7 +1144,7 @@ void SolutionArray::append(const vector& state, const AnyMap& extra) } void SolutionArray::save(const string& fname, const string& id, const string& sub, - const string& desc, bool overwrite, int compression) + const string& desc, bool overwrite, int compression) { if (m_size < m_dataSize) { throw NotImplementedError("SolutionArray::save", @@ -1167,6 +1172,12 @@ void SolutionArray::save(const string& fname, const string& id, const string& su AnyMap::clearCachedFile(fname); return; } + if (extension == "csv") { + if (id != "") { + warn_user("SolutionArray::save", "Parameter 'id' not used for CSV output."); + } + writeEntry(fname, overwrite); + } throw CanteraError("SolutionArray::save", "Unknown file extension '{}'.", extension); } From 796c3541b828b652a5ca1da71244ebad1596ab2d Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Mon, 19 Jun 2023 18:12:32 -0600 Subject: [PATCH 02/19] [base] Write CSV header line for SolutionArray::save --- src/base/SolutionArray.cpp | 45 +++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/src/base/SolutionArray.cpp b/src/base/SolutionArray.cpp index 5e2a51893c..2949b46f03 100644 --- a/src/base/SolutionArray.cpp +++ b/src/base/SolutionArray.cpp @@ -968,7 +968,49 @@ void SolutionArray::writeHeader(AnyMap& root, const string& id, void SolutionArray::writeEntry(const string& fname, bool overwrite) { - throw NotImplementedError("SolutionArray::writeEntry"); + if (std::ifstream(fname).good() && !overwrite) { + throw CanteraError("SolutionArray::writeEntry", + "File '{}' exists; use option 'overwrite' to replace CSV file.", fname); + } + if (apiNdim() != 1) { + throw CanteraError("SolutionArray::writeEntry", + "Tabular output of CSV data only works for 1D SolutionArray objects."); + } + set speciesNames; + for (const auto& species : m_sol->thermo()->speciesNames()) { + speciesNames.insert(species); + } + const auto& nativeState = m_sol->thermo()->nativeState(); + bool mole = nativeState.find("X") != nativeState.end(); + + auto names = componentNames(); + map components; + std::stringstream buffer; + for (const auto& key : names) { + components[key] = getComponent(key); + if (!components[key].isVector() && + !components[key].isVector() && + !components[key].isVector()) + { + throw CanteraError("SolutionArray::writeEntry", + "Multi-dimensional column '{}' is not supported for CSV output.", key); + } + string name = key; + if (speciesNames.find(key) != speciesNames.end()) { + if (mole) { + name = "X_" + name; + } else { + name = "Y_" + name; + } + } + if (name.find(",") != string::npos) { + name = "\"" + name + "\""; + } + buffer << name << ","; + } + // Potential exceptions have been thrown; start writing data to file + std::ofstream output(fname); + output << buffer.str() << std::endl; } void SolutionArray::writeEntry(const string& fname, const string& id, const string& sub, @@ -1177,6 +1219,7 @@ void SolutionArray::save(const string& fname, const string& id, const string& su warn_user("SolutionArray::save", "Parameter 'id' not used for CSV output."); } writeEntry(fname, overwrite); + return; } throw CanteraError("SolutionArray::save", "Unknown file extension '{}'.", extension); From f16a1d8c7180fcf2cc12ee69ed07ef8ebea100ea Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Mon, 19 Jun 2023 19:55:20 -0600 Subject: [PATCH 03/19] [base] Write CSV body for SolutionArray::save --- src/base/SolutionArray.cpp | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/base/SolutionArray.cpp b/src/base/SolutionArray.cpp index 2949b46f03..6b8751c55f 100644 --- a/src/base/SolutionArray.cpp +++ b/src/base/SolutionArray.cpp @@ -984,6 +984,7 @@ void SolutionArray::writeEntry(const string& fname, bool overwrite) bool mole = nativeState.find("X") != nativeState.end(); auto names = componentNames(); + auto last = names[names.size() - 1]; map components; std::stringstream buffer; for (const auto& key : names) { @@ -1006,11 +1007,38 @@ void SolutionArray::writeEntry(const string& fname, bool overwrite) if (name.find(",") != string::npos) { name = "\"" + name + "\""; } - buffer << name << ","; + if (key == last) { + buffer << name; + } else { + buffer << name << ","; + } } + // Potential exceptions have been thrown; start writing data to file std::ofstream output(fname); output << buffer.str() << std::endl; + + for (size_t i = 0; i < m_size; i++) { + for (const auto& key : names) { + auto& data = components[key]; + if (data.isVector()) { + output << data.asVector()[i]; + } else if (data.isVector()) { + output << data.asVector()[i]; + } else if (data.isVector()) { + auto value = data.asVector()[i]; + if (value.find(",") != string::npos) { + value = "\"" + value + "\""; + } + output << value; + } + if (key != last) { + output << ","; + } + } + output << std::endl; + } + output << std::endl; } void SolutionArray::writeEntry(const string& fname, const string& id, const string& sub, From b3021d0ba6d154e02bca9ad61d0567f5f8c80248 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Mon, 19 Jun 2023 20:42:07 -0600 Subject: [PATCH 04/19] [Python] Make SolutionArray.from_pandas more flexible Add support for data types other than float. --- interfaces/cython/cantera/composite.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/interfaces/cython/cantera/composite.py b/interfaces/cython/cantera/composite.py index 3810a83fb9..0d2de531e0 100644 --- a/interfaces/cython/cantera/composite.py +++ b/interfaces/cython/cantera/composite.py @@ -1225,13 +1225,12 @@ def from_pandas(self, df, normalize=True): 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) - labels = list(df.columns) - data_dict = {} - for i, label in enumerate(labels): - data_dict[label] = data[:, i] + for label in list(df.columns): + data_dict[label] = df[label].values + if data_dict[label].dtype.type == np.object_: + # convert object columns to string + data_dict[label] = data_dict[label].astype('U') self.restore_data(data_dict, normalize) def save(self, fname, name=None, key=None, description=None, From a3137eb6c7efcccec758f3e650ac51d7df49af8c Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Mon, 19 Jun 2023 20:44:46 -0600 Subject: [PATCH 05/19] [base] Simplify CSV SolutionArray::save --- src/base/SolutionArray.cpp | 42 ++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/base/SolutionArray.cpp b/src/base/SolutionArray.cpp index 6b8751c55f..fef8d48187 100644 --- a/src/base/SolutionArray.cpp +++ b/src/base/SolutionArray.cpp @@ -984,14 +984,15 @@ void SolutionArray::writeEntry(const string& fname, bool overwrite) bool mole = nativeState.find("X") != nativeState.end(); auto names = componentNames(); - auto last = names[names.size() - 1]; - map components; + size_t last = names.size() - 1; + vector components; std::stringstream buffer; for (const auto& key : names) { - components[key] = getComponent(key); - if (!components[key].isVector() && - !components[key].isVector() && - !components[key].isVector()) + components.emplace_back(getComponent(key)); + size_t col = components.size() - 1; + if (!components[col].isVector() && + !components[col].isVector() && + !components[col].isVector()) { throw CanteraError("SolutionArray::writeEntry", "Multi-dimensional column '{}' is not supported for CSV output.", key); @@ -1005,12 +1006,12 @@ void SolutionArray::writeEntry(const string& fname, bool overwrite) } } if (name.find(",") != string::npos) { - name = "\"" + name + "\""; - } - if (key == last) { - buffer << name; + buffer << "\"" << name << "\""; } else { - buffer << name << ","; + buffer << name; + } + if (col != last) { + buffer << ","; } } @@ -1018,21 +1019,22 @@ void SolutionArray::writeEntry(const string& fname, bool overwrite) std::ofstream output(fname); output << buffer.str() << std::endl; - for (size_t i = 0; i < m_size; i++) { - for (const auto& key : names) { - auto& data = components[key]; + for (size_t row = 0; row < m_size; row++) { + for (size_t col = 0; col < components.size(); col++) { + auto& data = components[col]; if (data.isVector()) { - output << data.asVector()[i]; + output << data.asVector()[row]; } else if (data.isVector()) { - output << data.asVector()[i]; + output << data.asVector()[row]; } else if (data.isVector()) { - auto value = data.asVector()[i]; + auto value = data.asVector()[row]; if (value.find(",") != string::npos) { - value = "\"" + value + "\""; + output << "\"" << value << "\""; + } else { + output << value; } - output << value; } - if (key != last) { + if (col != last) { output << ","; } } From 48cbdccf4cc234d593e58a006e5221b8173a77b0 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Mon, 19 Jun 2023 21:34:35 -0600 Subject: [PATCH 06/19] [base] Improve exception handling for CSV SolutionArray::save --- src/base/SolutionArray.cpp | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/src/base/SolutionArray.cpp b/src/base/SolutionArray.cpp index fef8d48187..ba8c5b5337 100644 --- a/src/base/SolutionArray.cpp +++ b/src/base/SolutionArray.cpp @@ -968,10 +968,6 @@ void SolutionArray::writeHeader(AnyMap& root, const string& id, void SolutionArray::writeEntry(const string& fname, bool overwrite) { - if (std::ifstream(fname).good() && !overwrite) { - throw CanteraError("SolutionArray::writeEntry", - "File '{}' exists; use option 'overwrite' to replace CSV file.", fname); - } if (apiNdim() != 1) { throw CanteraError("SolutionArray::writeEntry", "Tabular output of CSV data only works for 1D SolutionArray objects."); @@ -987,6 +983,7 @@ void SolutionArray::writeEntry(const string& fname, bool overwrite) size_t last = names.size() - 1; vector components; std::stringstream buffer; + bool escaped = false; for (const auto& key : names) { components.emplace_back(getComponent(key)); size_t col = components.size() - 1; @@ -1005,8 +1002,14 @@ void SolutionArray::writeEntry(const string& fname, bool overwrite) name = "Y_" + name; } } + if (name.find("\"") != string::npos || name.find("\n") != string::npos) { + throw NotImplementedError("SolutionArray::writeEntry", + "Detected column name containing double quotes or line feeds: '{}'.", + name); + } if (name.find(",") != string::npos) { buffer << "\"" << name << "\""; + escaped = true; } else { buffer << name; } @@ -1015,7 +1018,15 @@ void SolutionArray::writeEntry(const string& fname, bool overwrite) } } - // Potential exceptions have been thrown; start writing data to file + // (Most) potential exceptions have been thrown; start writing data to file + if (std::ifstream(fname).good()) { + if (!overwrite) { + throw CanteraError("SolutionArray::writeEntry", + "File '{}' already exists; use option 'overwrite' to replace CSV file.", + fname); + } + std::remove(fname.c_str()); + } std::ofstream output(fname); output << buffer.str() << std::endl; @@ -1028,8 +1039,16 @@ void SolutionArray::writeEntry(const string& fname, bool overwrite) output << data.asVector()[row]; } else if (data.isVector()) { auto value = data.asVector()[row]; + if (value.find("\"") != string::npos || + value.find("\n") != string::npos) + { + throw NotImplementedError("SolutionArray::writeEntry", + "Detected value containing double quotes or line feeds: '{}'", + value); + } if (value.find(",") != string::npos) { output << "\"" << value << "\""; + escaped = true; } else { output << value; } @@ -1041,6 +1060,13 @@ void SolutionArray::writeEntry(const string& fname, bool overwrite) output << std::endl; } output << std::endl; + + if (escaped) { + warn_user("SolutionArray::writeEntry", + "One or more CSV column names or values contain commas.\n" + "Values have been escaped with double quotes, which may not be supported " + "by all CSV readers."); + } } void SolutionArray::writeEntry(const string& fname, const string& id, const string& sub, From 1df5287b05cc678d10b844bf292c8a12d21b6ca1 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Tue, 20 Jun 2023 07:00:27 -0600 Subject: [PATCH 07/19] [base] Enable flexible species base for CSV output --- src/base/SolutionArray.cpp | 89 ++++++++++++++++++++++++-------------- 1 file changed, 56 insertions(+), 33 deletions(-) diff --git a/src/base/SolutionArray.cpp b/src/base/SolutionArray.cpp index ba8c5b5337..f35ea2c956 100644 --- a/src/base/SolutionArray.cpp +++ b/src/base/SolutionArray.cpp @@ -982,20 +982,30 @@ void SolutionArray::writeEntry(const string& fname, bool overwrite) auto names = componentNames(); size_t last = names.size() - 1; vector components; - std::stringstream buffer; + vector isSpecies; + std::stringstream header; bool escaped = false; for (const auto& key : names) { - components.emplace_back(getComponent(key)); - size_t col = components.size() - 1; - if (!components[col].isVector() && - !components[col].isVector() && - !components[col].isVector()) - { - throw CanteraError("SolutionArray::writeEntry", - "Multi-dimensional column '{}' is not supported for CSV output.", key); - } string name = key; - if (speciesNames.find(key) != speciesNames.end()) { + size_t col; + if (speciesNames.find(key) == speciesNames.end()) { + // Pre-read component vectors + isSpecies.push_back(false); + components.emplace_back(getComponent(key)); + col = components.size() - 1; + if (!components[col].isVector() && + !components[col].isVector() && + !components[col].isVector()) + { + throw CanteraError("SolutionArray::writeEntry", + "Multi-dimensional column '{}' is not supported for CSV output.", + key); + } + } else { + // Delay reading species data as basis can be either mole or mass + isSpecies.push_back(true); + components.emplace_back(AnyValue()); + col = components.size() - 1; if (mole) { name = "X_" + name; } else { @@ -1008,13 +1018,13 @@ void SolutionArray::writeEntry(const string& fname, bool overwrite) name); } if (name.find(",") != string::npos) { - buffer << "\"" << name << "\""; + header << "\"" << name << "\""; escaped = true; } else { - buffer << name; + header << name; } if (col != last) { - buffer << ","; + header << ","; } } @@ -1028,29 +1038,42 @@ void SolutionArray::writeEntry(const string& fname, bool overwrite) std::remove(fname.c_str()); } std::ofstream output(fname); - output << buffer.str() << std::endl; + output << header.str() << std::endl; + vector buf(speciesNames.size(), 0.); for (size_t row = 0; row < m_size; row++) { + setLoc(row); + if (mole) { + m_sol->thermo()->getMoleFractions(buf.data()); + } else { + m_sol->thermo()->getMassFractions(buf.data()); + } + + size_t idx = 0; for (size_t col = 0; col < components.size(); col++) { - auto& data = components[col]; - if (data.isVector()) { - output << data.asVector()[row]; - } else if (data.isVector()) { - output << data.asVector()[row]; - } else if (data.isVector()) { - auto value = data.asVector()[row]; - if (value.find("\"") != string::npos || - value.find("\n") != string::npos) - { - throw NotImplementedError("SolutionArray::writeEntry", - "Detected value containing double quotes or line feeds: '{}'", - value); - } - if (value.find(",") != string::npos) { - output << "\"" << value << "\""; - escaped = true; + if (isSpecies[col]) { + output << buf[idx++]; + } else { + auto& data = components[col]; + if (data.isVector()) { + output << data.asVector()[row]; + } else if (data.isVector()) { + output << data.asVector()[row]; } else { - output << value; + auto value = data.asVector()[row]; + if (value.find("\"") != string::npos || + value.find("\n") != string::npos) + { + throw NotImplementedError("SolutionArray::writeEntry", + "Detected value containing double quotes or line feeds: " + "'{}'", value); + } + if (value.find(",") != string::npos) { + output << "\"" << value << "\""; + escaped = true; + } else { + output << value; + } } } if (col != last) { From e727e1914b2237d054bd27d102ab759ddf68cf09 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Tue, 20 Jun 2023 08:00:36 -0600 Subject: [PATCH 08/19] Implement flexible basis for SolutionArray::save CSV --- include/cantera/base/SolutionArray.h | 9 +++-- interfaces/cython/cantera/composite.py | 19 +++++++---- interfaces/cython/cantera/solutionbase.pxd | 2 +- interfaces/cython/cantera/solutionbase.pyx | 5 +-- src/base/SolutionArray.cpp | 39 +++++++++++++++------- 5 files changed, 50 insertions(+), 24 deletions(-) diff --git a/include/cantera/base/SolutionArray.h b/include/cantera/base/SolutionArray.h index 0c356d4784..de0fff9eab 100644 --- a/include/cantera/base/SolutionArray.h +++ b/include/cantera/base/SolutionArray.h @@ -215,8 +215,10 @@ class SolutionArray * * @param fname Name of CSV file * @param overwrite Force overwrite if file exists; optional (default=false) + * @param basis Output mass ("Y"/"mass") or mole ("X"/"mole") fractions; + * if not specified (""), the native storage mode is used */ - void writeEntry(const string& fname, bool overwrite=false); + void writeEntry(const string& fname, bool overwrite=false, const string& basis=""); /*! * Write SolutionArray data to container file. @@ -251,9 +253,12 @@ class SolutionArray * @param overwrite Force overwrite if file and/or data entry exists; optional * (default=false) * @param compression Compression level; optional (default=0; HDF only) + * @param basis Output mass ("Y"/"mass") or mole ("X"/"mole") fractions (CSV + * only); if omitted (default=""), the native storage mode is used */ void save(const string& fname, const string& id="", const string& sub="", - const string& desc="", bool overwrite=false, int compression=0); + const string& desc="", bool overwrite=false, int compression=0, + const string& basis=""); /*! * Read header data from container file. diff --git a/interfaces/cython/cantera/composite.py b/interfaces/cython/cantera/composite.py index 0d2de531e0..ba401d1e19 100644 --- a/interfaces/cython/cantera/composite.py +++ b/interfaces/cython/cantera/composite.py @@ -1233,29 +1233,34 @@ def from_pandas(self, df, normalize=True): data_dict[label] = data_dict[label].astype('U') self.restore_data(data_dict, normalize) - def save(self, fname, name=None, key=None, description=None, - overwrite=False, compression=0): + def save(self, fname, name=None, key=None, description=None, *, + overwrite=False, compression=0, basis=None): """ - Save current `SolutionArray` and header to a container file. + Save current `SolutionArray` to a container or CSV file. :param fname: Name of output container file (YAML or HDF) :param name: Identifier of root location within the container file; the root location - contains header data and a subgroup holding the actual `SolutionArray`. + contains header data and a subgroup holding the actual `SolutionArray` + (HDF/YAML only). :param key: Name identifier for the subgroup holding the `SolutionArray` data and - metadata objects. If `None`, the subgroup name default to ``data``. + metadata objects. If `None`, the subgroup name default to ``data`` + (HDF/YAML only). :param description: - Custom comment describing the dataset to be stored. + Custom comment describing the dataset to be stored (HDF/YAML only). :param overwrite: Force overwrite if name exists; optional (default=`False`) :param compression: Compression level (0-9); optional (default=0; HDF only) + :param basis: + Output mass (``Y``/``mass``) or mole (``X``/``mole``) fractions (CSV only); + if not specified (`None`), the native storage mode is used .. versionadded:: 3.0 """ - self._cxx_save(fname, name, key, description, overwrite, compression) + self._cxx_save(fname, name, key, description, overwrite, compression, basis) def restore(self, fname, name=None, key=None): """ diff --git a/interfaces/cython/cantera/solutionbase.pxd b/interfaces/cython/cantera/solutionbase.pxd index aa56e6fb98..a00322e3ef 100644 --- a/interfaces/cython/cantera/solutionbase.pxd +++ b/interfaces/cython/cantera/solutionbase.pxd @@ -89,7 +89,7 @@ cdef extern from "cantera/base/SolutionArray.h" namespace "Cantera": CxxAnyMap getAuxiliary(size_t) except +translate_exception void setAuxiliary(size_t, CxxAnyMap&) except +translate_exception void append(vector[double]&, CxxAnyMap&) except +translate_exception - void save(string&, string&, string&, string&, cbool, int) except +translate_exception + void save(string&, string&, string&, string&, cbool, int, string&) except +translate_exception CxxAnyMap restore(string&, string&, string&) except +translate_exception cdef shared_ptr[CxxSolutionArray] CxxNewSolutionArray "Cantera::SolutionArray::create" ( diff --git a/interfaces/cython/cantera/solutionbase.pyx b/interfaces/cython/cantera/solutionbase.pyx index 108371beb2..8126d816d2 100644 --- a/interfaces/cython/cantera/solutionbase.pyx +++ b/interfaces/cython/cantera/solutionbase.pyx @@ -674,11 +674,12 @@ cdef class SolutionArrayBase: cxx_state.push_back(item) self.base.append(cxx_state, py_to_anymap(extra)) - def _cxx_save(self, filename, name, key, description, overwrite, compression): + def _cxx_save(self, filename, name, key, description, + overwrite, compression, basis): """ Interface `SolutionArray.save` with C++ core """ self.base.save( stringify(str(filename)), stringify(name), stringify(key), - stringify(description), overwrite, compression) + stringify(description), overwrite, compression, stringify(basis)) def _cxx_restore(self, filename, name, key): """ Interface `SolutionArray.restore` with C++ core """ diff --git a/src/base/SolutionArray.cpp b/src/base/SolutionArray.cpp index f35ea2c956..17cdca978c 100644 --- a/src/base/SolutionArray.cpp +++ b/src/base/SolutionArray.cpp @@ -966,7 +966,7 @@ void SolutionArray::writeHeader(AnyMap& root, const string& id, data.update(preamble(desc)); } -void SolutionArray::writeEntry(const string& fname, bool overwrite) +void SolutionArray::writeEntry(const string& fname, bool overwrite, const string& basis) { if (apiNdim() != 1) { throw CanteraError("SolutionArray::writeEntry", @@ -976,8 +976,18 @@ void SolutionArray::writeEntry(const string& fname, bool overwrite) for (const auto& species : m_sol->thermo()->speciesNames()) { speciesNames.insert(species); } - const auto& nativeState = m_sol->thermo()->nativeState(); - bool mole = nativeState.find("X") != nativeState.end(); + bool mole; + if (basis == "") { + const auto& nativeState = m_sol->thermo()->nativeState(); + mole = nativeState.find("X") != nativeState.end(); + } else if (basis == "X" || basis == "mole") { + mole = true; + } else if (basis == "Y" || basis == "mass") { + mole = false; + } else { + throw CanteraError("SolutionArray::writeEntry", + "Invalid species basis '{}'.", basis); + } auto names = componentNames(); size_t last = names.size() - 1; @@ -1002,7 +1012,7 @@ void SolutionArray::writeEntry(const string& fname, bool overwrite) key); } } else { - // Delay reading species data as basis can be either mole or mass + // Delay reading species data as base can be either mole or mass isSpecies.push_back(true); components.emplace_back(AnyValue()); col = components.size() - 1; @@ -1265,7 +1275,8 @@ void SolutionArray::append(const vector& state, const AnyMap& extra) } void SolutionArray::save(const string& fname, const string& id, const string& sub, - const string& desc, bool overwrite, int compression) + const string& desc, bool overwrite, int compression, + const string& basis) { if (m_size < m_dataSize) { throw NotImplementedError("SolutionArray::save", @@ -1273,6 +1284,17 @@ void SolutionArray::save(const string& fname, const string& id, const string& su } size_t dot = fname.find_last_of("."); string extension = (dot != npos) ? toLowerCopy(fname.substr(dot + 1)) : ""; + if (extension == "csv") { + if (id != "") { + warn_user("SolutionArray::save", "Parameter 'id' not used for CSV output."); + } + writeEntry(fname, overwrite, basis); + return; + } + if (basis != "") { + warn_user("SolutionArray::save", + "Species basis '{}' not implemented for HDF5 or YAML output.", basis); + } if (extension == "h5" || extension == "hdf" || extension == "hdf5") { writeHeader(fname, id, desc, overwrite); writeEntry(fname, id, sub, true, compression); @@ -1293,13 +1315,6 @@ void SolutionArray::save(const string& fname, const string& id, const string& su AnyMap::clearCachedFile(fname); return; } - if (extension == "csv") { - if (id != "") { - warn_user("SolutionArray::save", "Parameter 'id' not used for CSV output."); - } - writeEntry(fname, overwrite); - return; - } throw CanteraError("SolutionArray::save", "Unknown file extension '{}'.", extension); } From a3b57c2b3a1ad20eea062940fdec9b82f855c78c Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Tue, 20 Jun 2023 08:49:11 -0600 Subject: [PATCH 09/19] [base] Clarify exception for CSV SolutionArray::restore --- src/base/SolutionArray.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/base/SolutionArray.cpp b/src/base/SolutionArray.cpp index 17cdca978c..871c31085f 100644 --- a/src/base/SolutionArray.cpp +++ b/src/base/SolutionArray.cpp @@ -1367,6 +1367,11 @@ AnyMap SolutionArray::restore(const string& fname, size_t dot = fname.find_last_of("."); string extension = (dot != npos) ? toLowerCopy(fname.substr(dot + 1)) : ""; AnyMap header; + if (extension == "csv") { + throw NotImplementedError("SolutionArray::restore", + "CSV import not implemented; if using Python, data can be imported via " + "'read_csv' instead."); + } if (extension == "h5" || extension == "hdf" || extension == "hdf5") { readEntry(fname, id, sub); header = readHeader(fname, id); From 8c4e4106e5f10686f59d22433637ce4ba0ca5a8c Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Tue, 20 Jun 2023 08:56:47 -0600 Subject: [PATCH 10/19] [base] Increase precision for CSV output --- src/base/SolutionArray.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/base/SolutionArray.cpp b/src/base/SolutionArray.cpp index 871c31085f..7d1afceeff 100644 --- a/src/base/SolutionArray.cpp +++ b/src/base/SolutionArray.cpp @@ -1048,7 +1048,8 @@ void SolutionArray::writeEntry(const string& fname, bool overwrite, const string std::remove(fname.c_str()); } std::ofstream output(fname); - output << header.str() << std::endl; + const auto default_precision = output.precision(); + output << header.str() << std::endl << std::setprecision(9); vector buf(speciesNames.size(), 0.); for (size_t row = 0; row < m_size; row++) { @@ -1092,7 +1093,7 @@ void SolutionArray::writeEntry(const string& fname, bool overwrite, const string } output << std::endl; } - output << std::endl; + output << std::endl << std::setprecision(default_precision); if (escaped) { warn_user("SolutionArray::writeEntry", From a4796933a6234e76e297c03f2075577b31f9d1a1 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Tue, 20 Jun 2023 09:17:24 -0600 Subject: [PATCH 11/19] [Python] Add missing SolutionArrayBase.resize method --- interfaces/cython/cantera/solutionbase.pyx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/interfaces/cython/cantera/solutionbase.pyx b/interfaces/cython/cantera/solutionbase.pyx index 8126d816d2..b7f657bfab 100644 --- a/interfaces/cython/cantera/solutionbase.pyx +++ b/interfaces/cython/cantera/solutionbase.pyx @@ -610,6 +610,10 @@ cdef class SolutionArrayBase: out.append(pystr(item)) return out + def resize(self, size): + """ Resize `SolutionArrayBase` to given size """ + self.base.resize(size) + def _has_component(self, name): """ Check whether `SolutionArrayBase` has component """ return self.base.hasComponent(stringify(name)) From 2f00a0bd84d282221a68e06b185911ee26768c18 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Tue, 20 Jun 2023 09:40:12 -0600 Subject: [PATCH 12/19] [Python] Deprecate SolutionArray.write_csv --- interfaces/cython/cantera/composite.py | 48 ++++++++++++++++++-------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/interfaces/cython/cantera/composite.py b/interfaces/cython/cantera/composite.py index ba401d1e19..6ec2e49966 100644 --- a/interfaces/cython/cantera/composite.py +++ b/interfaces/cython/cantera/composite.py @@ -6,6 +6,7 @@ import numpy as np import csv as _csv import importlib.metadata +import warnings _h5py = None def _import_h5py(): @@ -447,30 +448,30 @@ class SolutionArray(SolutionArrayBase): >>> s.reaction_equation(10) 'CH4 + O <=> CH3 + OH' - Data represented 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 to a CSV file using the + `save` method:: - >>> states.write_csv('somefile.csv', cols=('T', 'P', 'X', 'net_rates_of_progress')) + >>> states.save('somefile.csv', basis="mole') - As long as stored columns specify a valid thermodynamic state, the contents of - a `SolutionArray` can be restored using the `read_csv` method:: - - >>> states = ct.SolutionArray(gas) - >>> states.read_csv('somefile.csv') - - As an alternative to comma separated export and import, data extracted from - `SolutionArray` objects can also be saved to and restored from YAML and HDF - container files using the `save` function:: + As an alternative to the CSV format, `SolutionArray` objects can also be saved in + YAML or HDF formats, where the keyword argument ``id`` allows for saving and + accessing of multiple solutions in a single container file:: >>> states.save('somefile.yaml', id='some_key') - and `restore` methods:: + YAML and HDF files can be read back into `SolutionArray` objects using the + `restore` method:: >>> states = ct.SolutionArray(gas) >>> states.restore('somefile.yaml', id='some_key') - For YAML and HDF export and import, the keyword argument ``id`` allows for saving - and accessing of multiple solutions in a single container file. + As long as stored columns in a CSV file specify a valid thermodynamic state, the + contents of a `SolutionArray` can be restored using the `read_csv` method, which + is specific to the Python API:: + + >>> states = ct.SolutionArray(gas) + >>> states.read_csv('somefile.csv') + Note that `save` and `restore` for HDF requires Cantera to be compiled with HDF support, as it depends on external *HighFive* and *HDF5* libraries. @@ -1162,7 +1163,14 @@ def write_csv(self, filename, cols=None, *args, **kwargs): Additional arguments are passed on to `collect_data`. This method works only with 1D `SolutionArray` objects. + + .. deprecated:: 3.0 + + Method to be removed after Cantera 3.0; superseded by `save`. Note that + `write_csv` does not support escaping of commas within string entries. """ + warnings.warn("'write_csv' is superseded by 'save' and will be removed " + "after Cantera 3.0.", DeprecationWarning) data_dict = self.collect_data(*args, cols=cols, tabular=True, **kwargs) data = np.hstack([d[:, np.newaxis] for d in data_dict.values()]) labels = list(data_dict.keys()) @@ -1181,6 +1189,16 @@ def read_csv(self, filename, normalize=True): The ``normalize`` argument is passed on to `restore_data` to normalize mole or mass fractions. By default, ``normalize`` is ``True``. """ + try: + # pandas handles escaped entries correctly + _import_pandas() + df = _pandas.read_csv(filename) + self.from_pandas(df) + return + except ImportError: + pass + + # fall back to numpy; this works unless CSV file contains escaped entries if np.lib.NumpyVersion(np.__version__) < "1.14.0": # bytestring needs to be converted for columns containing strings data = np.genfromtxt(filename, delimiter=',', deletechars='', From 894afa2c9e57274c31dbeb3f03a4ea4f8b9c9022 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Tue, 20 Jun 2023 09:55:53 -0600 Subject: [PATCH 13/19] [samples] Replace write_csv with save --- samples/python/onedim/adiabatic_flame.py | 2 +- samples/python/onedim/burner_flame.py | 2 +- samples/python/onedim/diffusion_flame.py | 2 +- samples/python/onedim/flame_fixed_T.py | 2 +- samples/python/onedim/flame_initial_guess.py | 2 +- samples/python/onedim/ion_burner_flame.py | 2 +- samples/python/onedim/ion_free_flame.py | 2 +- samples/python/onedim/premixed_counterflow_flame.py | 2 +- samples/python/onedim/premixed_counterflow_twin_flame.py | 2 +- samples/python/onedim/stagnation_flame.py | 2 +- samples/python/surface_chemistry/catalytic_combustion.py | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/samples/python/onedim/adiabatic_flame.py b/samples/python/onedim/adiabatic_flame.py index 7eb220cc0c..9e0c6bde38 100644 --- a/samples/python/onedim/adiabatic_flame.py +++ b/samples/python/onedim/adiabatic_flame.py @@ -52,4 +52,4 @@ f.save(output, name="multi", description="solution with multicomponent transport") # write the velocity, temperature, density, and mole fractions to a CSV file -f.write_csv('adiabatic_flame.csv', quiet=False) +f.save('adiabatic_flame.csv', basis="mole") diff --git a/samples/python/onedim/burner_flame.py b/samples/python/onedim/burner_flame.py index a641a0fd4e..4d36ae5f5d 100644 --- a/samples/python/onedim/burner_flame.py +++ b/samples/python/onedim/burner_flame.py @@ -40,4 +40,4 @@ f.show() f.save(output, name="multi", description="solution with multicomponent transport") -f.write_csv('burner_flame.csv', quiet=False) +f.save('burner_flame.csv.csv', basis="mole") diff --git a/samples/python/onedim/diffusion_flame.py b/samples/python/onedim/diffusion_flame.py index d6d4ebec77..f0d5f607cf 100644 --- a/samples/python/onedim/diffusion_flame.py +++ b/samples/python/onedim/diffusion_flame.py @@ -64,7 +64,7 @@ f.save(output) # write the velocity, temperature, and mole fractions to a CSV file -f.write_csv('diffusion_flame.csv', quiet=False) +f.save('diffusion_flame.csv', basis="mole") f.show_stats(0) diff --git a/samples/python/onedim/flame_fixed_T.py b/samples/python/onedim/flame_fixed_T.py index 3243ed3004..cb473914d4 100644 --- a/samples/python/onedim/flame_fixed_T.py +++ b/samples/python/onedim/flame_fixed_T.py @@ -78,5 +78,5 @@ f.save(output, name="multi", description="solution with multicomponent transport") # write the velocity, temperature, density, and mole fractions to a CSV file -f.write_csv('flame_fixed_T.csv', quiet=False) +f.save('flame_fixed_T.csv', basis="mole") f.show_stats() diff --git a/samples/python/onedim/flame_initial_guess.py b/samples/python/onedim/flame_initial_guess.py index 197b844858..e7c2037f0b 100644 --- a/samples/python/onedim/flame_initial_guess.py +++ b/samples/python/onedim/flame_initial_guess.py @@ -59,7 +59,7 @@ def describe(flame): print("Save CSV") csv_filepath = output_path / "flame.csv" -f.write_csv(csv_filepath) +f.save(csv_filepath, basis="mole") if "native" in ct.hdf_support(): # HDF is not a required dependency diff --git a/samples/python/onedim/ion_burner_flame.py b/samples/python/onedim/ion_burner_flame.py index 53c80231a8..e48204a9b2 100644 --- a/samples/python/onedim/ion_burner_flame.py +++ b/samples/python/onedim/ion_burner_flame.py @@ -36,4 +36,4 @@ f.save(output, name="mix", description="solution with mixture-averaged transport") -f.write_csv('ion_burner_flame.csv', quiet=False) +f.save('ion_burner_flame.csv', basis="mole") diff --git a/samples/python/onedim/ion_free_flame.py b/samples/python/onedim/ion_free_flame.py index 8271249791..47f443fb63 100644 --- a/samples/python/onedim/ion_free_flame.py +++ b/samples/python/onedim/ion_free_flame.py @@ -44,4 +44,4 @@ print(f"mixture-averaged flamespeed = {f.velocity[0]:7f} m/s") # write the velocity, temperature, density, and mole fractions to a CSV file -f.write_csv('ion_free_flame.csv', quiet=False) +f.save('ion_free_flame.csv', basis="mole") diff --git a/samples/python/onedim/premixed_counterflow_flame.py b/samples/python/onedim/premixed_counterflow_flame.py index 179bea1b7c..7d11d0fc05 100644 --- a/samples/python/onedim/premixed_counterflow_flame.py +++ b/samples/python/onedim/premixed_counterflow_flame.py @@ -53,6 +53,6 @@ sim.save(output, name="mix", description="solution with mixture-averaged transport") # write the velocity, temperature, and mole fractions to a CSV file -sim.write_csv("premixed_counterflow_flame.csv", quiet=False) +sim.save("premixed_counterflow_flame.csv", basis="mole") sim.show_stats() sim.show() diff --git a/samples/python/onedim/premixed_counterflow_twin_flame.py b/samples/python/onedim/premixed_counterflow_twin_flame.py index b36886f13c..61cf7f4676 100644 --- a/samples/python/onedim/premixed_counterflow_twin_flame.py +++ b/samples/python/onedim/premixed_counterflow_twin_flame.py @@ -127,7 +127,7 @@ def solveOpposedFlame(oppFlame, massFlux=0.12, loglevel=1, print(f"Peak temperature: {T:.1f} K") print(f"Strain Rate: {K:.1f} 1/s") print(f"Consumption Speed: {Sc * 100:.2f} cm/s") -oppFlame.write_csv("premixed_counterflow_twin_flame.csv", quiet=False) +oppFlame.save("premixed_counterflow_twin_flame.csv", basis="mole") # Generate plots to see results, if user desires if '--plot' in sys.argv: diff --git a/samples/python/onedim/stagnation_flame.py b/samples/python/onedim/stagnation_flame.py index 0a42f78586..cf71c433b7 100644 --- a/samples/python/onedim/stagnation_flame.py +++ b/samples/python/onedim/stagnation_flame.py @@ -85,6 +85,6 @@ sim.save(output, name=f"mdot-{m}", description=f"mdot = {md} kg/m2/s") # write the velocity, temperature, and mole fractions to a CSV file - sim.write_csv(output_path / f"stagnation_flame_{m}.csv", quiet=False) + sim.save(output_path / f"stagnation_flame_{m}.csv", basis="mole") sim.show_stats() diff --git a/samples/python/surface_chemistry/catalytic_combustion.py b/samples/python/surface_chemistry/catalytic_combustion.py index 49867f3811..b0ee6def80 100644 --- a/samples/python/surface_chemistry/catalytic_combustion.py +++ b/samples/python/surface_chemistry/catalytic_combustion.py @@ -117,6 +117,6 @@ sim.save(filename, "soln1", description="catalytic combustion example") # save selected solution components in a CSV file for plotting in Excel or MATLAB. -sim.write_csv('catalytic_combustion.csv', quiet=False) +sim.save('catalytic_combustion.csv', basis="mole") sim.show_stats(0) From fdb436734db0d318885c17ddafa7275868a83244 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Tue, 20 Jun 2023 10:22:00 -0600 Subject: [PATCH 14/19] [oneD] Replace write_csv by save --- include/cantera/oneD/Sim1D.h | 11 +++++++++-- interfaces/cython/cantera/_onedim.pxd | 2 +- interfaces/cython/cantera/_onedim.pyx | 16 +++++++++++----- interfaces/cython/cantera/onedim.py | 8 +++++++- src/oneD/Sim1D.cpp | 17 ++++++++++++++++- 5 files changed, 44 insertions(+), 10 deletions(-) diff --git a/include/cantera/oneD/Sim1D.h b/include/cantera/oneD/Sim1D.h index 8f75143639..63b8adae01 100644 --- a/include/cantera/oneD/Sim1D.h +++ b/include/cantera/oneD/Sim1D.h @@ -142,15 +142,22 @@ class Sim1D : public OneDim const std::string& desc, int loglevel); /** - * Save the current solution to a container file. + * Save the current solution to a container or CSV file. + * + * For HDF and YAML, the entire content of the object is saved; for CSV, only the + * main 1D domain is saved. + * * @param fname Name of output container file * @param id Identifier of solution within the container file * @param desc Description of the solution * @param overwrite Force overwrite if name exists; optional (default=false) * @param compression Compression level (optional; HDF only) + * @param basis Output mass ("Y"/"mass") or mole ("X"/"mole") fractions (CSV only); + * if omitted (default=""), the native storage mode is used */ void save(const std::string& fname, const std::string& id, - const std::string& desc, bool overwrite=false, int compression=0); + const std::string& desc, bool overwrite=false, int compression=0, + const string& basis=""); /** * Save the residual of the current solution to a container file. diff --git a/interfaces/cython/cantera/_onedim.pxd b/interfaces/cython/cantera/_onedim.pxd index 432535147c..8efe5d215b 100644 --- a/interfaces/cython/cantera/_onedim.pxd +++ b/interfaces/cython/cantera/_onedim.pxd @@ -114,7 +114,7 @@ cdef extern from "cantera/oneD/Sim1D.h": void refine(int) except +translate_exception void setRefineCriteria(size_t, double, double, double, double) except +translate_exception vector[double] getRefineCriteria(int) except +translate_exception - void save(string&, string&, string&, cbool, int) except +translate_exception + void save(string&, string&, string&, cbool, int, string&) except +translate_exception CxxAnyMap restore(string&, string&) except +translate_exception void writeStats(int) except +translate_exception void clearStats() diff --git a/interfaces/cython/cantera/_onedim.pyx b/interfaces/cython/cantera/_onedim.pyx index 724f2c331a..01c8584edb 100644 --- a/interfaces/cython/cantera/_onedim.pyx +++ b/interfaces/cython/cantera/_onedim.pyx @@ -1537,20 +1537,26 @@ cdef class Sim1D: return self.sim.fixedTemperatureLocation() def save(self, filename='soln.yaml', name='solution', description=None, - loglevel=None, overwrite=False, compression=0): + loglevel=None, *, overwrite=False, compression=0, basis=None): """ - Save the solution in YAML or HDF format. + Save the solution to a container or CSV format. + + For HDF and YAML files, the entire content of the `Sim1D` object is saved; for + CSV, only the main 1D domain is saved. :param filename: solution file :param name: - solution name within the file + solution name within the file (HDF/YAML only) :param description: - custom description text + custom description text (HDF/YAML only) :param overwrite: Force overwrite if name exists; optional (default=`False`) :param compression: compression level 0..9; optional (HDF only) + :param basis: + Output mass (``Y``/``mass``) or mole (``X``/``mole``) fractions (CSV only); + if not specified (`None`), the native storage mode is used >>> s.save(filename='save.yaml', name='energy_off', ... description='solution with energy eqn. disabled') @@ -1562,7 +1568,7 @@ cdef class Sim1D: warnings.warn("Argument 'loglevel' is deprecated and will be ignored.", DeprecationWarning) self.sim.save(stringify(str(filename)), stringify(name), - stringify(description), overwrite, compression) + stringify(description), overwrite, compression, stringify(basis)) def restore(self, filename='soln.yaml', name='solution', loglevel=None): """Set the solution vector to a previously-saved solution. diff --git a/interfaces/cython/cantera/onedim.py b/interfaces/cython/cantera/onedim.py index 53e1a0a5af..0e7a555551 100644 --- a/interfaces/cython/cantera/onedim.py +++ b/interfaces/cython/cantera/onedim.py @@ -406,7 +406,13 @@ def write_csv(self, filename, species='X', quiet=True, normalize=True): :param normalize: Boolean flag to indicate whether the mole/mass fractions should be normalized. + + .. deprecated:: 3.0 + + Method to be removed after Cantera 3.0; superseded by `save`. """ + warnings.warn("'write_csv' is superseded by 'save' and will be removed " + "after Cantera 3.0.", DeprecationWarning) # save data cols = ('extra', 'T', 'D', species) @@ -773,7 +779,7 @@ def getter(self): 'entropy_mass', 'g', 'gibbs_mole', 'gibbs_mass', 'cv', 'cv_mole', 'cv_mass', 'cp', 'cp_mole', 'cp_mass', 'isothermal_compressibility', 'thermal_expansion_coeff', - 'sound_speed', 'viscosity', 'thermal_conductivity', + 'sound_speed', 'viscosity', 'thermal_conductivity', 'heat_release_rate', 'mean_molecular_weight']: setattr(FlameBase, _attr, _array_property(_attr)) FlameBase.volume = _array_property('v') # avoid confusion with velocity gradient 'V' diff --git a/src/oneD/Sim1D.cpp b/src/oneD/Sim1D.cpp index 682caf91a6..b290cb3e26 100644 --- a/src/oneD/Sim1D.cpp +++ b/src/oneD/Sim1D.cpp @@ -126,10 +126,25 @@ void Sim1D::save(const std::string& fname, const std::string& id, } void Sim1D::save(const std::string& fname, const std::string& id, - const std::string& desc, bool overwrite, int compression) + const std::string& desc, bool overwrite, int compression, + const string& basis) { size_t dot = fname.find_last_of("."); string extension = (dot != npos) ? toLowerCopy(fname.substr(dot+1)) : ""; + if (extension == "csv") { + for (auto dom : m_dom) { + auto arr = dom->asArray(m_state->data() + dom->loc()); + if (dom->size() > 1) { + arr->writeEntry(fname, overwrite, basis); + break; + } + } + return; + } + if (basis != "") { + warn_user("Sim1D::save", + "Species basis '{}' not implemented for HDF5 or YAML output.", basis); + } if (extension == "h5" || extension == "hdf" || extension == "hdf5") { SolutionArray::writeHeader(fname, id, desc, overwrite); for (auto dom : m_dom) { From 24934ae785ee293fc414b3c06d3da43101e40053 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Mon, 19 Jun 2023 20:48:32 -0600 Subject: [PATCH 15/19] [unittests] Add coverage for CSV SolutionArray.save --- test/python/test_composite.py | 131 +++++++++++++++++++++++++++++----- test/python/test_onedim.py | 26 +++++-- 2 files changed, 133 insertions(+), 24 deletions(-) diff --git a/test/python/test_composite.py b/test/python/test_composite.py index f248ba082b..76e7a51c1b 100644 --- a/test/python/test_composite.py +++ b/test/python/test_composite.py @@ -461,7 +461,23 @@ def test_import_no_norm_data(self): self.assertArrayNear(states.P, b.P) self.assertArrayNear(states.X, b.X) - def test_write_csv(self): + def check_arrays(self, a, b, rtol=1e-8): + self.assertArrayNear(a.T, b.T, rtol=rtol) + self.assertArrayNear(a.P, b.P, rtol=rtol) + self.assertArrayNear(a.X, b.X, rtol=rtol) + for key in a.extra: + value = getattr(a, key) + if isinstance(value[0], str): + assert (getattr(b, key) == value).all() + else: + self.assertArrayNear(getattr(b, key), value, rtol=rtol) + if b.meta: + # not all output formats preserve metadata + for key, value in a.meta.items(): + assert b.meta[key] == value + + @pytest.mark.usefixtures("allow_deprecated") + def test_write_csv_legacy(self): states = ct.SolutionArray(self.gas, 7) states.TPX = np.linspace(300, 1000, 7), 2e5, 'H2:0.5, O2:0.4' states.equilibrate('HP') @@ -476,10 +492,9 @@ def test_write_csv(self): b = ct.SolutionArray(self.gas) b.read_csv(outfile) - self.assertArrayNear(states.T, b.T) - self.assertArrayNear(states.P, b.P) - self.assertArrayNear(states.X, b.X) + self.check_arrays(states, b) + @pytest.mark.usefixtures("allow_deprecated") def test_write_csv_single_row(self): gas = ct.Solution("gri30.yaml") states = ct.SolutionArray(gas) @@ -491,10 +506,9 @@ def test_write_csv_single_row(self): b = ct.SolutionArray(gas) b.read_csv(outfile) - self.assertArrayNear(states.T, b.T) - self.assertArrayNear(states.P, b.P) - self.assertArrayNear(states.X, b.X) + self.check_arrays(states, b) + @pytest.mark.usefixtures("allow_deprecated") def test_write_csv_str_column(self): states = ct.SolutionArray(self.gas, 3, extra={'spam': 'eggs'}) @@ -504,7 +518,9 @@ def test_write_csv_str_column(self): b = ct.SolutionArray(self.gas, extra={'spam'}) b.read_csv(outfile) self.assertEqual(list(states.spam), list(b.spam)) + self.check_arrays(states, b) + @pytest.mark.usefixtures("allow_deprecated") def test_write_csv_multidim_column(self): states = ct.SolutionArray(self.gas, 3, extra={'spam': np.zeros((3, 5,))}) @@ -512,6 +528,93 @@ def test_write_csv_multidim_column(self): with self.assertRaisesRegex(NotImplementedError, 'not supported'): states.write_csv(outfile) + def test_write_csv(self): + outfile = self.test_work_path / "solutionarray_new.csv" + outfile.unlink(missing_ok=True) + + arr = ct.SolutionArray(self.gas, 7) + arr.TPX = np.linspace(300, 1000, 7), 2e5, "H2:0.5, O2:0.4" + arr.equilibrate("HP") + arr.save(outfile, basis="mole") + + with open(outfile, "r") as fid: + header = fid.readline() + assert "X_H2" in header.split(",") + + b = ct.SolutionArray(self.gas) + b.read_csv(outfile) + self.check_arrays(arr, b) + + with pytest.raises(ct.CanteraError, match="already exists"): + arr.save(outfile) + + def test_write_csv_fancy(self): + outfile = self.test_work_path / "solutionarray_fancy.csv" + outfile.unlink(missing_ok=True) + + extra = {"foo": range(7), "bar": range(7), "spam": "eggs"} + arr = ct.SolutionArray(self.gas, 7, extra=extra) + arr.TPX = np.linspace(300, 1000, 7), 2e5, "H2:0.5, O2:0.4" + arr.equilibrate("HP") + arr.save(outfile) + + with open(outfile, "r") as fid: + header = fid.readline() + assert "Y_H2" in header.split(",") + + b = ct.SolutionArray(self.gas) + b.read_csv(outfile) + self.check_arrays(arr, b) + + def test_write_csv_escaped(self): + outfile = self.test_work_path / "solutionarray_escaped.csv" + outfile.unlink(missing_ok=True) + + extra = {"foo": range(7), "bar": range(7), "spam,eggs": "a,b,"} + arr = ct.SolutionArray(self.gas, 7, extra=extra) + arr.TPX = np.linspace(300, 1000, 7), 2e5, "H2:0.5, O2:0.4" + arr.equilibrate("HP") + with pytest.warns(UserWarning, match="escaped"): + arr.save(outfile, basis="mass") + + with open(outfile, "r") as fid: + header = fid.readline() + assert "Y_H2" in header.split(",") + + b = ct.SolutionArray(self.gas) + if _pandas is None: + with pytest.raises(ValueError): + # np.genfromtxt does not support escaped characters + b.read_csv(outfile) + return + + b.read_csv(outfile) + self.check_arrays(arr, b) + + df = _pandas.read_csv(outfile) + b.from_pandas(df) + self.check_arrays(arr, b) + + def test_write_csv_exceptions(self): + outfile = self.test_work_path / f"solutionarray_invalid.csv" + outfile.unlink(missing_ok=True) + + arr = ct.SolutionArray(self.gas, (2, 5)) + with pytest.raises(ct.CanteraError, match="only works for 1D SolutionArray"): + arr.save(outfile) + + arr = ct.SolutionArray(self.gas, 10, extra={'spam"eggs': "foo"}) + with pytest.raises(NotImplementedError, match="double quotes or line feeds"): + arr.save(outfile) + + arr = ct.SolutionArray(self.gas, 10, extra={"foo": 'spam\neggs'}) + with pytest.raises(NotImplementedError, match="double quotes or line feeds"): + arr.save(outfile) + + arr = ct.SolutionArray(self.gas, 10) + with pytest.raises(ct.CanteraError, match="Invalid species basis"): + arr.save(outfile, basis="foo") + @utilities.unittest.skipIf(_pandas is None, "pandas is not installed") def test_to_pandas(self): states = ct.SolutionArray(self.gas, 7, extra={"props": range(7)}) @@ -524,7 +627,6 @@ def test_to_pandas(self): @pytest.mark.skipif("native" not in ct.hdf_support(), reason="Cantera compiled without HDF support") - @utilities.unittest.skipIf(_h5py is None, "h5py is not installed") def test_write_hdf(self): outfile = self.test_work_path / "solutionarray_fancy.h5" outfile.unlink(missing_ok=True) @@ -539,13 +641,7 @@ def test_write_hdf(self): b = ct.SolutionArray(self.gas) attr = b.restore(outfile, "group0") - self.assertArrayNear(states.T, b.T) - self.assertArrayNear(states.P, b.P) - self.assertArrayNear(states.X, b.X) - self.assertArrayNear(states.foo, b.foo) - self.assertArrayNear(states.bar, b.bar) - self.assertEqual(b.meta['spam'], 'eggs') - self.assertEqual(b.meta['hello'], 'world') + self.check_arrays(states, b) @pytest.mark.skipif("native" not in ct.hdf_support(), reason="Cantera compiled without HDF support") @@ -564,7 +660,7 @@ def run_write_str_column(self, mode): b = ct.SolutionArray(self.gas, extra={'spam'}) b.restore(outfile, "arr") - self.assertEqual(list(states.spam), list(b.spam)) + self.check_arrays(states, b) @pytest.mark.skipif("native" not in ct.hdf_support(), reason="Cantera compiled without HDF support") @@ -583,7 +679,7 @@ def run_write_multidim_column(self, mode): b = ct.SolutionArray(self.gas, extra={'spam'}) b.restore(outfile, "arr") - self.assertArrayNear(states.spam, b.spam) + self.check_arrays(states, b) @pytest.mark.skipif("native" not in ct.hdf_support(), reason="Cantera compiled without HDF support") @@ -604,6 +700,7 @@ def run_write_2d(self, mode): b.restore(outfile, "arr") assert b.shape == states.shape + class TestLegacyHDF(utilities.CanteraTest): # Test SolutionArray legacy HDF file input # diff --git a/test/python/test_onedim.py b/test/python/test_onedim.py index 52c06961e5..be6dac1d5f 100644 --- a/test/python/test_onedim.py +++ b/test/python/test_onedim.py @@ -335,7 +335,7 @@ def run_restart(self, mode): data = self.test_work_path / f"freeflame_restart.{mode}" data.unlink(missing_ok=True) if mode == "csv": - self.sim.write_csv(data) + self.sim.save(data, basis="mole") else: self.sim.save(data, group) @@ -761,7 +761,8 @@ def test_save_restore_remove_species_yaml(self): k1 = gas1.species_index(species) self.assertArrayNear(Y1[k1], Y2[k2]) - def test_write_csv(self): + @pytest.mark.usefixtures("allow_deprecated") + def test_write_csv_legacy(self): filename = self.test_work_path / "onedim-write_csv.csv" # In Python >= 3.8, this can be replaced by the missing_ok argument if filename.is_file(): @@ -776,6 +777,19 @@ def test_write_csv(self): k = self.gas.species_index('H2') self.assertArrayNear(data.X[:, k], self.sim.X[k, :]) + def test_write_csv(self): + filename = self.test_work_path / "onedim-save.csv" + filename.unlink(missing_ok=True) + + self.create_sim(2e5, 350, 'H2:1.0, O2:2.0', mech="h2o2.yaml") + self.sim.save(filename, basis="mole") + data = ct.SolutionArray(self.gas) + data.read_csv(filename) + self.assertArrayNear(data.grid, self.sim.grid) + self.assertArrayNear(data.T, self.sim.T) + k = self.gas.species_index('H2') + self.assertArrayNear(data.X[:, k], self.sim.X[k, :]) + @pytest.mark.usefixtures("allow_deprecated") @utilities.unittest.skipIf("h5py" not in ct.hdf_support(), "h5py not installed") def test_restore_legacy_hdf_h5py(self): @@ -1139,7 +1153,7 @@ def test_mixture_averaged_rad(self, saveReference=False): if filename.is_file(): filename.unlink() - self.sim.write_csv(filename) # check output + self.sim.save(filename, basis="mole") # check output self.assertTrue(filename.is_file()) csv_data = np.genfromtxt(filename, dtype=float, delimiter=',', names=True) self.assertIn('radiativeheatloss', csv_data.dtype.names) @@ -1245,11 +1259,9 @@ def test_mixture_averaged(self, saveReference=False): self.assertFalse(bad, bad) filename = self.test_work_path / "CounterflowPremixedFlame-h2-mix.csv" - # In Python >= 3.8, this can be replaced by the missing_ok argument - if filename.is_file(): - filename.unlink() + filename.unlink(missing_ok=True) - sim.write_csv(filename) # check output + sim.save(filename) # check output self.assertTrue(filename.is_file()) csv_data = np.genfromtxt(filename, dtype=float, delimiter=',', names=True) self.assertNotIn('qdot', csv_data.dtype.names) From 8f3882ccf70de9a5bd749dbd666d71be5cc24c91 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Wed, 21 Jun 2023 08:44:08 -0600 Subject: [PATCH 16/19] Improve SolutionArray IO documentation and exceptions Resolve discrepancies of nomenclature in C++/Python --- include/cantera/base/SolutionArray.h | 108 +++++++++++++++---------- interfaces/cython/cantera/_onedim.pyx | 8 +- interfaces/cython/cantera/composite.py | 62 +++++++++----- interfaces/cython/cantera/onedim.py | 4 +- src/base/SolutionArray.cpp | 12 +-- test/python/test_composite.py | 3 +- 6 files changed, 118 insertions(+), 79 deletions(-) diff --git a/include/cantera/base/SolutionArray.h b/include/cantera/base/SolutionArray.h index de0fff9eab..7d3587a9eb 100644 --- a/include/cantera/base/SolutionArray.h +++ b/include/cantera/base/SolutionArray.h @@ -22,8 +22,6 @@ class ThermoPhase; * stored, reshaping operations need to be implemented in high-level API's. * * @since New in Cantera 3.0. - * @warning This class is an experimental part of the %Cantera API and may be - * changed or removed without notice. */ class SolutionArray { @@ -189,119 +187,147 @@ class SolutionArray void append(const vector& state, const AnyMap& extra); /*! - * Write header data to container file. + * Write header data to a HDF container file. * * @param fname Name of HDF container file - * @param id Identifier of root location within the container file - * @param desc Description - * @param overwrite Force overwrite if id exists; optional (default=false) + * @param id Identifier of group holding header information + * @param desc Custom comment describing dataset + * @param overwrite Force overwrite if file/group exists; optional (default=false) */ static void writeHeader(const string& fname, const string& id, const string& desc, bool overwrite=false); /*! - * Write header data to AnyMap. + * Write header data to AnyMap; used by YAML serialization. * * @param root Root node of AnyMap structure - * @param id Identifier of root location within the container file - * @param desc Description - * @param overwrite Force overwrite if id exists; optional (default=false) + * @param id Identifier of node holding header information + * @param desc Custom comment describing dataset + * @param overwrite Force overwrite if node exists; optional (default=false) */ static void writeHeader(AnyMap& root, const string& id, const string& desc, bool overwrite=false); /*! - * Write SolutionArray data to comma-separated data file. + * Write SolutionArray data to a CSV file. * * @param fname Name of CSV file * @param overwrite Force overwrite if file exists; optional (default=false) * @param basis Output mass ("Y"/"mass") or mole ("X"/"mole") fractions; - * if not specified (""), the native storage mode is used + * if omitted (default=""), the native basis of the underlying ThermoPhase + * manager is used - @see nativeState */ void writeEntry(const string& fname, bool overwrite=false, const string& basis=""); /*! - * Write SolutionArray data to container file. + * Write SolutionArray data to a HDF container file. * * @param fname Name of HDF container file - * @param id Identifier of root location within the container file - * @param sub Name identifier for the subgroup holding actual data - * @param overwrite Force overwrite if sub exists; optional (default=false) + * @param id Identifier of group holding header information + * @param sub Name identifier of subgroup holding SolutionArray data + * @param overwrite Force overwrite if subgroup exists; optional (default=false) * @param compression Compression level; optional (default=0; HDF only) */ void writeEntry(const string& fname, const string& id, const string& sub, bool overwrite=false, int compression=0); /*! - * Write SolutionArray data to AnyMap. + * Write SolutionArray data to AnyMap; used by YAML serialization. * * @param root Root node of AnyMap structure - * @param id Identifier of root location within the container file - * @param sub Name identifier for the subgroup holding actual data - * @param overwrite Force overwrite if sub exists; optional (default=false) + * @param id Identifier of node holding header information and subgroup + * @param sub Name identifier of subgroup holding SolutionArray data + * @param overwrite Force overwrite if subgroup exists; optional (default=false) */ void writeEntry(AnyMap& root, const string& id, const string& sub, bool overwrite=false); /*! - * Save current SolutionArray and header to a data file. + * Save current SolutionArray contents to a data file. * - * @param fname Name of output container file (CSV, YAML or HDF) - * @param id Identifier of root location within container file (YAML/HDF only) - * @param sub Name identifier for subgroup holding actual data (YAML/HDF only) + * Data can be saved either in CSV format (extension '*.csv'), YAML container + * format (extension '*.yaml'/'*.yml') or HDF container format (extension + * '*.h5'/'*.hdf5'/'*.hdf'). The output format is automatically inferred from the + * file extension. + * + * CSV files preserve state data and auxiliary data for a single SolutionArray in a + * comma-separated text format, container files may hold multiple SolutionArray + * entries in an internal hierarchical structure. While YAML is a human-readable + * text format, HDF is a binary format that supports compression and is recommended + * for large datasets. + * + * For container files (YAML and HDF), header information contains automatically + * generated time stamps, version information and an optional description. + * Container files also preserve SolutionArray metadata (example: SolutionArray + * objects generated by Sim1D hold simulation settings). + * + * @param fname Name of output file (CSV, YAML or HDF) + * @param id Identifier of location within the container file; this node/group + * contains header information and a subgroup holding actual SolutionArray data + * (YAML/HDF only) + * @param sub Name identifier for the subgroup holding the SolutionArray data and + * metadata objects. If omitted (""), the subgroup name defaults to "data" + * (YAML/HDF only) * @param desc Custom comment describing dataset to be stored (YAML/HDF only) * @param overwrite Force overwrite if file and/or data entry exists; optional * (default=false) - * @param compression Compression level; optional (default=0; HDF only) - * @param basis Output mass ("Y"/"mass") or mole ("X"/"mole") fractions (CSV - * only); if omitted (default=""), the native storage mode is used + * @param compression Compression level (0-9); (default=0; HDF only) + * @param basis Output mass ("Y"/"mass") or mole ("X"/"mole") fractions; + * if not specified (default=""), the native basis of the underlying + * ThermoPhase manager is used - @see nativeState (CSV only) */ void save(const string& fname, const string& id="", const string& sub="", const string& desc="", bool overwrite=false, int compression=0, const string& basis=""); /*! - * Read header data from container file. + * Read header information from a HDF container file. * * @param fname Name of HDF container file - * @param id Identifier of root location within the container file + * @param id Identifier of group holding header information */ static AnyMap readHeader(const string& fname, const string& id); /*! - * Read header data from AnyMap. + * Read header information from AnyMap; used by YAML serialization. * * @param root Root node of AnyMap structure - * @param id Identifier of root location within the container file + * @param id Identifier of node holding header information */ static AnyMap readHeader(const AnyMap& root, const string& id); /*! - * Restore SolutionArray entry from a container file. + * Restore SolutionArray data from a HDF container file. * * @param fname Name of HDF container file - * @param id Identifier of root location within the container file - * @param sub Name of the subgroup holding actual data + * @param id Identifier of group holding header information + * @param sub Name identifier of subgroup holding SolutionArray data */ void readEntry(const string& fname, const string& id, const string& sub); /*! - * Restore SolutionArray entry from AnyMap. + * Restore SolutionArray data from AnyMap; used by YAML serialization. * * @param root Root node of AnyMap structure - * @param id Identifier of root location within the container file - * @param sub Name of the subgroup holding actual data + * @param id Identifier of node holding header information + * @param sub Name identifier of subgroup holding SolutionArray data */ void readEntry(const AnyMap& root, const string& id, const string& sub); /*! - * Restore SolutionArray entry and header from a container file. + * Restore SolutionArray data and header information from a container file. + * + * This method retrieves data from a YAML or HDF files that were previously saved + * using the @see save method. * * @param fname Name of container file (YAML or HDF) - * @param id Identifier of SolutionArray within the container file - * @param sub Name of the subgroup holding actual data + * @param id Identifier of location within the container file; this node/group + * contains header information and a subgroup holding actual SolutionArray data + * @param sub Name identifier for the subgroup holding the SolutionArray data and + * metadata objects. If omitted (""), the subgroup name defaults to "data" + * @return AnyMap containing header information */ - AnyMap restore(const string& fname, const string& id, const string& sub); + AnyMap restore(const string& fname, const string& id, const string& sub=""); protected: //! Service function used to resize SolutionArray diff --git a/interfaces/cython/cantera/_onedim.pyx b/interfaces/cython/cantera/_onedim.pyx index 01c8584edb..5520ebd01c 100644 --- a/interfaces/cython/cantera/_onedim.pyx +++ b/interfaces/cython/cantera/_onedim.pyx @@ -1541,8 +1541,12 @@ cdef class Sim1D: """ Save the solution to a container or CSV format. - For HDF and YAML files, the entire content of the `Sim1D` object is saved; for - CSV, only the main 1D domain is saved. + In order to save the content of a `Sim1D` object, individual domains are + converted to `SolutionArray` objects and saved using the `SolutionArray.save` + method. For HDF and YAML output, all domains are written to a single container + file with shared header information. Simulation settings of individual domains + are preserved as meta data of the corresponding `SolutionArray` objects. + For CSV files, only state and auxiliary data of the main 1D domain are saved. :param filename: solution file diff --git a/interfaces/cython/cantera/composite.py b/interfaces/cython/cantera/composite.py index 6ec2e49966..cac3b2da2a 100644 --- a/interfaces/cython/cantera/composite.py +++ b/interfaces/cython/cantera/composite.py @@ -1251,53 +1251,73 @@ def from_pandas(self, df, normalize=True): data_dict[label] = data_dict[label].astype('U') self.restore_data(data_dict, normalize) - def save(self, fname, name=None, key=None, description=None, *, + def save(self, fname, name=None, sub=None, description=None, *, overwrite=False, compression=0, basis=None): """ - Save current `SolutionArray` to a container or CSV file. + Save current `SolutionArray` contents to a data file. + + Data can be saved either in CSV format (extension ``*.csv``), YAML container + format (extension ``*.yaml``/``*.yml``) or HDF container format (extension + ``*.h5``/``*.hdf5``/``*.hdf``). The output format is automatically inferred from + the file extension. + + CSV files preserve state data and auxiliary data for a single `SolutionArray` in + a comma-separated text format, container files may hold multiple `SolutionArray` + entries in an internal hierarchical structure. While YAML is a human-readable + text format, HDF is a binary format that supports compression and is recommended + for large datasets. + + For container files (YAML and HDF), header information contains automatically + generated time stamps, version information and an optional description. + Container files also preserve `SolutionArray` metadata (example: + `SolutionArray` objects generated by `Sim1D` store simulation settings). :param fname: - Name of output container file (YAML or HDF) + Name of output file (CSV, YAML or HDF) :param name: - Identifier of root location within the container file; the root location - contains header data and a subgroup holding the actual `SolutionArray` - (HDF/YAML only). - :param key: + Identifier of location within the container file; this node/group contains + header information and a subgroup holding actual `SolutionArray` data + (YAML/HDF only). + :param sub: Name identifier for the subgroup holding the `SolutionArray` data and - metadata objects. If `None`, the subgroup name default to ``data`` - (HDF/YAML only). + metadata objects. If `None`, the subgroup name defaults to ``data`` + (YAML/HDF only). :param description: - Custom comment describing the dataset to be stored (HDF/YAML only). + Custom comment describing the dataset to be stored (YAML/HDF only). :param overwrite: Force overwrite if name exists; optional (default=`False`) :param compression: Compression level (0-9); optional (default=0; HDF only) :param basis: - Output mass (``Y``/``mass``) or mole (``X``/``mole``) fractions (CSV only); - if not specified (`None`), the native storage mode is used + Output mass (``Y``/``mass``) or mole (``Y``/``mass``) fractions; + if not specified (`None`), the native basis of the underlying `ThermoPhase` + manager is used. .. versionadded:: 3.0 """ - self._cxx_save(fname, name, key, description, overwrite, compression, basis) + self._cxx_save(fname, name, sub, description, overwrite, compression, basis) - def restore(self, fname, name=None, key=None): + def restore(self, fname, name=None, sub=None): """ - Retrieve `SolutionArray` and header from a container file. + Restore `SolutionArray` data and header information from a container file. + + This method retrieves data from a YAML or HDF files that were previously saved + using the `save` method. :param fname: Name of container file (YAML or HDF) :param name: - Identifier of root location within the container file; the root location - contains header data and a subgroup holding the actual `SolutionArray`. - :param key: + Identifier of location within the container file; this node/group contains + header information and a subgroup holding actual `SolutionArray` data + :param sub: Name identifier for the subgroup holding the `SolutionArray` data and - metadata objects. + metadata objects. If `None`, the subgroup name defaults to ``data`` :return: Dictionary holding `SolutionArray` meta data. .. versionadded:: 3.0 """ - meta = self._cxx_restore(fname, name, key) + meta = self._cxx_restore(fname, name, sub) # ensure self._indices and self._output_dummy are set self.shape = self._api_shape() @@ -1384,7 +1404,7 @@ def write_hdf(self, filename, *args, cols=None, group=None, subgroup=None, if group is None: raise KeyError("Missing required parameter 'group'.") - self.save(filename, name=group, key=subgroup) + self.save(filename, name=group, sub=subgroup) return group def read_hdf(self, filename, group=None, subgroup=None, force=False, normalize=True): diff --git a/interfaces/cython/cantera/onedim.py b/interfaces/cython/cantera/onedim.py index 0e7a555551..3ef4d4487b 100644 --- a/interfaces/cython/cantera/onedim.py +++ b/interfaces/cython/cantera/onedim.py @@ -138,12 +138,12 @@ def set_initial_guess(self, *args, data=None, group=None, **kwargs): if any(data.endswith(suffix) for suffix in [".hdf5", ".h5", ".hdf"]): # data source identifies a HDF file if "native" in hdf_support(): - arr.restore(data, name=group, key=self.domains[1].name) + arr.restore(data, name=group, sub=self.domains[1].name) else: arr.read_hdf(data, group=group, subgroup=self.domains[1].name) elif data.endswith(".yaml") or data.endswith(".yml"): # data source identifies a YAML file - arr.restore(data, name=group, key=self.domains[1].name) + arr.restore(data, name=group, sub=self.domains[1].name) elif data.endswith('.csv'): # data source identifies a CSV file arr.read_csv(data) diff --git a/src/base/SolutionArray.cpp b/src/base/SolutionArray.cpp index 7d1afceeff..0f547d248c 100644 --- a/src/base/SolutionArray.cpp +++ b/src/base/SolutionArray.cpp @@ -994,7 +994,6 @@ void SolutionArray::writeEntry(const string& fname, bool overwrite, const string vector components; vector isSpecies; std::stringstream header; - bool escaped = false; for (const auto& key : names) { string name = key; size_t col; @@ -1029,7 +1028,6 @@ void SolutionArray::writeEntry(const string& fname, bool overwrite, const string } if (name.find(",") != string::npos) { header << "\"" << name << "\""; - escaped = true; } else { header << name; } @@ -1081,7 +1079,6 @@ void SolutionArray::writeEntry(const string& fname, bool overwrite, const string } if (value.find(",") != string::npos) { output << "\"" << value << "\""; - escaped = true; } else { output << value; } @@ -1094,13 +1091,6 @@ void SolutionArray::writeEntry(const string& fname, bool overwrite, const string output << std::endl; } output << std::endl << std::setprecision(default_precision); - - if (escaped) { - warn_user("SolutionArray::writeEntry", - "One or more CSV column names or values contain commas.\n" - "Values have been escaped with double quotes, which may not be supported " - "by all CSV readers."); - } } void SolutionArray::writeEntry(const string& fname, const string& id, const string& sub, @@ -1294,7 +1284,7 @@ void SolutionArray::save(const string& fname, const string& id, const string& su } if (basis != "") { warn_user("SolutionArray::save", - "Species basis '{}' not implemented for HDF5 or YAML output.", basis); + "Argument 'basis' is not used for HDF or YAML output.", basis); } if (extension == "h5" || extension == "hdf" || extension == "hdf5") { writeHeader(fname, id, desc, overwrite); diff --git a/test/python/test_composite.py b/test/python/test_composite.py index 76e7a51c1b..3b21161410 100644 --- a/test/python/test_composite.py +++ b/test/python/test_composite.py @@ -574,8 +574,7 @@ def test_write_csv_escaped(self): arr = ct.SolutionArray(self.gas, 7, extra=extra) arr.TPX = np.linspace(300, 1000, 7), 2e5, "H2:0.5, O2:0.4" arr.equilibrate("HP") - with pytest.warns(UserWarning, match="escaped"): - arr.save(outfile, basis="mass") + arr.save(outfile, basis="mass") with open(outfile, "r") as fid: header = fid.readline() From 39f1bfbdb9e49fa750a2a4327cfc8558ea8977f0 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Wed, 21 Jun 2023 09:49:26 -0600 Subject: [PATCH 17/19] Resolve discrepancies of nomenclature in C++/Python Argument names of SolutionArray IO in C++ and Python were not consistent, which is resolved in this commit. --- include/cantera/base/SolutionArray.h | 40 ++--- include/cantera/oneD/Sim1D.h | 12 +- interfaces/cython/cantera/solutionbase.pyx | 8 +- src/base/SolutionArray.cpp | 178 +++++++++++---------- src/oneD/Sim1D.cpp | 24 +-- 5 files changed, 132 insertions(+), 130 deletions(-) diff --git a/include/cantera/base/SolutionArray.h b/include/cantera/base/SolutionArray.h index 7d3587a9eb..861ee24237 100644 --- a/include/cantera/base/SolutionArray.h +++ b/include/cantera/base/SolutionArray.h @@ -190,22 +190,22 @@ class SolutionArray * Write header data to a HDF container file. * * @param fname Name of HDF container file - * @param id Identifier of group holding header information + * @param name Identifier of group holding header information * @param desc Custom comment describing dataset * @param overwrite Force overwrite if file/group exists; optional (default=false) */ - static void writeHeader(const string& fname, const string& id, const string& desc, + static void writeHeader(const string& fname, const string& name, const string& desc, bool overwrite=false); /*! * Write header data to AnyMap; used by YAML serialization. * * @param root Root node of AnyMap structure - * @param id Identifier of node holding header information + * @param name Identifier of node holding header information * @param desc Custom comment describing dataset * @param overwrite Force overwrite if node exists; optional (default=false) */ - static void writeHeader(AnyMap& root, const string& id, const string& desc, + static void writeHeader(AnyMap& root, const string& name, const string& desc, bool overwrite=false); /*! @@ -223,23 +223,23 @@ class SolutionArray * Write SolutionArray data to a HDF container file. * * @param fname Name of HDF container file - * @param id Identifier of group holding header information + * @param name Identifier of group holding header information * @param sub Name identifier of subgroup holding SolutionArray data * @param overwrite Force overwrite if subgroup exists; optional (default=false) * @param compression Compression level; optional (default=0; HDF only) */ - void writeEntry(const string& fname, const string& id, const string& sub, + void writeEntry(const string& fname, const string& name, const string& sub, bool overwrite=false, int compression=0); /*! * Write SolutionArray data to AnyMap; used by YAML serialization. * * @param root Root node of AnyMap structure - * @param id Identifier of node holding header information and subgroup + * @param name Identifier of node holding header information and subgroup * @param sub Name identifier of subgroup holding SolutionArray data * @param overwrite Force overwrite if subgroup exists; optional (default=false) */ - void writeEntry(AnyMap& root, const string& id, const string& sub, + void writeEntry(AnyMap& root, const string& name, const string& sub, bool overwrite=false); /*! @@ -262,7 +262,7 @@ class SolutionArray * objects generated by Sim1D hold simulation settings). * * @param fname Name of output file (CSV, YAML or HDF) - * @param id Identifier of location within the container file; this node/group + * @param name Identifier of location within the container file; this node/group * contains header information and a subgroup holding actual SolutionArray data * (YAML/HDF only) * @param sub Name identifier for the subgroup holding the SolutionArray data and @@ -276,7 +276,7 @@ class SolutionArray * if not specified (default=""), the native basis of the underlying * ThermoPhase manager is used - @see nativeState (CSV only) */ - void save(const string& fname, const string& id="", const string& sub="", + void save(const string& fname, const string& name="", const string& sub="", const string& desc="", bool overwrite=false, int compression=0, const string& basis=""); @@ -284,35 +284,35 @@ class SolutionArray * Read header information from a HDF container file. * * @param fname Name of HDF container file - * @param id Identifier of group holding header information + * @param name Identifier of group holding header information */ - static AnyMap readHeader(const string& fname, const string& id); + static AnyMap readHeader(const string& fname, const string& name); /*! * Read header information from AnyMap; used by YAML serialization. * * @param root Root node of AnyMap structure - * @param id Identifier of node holding header information + * @param name Identifier of node holding header information */ - static AnyMap readHeader(const AnyMap& root, const string& id); + static AnyMap readHeader(const AnyMap& root, const string& name); /*! * Restore SolutionArray data from a HDF container file. * * @param fname Name of HDF container file - * @param id Identifier of group holding header information + * @param name Identifier of group holding header information * @param sub Name identifier of subgroup holding SolutionArray data */ - void readEntry(const string& fname, const string& id, const string& sub); + void readEntry(const string& fname, const string& name, const string& sub); /*! * Restore SolutionArray data from AnyMap; used by YAML serialization. * * @param root Root node of AnyMap structure - * @param id Identifier of node holding header information + * @param name Identifier of node holding header information * @param sub Name identifier of subgroup holding SolutionArray data */ - void readEntry(const AnyMap& root, const string& id, const string& sub); + void readEntry(const AnyMap& root, const string& name, const string& sub); /*! * Restore SolutionArray data and header information from a container file. @@ -321,13 +321,13 @@ class SolutionArray * using the @see save method. * * @param fname Name of container file (YAML or HDF) - * @param id Identifier of location within the container file; this node/group + * @param name Identifier of location within the container file; this node/group * contains header information and a subgroup holding actual SolutionArray data * @param sub Name identifier for the subgroup holding the SolutionArray data and * metadata objects. If omitted (""), the subgroup name defaults to "data" * @return AnyMap containing header information */ - AnyMap restore(const string& fname, const string& id, const string& sub=""); + AnyMap restore(const string& fname, const string& name, const string& sub=""); protected: //! Service function used to resize SolutionArray diff --git a/include/cantera/oneD/Sim1D.h b/include/cantera/oneD/Sim1D.h index 63b8adae01..12d8eefd63 100644 --- a/include/cantera/oneD/Sim1D.h +++ b/include/cantera/oneD/Sim1D.h @@ -148,14 +148,14 @@ class Sim1D : public OneDim * main 1D domain is saved. * * @param fname Name of output container file - * @param id Identifier of solution within the container file + * @param name Identifier of solution within the container file * @param desc Description of the solution * @param overwrite Force overwrite if name exists; optional (default=false) * @param compression Compression level (optional; HDF only) * @param basis Output mass ("Y"/"mass") or mole ("X"/"mole") fractions (CSV only); * if omitted (default=""), the native storage mode is used */ - void save(const std::string& fname, const std::string& id, + void save(const std::string& fname, const std::string& name, const std::string& desc, bool overwrite=false, int compression=0, const string& basis=""); @@ -173,12 +173,12 @@ class Sim1D : public OneDim /** * Save the residual of the current solution to a container file. * @param fname Name of output container file - * @param id Identifier of solution within the container file + * @param name Identifier of solution within the container file * @param desc Description of the solution * @param overwrite Force overwrite if name exists; optional (default=false) * @param compression Compression level (optional; HDF only) */ - void saveResidual(const std::string& fname, const std::string& id, + void saveResidual(const std::string& fname, const std::string& name, const std::string& desc, bool overwrite=false, int compression=0); /** @@ -194,10 +194,10 @@ class Sim1D : public OneDim /** * Initialize the solution with a previously-saved solution. * @param fname Name of container file - * @param id Identifier of solution within the container file + * @param name Identifier of solution within the container file * @return AnyMap containing header information */ - AnyMap restore(const std::string& fname, const std::string& id); + AnyMap restore(const std::string& fname, const std::string& name); //! @} diff --git a/interfaces/cython/cantera/solutionbase.pyx b/interfaces/cython/cantera/solutionbase.pyx index b7f657bfab..56378bb319 100644 --- a/interfaces/cython/cantera/solutionbase.pyx +++ b/interfaces/cython/cantera/solutionbase.pyx @@ -678,16 +678,16 @@ cdef class SolutionArrayBase: cxx_state.push_back(item) self.base.append(cxx_state, py_to_anymap(extra)) - def _cxx_save(self, filename, name, key, description, + def _cxx_save(self, filename, name, sub, description, overwrite, compression, basis): """ Interface `SolutionArray.save` with C++ core """ self.base.save( - stringify(str(filename)), stringify(name), stringify(key), + stringify(str(filename)), stringify(name), stringify(sub), stringify(description), overwrite, compression, stringify(basis)) - def _cxx_restore(self, filename, name, key): + def _cxx_restore(self, filename, name, sub): """ Interface `SolutionArray.restore` with C++ core """ cdef CxxAnyMap header header = self.base.restore( - stringify(str(filename)), stringify(name), stringify(key)) + stringify(str(filename)), stringify(name), stringify(sub)) return anymap_to_py(header) diff --git a/src/base/SolutionArray.cpp b/src/base/SolutionArray.cpp index 0f547d248c..87e9c4300f 100644 --- a/src/base/SolutionArray.cpp +++ b/src/base/SolutionArray.cpp @@ -915,15 +915,15 @@ AnyMap preamble(const string& desc) return data; } -AnyMap& openField(AnyMap& root, const string& id) +AnyMap& openField(AnyMap& root, const string& name) { - if (!id.size()) { + if (!name.size()) { return root; } - // locate field based on 'id' + // locate field based on 'name' vector tokens; - tokenizePath(id, tokens); + tokenizePath(name, tokens); AnyMap* ptr = &root; // use raw pointer to avoid copying string path = ""; for (auto& field : tokens) { @@ -940,28 +940,28 @@ AnyMap& openField(AnyMap& root, const string& id) return *ptr; } -void SolutionArray::writeHeader(const string& fname, const string& id, +void SolutionArray::writeHeader(const string& fname, const string& name, const string& desc, bool overwrite) { Storage file(fname, true); - if (file.checkGroup(id, true)) { + if (file.checkGroup(name, true)) { if (!overwrite) { throw CanteraError("SolutionArray::writeHeader", - "Group id '{}' exists; use 'overwrite' argument to overwrite.", id); + "Group name '{}' exists; use 'overwrite' argument to overwrite.", name); } - file.deleteGroup(id); - file.checkGroup(id, true); + file.deleteGroup(name); + file.checkGroup(name, true); } - file.writeAttributes(id, preamble(desc)); + file.writeAttributes(name, preamble(desc)); } -void SolutionArray::writeHeader(AnyMap& root, const string& id, +void SolutionArray::writeHeader(AnyMap& root, const string& name, const string& desc, bool overwrite) { - AnyMap& data = openField(root, id); + AnyMap& data = openField(root, name); if (!data.empty() && !overwrite) { throw CanteraError("SolutionArray::writeHeader", - "Field id '{}' exists; use 'overwrite' argument to overwrite.", id); + "Field name '{}' exists; use 'overwrite' argument to overwrite.", name); } data.update(preamble(desc)); } @@ -995,7 +995,7 @@ void SolutionArray::writeEntry(const string& fname, bool overwrite, const string vector isSpecies; std::stringstream header; for (const auto& key : names) { - string name = key; + string label = key; size_t col; if (speciesNames.find(key) == speciesNames.end()) { // Pre-read component vectors @@ -1016,20 +1016,20 @@ void SolutionArray::writeEntry(const string& fname, bool overwrite, const string components.emplace_back(AnyValue()); col = components.size() - 1; if (mole) { - name = "X_" + name; + label = "X_" + label; } else { - name = "Y_" + name; + label = "Y_" + label; } } - if (name.find("\"") != string::npos || name.find("\n") != string::npos) { + if (label.find("\"") != string::npos || label.find("\n") != string::npos) { throw NotImplementedError("SolutionArray::writeEntry", "Detected column name containing double quotes or line feeds: '{}'.", - name); + label); } - if (name.find(",") != string::npos) { - header << "\"" << name << "\""; + if (label.find(",") != string::npos) { + header << "\"" << label << "\""; } else { - header << name; + header << label; } if (col != last) { header << ","; @@ -1093,12 +1093,12 @@ void SolutionArray::writeEntry(const string& fname, bool overwrite, const string output << std::endl << std::setprecision(default_precision); } -void SolutionArray::writeEntry(const string& fname, const string& id, const string& sub, - bool overwrite, int compression) +void SolutionArray::writeEntry(const string& fname, const string& name, + const string& sub, bool overwrite, int compression) { - if (id == "") { + if (name == "") { throw CanteraError("SolutionArray::writeEntry", - "Group id specifying root location must not be empty."); + "Group name specifying root location must not be empty."); } if (m_size < m_dataSize) { throw NotImplementedError("SolutionArray::writeEntry", @@ -1108,7 +1108,7 @@ void SolutionArray::writeEntry(const string& fname, const string& id, const stri if (compression) { file.setCompressionLevel(compression); } - string path = id; + string path = name; if (sub != "") { path += "/" + sub; } else { @@ -1117,7 +1117,7 @@ void SolutionArray::writeEntry(const string& fname, const string& id, const stri if (file.checkGroup(path, true)) { if (!overwrite) { throw CanteraError("SolutionArray::writeEntry", - "Group id '{}' exists; use 'overwrite' argument to overwrite.", id); + "Group name '{}' exists; use 'overwrite' argument to overwrite.", name); } file.deleteGroup(path); file.checkGroup(path, true); @@ -1137,8 +1137,8 @@ void SolutionArray::writeEntry(const string& fname, const string& id, const stri const auto& nativeState = m_sol->thermo()->nativeState(); size_t nSpecies = m_sol->thermo()->nSpecies(); - for (auto& [name, offset] : nativeState) { - if (name == "X" || name == "Y") { + for (auto& [key, offset] : nativeState) { + if (key == "X" || key == "Y") { vector> prop; for (size_t i = 0; i < m_size; i++) { size_t first = offset + i * m_stride; @@ -1147,38 +1147,38 @@ void SolutionArray::writeEntry(const string& fname, const string& id, const stri } AnyValue data; data = prop; - file.writeData(path, name, data); + file.writeData(path, key, data); } else { - auto data = getComponent(name); - file.writeData(path, name, data); + auto data = getComponent(key); + file.writeData(path, key, data); } } - for (const auto& [name, value] : *m_extra) { + for (const auto& [key, value] : *m_extra) { if (isSimpleVector(value)) { - file.writeData(path, name, value); + file.writeData(path, key, value); } else if (value.is()) { // skip unintialized component } else { throw NotImplementedError("SolutionArray::writeEntry", "Unable to save component '{}' with data type {}.", - name, value.type_str()); + key, value.type_str()); } } } -void SolutionArray::writeEntry(AnyMap& root, const string& id, const string& sub, +void SolutionArray::writeEntry(AnyMap& root, const string& name, const string& sub, bool overwrite) { - if (id == "") { + if (name == "") { throw CanteraError("SolutionArray::writeEntry", - "Field id specifying root location must not be empty."); + "Field name specifying root location must not be empty."); } if (m_size < m_dataSize) { throw NotImplementedError("SolutionArray::writeEntry", "Unable to save sliced data."); } - string path = id; + string path = name; if (sub != "") { path += "/" + sub; } else { @@ -1188,7 +1188,7 @@ void SolutionArray::writeEntry(AnyMap& root, const string& id, const string& sub bool preexisting = !data.empty(); if (preexisting && !overwrite) { throw CanteraError("SolutionArray::writeEntry", - "Field id '{}' exists; use 'overwrite' argument to overwrite.", id); + "Field name '{}' exists; use 'overwrite' argument to overwrite.", name); } if (apiNdim() == 1) { data["size"] = int(m_dataSize); @@ -1197,8 +1197,8 @@ void SolutionArray::writeEntry(AnyMap& root, const string& id, const string& sub } data.update(m_meta); - for (auto& [name, value] : *m_extra) { - data[name] = value; + for (auto& [key, value] : *m_extra) { + data[key] = value; } auto phase = m_sol->thermo(); @@ -1227,14 +1227,14 @@ void SolutionArray::writeEntry(AnyMap& root, const string& id, const string& sub } } else if (m_size > 1) { const auto& nativeState = phase->nativeState(); - for (auto& [name, offset] : nativeState) { - if (name == "X" || name == "Y") { + for (auto& [key, offset] : nativeState) { + if (key == "X" || key == "Y") { for (auto& spc : phase->speciesNames()) { data[spc] = getComponent(spc); } - data["basis"] = name == "X" ? "mole" : "mass"; + data["basis"] = key == "X" ? "mole" : "mass"; } else { - data[name] = getComponent(name); + data[key] = getComponent(key); } } data["components"] = componentNames(); @@ -1265,7 +1265,7 @@ void SolutionArray::append(const vector& state, const AnyMap& extra) } } -void SolutionArray::save(const string& fname, const string& id, const string& sub, +void SolutionArray::save(const string& fname, const string& name, const string& sub, const string& desc, bool overwrite, int compression, const string& basis) { @@ -1276,8 +1276,9 @@ void SolutionArray::save(const string& fname, const string& id, const string& su size_t dot = fname.find_last_of("."); string extension = (dot != npos) ? toLowerCopy(fname.substr(dot + 1)) : ""; if (extension == "csv") { - if (id != "") { - warn_user("SolutionArray::save", "Parameter 'id' not used for CSV output."); + if (name != "") { + warn_user("SolutionArray::save", + "Parameter 'name' not used for CSV output."); } writeEntry(fname, overwrite, basis); return; @@ -1287,8 +1288,8 @@ void SolutionArray::save(const string& fname, const string& id, const string& su "Argument 'basis' is not used for HDF or YAML output.", basis); } if (extension == "h5" || extension == "hdf" || extension == "hdf5") { - writeHeader(fname, id, desc, overwrite); - writeEntry(fname, id, sub, true, compression); + writeHeader(fname, name, desc, overwrite); + writeEntry(fname, name, sub, true, compression); return; } if (extension == "yaml" || extension == "yml") { @@ -1297,8 +1298,8 @@ void SolutionArray::save(const string& fname, const string& id, const string& su if (std::ifstream(fname).good()) { data = AnyMap::fromYamlFile(fname); } - writeHeader(data, id, desc, overwrite); - writeEntry(data, id, sub, true); + writeHeader(data, name, desc, overwrite); + writeEntry(data, name, sub, true); // Write the output file and remove the now-outdated cached file std::ofstream out(fname); @@ -1310,22 +1311,22 @@ void SolutionArray::save(const string& fname, const string& id, const string& su "Unknown file extension '{}'.", extension); } -AnyMap SolutionArray::readHeader(const string& fname, const string& id) +AnyMap SolutionArray::readHeader(const string& fname, const string& name) { Storage file(fname, false); - file.checkGroup(id); - return file.readAttributes(id, false); + file.checkGroup(name); + return file.readAttributes(name, false); } -const AnyMap& locateField(const AnyMap& root, const string& id) +const AnyMap& locateField(const AnyMap& root, const string& name) { - if (!id.size()) { + if (!name.size()) { return root; } - // locate field based on 'id' + // locate field based on 'name' vector tokens; - tokenizePath(id, tokens); + tokenizePath(name, tokens); const AnyMap* ptr = &root; // use raw pointer to avoid copying string path = ""; for (auto& field : tokens) { @@ -1333,27 +1334,27 @@ const AnyMap& locateField(const AnyMap& root, const string& id) const AnyMap& sub = *ptr; if (!sub.hasKey(field) || !sub[field].is()) { throw CanteraError("SolutionArray::locateField", - "No field or solution with id '{}'.", path); + "No field or solution with name '{}'.", path); } ptr = &sub[field].as(); } return *ptr; } -AnyMap SolutionArray::readHeader(const AnyMap& root, const string& id) +AnyMap SolutionArray::readHeader(const AnyMap& root, const string& name) { - auto sub = locateField(root, id); + auto sub = locateField(root, name); AnyMap header; - for (const auto& [name, value] : sub) { - if (!sub[name].is()) { - header[name] = value; + for (const auto& [key, value] : sub) { + if (!sub[key].is()) { + header[key] = value; } } return header; } AnyMap SolutionArray::restore(const string& fname, - const string& id, const string& sub) + const string& name, const string& sub) { size_t dot = fname.find_last_of("."); string extension = (dot != npos) ? toLowerCopy(fname.substr(dot + 1)) : ""; @@ -1364,12 +1365,12 @@ AnyMap SolutionArray::restore(const string& fname, "'read_csv' instead."); } if (extension == "h5" || extension == "hdf" || extension == "hdf5") { - readEntry(fname, id, sub); - header = readHeader(fname, id); + readEntry(fname, name, sub); + header = readHeader(fname, name); } else if (extension == "yaml" || extension == "yml") { const AnyMap& root = AnyMap::fromYamlFile(fname); - readEntry(root, id, sub); - header = readHeader(root, id); + readEntry(root, name, sub); + header = readHeader(root, name); } else { throw CanteraError("SolutionArray::restore", "Unknown file extension '{}'; supported extensions include " @@ -1609,23 +1610,24 @@ string getName(const set& names, const string& name) return name; // let exception be thrown elsewhere } -void SolutionArray::readEntry(const string& fname, const string& id, const string& sub) +void SolutionArray::readEntry(const string& fname, const string& name, + const string& sub) { Storage file(fname, false); - if (id == "") { + if (name == "") { throw CanteraError("SolutionArray::readEntry", - "Group id specifying root location must not be empty."); + "Group name specifying root location must not be empty."); } - string path = id; - if (sub != "" && file.checkGroup(id + "/" + sub, true)) { + string path = name; + if (sub != "" && file.checkGroup(name + "/" + sub, true)) { path += "/" + sub; - } else if (sub == "" && file.checkGroup(id + "/data", true)) { + } else if (sub == "" && file.checkGroup(name + "/data", true)) { // default data location path += "/data"; } if (!file.checkGroup(path)) { throw CanteraError("SolutionArray::readEntry", - "Group id specifying data entry is empty."); + "Group name specifying data entry is empty."); } m_extra->clear(); auto [size, names] = file.contents(path); @@ -1735,8 +1737,8 @@ void SolutionArray::readEntry(const string& fname, const string& id, const strin m_sol->thermo()->saveState(nState, m_data->data() + i * m_stride); } warn_user("SolutionArray::readEntry", - "Detected legacy HDF format with incomplete state information\nfor id '{}' " - "(pressure missing).", path); + "Detected legacy HDF format with incomplete state information\nfor name " + "'{}' (pressure missing).", path); } else if (mode == "") { throw CanteraError("SolutionArray::readEntry", "Data are not consistent with full state modes."); @@ -1774,19 +1776,19 @@ void SolutionArray::readEntry(const string& fname, const string& id, const strin } } -void SolutionArray::readEntry(const AnyMap& root, const string& id, const string& sub) +void SolutionArray::readEntry(const AnyMap& root, const string& name, const string& sub) { - if (id == "") { + if (name == "") { throw CanteraError("SolutionArray::readEntry", - "Field id specifying root location must not be empty."); + "Field name specifying root location must not be empty."); } - auto path = locateField(root, id); + auto path = locateField(root, name); if (path.hasKey("generator") && sub != "") { // read entry from subfolder (since Cantera 3.0) - path = locateField(root, id + "/" + sub); + path = locateField(root, name + "/" + sub); } else if (sub == "" && path.hasKey("data")) { // default data location - path = locateField(root, id + "/data"); + path = locateField(root, name + "/data"); } // set size and initialize @@ -1837,8 +1839,8 @@ void SolutionArray::readEntry(const AnyMap& root, const string& id, const string auto Y = path["mass-fractions"].asMap(); m_sol->thermo()->setState_TPY(T, m_sol->thermo()->pressure(), Y); warn_user("SolutionArray::readEntry", - "Detected legacy YAML format with incomplete state information\nfor id " - "'{}' (pressure missing).", id + "/" + sub); + "Detected legacy YAML format with incomplete state information\n" + "for name '{}' (pressure missing).", name + "/" + sub); } else if (mode == "") { throw CanteraError("SolutionArray::readEntry", "Data are not consistent with full state modes."); diff --git a/src/oneD/Sim1D.cpp b/src/oneD/Sim1D.cpp index b290cb3e26..07f27ade1c 100644 --- a/src/oneD/Sim1D.cpp +++ b/src/oneD/Sim1D.cpp @@ -125,7 +125,7 @@ void Sim1D::save(const std::string& fname, const std::string& id, } } -void Sim1D::save(const std::string& fname, const std::string& id, +void Sim1D::save(const std::string& fname, const std::string& name, const std::string& desc, bool overwrite, int compression, const string& basis) { @@ -146,10 +146,10 @@ void Sim1D::save(const std::string& fname, const std::string& id, "Species basis '{}' not implemented for HDF5 or YAML output.", basis); } if (extension == "h5" || extension == "hdf" || extension == "hdf5") { - SolutionArray::writeHeader(fname, id, desc, overwrite); + SolutionArray::writeHeader(fname, name, desc, overwrite); for (auto dom : m_dom) { auto arr = dom->asArray(m_state->data() + dom->loc()); - arr->writeEntry(fname, id, dom->id(), overwrite, compression); + arr->writeEntry(fname, name, dom->id(), overwrite, compression); } return; } @@ -159,11 +159,11 @@ void Sim1D::save(const std::string& fname, const std::string& id, if (std::ifstream(fname).good()) { data = AnyMap::fromYamlFile(fname); } - SolutionArray::writeHeader(data, id, desc, overwrite); + SolutionArray::writeHeader(data, name, desc, overwrite); for (auto dom : m_dom) { auto arr = dom->asArray(m_state->data() + dom->loc()); - arr->writeEntry(data, id, dom->id(), overwrite); + arr->writeEntry(data, name, dom->id(), overwrite); } // Write the output file and remove the now-outdated cached file @@ -186,7 +186,7 @@ void Sim1D::saveResidual(const std::string& fname, const std::string& id, } } -void Sim1D::saveResidual(const std::string& fname, const std::string& id, +void Sim1D::saveResidual(const std::string& fname, const std::string& name, const std::string& desc, bool overwrite, int compression) { vector_fp res(m_state->size(), -999); @@ -195,7 +195,7 @@ void Sim1D::saveResidual(const std::string& fname, const std::string& id, // save() function reads. vector backup(*m_state); *m_state = res; - save(fname, id, desc, overwrite, compression); + save(fname, name, desc, overwrite, compression); *m_state = backup; } @@ -293,7 +293,7 @@ AnyMap Sim1D::restore(const std::string& fname, const std::string& id, int logle return restore(fname, id); } -AnyMap Sim1D::restore(const std::string& fname, const std::string& id) +AnyMap Sim1D::restore(const std::string& fname, const std::string& name) { size_t dot = fname.find_last_of("."); string extension = (dot != npos) ? toLowerCopy(fname.substr(dot+1)) : ""; @@ -304,11 +304,11 @@ AnyMap Sim1D::restore(const std::string& fname, const std::string& id) AnyMap header; if (extension == "h5" || extension == "hdf" || extension == "hdf5") { std::map> arrs; - header = SolutionArray::readHeader(fname, id); + header = SolutionArray::readHeader(fname, name); for (auto dom : m_dom) { auto arr = SolutionArray::create(dom->solution()); - arr->readEntry(fname, id, dom->id()); + arr->readEntry(fname, name, dom->id()); dom->resize(dom->nComponents(), arr->size()); if (!header.hasKey("generator")) { arr->meta() = legacyH5(arr, header); @@ -324,11 +324,11 @@ AnyMap Sim1D::restore(const std::string& fname, const std::string& id) } else if (extension == "yaml" || extension == "yml") { AnyMap root = AnyMap::fromYamlFile(fname); std::map> arrs; - header = SolutionArray::readHeader(root, id); + header = SolutionArray::readHeader(root, name); for (auto dom : m_dom) { auto arr = SolutionArray::create(dom->solution()); - arr->readEntry(root, id, dom->id()); + arr->readEntry(root, name, dom->id()); dom->resize(dom->nComponents(), arr->size()); arrs[dom->id()] = arr; } From 0a2dbce63bf9e760d8b32fdf2764d60ecb6589b1 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Wed, 21 Jun 2023 12:14:50 -0600 Subject: [PATCH 18/19] Additional documentation updates for Sim1D file IO --- include/cantera/oneD/Sim1D.h | 45 ++++++++++++++++++-------- interfaces/cython/cantera/_onedim.pyx | 41 +++++++++++++++-------- interfaces/cython/cantera/composite.py | 8 ++--- 3 files changed, 63 insertions(+), 31 deletions(-) diff --git a/include/cantera/oneD/Sim1D.h b/include/cantera/oneD/Sim1D.h index 12d8eefd63..842128761d 100644 --- a/include/cantera/oneD/Sim1D.h +++ b/include/cantera/oneD/Sim1D.h @@ -142,18 +142,31 @@ class Sim1D : public OneDim const std::string& desc, int loglevel); /** - * Save the current solution to a container or CSV file. + * Save current simulation data to a container file or CSV format. * - * For HDF and YAML, the entire content of the object is saved; for CSV, only the - * main 1D domain is saved. + * In order to save the content of a Sim1D object, individual domains are + * converted to SolutionArray objects and saved using the SolutionArray::save() + * method. For HDF and YAML output, all domains are written to a single container + * file with shared header information. Simulation settings of individual domains + * are preserved as meta data of the corresponding SolutionArray objects. + * For CSV files, only state and auxiliary data of the main 1D domain are saved. * - * @param fname Name of output container file - * @param name Identifier of solution within the container file - * @param desc Description of the solution - * @param overwrite Force overwrite if name exists; optional (default=false) - * @param compression Compression level (optional; HDF only) - * @param basis Output mass ("Y"/"mass") or mole ("X"/"mole") fractions (CSV only); - * if omitted (default=""), the native storage mode is used + * The complete state of the current object can be restored from HDF and YAML + * container files using the restore() method, while individual domains can be + * loaded using SolutionArray::restore() for further analysis. While CSV do not + * contain complete information, they can still be used for setting initial states + * of individual simulation objects for some Cantera API's. + * + * @param fname Name of output file (CSV, YAML or HDF) + * @param name Identifier of storage location within the container file; this + * node/group contains header information and multiple subgroups holding + * domain-specific SolutionArray data (YAML/HDF only) + * @param desc Custom comment describing the dataset to be stored (YAML/HDF only) + * @param overwrite Force overwrite if file/name exists; optional (default=false) + * @param compression Compression level (0-9); optional (default=0; HDF only) + * @param basis Output mass ("Y"/"mass") or mole ("X"/"mole") fractions; + * if not specified (default=""), the native basis of the underlying + * ThermoPhase manager is used - @see nativeState (CSV only) */ void save(const std::string& fname, const std::string& name, const std::string& desc, bool overwrite=false, int compression=0, @@ -192,9 +205,15 @@ class Sim1D : public OneDim AnyMap restore(const std::string& fname, const std::string& id, int loglevel); /** - * Initialize the solution with a previously-saved solution. - * @param fname Name of container file - * @param name Identifier of solution within the container file + * Retrieve data and settings from a previously saved simulation. + * + * This method restores a simulation object from YAML or HDF data previously saved + * using the save() method. + * + * @param fname Name of container file (YAML or HDF) + * @param name Identifier of location within the container file; this node/group + * contains header information and subgroups with domain-specific SolutionArray + * data * @return AnyMap containing header information */ AnyMap restore(const std::string& fname, const std::string& name); diff --git a/interfaces/cython/cantera/_onedim.pyx b/interfaces/cython/cantera/_onedim.pyx index 5520ebd01c..beaf04d6d5 100644 --- a/interfaces/cython/cantera/_onedim.pyx +++ b/interfaces/cython/cantera/_onedim.pyx @@ -1539,28 +1539,37 @@ cdef class Sim1D: def save(self, filename='soln.yaml', name='solution', description=None, loglevel=None, *, overwrite=False, compression=0, basis=None): """ - Save the solution to a container or CSV format. + Save current simulation data to a data file (CSV, YAML or HDF). - In order to save the content of a `Sim1D` object, individual domains are - converted to `SolutionArray` objects and saved using the `SolutionArray.save` + In order to save the content of the current object, individual domains are + converted to `SolutionArray` objects and saved using the `~SolutionArray.save` method. For HDF and YAML output, all domains are written to a single container file with shared header information. Simulation settings of individual domains are preserved as meta data of the corresponding `SolutionArray` objects. For CSV files, only state and auxiliary data of the main 1D domain are saved. + The complete state of the current object can be restored from HDF and YAML + container files using the `restore` method, while individual domains can be + loaded using `SolutionArray.restore` for further analysis. While CSV files do + not contain complete information, they can be used for setting initial states + of individual simulation objects (example: `~FreeFlame.set_initial_guess`). + :param filename: - solution file + Name of output file (CSV, YAML or HDF) :param name: - solution name within the file (HDF/YAML only) + Identifier of storage location within the container file; this node/group + contains header information and multiple subgroups holding domain-specific + `SolutionArray` data (YAML/HDF only). :param description: - custom description text (HDF/YAML only) + Custom comment describing the dataset to be stored (YAML/HDF only). :param overwrite: - Force overwrite if name exists; optional (default=`False`) + Force overwrite if file and/or data entry exists; optional (default=`False`) :param compression: - compression level 0..9; optional (HDF only) + Compression level (0-9); optional (default=0; HDF only) :param basis: - Output mass (``Y``/``mass``) or mole (``X``/``mole``) fractions (CSV only); - if not specified (`None`), the native storage mode is used + Output mass (``Y``/``mass``) or mole (``Y``/``mass``) fractions; + if not specified (`None`), the native basis of the underlying `ThermoPhase` + manager is used. >>> s.save(filename='save.yaml', name='energy_off', ... description='solution with energy eqn. disabled') @@ -1575,17 +1584,21 @@ cdef class Sim1D: stringify(description), overwrite, compression, stringify(basis)) def restore(self, filename='soln.yaml', name='solution', loglevel=None): - """Set the solution vector to a previously-saved solution. + """Retrieve data and settings from a previously saved simulation. + + This method restores a simulation object from YAML or HDF data previously saved + using the `save` method. :param filename: - solution file + Name of container file (YAML or HDF) :param name: - solution name within the file + Identifier of location within the container file; this node/group contains + header information and subgroups with domain-specific `SolutionArray` data :param loglevel: Amount of logging information to display while restoring, from 0 (disabled) to 2 (most verbose). :return: - dictionary containing meta data + Dictionary containing header information >>> s.restore(filename='save.yaml', name='energy_off') diff --git a/interfaces/cython/cantera/composite.py b/interfaces/cython/cantera/composite.py index cac3b2da2a..bcc77c646a 100644 --- a/interfaces/cython/cantera/composite.py +++ b/interfaces/cython/cantera/composite.py @@ -1275,9 +1275,9 @@ def save(self, fname, name=None, sub=None, description=None, *, :param fname: Name of output file (CSV, YAML or HDF) :param name: - Identifier of location within the container file; this node/group contains - header information and a subgroup holding actual `SolutionArray` data - (YAML/HDF only). + Identifier of storage location within the container file; this node/group + contains header information and a subgroup holding actual `SolutionArray` + data (YAML/HDF only). :param sub: Name identifier for the subgroup holding the `SolutionArray` data and metadata objects. If `None`, the subgroup name defaults to ``data`` @@ -1285,7 +1285,7 @@ def save(self, fname, name=None, sub=None, description=None, *, :param description: Custom comment describing the dataset to be stored (YAML/HDF only). :param overwrite: - Force overwrite if name exists; optional (default=`False`) + Force overwrite if file/name exists; optional (default=`False`) :param compression: Compression level (0-9); optional (default=0; HDF only) :param basis: From 4800cd41d9d0a1bbe7c352eeabe6c969151a1e77 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Wed, 21 Jun 2023 13:15:37 -0600 Subject: [PATCH 19/19] [samples] Update keyword in failing test --- samples/python/onedim/flame_initial_guess.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/python/onedim/flame_initial_guess.py b/samples/python/onedim/flame_initial_guess.py index e7c2037f0b..00abca337a 100644 --- a/samples/python/onedim/flame_initial_guess.py +++ b/samples/python/onedim/flame_initial_guess.py @@ -104,7 +104,7 @@ def describe(flame): print("Load initial guess from HDF file via SolutionArray") arr2 = ct.SolutionArray(gas) # the flame domain needs to be specified as subgroup - arr2.restore(hdf_filepath, name="freeflame", key="flame") + arr2.restore(hdf_filepath, name="freeflame", sub="flame") gas.TPX = Tin, p, reactants # set the gas T back to the inlet before making new flame f2 = ct.FreeFlame(gas, width=width) f2.set_initial_guess(data=arr2)