From fe7be79552433c3779ffbd5b018a493114fffdbc Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Thu, 26 Sep 2019 13:22:42 -0500 Subject: [PATCH] [Thermo] improve error messages Incorporate reviewer suggestions for error messages (user feedback) in PureFluid.TPX setter and SolutionArray.restore_data --- interfaces/cython/cantera/composite.py | 69 ++++++++++--------- .../cython/cantera/test/test_purefluid.py | 6 +- interfaces/cython/cantera/thermo.pyx | 12 +++- 3 files changed, 52 insertions(+), 35 deletions(-) diff --git a/interfaces/cython/cantera/composite.py b/interfaces/cython/cantera/composite.py index f8fb0f1e1f..5ab858faa0 100644 --- a/interfaces/cython/cantera/composite.py +++ b/interfaces/cython/cantera/composite.py @@ -299,7 +299,7 @@ class SolutionArray: states using the same `Solution` object and computing properties for that array of states. - SolutionArray can represent both 1D and multi-dimensional arrays of states, + `SolutionArray` can represent both 1D and multi-dimensional arrays of states, with shapes described in the same way as Numpy arrays. All of the states can be set in a single call:: @@ -311,7 +311,7 @@ class SolutionArray: >>> states.TPX = T, P, X Similar to Numpy arrays, input with fewer non-singleton dimensions than the - SolutionArray is 'broadcast' to generate input of the appropriate shape. In + `SolutionArray` is 'broadcast' to generate input of the appropriate shape. In the above example, the single value for the mole fraction input is applied to each input, while each row has a constant temperature and each column has a constant pressure. @@ -337,7 +337,7 @@ class SolutionArray: >>> states.equilibrate('HP') >>> states.T # -> adiabatic flame temperature at various equivalence ratios - SolutionArray objects can also be 'sliced' like Numpy arrays, which can be + `SolutionArray` objects can also be 'sliced' like Numpy arrays, which can be used both for accessing and setting properties:: >>> states = ct.SolutionArray(gas, (6, 10)) @@ -347,7 +347,7 @@ class SolutionArray: If many slices or elements of a property are going to be accessed (i.e. within a loop), it is generally more efficient to compute the property array once and access this directly, rather than repeatedly slicing the - SolutionArray object, e.g.:: + `SolutionArray` object, e.g.:: >>> mu = states.viscosity >>> for i,j in np.ndindex(mu.shape): @@ -368,19 +368,19 @@ class SolutionArray: >>> s.reaction_equation(10) 'CH4 + O <=> CH3 + OH' - Data represented by a SolutionArray can be extracted and saved to a CSV file + Data represented by a `SolutionArray` can be extracted and saved to a CSV file using the `write_csv` method:: >>> states.write_csv('somefile.csv', cols=('T', 'P', 'X', 'net_rates_of_progress')) As long as stored columns specify a valid thermodynamic state, the contents of - a SolutionArray can be restored using the `read_csv` method:: + 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 a pandas compatible + `SolutionArray` objects can also be saved to and restored from a pandas compatible HDF container file using the `write_hdf`:: >>> states.write_hdf('somefile.h5', cols=('T', 'P', 'X'), key='some_key') @@ -399,7 +399,7 @@ class SolutionArray: :param phase: The `Solution` object used to compute the thermodynamic, kinetic, and transport properties :param shape: A tuple or integer indicating the dimensions of the - SolutionArray. If the shape is 1D, the array may be extended using the + `SolutionArray`. If the shape is 1D, the array may be extended using the `append` method. Otherwise, the shape is fixed. :param states: The initial array of states. Used internally to provide slicing support. @@ -597,35 +597,40 @@ def equilibrate(self, *args, **kwargs): def restore_data(self, data, labels): """ - Restores a SolutionArray based on *data* specified in a single + Restores a `SolutionArray` based on *data* specified in a single 2D Numpy array and a list of corresponding column *labels*. Thus, this method allows to restore data exported by `collect_data`. :param data: a 2D Numpy array holding data to be restored. - :param labels: a list of labels corresponding to SolutionArray entries. + :param labels: a list of labels corresponding to `SolutionArray` entries. - The receiving SolutionArray either has to be empty or should have + The receiving `SolutionArray` either has to be empty or should have matching dimensions. Essential state properties and extra entries are detected automatically whereas stored information of calculated - properties is omitted. If the receiving SolutionArray has extra - entries already specified, only those will be imported; if *labels* does - not contain those entries, an error is raised. + properties is omitted. If the receiving `SolutionArray` has extra + entries already specified, only those will be imported; if *labels* + does not contain those entries, an error is raised. """ # check arguments if not isinstance(data, np.ndarray) or data.ndim != 2: raise TypeError("restore_data only works for 2D ndarrays") elif len(labels) != data.shape[1]: - raise ValueError("inconsistent data and label dimensions: " + raise ValueError("inconsistent data and label dimensions " "({} vs. {})".format(len(labels), data.shape[1])) rows = data.shape[0] if self._shape != (0,) and self._shape != (rows,): - raise ValueError('incompatible dimensions.') - - # get full state information (may differ depending on type of ThermoPhase) + raise ValueError( + "incompatible dimensions ({} vs. {}): the receiving " + "SolutionArray object either needs to be empty " + "or have a length that matches data rows " + "to be restored".format(self._shape[0], rows) + ) + + # get full state information (may differ depending on ThermoPhase type) full_states = self._phase._full_states.values() if isinstance(self._phase, PureFluid): - # make sure that potentially non-unique state definitions are checked last + # ensure that potentially non-unique state definitions are checked last last = ['TP', 'TX', 'PX'] full_states = [fs for fs in full_states if fs not in last] + ['TPX'] + last @@ -633,14 +638,13 @@ def restore_data(self, data, labels): # determine whether complete concentration is available (mass or mole) # assumes that `X` or `Y` is always in last place mode = '' - has_species = False for prefix in ['X_', 'Y_']: spc = ['{}{}'.format(prefix, s) for s in self.species_names] # solution species names also found in labels valid_species = {s[2:]: labels.index(s) for s in spc if s in labels} # labels that start with prefix (indicating concentration) - all_species = [l for l in labels if l[:2] == prefix] + all_species = [l for l in labels if l.startswith(prefix)] if valid_species: # save species mode and remaining full_state candidates mode = prefix[0] @@ -655,7 +659,7 @@ def restore_data(self, data, labels): full_states = {v[:2] for v in full_states} # determine suitable thermo properties for reconstruction - basis = {'molar': 'mole', 'mass': 'mass'}[self.basis] + basis = 'mass' if self.basis == 'mass' else 'mole' prop = {'T': ('T'), 'P': ('P'), 'D': ('density', 'density_{}'.format(basis)), 'U': ('u', 'int_energy_{}'.format(basis)), @@ -671,7 +675,10 @@ def restore_data(self, data, labels): mode = fs + mode break if len(mode) == 1: - raise ValueError('invalid/incomplete state information.') + raise ValueError( + "invalid/incomplete state information (detected " + "partial information as mode='{}')".format(mode) + ) # assemble and restore state information state_data = tuple([data[:, state[i][mode[i]]] for i in range(len(state))]) @@ -734,8 +741,8 @@ def collect_data(self, cols=('extra', 'T', 'density', 'Y'), threshold=0, are specified, then either the mass or mole fraction of that species will be taken, depending on the value of *species*. *cols* may also include any arrays which were specified as 'extra' variables when - defining the SolutionArray object. The special value 'extra' can be - used to include all 'extra' variables. + defining the `SolutionArray` object. The special value 'extra' can + be used to include all 'extra' variables. :param threshold: Relative tolerance for including a particular column. The tolerance is applied by comparing the maximum absolute value for a particular column to the maximum absolute value in all columns for @@ -802,7 +809,7 @@ def write_csv(self, filename, cols=('extra', 'T', 'density', 'Y'), *cols*. The first row of the CSV file will contain column labels. Additional arguments are passed on to `collect_data`. This method works - only with 1D SolutionArray objects. + only with 1D `SolutionArray` objects. """ data, labels = self.collect_data(cols, *args, **kwargs) with open(filename, 'w') as outfile: @@ -813,7 +820,7 @@ def write_csv(self, filename, cols=('extra', 'T', 'density', 'Y'), def read_csv(self, filename): """ - Read a CSV file named *filename* and restore data to the SolutionArray + Read a CSV file named *filename* and restore data to the `SolutionArray` using `restore_data`. This method allows for recreation of data previously exported by `write_csv`. """ @@ -829,7 +836,7 @@ def to_pandas(self, cols=('extra', 'T', 'density', 'Y'), Returns the data specified by *cols* in a single pandas DataFrame. Additional arguments are passed on to `collect_data`. This method works - only with 1D SolutionArray objects and requires a working pandas + only with 1D `SolutionArray` objects and requires a working pandas installation. Use pip or conda to install `pandas` to enable this method. """ @@ -841,7 +848,7 @@ def to_pandas(self, cols=('extra', 'T', 'density', 'Y'), def from_pandas(self, df): """ - Restores SolutionArray data from a pandas DataFrame *df*. + Restores `SolutionArray` data from a pandas DataFrame *df*. This method is intendend for loading of data that were previously exported by `to_pandas`. The method requires a working pandas @@ -880,7 +887,7 @@ def write_hdf(self, filename, cols=('extra', 'T', 'density', 'Y'), Additional arguments (i.e. *args* and *kwargs*) are passed on to `collect_data` via `to_pandas`; see `collect_data` for further - information. This method works only with 1D SolutionArray objects + information. This method works only with 1D `SolutionArray` objects and requires working installations of pandas and PyTables. These packages can be installed using pip (`pandas` and `tables`) or conda (`pandas` and `pytables`). @@ -895,7 +902,7 @@ def write_hdf(self, filename, cols=('extra', 'T', 'density', 'Y'), def read_hdf(self, filename, key=None): """ Read a dataset identified by *key* from a HDF file named *filename* - and restore data to the SolutionArray object. This method allows for + and restore data to the `SolutionArray` object. This method allows for recreation of data previously exported by `write_hdf`. The method imports data using `restore_data` via `from_pandas` and diff --git a/interfaces/cython/cantera/test/test_purefluid.py b/interfaces/cython/cantera/test/test_purefluid.py index 9ab42d48dc..be68702719 100644 --- a/interfaces/cython/cantera/test/test_purefluid.py +++ b/interfaces/cython/cantera/test/test_purefluid.py @@ -155,10 +155,12 @@ def test_TPX(self): self.water.TPX = T, P, X self.assertNear(X, 0.8) - with self.assertRaises(ValueError): + with self.assertRaisesRegex(ValueError, 'invalid thermodynamic'): self.water.TPX = T, .999*P, X - with self.assertRaises(ValueError): + with self.assertRaisesRegex(ValueError, 'invalid thermodynamic'): self.water.TPX = T, 1.001*P, X + with self.assertRaisesRegex(ValueError, 'numeric value is required'): + self.water.TPX = T, P, 'spam' # To minimize errors when transcribing tabulated data, the input units here are: diff --git a/interfaces/cython/cantera/thermo.pyx b/interfaces/cython/cantera/thermo.pyx index 68719d8500..5857ccf11d 100644 --- a/interfaces/cython/cantera/thermo.pyx +++ b/interfaces/cython/cantera/thermo.pyx @@ -3,6 +3,7 @@ import warnings import weakref +import numbers as _numbers cdef enum ThermoBasis: mass_basis = 0 @@ -1565,7 +1566,7 @@ cdef class PureFluid(ThermoPhase): property TPX: """ - Get/Set the temperature [K], pressure [Pa], and vapor fraction of a + Get/Set the temperature [K], pressure [Pa], and vapor fraction of a PureFluid. An Exception is raised if the thermodynamic state is not consistent. @@ -1576,6 +1577,11 @@ cdef class PureFluid(ThermoPhase): T = values[0] if values[0] is not None else self.T P = values[1] if values[1] is not None else self.P X = values[2] if values[2] is not None else self.X + if not isinstance(X, (np.ndarray, _numbers.Number)): + raise ValueError( + 'a numeric value is required to quanitify ' + 'the vapor fraction (X)' + ) if np.isclose(P, self.thermo.satPressure(T)): self.TX = T, X elif np.isclose(X, 0.) or np.isclose(X, 1.): @@ -1586,7 +1592,9 @@ cdef class PureFluid(ThermoPhase): 'received a combination of property values that ' 'do not represent a valid state. As an alternative, ' 'specify the state using two fully independent ' - 'properties (e.g. TD)') + 'properties (e.g. TD) instead of: ' + 'T={}, P={}, X={}'.format(T, P, X) + ) property UVX: """