From 0d63dd1c5ac8d074dcbd46feaddf0a60a6a8637f Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Mon, 10 Feb 2020 17:33:06 -0600 Subject: [PATCH 01/29] [1D] Return FlameBase solution vector as SolutionArray Retrieve information that describes the current solution state, including grid point locations, normal velocity and tangential velocity gradient. --- interfaces/cython/cantera/onedim.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/interfaces/cython/cantera/onedim.py b/interfaces/cython/cantera/onedim.py index 46d75d47c7..fde874bc92 100644 --- a/interfaces/cython/cantera/onedim.py +++ b/interfaces/cython/cantera/onedim.py @@ -3,7 +3,7 @@ import numpy as np from ._cantera import * -from .composite import Solution +from .composite import Solution, SolutionArray import csv as _csv from math import erf @@ -279,6 +279,29 @@ def write_csv(self, filename, species='X', quiet=True): if not quiet: print("Solution saved to '{0}'.".format(filename)) + def to_solution_array(self): + """Return the solution vector as a Cantera SolutionArray object. + + The SolutionArray has the following ``extra`` entries: + * grid: z (m) + * normal_velocity: u (m/s) + * tangential_velocity_gradient: V (1/s) + """ + # create solution array object + extra = {'grid': self.grid, + 'normal_velocity': self.u, + 'tangential_velocity_gradient': self.V} + arr = SolutionArray(self.gas, self.flame.n_points, extra=extra) + + # retrieve species concentrations and set states + X = [] + for n in range(self.flame.n_points): + self.set_gas_state(n) + X.append(self.gas.X) + arr.TPX = self.T, self.gas.P, np.vstack(X) + + return arr + def _trim(docstring): """Remove block indentation from a docstring.""" From a2e590e852ae95033020d8ccf5c83513409dd229 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Tue, 11 Feb 2020 16:15:32 -0600 Subject: [PATCH 02/29] [1D] Restore FlameBase solution vector from SolutionArray --- interfaces/cython/cantera/onedim.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/interfaces/cython/cantera/onedim.py b/interfaces/cython/cantera/onedim.py index fde874bc92..d3012962d2 100644 --- a/interfaces/cython/cantera/onedim.py +++ b/interfaces/cython/cantera/onedim.py @@ -302,6 +302,32 @@ def to_solution_array(self): return arr + def from_solution_array(self, arr): + """Restore the solution vector from a Cantera SolutionArray object. + + The SolutionArray requires the following ``extra`` entries: + * grid: z (m) + * normal_velocity: u (m/s) + * tangential_velocity_gradient: V (1/s) + """ + # restore grid + self.domains[1].grid = arr.grid + self._get_initial_solution() + xi = (arr.grid - arr.grid[0]) / (arr.grid[-1] - arr.grid[0]) + + # restore temperature and velocity profiles + self.set_profile('T', xi, arr.T) + self.set_profile('u', xi, arr.normal_velocity) + self.set_profile('V', xi, arr.tangential_velocity_gradient) + + # restore species profiles + X = arr.X + for i, spc in enumerate(self.gas.species_names): + self.set_profile(spc, xi, X[:, i]) + + # restore pressure + self.P = arr.P[0] + def _trim(docstring): """Remove block indentation from a docstring.""" From f34e0603a903ac448afdf2c6c15b5fc97dc39c03 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Wed, 12 Feb 2020 11:15:04 -0600 Subject: [PATCH 03/29] [Thermo] Fix restoration of extra columns in SolutionArray import The previous implementation excluded column labels that were distinct from reserved Cantera names if only a part matched. As some reserved Cantera names have single letters, many valid column names were excluded. --- interfaces/cython/cantera/composite.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/interfaces/cython/cantera/composite.py b/interfaces/cython/cantera/composite.py index 1248bf090c..87f6b121af 100644 --- a/interfaces/cython/cantera/composite.py +++ b/interfaces/cython/cantera/composite.py @@ -731,12 +731,16 @@ def restore_data(self, data, labels): if s in valid_species: state_data[-1][:, i] = data[:, valid_species[s]] - # labels may include calculated properties that must not be restored - calculated = self._scalar + self._n_species + self._n_reactions - exclude = [l for l in labels - if any([v in l for v in calculated])] - extra = {l: list(data[:, i]) for i, l in enumerate(labels) - if l not in exclude} + # labels may include calculated properties that must not be restored: + # compare column labels to names that are reserved for SolutionArray + # attributes (see `SolutionArray.collect_data`), i.e. scalar values, + # arrays with number of species, and arrays with number of reactions. + exclude = [lab for lab in labels + if any([lab in self._scalar, + '_'.join(lab.split('_')[:-1]) in self._n_species, + lab.split(' ')[0] in self._n_reactions])] + extra = {lab: list(data[:, i]) for i, lab in enumerate(labels) + if lab not in exclude} if len(self._extra): extra_lists = {k: extra[k] for k in self._extra} else: From 16b937dd2b5df3d78ee8a50c19434663977b3da0 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Tue, 11 Feb 2020 16:53:14 -0600 Subject: [PATCH 04/29] [1D] Implement pandas/HDF input/output for FlameBase objects The new pandas and HDF interfaces use Cantera's SolutionArray framework. --- interfaces/cython/cantera/onedim.py | 52 +++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/interfaces/cython/cantera/onedim.py b/interfaces/cython/cantera/onedim.py index d3012962d2..80cf7ffee9 100644 --- a/interfaces/cython/cantera/onedim.py +++ b/interfaces/cython/cantera/onedim.py @@ -328,6 +328,58 @@ def from_solution_array(self, arr): # restore pressure self.P = arr.P[0] + def to_pandas(self): + """Return the solution vector as a pandas DataFrame. + + This method requires a working pandas installation. Use pip or conda to + install `pandas` to enable this method. + """ + cols = ('extra', 'T', 'D', 'X') + return self.to_solution_array().to_pandas(cols=cols) + + def from_pandas(self, df): + """Return the solution vector as a pandas DataFrame. + + This method is intendend for loading of data that were previously + exported by `to_pandas`. The method requires a working pandas + installation. The package 'pandas' can be installed using pip or conda. + """ + extra = ('grid', 'normal_velocity', 'tangential_velocity_gradient') + arr = SolutionArray(self.gas, extra=extra) + self.from_solution_array(arr.from_pandas(df)) + + def write_hdf(self, filename, key='df', + mode=None, append=None, complevel=None): + """ + Write the solution vector to a HDF container file named *filename*. + Note that it is possible to write multiple data entries to a single HDF + container file, where *key* is used to differentiate data. + + The method exports data using `SolutionArray.write_hdf` and requires + working installations of pandas and PyTables. These packages can be + installed using pip (`pandas` and `tables`) or conda (`pandas` and + `pytables`). + """ + cols = ('extra', 'T', 'D', 'X') + self.to_solution_array().write_hdf(filename, cols=cols, key=key, + mode=mode, append=append, + complevel=complevel) + + def read_hdf(self, filename, key=None): + """ + Read a dataset identified by *key* from a HDF file named *filename* + to restore the solution vector. This method allows for recreation of + data previously exported by `write_hdf`. + + The method imports data using `SolutionArray.read_hdf` and requires + working installations of pandas and PyTables. These packages can be + installed using pip (`pandas` and `tables`) or conda (`pandas` and + `pytables`). + """ + extra = ('grid', 'normal_velocity', 'tangential_velocity_gradient') + arr = SolutionArray(self.gas, extra=extra) + self.from_solution_array(arr.read_hdf(filename, key=key)) + def _trim(docstring): """Remove block indentation from a docstring.""" From 0c6c675f6908e66a02efef6d41cd1c770bfd3b36 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Wed, 12 Feb 2020 12:19:12 -0600 Subject: [PATCH 05/29] [1D] Differentiate SolutionArray IO for FlameBase-derived classes Omit unused entries (e.g. 'lambda', 'eField') for 1D objects that do not use them. Further, simplify/adjust nomenclature: * component 'u' is exported as 'velocity' ('u' clashes with thermo property) * component 'V' is exported as 'gradient' (short for velocity gradient) * components 'lambda' and 'eField' are exported with original names --- interfaces/cython/cantera/onedim.py | 93 +++++++++++++++++++---------- 1 file changed, 62 insertions(+), 31 deletions(-) diff --git a/interfaces/cython/cantera/onedim.py b/interfaces/cython/cantera/onedim.py index 80cf7ffee9..0e5b07e4d0 100644 --- a/interfaces/cython/cantera/onedim.py +++ b/interfaces/cython/cantera/onedim.py @@ -11,6 +11,7 @@ class FlameBase(Sim1D): """ Base class for flames with a single flow domain """ __slots__ = ('gas',) + _extra = () def __init__(self, domains, gas, grid=None): """ @@ -283,14 +284,26 @@ def to_solution_array(self): """Return the solution vector as a Cantera SolutionArray object. The SolutionArray has the following ``extra`` entries: - * grid: z (m) - * normal_velocity: u (m/s) - * tangential_velocity_gradient: V (1/s) - """ + * `grid`: grid point positions along the flame [m] + * `velocity`: normal velocity [m/s] + * `gradient`: tangential velocity gradient [1/s] (if applicable) + * `lambda`: radial pressure gradient [N/m^4] (if applicable) + * `eField`: electric field strength (if applicable) + """ + # create extra columns + dom = self.domains[1] + extra = {} + for e in self._extra: + if e == 'grid': + extra[e] = self.grid + elif e == 'velocity': + extra[e] = self.profile(dom, 'u') + elif e == 'gradient': + extra[e] = self.profile(dom, 'V') + else: + extra[e] = self.profile(dom, e) + # create solution array object - extra = {'grid': self.grid, - 'normal_velocity': self.u, - 'tangential_velocity_gradient': self.V} arr = SolutionArray(self.gas, self.flame.n_points, extra=extra) # retrieve species concentrations and set states @@ -306,19 +319,27 @@ def from_solution_array(self, arr): """Restore the solution vector from a Cantera SolutionArray object. The SolutionArray requires the following ``extra`` entries: - * grid: z (m) - * normal_velocity: u (m/s) - * tangential_velocity_gradient: V (1/s) + * `grid`: grid point positions along the flame [m] + * `velocity`: normal velocity [m/s] + * `gradient`: tangential velocity gradient [1/s] (if applicable) + * `lambda`: radial pressure gradient [N/m^4] (if applicable) + * `eField`: electric field strength (if applicable) """ # restore grid self.domains[1].grid = arr.grid self._get_initial_solution() xi = (arr.grid - arr.grid[0]) / (arr.grid[-1] - arr.grid[0]) - # restore temperature and velocity profiles + # restore temperature and 'extra' profiles self.set_profile('T', xi, arr.T) - self.set_profile('u', xi, arr.normal_velocity) - self.set_profile('V', xi, arr.tangential_velocity_gradient) + for e in self._extra[1:]: + val = getattr(arr, e) + if e == 'velocity': + self.set_profile('u', xi, val) + elif e == 'gradient': + self.set_profile('V', xi, val) + else: + self.set_profile(e, xi, val) # restore species profiles X = arr.X @@ -331,8 +352,9 @@ def from_solution_array(self, arr): def to_pandas(self): """Return the solution vector as a pandas DataFrame. - This method requires a working pandas installation. Use pip or conda to - install `pandas` to enable this method. + This method uses `to_solution_array` and requires a working pandas + installation. Use pip or conda to install `pandas` to enable this + method. """ cols = ('extra', 'T', 'D', 'X') return self.to_solution_array().to_pandas(cols=cols) @@ -341,12 +363,13 @@ def from_pandas(self, df): """Return the solution vector as a pandas DataFrame. This method is intendend for loading of data that were previously - exported by `to_pandas`. The method requires a working pandas - installation. The package 'pandas' can be installed using pip or conda. + exported by `to_pandas`. The method uses `from_solution_array` and + requires a working pandas installation. The package 'pandas' can be + installed using pip or conda. """ - extra = ('grid', 'normal_velocity', 'tangential_velocity_gradient') - arr = SolutionArray(self.gas, extra=extra) - self.from_solution_array(arr.from_pandas(df)) + arr = SolutionArray(self.gas, extra=self._extra) + arr.from_pandas(df) + self.from_solution_array(arr) def write_hdf(self, filename, key='df', mode=None, append=None, complevel=None): @@ -355,10 +378,10 @@ def write_hdf(self, filename, key='df', Note that it is possible to write multiple data entries to a single HDF container file, where *key* is used to differentiate data. - The method exports data using `SolutionArray.write_hdf` and requires - working installations of pandas and PyTables. These packages can be - installed using pip (`pandas` and `tables`) or conda (`pandas` and - `pytables`). + The method exports data using `SolutionArray.write_hdf` via + `to_solution_array` and requires working installations of pandas and + PyTables. These packages can be installed using pip (`pandas` and + `tables`) or conda (`pandas` and `pytables`). """ cols = ('extra', 'T', 'D', 'X') self.to_solution_array().write_hdf(filename, cols=cols, key=key, @@ -371,14 +394,14 @@ def read_hdf(self, filename, key=None): to restore the solution vector. This method allows for recreation of data previously exported by `write_hdf`. - The method imports data using `SolutionArray.read_hdf` and requires - working installations of pandas and PyTables. These packages can be - installed using pip (`pandas` and `tables`) or conda (`pandas` and - `pytables`). + The method imports data using `SolutionArray.read_hdf` via + `from_solution_array` and requires working installations of pandas and + PyTables. These packages can be installed using pip (`pandas` and + `tables`) or conda (`pandas` and `pytables`). """ - extra = ('grid', 'normal_velocity', 'tangential_velocity_gradient') - arr = SolutionArray(self.gas, extra=extra) - self.from_solution_array(arr.read_hdf(filename, key=key)) + arr = SolutionArray(self.gas, extra=self._extra) + arr.read_hdf(filename, key=key) + self.from_solution_array(arr) def _trim(docstring): @@ -465,6 +488,7 @@ def getter(self): class FreeFlame(FlameBase): """A freely-propagating flat flame.""" __slots__ = ('inlet', 'outlet', 'flame') + _extra = ('grid', 'velocity') def __init__(self, gas, grid=None, width=None): """ @@ -694,6 +718,7 @@ def solve(self, loglevel=1, refine_grid=True, auto=False, stage=1, enable_energy class IonFreeFlame(IonFlameBase, FreeFlame): """A freely-propagating flame with ionized gas.""" __slots__ = ('inlet', 'outlet', 'flame') + _extra = ('grid', 'velocity', 'eField') def __init__(self, gas, grid=None, width=None): if not hasattr(self, 'flame'): @@ -707,6 +732,7 @@ def __init__(self, gas, grid=None, width=None): class BurnerFlame(FlameBase): """A burner-stabilized flat flame.""" __slots__ = ('burner', 'flame', 'outlet') + _extra = ('grid', 'velocity') def __init__(self, gas, grid=None, width=None): """ @@ -832,6 +858,7 @@ def check_blowoff(t): class IonBurnerFlame(IonFlameBase, BurnerFlame): """A burner-stabilized flat flame with ionized gas.""" __slots__ = ('burner', 'flame', 'outlet') + _extra = ('grid', 'velocity', 'eField') def __init__(self, gas, grid=None, width=None): if not hasattr(self, 'flame'): @@ -845,6 +872,7 @@ def __init__(self, gas, grid=None, width=None): class CounterflowDiffusionFlame(FlameBase): """ A counterflow diffusion flame """ __slots__ = ('fuel_inlet', 'flame', 'oxidizer_inlet') + _extra = ('grid', 'velocity', 'gradient', 'lambda') def __init__(self, gas, grid=None, width=None): """ @@ -1123,6 +1151,7 @@ def mixture_fraction(self, m): class ImpingingJet(FlameBase): """An axisymmetric flow impinging on a surface at normal incidence.""" __slots__ = ('inlet', 'flame', 'surface') + _extra = ('grid', 'velocity', 'gradient', 'lambda') def __init__(self, gas, grid=None, width=None, surface=None): """ @@ -1203,6 +1232,7 @@ def set_initial_guess(self, products='inlet'): class CounterflowPremixedFlame(FlameBase): """ A premixed counterflow flame """ __slots__ = ('reactants', 'flame', 'products') + _extra = ('grid', 'velocity', 'gradient', 'lambda') def __init__(self, gas, grid=None, width=None): """ @@ -1296,6 +1326,7 @@ class CounterflowTwinPremixedFlame(FlameBase): shooting into each other. """ __slots__ = ('reactants', 'flame', 'products') + _extra = ('grid', 'velocity', 'gradient', 'lambda') def __init__(self, gas, grid=None, width=None): """ From 8872b3966c2aee693db4cbf209813c5e43fac061 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Wed, 12 Feb 2020 13:08:31 -0600 Subject: [PATCH 06/29] [1D] Add unit tests for SolutionArray interface --- interfaces/cython/cantera/test/test_onedim.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/interfaces/cython/cantera/test/test_onedim.py b/interfaces/cython/cantera/test/test_onedim.py index 4f6345c38f..254725c355 100644 --- a/interfaces/cython/cantera/test/test_onedim.py +++ b/interfaces/cython/cantera/test/test_onedim.py @@ -233,6 +233,19 @@ def run_mix(self, phi, T, width, p, refine): for rhou_j in self.sim.density * self.sim.u: self.assertNear(rhou_j, rhou, 1e-4) + def test_solution_array_output(self): + self.run_mix(phi=1.0, T=300, width=2.0, p=1.0, refine=False) + arr = self.sim.to_solution_array() + self.assertArrayNear(self.sim.grid, arr.grid) + self.assertArrayNear(self.sim.T, arr.T) + for k in arr._extra.keys(): + self.assertIn(k, self.sim._extra) + + f2 = ct.FreeFlame(self.gas) + f2.from_solution_array(arr) + self.assertArrayNear(self.sim.grid, f2.grid) + self.assertArrayNear(self.sim.T, f2.T) + def test_mixture_averaged_case1(self): self.run_mix(phi=0.65, T=300, width=0.03, p=1.0, refine=True) From a6e026101600ab538191b2d217599f81f1bebca7 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Thu, 13 Feb 2020 09:42:35 -0600 Subject: [PATCH 07/29] [1D] Implement restart for FreeFlame --- interfaces/cython/cantera/onedim.py | 48 +++++++++++++++++-- interfaces/cython/cantera/test/test_onedim.py | 14 ++++++ 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/interfaces/cython/cantera/onedim.py b/interfaces/cython/cantera/onedim.py index 0e5b07e4d0..6018193679 100644 --- a/interfaces/cython/cantera/onedim.py +++ b/interfaces/cython/cantera/onedim.py @@ -403,6 +403,24 @@ def read_hdf(self, filename, key=None): arr.read_hdf(filename, key=key) self.from_solution_array(arr) + def _load_restart_data(self, source, **kwargs): + """ + Load data for restart (called by set_initial_guess) + """ + if isinstance(source, SolutionArray): + # already a solution array + return source + elif isinstance(source, str): + # source identifies a HDF file + arr = SolutionArray(self.gas, extra=self._extra) + arr.read_hdf(source, **kwargs) + return arr + else: + # source is a pandas DataFrame + arr = SolutionArray(self.gas, extra=self._extra) + arr.from_pandas(source) + return arr + def _trim(docstring): """Remove block indentation from a docstring.""" @@ -520,18 +538,40 @@ def __init__(self, gas, grid=None, width=None): self.inlet.T = gas.T self.inlet.X = gas.X - def set_initial_guess(self, locs=[0.0, 0.3, 0.5, 1.0]): + def set_initial_guess(self, locs=[0.0, 0.3, 0.5, 1.0], data=None, key=None): """ Set the initial guess for the solution. The adiabatic flame temperature and equilibrium composition are computed for the inlet gas composition. :param locs: - A list of four locations to define the temperature and mass fraction profiles. - Profiles rise linearly between the second and third location. - Locations are given as a fraction of the entire domain + A list of four locations to define the temperature and mass fraction + profiles. Profiles rise linearly between the second and third + location. Locations are given as a fraction of the entire domain + :param data: + Restart data, which are typically based on an earlier simulation + result. Restart data may be specified using a SolutionArray, + pandas' DataFrame, or a saved HDF container file. DataFrame and HDF + input require working installations of pandas and PyTables. These + packages can be installed using pip (`pandas` and `tables`) or conda + (`pandas` and `pytables`). + :param key: + Group identifier within a HDF container file (only used in + combination with HDF restart data). """ super().set_initial_guess() + if data: + data = self._load_restart_data(data, key=key) + data.TP = data.T + self.inlet.T - data.T[0], self.P + self.from_solution_array(data) + + # set fixed temperature + Tmid = .75 * data.T[0] + .25 * data.T[-1] + i = np.flatnonzero(data.T < Tmid)[-1] + self.set_fixed_temperature(data.T[i]) + + return + self.gas.TPY = self.inlet.T, self.P, self.inlet.Y if not self.inlet.mdot: diff --git a/interfaces/cython/cantera/test/test_onedim.py b/interfaces/cython/cantera/test/test_onedim.py index 254725c355..c78fd87b55 100644 --- a/interfaces/cython/cantera/test/test_onedim.py +++ b/interfaces/cython/cantera/test/test_onedim.py @@ -246,6 +246,20 @@ def test_solution_array_output(self): self.assertArrayNear(self.sim.grid, f2.grid) self.assertArrayNear(self.sim.T, f2.T) + def test_restart(self): + self.run_mix(phi=1.0, T=300, width=2.0, p=1.0, refine=False) + arr = self.sim.to_solution_array() + + reactants = {'H2': 0.9, 'O2': 0.5, 'AR': 2} + self.create_sim(1.1 * ct.one_atm, 500, reactants, 2.0) + self.sim.set_initial_guess(data=arr) + self.solve_mix(refine=False) + + # Check continuity + rhou = self.sim.inlet.mdot + for rhou_j in self.sim.density * self.sim.u: + self.assertNear(rhou_j, rhou, 1e-4) + def test_mixture_averaged_case1(self): self.run_mix(phi=0.65, T=300, width=0.03, p=1.0, refine=True) From bd3d7eb333ba390e65fb6b60a1099c315412c223 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Mon, 24 Feb 2020 16:32:39 -0600 Subject: [PATCH 08/29] [1D] Improve docstrings and minor edits --- interfaces/cython/cantera/onedim.py | 51 +++++++++++++++-------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/interfaces/cython/cantera/onedim.py b/interfaces/cython/cantera/onedim.py index 6018193679..694154fda4 100644 --- a/interfaces/cython/cantera/onedim.py +++ b/interfaces/cython/cantera/onedim.py @@ -11,7 +11,7 @@ class FlameBase(Sim1D): """ Base class for flames with a single flow domain """ __slots__ = ('gas',) - _extra = () + _extra = () # extra columns used for saving/restoring of simulation data def __init__(self, domains, gas, grid=None): """ @@ -281,52 +281,51 @@ def write_csv(self, filename, species='X', quiet=True): print("Solution saved to '{0}'.".format(filename)) def to_solution_array(self): - """Return the solution vector as a Cantera SolutionArray object. + """ + Return the solution vector as a Cantera `SolutionArray` object. - The SolutionArray has the following ``extra`` entries: - * `grid`: grid point positions along the flame [m] - * `velocity`: normal velocity [m/s] - * `gradient`: tangential velocity gradient [1/s] (if applicable) - * `lambda`: radial pressure gradient [N/m^4] (if applicable) - * `eField`: electric field strength (if applicable) + The `SolutionArray` has the following ``extra`` entries: + * ``grid``: grid point positions along the flame [m] + * ``velocity``: normal velocity [m/s] + * ``gradient``: tangential velocity gradient [1/s] (if applicable) + * ``lambda``: radial pressure gradient [N/m^4] (if applicable) + * ``eField``: electric field strength (if applicable) """ # create extra columns - dom = self.domains[1] extra = {} for e in self._extra: if e == 'grid': extra[e] = self.grid elif e == 'velocity': - extra[e] = self.profile(dom, 'u') + extra[e] = self.profile(self.flame, 'u') elif e == 'gradient': - extra[e] = self.profile(dom, 'V') + extra[e] = self.profile(self.flame, 'V') else: - extra[e] = self.profile(dom, e) + extra[e] = self.profile(self.flame, e) # create solution array object arr = SolutionArray(self.gas, self.flame.n_points, extra=extra) # retrieve species concentrations and set states - X = [] for n in range(self.flame.n_points): self.set_gas_state(n) - X.append(self.gas.X) - arr.TPX = self.T, self.gas.P, np.vstack(X) + arr[n].TPY = self.gas.T, self.gas.P, self.gas.Y return arr def from_solution_array(self, arr): - """Restore the solution vector from a Cantera SolutionArray object. + """ + Restore the solution vector from a Cantera `SolutionArray` object. - The SolutionArray requires the following ``extra`` entries: - * `grid`: grid point positions along the flame [m] - * `velocity`: normal velocity [m/s] - * `gradient`: tangential velocity gradient [1/s] (if applicable) - * `lambda`: radial pressure gradient [N/m^4] (if applicable) - * `eField`: electric field strength (if applicable) + The `SolutionArray` requires the following ``extra`` entries: + * ``grid``: grid point positions along the flame [m] + * ``velocity``: normal velocity [m/s] + * ``gradient``: tangential velocity gradient [1/s] (if applicable) + * ``lambda``: radial pressure gradient [N/m^4] (if applicable) + * ``eField``: electric field strength (if applicable) """ # restore grid - self.domains[1].grid = arr.grid + self.flame.grid = arr.grid self._get_initial_solution() xi = (arr.grid - arr.grid[0]) / (arr.grid[-1] - arr.grid[0]) @@ -350,7 +349,8 @@ def from_solution_array(self, arr): self.P = arr.P[0] def to_pandas(self): - """Return the solution vector as a pandas DataFrame. + """ + Return the solution vector as a `pandas.DataFrame`. This method uses `to_solution_array` and requires a working pandas installation. Use pip or conda to install `pandas` to enable this @@ -360,7 +360,8 @@ def to_pandas(self): return self.to_solution_array().to_pandas(cols=cols) def from_pandas(self, df): - """Return the solution vector as a pandas DataFrame. + """ + Return the solution vector as a `pandas.DataFrame`. This method is intendend for loading of data that were previously exported by `to_pandas`. The method uses `from_solution_array` and From d2e0a7f6159829595312a1a3c4c1e27c01052f13 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Wed, 26 Feb 2020 09:32:04 -0600 Subject: [PATCH 09/29] [1D] Restore boundaries from SolutionArray --- interfaces/cython/cantera/onedim.py | 94 ++++++++++++++----- interfaces/cython/cantera/test/test_onedim.py | 8 +- 2 files changed, 77 insertions(+), 25 deletions(-) diff --git a/interfaces/cython/cantera/onedim.py b/interfaces/cython/cantera/onedim.py index 694154fda4..a16718c6b5 100644 --- a/interfaces/cython/cantera/onedim.py +++ b/interfaces/cython/cantera/onedim.py @@ -295,45 +295,76 @@ def to_solution_array(self): extra = {} for e in self._extra: if e == 'grid': - extra[e] = self.grid + val = self.grid elif e == 'velocity': - extra[e] = self.profile(self.flame, 'u') + val = self.profile(self.flame, 'u') elif e == 'gradient': - extra[e] = self.profile(self.flame, 'V') + val = self.profile(self.flame, 'V') else: - extra[e] = self.profile(self.flame, e) + val = self.profile(self.flame, e) + extra[e] = np.hstack([np.nan, val, np.nan]) + + # consider inlet boundaries + left = self.domains[0] # left boundary is always an inlet + if isinstance(self.domains[2], Inlet1D): + right = self.domains[2] + n_arr = self.flame.n_points + 2 + else: + right = None + n_arr = self.flame.n_points + 1 + for e in extra: + extra[e] = extra[e][:-1] # create solution array object - arr = SolutionArray(self.gas, self.flame.n_points, extra=extra) + arr = SolutionArray(self.gas, n_arr, extra=extra) + + # add left boundary + self.gas.TPY = left.T, self.P, left.Y + arr[0].TPY = self.gas.TPY + arr._extra['grid'][0] = -np.inf + arr._extra['velocity'][0] = left.mdot/self.gas.density # retrieve species concentrations and set states for n in range(self.flame.n_points): self.set_gas_state(n) - arr[n].TPY = self.gas.T, self.gas.P, self.gas.Y + arr[n + 1].TPY = self.gas.T, self.P, self.gas.Y + + # add right boundary + if right: + self.gas.TPY = right.T, self.P, right.Y + arr[-1].TPY = self.gas.TPY + arr._extra['grid'][-1] = np.inf + arr._extra['velocity'][-1] = -right.mdot/self.gas.density return arr - def from_solution_array(self, arr): + def from_solution_array(self, arr, restore_boundaries=True): """ Restore the solution vector from a Cantera `SolutionArray` object. - The `SolutionArray` requires the following ``extra`` entries: + The `SolutionArray` ``arr`` requires the following ``extra`` entries: * ``grid``: grid point positions along the flame [m] * ``velocity``: normal velocity [m/s] * ``gradient``: tangential velocity gradient [1/s] (if applicable) * ``lambda``: radial pressure gradient [N/m^4] (if applicable) * ``eField``: electric field strength (if applicable) """ + # extent (indices) of flame domain + idx = np.isfinite(arr.grid) + grid = arr.grid[idx] + # restore grid - self.flame.grid = arr.grid + self.flame.grid = grid self._get_initial_solution() - xi = (arr.grid - arr.grid[0]) / (arr.grid[-1] - arr.grid[0]) + xi = (grid - grid[0]) / (grid[-1] - grid[0]) # restore temperature and 'extra' profiles - self.set_profile('T', xi, arr.T) - for e in self._extra[1:]: - val = getattr(arr, e) - if e == 'velocity': + self.set_profile('T', xi, arr.T[idx]) + for e in self._extra: + val = getattr(arr, e)[idx] + if e == 'grid': + pass + elif e == 'velocity': self.set_profile('u', xi, val) elif e == 'gradient': self.set_profile('V', xi, val) @@ -341,13 +372,29 @@ def from_solution_array(self, arr): self.set_profile(e, xi, val) # restore species profiles - X = arr.X + X = arr.X[idx, :] for i, spc in enumerate(self.gas.species_names): self.set_profile(spc, xi, X[:, i]) # restore pressure self.P = arr.P[0] + # restore boundaries + if restore_boundaries: + + # left boundary + left = self.domains[0] + left.T = arr[0].T + left.Y = arr[0].Y + left.mdot = arr.velocity[0] * arr[0].density + + # right boundary + if np.isinf(arr.grid[-1]): + right = self.domains[2] + right.T = arr[-1].T + right.Y = arr[-1].Y + right.mdot = -arr.velocity[-1] * arr[-1].density + def to_pandas(self): """ Return the solution vector as a `pandas.DataFrame`. @@ -359,7 +406,7 @@ def to_pandas(self): cols = ('extra', 'T', 'D', 'X') return self.to_solution_array().to_pandas(cols=cols) - def from_pandas(self, df): + def from_pandas(self, df, **kwargs): """ Return the solution vector as a `pandas.DataFrame`. @@ -370,7 +417,7 @@ def from_pandas(self, df): """ arr = SolutionArray(self.gas, extra=self._extra) arr.from_pandas(df) - self.from_solution_array(arr) + self.from_solution_array(arr, **kwargs) def write_hdf(self, filename, key='df', mode=None, append=None, complevel=None): @@ -389,7 +436,7 @@ def write_hdf(self, filename, key='df', mode=mode, append=append, complevel=complevel) - def read_hdf(self, filename, key=None): + def read_hdf(self, filename, key=None, **kwargs): """ Read a dataset identified by *key* from a HDF file named *filename* to restore the solution vector. This method allows for recreation of @@ -402,7 +449,7 @@ def read_hdf(self, filename, key=None): """ arr = SolutionArray(self.gas, extra=self._extra) arr.read_hdf(filename, key=key) - self.from_solution_array(arr) + self.from_solution_array(arr, **kwargs) def _load_restart_data(self, source, **kwargs): """ @@ -552,9 +599,10 @@ def set_initial_guess(self, locs=[0.0, 0.3, 0.5, 1.0], data=None, key=None): :param data: Restart data, which are typically based on an earlier simulation result. Restart data may be specified using a SolutionArray, - pandas' DataFrame, or a saved HDF container file. DataFrame and HDF - input require working installations of pandas and PyTables. These - packages can be installed using pip (`pandas` and `tables`) or conda + pandas' DataFrame, or a saved HDF container file. Note that restart + data do not overwrite boundary conditions. DataFrame and HDF input + require working installations of pandas and PyTables. These packages + can be installed using pip (`pandas` and `tables`) or conda (`pandas` and `pytables`). :param key: Group identifier within a HDF container file (only used in @@ -564,7 +612,7 @@ def set_initial_guess(self, locs=[0.0, 0.3, 0.5, 1.0], data=None, key=None): if data: data = self._load_restart_data(data, key=key) data.TP = data.T + self.inlet.T - data.T[0], self.P - self.from_solution_array(data) + self.from_solution_array(data, restore_boundaries=False) # set fixed temperature Tmid = .75 * data.T[0] + .25 * data.T[-1] diff --git a/interfaces/cython/cantera/test/test_onedim.py b/interfaces/cython/cantera/test/test_onedim.py index c78fd87b55..a199050b64 100644 --- a/interfaces/cython/cantera/test/test_onedim.py +++ b/interfaces/cython/cantera/test/test_onedim.py @@ -236,8 +236,9 @@ def run_mix(self, phi, T, width, p, refine): def test_solution_array_output(self): self.run_mix(phi=1.0, T=300, width=2.0, p=1.0, refine=False) arr = self.sim.to_solution_array() - self.assertArrayNear(self.sim.grid, arr.grid) - self.assertArrayNear(self.sim.T, arr.T) + ix = np.isfinite(arr.grid) + self.assertArrayNear(self.sim.grid, arr.grid[ix]) + self.assertArrayNear(self.sim.T, arr.T[ix]) for k in arr._extra.keys(): self.assertIn(k, self.sim._extra) @@ -245,6 +246,9 @@ def test_solution_array_output(self): f2.from_solution_array(arr) self.assertArrayNear(self.sim.grid, f2.grid) self.assertArrayNear(self.sim.T, f2.T) + self.assertNear(self.sim.inlet.T, f2.inlet.T) + self.assertNear(self.sim.inlet.mdot, f2.inlet.mdot) + self.assertArrayNear(self.sim.inlet.Y, f2.inlet.Y) def test_restart(self): self.run_mix(phi=1.0, T=300, width=2.0, p=1.0, refine=False) From 6ab1ee354f7729e511498fe5d9705775c2f1d71c Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Wed, 26 Feb 2020 10:08:29 -0600 Subject: [PATCH 10/29] [1D] Enable mass fraction export to pandas/HDF --- interfaces/cython/cantera/onedim.py | 74 ++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 22 deletions(-) diff --git a/interfaces/cython/cantera/onedim.py b/interfaces/cython/cantera/onedim.py index a16718c6b5..1614cb2278 100644 --- a/interfaces/cython/cantera/onedim.py +++ b/interfaces/cython/cantera/onedim.py @@ -342,7 +342,13 @@ def from_solution_array(self, arr, restore_boundaries=True): """ Restore the solution vector from a Cantera `SolutionArray` object. - The `SolutionArray` ``arr`` requires the following ``extra`` entries: + :param arr: + SolutionArray to be restored + :param restore_boundaries: + Boolean flag to indicate whether boundaries should be restored + (default is ``True``) + + The `SolutionArray` requires the following ``extra`` entries: * ``grid``: grid point positions along the flame [m] * ``velocity``: normal velocity [m/s] * ``gradient``: tangential velocity gradient [1/s] (if applicable) @@ -395,20 +401,30 @@ def from_solution_array(self, arr, restore_boundaries=True): right.Y = arr[-1].Y right.mdot = -arr.velocity[-1] * arr[-1].density - def to_pandas(self): + def to_pandas(self, species='X'): """ Return the solution vector as a `pandas.DataFrame`. + :param species: + Attribute to use obtaining species profiles, e.g. ``X`` for + mole fractions or ``Y`` for mass fractions. + This method uses `to_solution_array` and requires a working pandas installation. Use pip or conda to install `pandas` to enable this method. """ - cols = ('extra', 'T', 'D', 'X') + cols = ('extra', 'T', 'D', species) return self.to_solution_array().to_pandas(cols=cols) - def from_pandas(self, df, **kwargs): + def from_pandas(self, df, restore_boundaries=True): """ - Return the solution vector as a `pandas.DataFrame`. + Restore the solution vector from a `pandas.DataFrame`. + + :param df: + `pandas.DataFrame` containing data to be restored + :param restore_boundaries: + Boolean flag to indicate whether boundaries should be restored + (default is ``True``) This method is intendend for loading of data that were previously exported by `to_pandas`. The method uses `from_solution_array` and @@ -417,30 +433,46 @@ def from_pandas(self, df, **kwargs): """ arr = SolutionArray(self.gas, extra=self._extra) arr.from_pandas(df) - self.from_solution_array(arr, **kwargs) + self.from_solution_array(arr, restore_boundaries=restore_boundaries) - def write_hdf(self, filename, key='df', - mode=None, append=None, complevel=None): + def write_hdf(self, filename, key='df', species='X', + mode=None, complevel=None): """ - Write the solution vector to a HDF container file named *filename*. - Note that it is possible to write multiple data entries to a single HDF - container file, where *key* is used to differentiate data. + Write the solution vector to a HDF container file. Note that it is + possible to write multiple data entries to a single HDF container file. + + :param filename: + HDF container file containing data to be restored + :param key: + String identifying the HDF group containing the data + :param species: + Attribute to use obtaining species profiles, e.g. ``X`` for + mole fractions or ``Y`` for mass fractions. + :param mode: + Mode to open file (see `pandas.DataFrame.to_hdf`) + :param complevel: + Compression level (see `pandas.DataFrame.to_hdf`) The method exports data using `SolutionArray.write_hdf` via `to_solution_array` and requires working installations of pandas and PyTables. These packages can be installed using pip (`pandas` and `tables`) or conda (`pandas` and `pytables`). """ - cols = ('extra', 'T', 'D', 'X') + cols = ('extra', 'T', 'D', species) self.to_solution_array().write_hdf(filename, cols=cols, key=key, - mode=mode, append=append, - complevel=complevel) + mode=mode, complevel=complevel) - def read_hdf(self, filename, key=None, **kwargs): + def read_hdf(self, filename, key=None, restore_boundaries=True): """ - Read a dataset identified by *key* from a HDF file named *filename* - to restore the solution vector. This method allows for recreation of - data previously exported by `write_hdf`. + Restore the solution vector from a HDF container file. + + :param filename: + HDF container file containing data to be restored + :param key: + String identifying the HDF group containing the data + :param restore_boundaries: + Boolean flag to indicate whether boundaries should be restored + (default is ``True``) The method imports data using `SolutionArray.read_hdf` via `from_solution_array` and requires working installations of pandas and @@ -449,12 +481,10 @@ def read_hdf(self, filename, key=None, **kwargs): """ arr = SolutionArray(self.gas, extra=self._extra) arr.read_hdf(filename, key=key) - self.from_solution_array(arr, **kwargs) + self.from_solution_array(arr, restore_boundaries=restore_boundaries) def _load_restart_data(self, source, **kwargs): - """ - Load data for restart (called by set_initial_guess) - """ + """ Load data for restart (called by set_initial_guess) """ if isinstance(source, SolutionArray): # already a solution array return source From 909813b0fff74d55c6c8efd776965b192dcca7d4 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Wed, 26 Feb 2020 12:17:58 -0600 Subject: [PATCH 11/29] [1D] Enable saving of simulation settings to HDF --- interfaces/cython/cantera/onedim.py | 58 ++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/interfaces/cython/cantera/onedim.py b/interfaces/cython/cantera/onedim.py index 1614cb2278..f400473c94 100644 --- a/interfaces/cython/cantera/onedim.py +++ b/interfaces/cython/cantera/onedim.py @@ -7,6 +7,12 @@ import csv as _csv from math import erf +# avoid explicit dependence of cantera on pandas +try: + import pandas as _pandas +except ImportError as err: + _pandas = err + class FlameBase(Sim1D): """ Base class for flames with a single flow domain """ @@ -435,16 +441,19 @@ def from_pandas(self, df, restore_boundaries=True): arr.from_pandas(df) self.from_solution_array(arr, restore_boundaries=restore_boundaries) - def write_hdf(self, filename, key='df', species='X', + def write_hdf(self, filename, key=None, species='X', mode=None, complevel=None): """ Write the solution vector to a HDF container file. Note that it is possible to write multiple data entries to a single HDF container file. + Simulation settings are stored in tabular form as a separate HDF group + named ``settings``. :param filename: HDF container file containing data to be restored :param key: - String identifying the HDF group containing the data + String identifying the HDF group containing the data. The default + is the name of the simulated configuration, e.g. ``FreeFlame``. :param species: Attribute to use obtaining species profiles, e.g. ``X`` for mole fractions or ``Y`` for mass fractions. @@ -458,10 +467,24 @@ def write_hdf(self, filename, key='df', species='X', PyTables. These packages can be installed using pip (`pandas` and `tables`) or conda (`pandas` and `pytables`). """ + if not key: + key = type(self).__name__ + + # save data cols = ('extra', 'T', 'D', species) self.to_solution_array().write_hdf(filename, cols=cols, key=key, mode=mode, complevel=complevel) + # convert simulation settings to tabular format + df = _pandas.DataFrame() + df['key'] = [key] + for key, val in self.settings.items(): + df[key] = [val] + df.set_index('key') + + # store settings to HDF container file as a separate group + df.to_hdf(filename, key='settings', append=True) + def read_hdf(self, filename, key=None, restore_boundaries=True): """ Restore the solution vector from a HDF container file. @@ -483,6 +506,37 @@ def read_hdf(self, filename, key=None, restore_boundaries=True): arr.read_hdf(filename, key=key) self.from_solution_array(arr, restore_boundaries=restore_boundaries) + @property + def settings(self): + """ Return a dictionary listing simulation settings """ + out = {'type': type(self).__name__} + out['transport_model'] = self.transport_model + out['energy_enabled'] = self.energy_enabled + out['soret_enabled'] = self.soret_enabled + out['radiation_enabled'] = self.radiation_enabled + out.update(self.get_refine_criteria()) + out['max_time_step_count'] = self.max_time_step_count + out['max_grid_points'] = self.get_max_grid_points(self.flame) + + # add tolerance settings + tols = {'steady_abstol': self.flame.steady_abstol(), + 'steady_reltol': self.flame.steady_reltol(), + 'transient_abstol': self.flame.transient_abstol(), + 'transient_reltol': self.flame.transient_reltol()} + comp = np.array(self.flame.component_names) + for tname, tol in tols.items(): + # add mode (most frequent tolerance setting) + values, counts = np.unique(tol, return_counts=True) + ix = np.argmax(counts) + out.update({tname: values[ix]}) + + # add values deviating from mode + ix = np.logical_not(np.isclose(tol, values[ix])) + out.update({'{}_{}'.format(tname, c) + for c, t in zip(comp[ix], tol[ix])}) + + return out + def _load_restart_data(self, source, **kwargs): """ Load data for restart (called by set_initial_guess) """ if isinstance(source, SolutionArray): From 0443d26e5c3b80977f1e96b0690f7002671dee7d Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Wed, 26 Feb 2020 13:44:28 -0600 Subject: [PATCH 12/29] [1D] Add missing getters for radiation-enabled simulations --- include/cantera/oneD/StFlow.h | 22 +++++++++++-- interfaces/cython/cantera/_cantera.pxd | 3 ++ interfaces/cython/cantera/onedim.py | 8 ++++- interfaces/cython/cantera/onedim.pyx | 31 ++++++++++++++++++- interfaces/cython/cantera/test/test_onedim.py | 2 +- 5 files changed, 61 insertions(+), 5 deletions(-) diff --git a/include/cantera/oneD/StFlow.h b/include/cantera/oneD/StFlow.h index aeaf0b668f..8d5c9ff9f9 100644 --- a/include/cantera/oneD/StFlow.h +++ b/include/cantera/oneD/StFlow.h @@ -198,13 +198,24 @@ class StFlow : public Domain1D return m_do_radiation; } + //! Return radiative heat loss at grid point j + double radiativeHeatLoss(size_t j) const { + return m_qdotRadiation[j]; + } + //! Set the emissivities for the boundary values /*! * Reads the emissivities for the left and right boundary values in the * radiative term and writes them into the variables, which are used for the * calculation. */ - void setBoundaryEmissivities(doublereal e_left, doublereal e_right); + void setBoundaryEmissivities(double e_left, double e_right); + + //! Return emissivitiy at left boundary + double getLeftEmissivity() const { return m_epsilon_left; } + + //! Return emissivitiy at right boundary + double getRightEmissivity() const { return m_epsilon_right; } void fixTemperature(size_t j=npos); @@ -215,7 +226,14 @@ class StFlow : public Domain1D //! Change the grid size. Called after grid refinement. virtual void resize(size_t components, size_t points); - virtual void setFixedPoint(int j0, doublereal t0) {} + /*! + * @deprecated To be removed after Cantera 2.5. + */ + virtual void setFixedPoint(int j0, doublereal t0) { + // this does nothing and does not appear to be overloaded + warn_deprecated("StFlow::setFixedPoint", + "To be removed after Cantera 2.5."); + } //! Set the gas object state to be consistent with the solution at point j. void setGas(const doublereal* x, size_t j); diff --git a/interfaces/cython/cantera/_cantera.pxd b/interfaces/cython/cantera/_cantera.pxd index 20746d9d2e..8b4d9b326e 100644 --- a/interfaces/cython/cantera/_cantera.pxd +++ b/interfaces/cython/cantera/_cantera.pxd @@ -750,9 +750,12 @@ cdef extern from "cantera/oneD/StFlow.h": void setPressure(double) void enableRadiation(cbool) cbool radiationEnabled() + double radiativeHeatLoss(size_t) double pressure() void setFixedTempProfile(vector[double]&, vector[double]&) void setBoundaryEmissivities(double, double) + double getLeftEmissivity() + double getRightEmissivity() void solveEnergyEqn() void fixTemperature() cbool doEnergy(size_t) diff --git a/interfaces/cython/cantera/onedim.py b/interfaces/cython/cantera/onedim.py index f400473c94..9109969126 100644 --- a/interfaces/cython/cantera/onedim.py +++ b/interfaces/cython/cantera/onedim.py @@ -309,6 +309,9 @@ def to_solution_array(self): else: val = self.profile(self.flame, e) extra[e] = np.hstack([np.nan, val, np.nan]) + if self.radiation_enabled: + qdot = self.flame.radiative_heat_loss() + extra['qdot'] = np.hstack([np.nan, qdot, np.nan]) # consider inlet boundaries left = self.domains[0] # left boundary is always an inlet @@ -374,7 +377,7 @@ def from_solution_array(self, arr, restore_boundaries=True): self.set_profile('T', xi, arr.T[idx]) for e in self._extra: val = getattr(arr, e)[idx] - if e == 'grid': + if e in ['grid', 'qdot']: pass elif e == 'velocity': self.set_profile('u', xi, val) @@ -514,6 +517,9 @@ def settings(self): out['energy_enabled'] = self.energy_enabled out['soret_enabled'] = self.soret_enabled out['radiation_enabled'] = self.radiation_enabled + epsilon = self.flame.boundary_emissivities + out['emissivity_left'] = epsilon[0] + out['emissivity_right'] = epsilon[1] out.update(self.get_refine_criteria()) out['max_time_step_count'] = self.max_time_step_count out['max_grid_points'] = self.get_max_grid_points(self.flame) diff --git a/interfaces/cython/cantera/onedim.pyx b/interfaces/cython/cantera/onedim.pyx index 1321676317..65a1234bc9 100644 --- a/interfaces/cython/cantera/onedim.pyx +++ b/interfaces/cython/cantera/onedim.pyx @@ -467,7 +467,26 @@ cdef class _FlowBase(Domain1D): del self.flow def set_boundary_emissivities(self, e_left, e_right): - self.flow.setBoundaryEmissivities(e_left, e_right) + """ + .. deprecated:: 2.5 + + To be deprecated with version 2.5, and removed thereafter. + Replaced by property `boundary_emissivities`. + """ + warnings.warn("To be removed after Cantera 2.5. " + "Replaced by property 'boundary_emissivities'", + DeprecationWarning) + self.boundary_emissivities = e_left, e_right + + property boundary_emissivities: + """ Set/get boundary emissivities. """ + def __get__(self): + return self.flow.getLeftEmissivity(), self.flow.getRightEmissivity() + def __set__(self, tuple epsilon): + if len(epsilon) == 2: + self.flow.setBoundaryEmissivities(epsilon[0], epsilon[1]) + else: + raise ValueError("Setter requires tuple of length 2.") property radiation_enabled: """ Determines whether or not to include radiative heat transfer """ @@ -476,6 +495,16 @@ cdef class _FlowBase(Domain1D): def __set__(self, do_radiation): self.flow.enableRadiation(do_radiation) + def radiative_heat_loss(self): + """ + Return radiative heat loss (only non-zero if radiation is enabled). + """ + cdef int j + cdef np.ndarray[np.double_t, ndim=1] data = np.empty(self.n_points) + for j in range(self.n_points): + data[j] = self.flow.radiativeHeatLoss(j) + return data + def set_free_flow(self): """ Set flow configuration for freely-propagating flames, using an internal diff --git a/interfaces/cython/cantera/test/test_onedim.py b/interfaces/cython/cantera/test/test_onedim.py index a199050b64..9137e9935e 100644 --- a/interfaces/cython/cantera/test/test_onedim.py +++ b/interfaces/cython/cantera/test/test_onedim.py @@ -736,7 +736,7 @@ def test_mixture_averaged_rad(self, saveReference=False): self.assertFalse(self.sim.radiation_enabled) self.sim.radiation_enabled = True self.assertTrue(self.sim.radiation_enabled) - self.sim.set_boundary_emissivities(0.25,0.15) + self.sim.flame.boundary_emissivities = 0.25, 0.15 self.solve_mix() data = np.empty((self.sim.flame.n_points, self.gas.n_species + 4)) From a605fa8d4e128bfffaa8c90401dabffc9efcf948 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Wed, 26 Feb 2020 15:20:11 -0600 Subject: [PATCH 13/29] [1D] Enable output of fixed temperature settings for FreeFlame --- include/cantera/oneD/Sim1D.h | 13 ++++++-- interfaces/cython/cantera/_cantera.pxd | 2 ++ interfaces/cython/cantera/onedim.py | 9 +++--- interfaces/cython/cantera/onedim.pyx | 28 +++++++++++++++- interfaces/cython/cantera/test/test_onedim.py | 11 +++++++ src/oneD/Sim1D.cpp | 32 +++++++++++++++++-- src/oneD/StFlow.cpp | 2 +- 7 files changed, 85 insertions(+), 12 deletions(-) diff --git a/include/cantera/oneD/Sim1D.h b/include/cantera/oneD/Sim1D.h index 967206e3d9..583809b2f2 100644 --- a/include/cantera/oneD/Sim1D.h +++ b/include/cantera/oneD/Sim1D.h @@ -129,15 +129,22 @@ class Sim1D : public OneDim int refine(int loglevel=0); //! Add node for fixed temperature point of freely propagating flame - int setFixedTemperature(doublereal t); + int setFixedTemperature(double t); + + //! Return temperature at the point used to fix the flame location + double getFixedTemperature(); + + //! Return location of the point where temperature is fixed + double getFixedTemperatureLocation(); /** * Set grid refinement criteria. If dom >= 0, then the settings * apply only to the specified domain. If dom < 0, the settings * are applied to each domain. @see Refiner::setCriteria. */ - void setRefineCriteria(int dom = -1, doublereal ratio = 10.0, - doublereal slope = 0.8, doublereal curve = 0.8, doublereal prune = -0.1); + void setRefineCriteria(int dom = -1, double ratio = 10.0, + double slope = 0.8, double curve = 0.8, + double prune = -0.1); /** * Get the grid refinement criteria. dom must be greater than diff --git a/interfaces/cython/cantera/_cantera.pxd b/interfaces/cython/cantera/_cantera.pxd index 8b4d9b326e..aa1540764e 100644 --- a/interfaces/cython/cantera/_cantera.pxd +++ b/interfaces/cython/cantera/_cantera.pxd @@ -818,6 +818,8 @@ cdef extern from "cantera/oneD/Sim1D.h": size_t maxGridPoints(size_t) except +translate_exception void setGridMin(int, double) except +translate_exception void setFixedTemperature(double) except +translate_exception + double getFixedTemperature() + double getFixedTemperatureLocation() void setInterrupt(CxxFunc1*) except +translate_exception void setTimeStepCallback(CxxFunc1*) void setSteadyCallback(CxxFunc1*) diff --git a/interfaces/cython/cantera/onedim.py b/interfaces/cython/cantera/onedim.py index 9109969126..de30c32f08 100644 --- a/interfaces/cython/cantera/onedim.py +++ b/interfaces/cython/cantera/onedim.py @@ -520,6 +520,7 @@ def settings(self): epsilon = self.flame.boundary_emissivities out['emissivity_left'] = epsilon[0] out['emissivity_right'] = epsilon[1] + out['fixed_temperature'] = self.fixed_temperature out.update(self.get_refine_criteria()) out['max_time_step_count'] = self.max_time_step_count out['max_grid_points'] = self.get_max_grid_points(self.flame) @@ -707,7 +708,7 @@ def set_initial_guess(self, locs=[0.0, 0.3, 0.5, 1.0], data=None, key=None): # set fixed temperature Tmid = .75 * data.T[0] + .25 * data.T[-1] i = np.flatnonzero(data.T < Tmid)[-1] - self.set_fixed_temperature(data.T[i]) + self.fixed_temperature = data.T[i] return @@ -736,11 +737,11 @@ def set_initial_guess(self, locs=[0.0, 0.3, 0.5, 1.0], data=None, key=None): Tmid = 0.75 * T0 + 0.25 * Teq i = np.flatnonzero(T < Tmid)[-1] # last point less than Tmid if Tmid - T[i] < 0.2 * (Tmid - T0): - self.set_fixed_temperature(T[i]) + self.fixed_temperature = T[i] elif T[i+1] - Tmid < 0.2 * (Teq - Tmid): - self.set_fixed_temperature(T[i+1]) + self.fixed_temperature = T[i+1] else: - self.set_fixed_temperature(Tmid) + self.fixed_temperature = Tmid for n in range(self.gas.n_species): self.set_profile(self.gas.species_name(n), diff --git a/interfaces/cython/cantera/onedim.pyx b/interfaces/cython/cantera/onedim.pyx index 65a1234bc9..d9c91f87d1 100644 --- a/interfaces/cython/cantera/onedim.pyx +++ b/interfaces/cython/cantera/onedim.pyx @@ -1153,8 +1153,34 @@ cdef class Sim1D: """ Set the temperature used to fix the spatial location of a freely propagating flame. + + .. deprecated:: 2.5 + + To be deprecated with version 2.5, and removed thereafter. + Replaced by property `fixed_temperature`. + """ + warnings.warn("To be removed after Cantera 2.5. " + "Replaced by property 'fixed_temperature'", + DeprecationWarning) + self.fixed_temperature = T + + property fixed_temperature: + """ + Set the temperature used to fix the spatial location of a freely + propagating flame. """ - self.sim.setFixedTemperature(T) + def __get__(self): + return self.sim.getFixedTemperature() + def __set__(self, T): + self.sim.setFixedTemperature(T) + + property fixed_temperature_location: + """ + Return the location of the point where temperature is fixed for a freely + propagating flame. + """ + def __get__(self): + return self.sim.getFixedTemperatureLocation() def save(self, filename='soln.xml', name='solution', description='none', loglevel=1): diff --git a/interfaces/cython/cantera/test/test_onedim.py b/interfaces/cython/cantera/test/test_onedim.py index 9137e9935e..3fc4110d9e 100644 --- a/interfaces/cython/cantera/test/test_onedim.py +++ b/interfaces/cython/cantera/test/test_onedim.py @@ -157,6 +157,17 @@ def solve_multi(self): self.assertEqual(self.sim.transport_model, 'Multi') + def test_fixed_temperature(self): + # test setting of fixed temperature + Tin = 300 + p = ct.one_atm + reactants = 'H2:0.65, O2:0.5, AR:2' + self.create_sim(p, Tin, reactants, width=0.0001) + self.sim.set_initial_guess() + tfixed = 800. + self.sim.fixed_temperature = tfixed + self.assertEqual(self.sim.fixed_temperature, tfixed) + def test_auto_width(self): Tin = 300 p = ct.one_atm diff --git a/src/oneD/Sim1D.cpp b/src/oneD/Sim1D.cpp index 5231bdf35e..202fd7a415 100644 --- a/src/oneD/Sim1D.cpp +++ b/src/oneD/Sim1D.cpp @@ -424,7 +424,7 @@ int Sim1D::refine(int loglevel) return np; } -int Sim1D::setFixedTemperature(doublereal t) +int Sim1D::setFixedTemperature(double t) { int np = 0; vector_fp znew, xnew; @@ -511,8 +511,34 @@ int Sim1D::setFixedTemperature(doublereal t) return np; } -void Sim1D::setRefineCriteria(int dom, doublereal ratio, - doublereal slope, doublereal curve, doublereal prune) +double Sim1D::getFixedTemperature() +{ + double t_fixed = -1.; + for (size_t n = 0; n < nDomains(); n++) { + StFlow* d = dynamic_cast(&domain(n)); + if (d && d->domainType() == cFreeFlow && d->m_tfixed > 0) { + t_fixed = d->m_tfixed; + break; + } + } + return t_fixed; +} + +double Sim1D::getFixedTemperatureLocation() +{ + double z_fixed = -1.; + for (size_t n = 0; n < nDomains(); n++) { + StFlow* d = dynamic_cast(&domain(n)); + if (d && d->domainType() == cFreeFlow && d->m_tfixed > 0) { + z_fixed = d->m_zfixed; + break; + } + } + return z_fixed; +} + +void Sim1D::setRefineCriteria(int dom, double ratio, + double slope, double curve, double prune) { if (dom >= 0) { Refiner& r = domain(dom).refiner(); diff --git a/src/oneD/StFlow.cpp b/src/oneD/StFlow.cpp index 1185cc8d82..e8faee3169 100644 --- a/src/oneD/StFlow.cpp +++ b/src/oneD/StFlow.cpp @@ -28,7 +28,7 @@ StFlow::StFlow(ThermoPhase* ph, size_t nsp, size_t points) : m_kExcessLeft(0), m_kExcessRight(0), m_zfixed(Undef), - m_tfixed(Undef) + m_tfixed(-1.) { if (ph->type() == "IdealGas") { m_thermo = static_cast(ph); From efa524c206519c955aa7a5b59b560372812abab1 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Wed, 26 Feb 2020 15:31:26 -0600 Subject: [PATCH 14/29] [1D] Add time stamp to HDF output --- interfaces/cython/cantera/onedim.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/interfaces/cython/cantera/onedim.py b/interfaces/cython/cantera/onedim.py index de30c32f08..9594701b1a 100644 --- a/interfaces/cython/cantera/onedim.py +++ b/interfaces/cython/cantera/onedim.py @@ -6,6 +6,7 @@ from .composite import Solution, SolutionArray import csv as _csv from math import erf +from email.utils import formatdate # avoid explicit dependence of cantera on pandas try: @@ -481,6 +482,7 @@ def write_hdf(self, filename, key=None, species='X', # convert simulation settings to tabular format df = _pandas.DataFrame() df['key'] = [key] + df['date'] = formatdate(localtime=True) for key, val in self.settings.items(): df[key] = [val] df.set_index('key') From 443aa1834154c89c8eb053449a943bfcbfcb9e4d Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Wed, 26 Feb 2020 16:29:20 -0600 Subject: [PATCH 15/29] [1D] Deprecate component name 'u' in favor of 'velocity' --- .../examples/onedim/adiabatic_flame.py | 4 +- .../examples/onedim/diffusion_flame_batch.py | 10 ++-- .../onedim/diffusion_flame_extinction.py | 6 +-- .../examples/onedim/flamespeed_sensitivity.py | 2 +- .../cantera/examples/onedim/ion_free_flame.py | 2 +- .../onedim/premixed_counterflow_twin_flame.py | 12 +++-- interfaces/cython/cantera/onedim.py | 49 ++++++++++++------- interfaces/cython/cantera/test/test_onedim.py | 45 +++++++++-------- samples/cxx/flamespeed/flamespeed.cpp | 20 +++++--- src/oneD/StFlow.cpp | 7 ++- 10 files changed, 93 insertions(+), 64 deletions(-) diff --git a/interfaces/cython/cantera/examples/onedim/adiabatic_flame.py b/interfaces/cython/cantera/examples/onedim/adiabatic_flame.py index 5c3b3f9f4d..f3b0dbd05f 100644 --- a/interfaces/cython/cantera/examples/onedim/adiabatic_flame.py +++ b/interfaces/cython/cantera/examples/onedim/adiabatic_flame.py @@ -31,13 +31,13 @@ # Solve with the energy equation enabled f.save('h2_adiabatic.xml', 'mix', 'solution with mixture-averaged transport') f.show_solution() -print('mixture-averaged flamespeed = {0:7f} m/s'.format(f.u[0])) +print('mixture-averaged flamespeed = {0:7f} m/s'.format(f.velocity[0])) # Solve with multi-component transport properties f.transport_model = 'Multi' f.solve(loglevel) # don't use 'auto' on subsequent solves f.show_solution() -print('multicomponent flamespeed = {0:7f} m/s'.format(f.u[0])) +print('multicomponent flamespeed = {0:7f} m/s'.format(f.velocity[0])) f.save('h2_adiabatic.xml', 'multi', 'solution with multicomponent transport') # write the velocity, temperature, density, and mole fractions to a CSV file diff --git a/interfaces/cython/cantera/examples/onedim/diffusion_flame_batch.py b/interfaces/cython/cantera/examples/onedim/diffusion_flame_batch.py index 311b7dfb91..83a54c50c1 100644 --- a/interfaces/cython/cantera/examples/onedim/diffusion_flame_batch.py +++ b/interfaces/cython/cantera/examples/onedim/diffusion_flame_batch.py @@ -111,8 +111,8 @@ def interrupt_extinction(t): f.fuel_inlet.mdot *= rel_pressure_increase ** exp_mdot_p f.oxidizer_inlet.mdot *= rel_pressure_increase ** exp_mdot_p # Update velocities - f.set_profile('u', normalized_grid, - f.u * rel_pressure_increase ** exp_u_p) + f.set_profile('velocity', normalized_grid, + f.velocity * rel_pressure_increase ** exp_u_p) f.set_profile('V', normalized_grid, f.V * rel_pressure_increase ** exp_V_p) # Update pressure curvature @@ -167,7 +167,7 @@ def interrupt_extinction(t): f.fuel_inlet.mdot *= strain_factor ** exp_mdot_a f.oxidizer_inlet.mdot *= strain_factor ** exp_mdot_a # Update velocities - f.set_profile('u', normalized_grid, f.u * strain_factor ** exp_u_a) + f.set_profile('velocity', normalized_grid, f.velocity * strain_factor ** exp_u_a) f.set_profile('V', normalized_grid, f.V * strain_factor ** exp_V_a) # Update pressure curvature f.set_profile('lambda', normalized_grid, f.L * strain_factor ** exp_lam_a) @@ -203,7 +203,7 @@ def interrupt_extinction(t): # Plot the axial velocity profiles (normalized by the fuel inlet velocity) # for selected pressures - ax2.plot(f.grid / f.grid[-1], f.u / f.u[0], + ax2.plot(f.grid / f.grid[-1], f.velocity / f.velocity[0], label='{0:05.1f} bar'.format(p)) ax1.legend(loc=0) @@ -231,7 +231,7 @@ def interrupt_extinction(t): # Plot the axial velocity profiles (normalized by the fuel inlet velocity) # for the strain rate loop (selected) - ax4.plot(f.grid / f.grid[-1], f.u / f.u[0], + ax4.plot(f.grid / f.grid[-1], f.velocity / f.velocity[0], label=format(a_max, '.2e') + ' 1/s') ax3.legend(loc=0) diff --git a/interfaces/cython/cantera/examples/onedim/diffusion_flame_extinction.py b/interfaces/cython/cantera/examples/onedim/diffusion_flame_extinction.py index 1183f2a8c7..5bd60abd10 100644 --- a/interfaces/cython/cantera/examples/onedim/diffusion_flame_extinction.py +++ b/interfaces/cython/cantera/examples/onedim/diffusion_flame_extinction.py @@ -88,7 +88,7 @@ # List of peak temperatures T_max = [np.max(f.T)] # List of maximum axial velocity gradients -a_max = [np.max(np.abs(np.gradient(f.u) / np.gradient(f.grid)))] +a_max = [np.max(np.abs(np.gradient(f.velocity) / np.gradient(f.grid)))] # Simulate counterflow flames at increasing strain rates until the flame is # extinguished. To achieve a fast simulation, an initial coarse strain rate @@ -109,7 +109,7 @@ f.fuel_inlet.mdot *= strain_factor ** exp_mdot_a f.oxidizer_inlet.mdot *= strain_factor ** exp_mdot_a # Update velocities - f.set_profile('u', normalized_grid, f.u * strain_factor ** exp_u_a) + f.set_profile('velocity', normalized_grid, f.velocity * strain_factor ** exp_u_a) f.set_profile('V', normalized_grid, f.V * strain_factor ** exp_V_a) # Update pressure curvature f.set_profile('lambda', normalized_grid, f.L * strain_factor ** exp_lam_a) @@ -125,7 +125,7 @@ description='Cantera version ' + ct.__version__ + ', reaction mechanism ' + reaction_mechanism) T_max.append(np.max(f.T)) - a_max.append(np.max(np.abs(np.gradient(f.u) / np.gradient(f.grid)))) + a_max.append(np.max(np.abs(np.gradient(f.velocity) / np.gradient(f.grid)))) # If the temperature difference is too small and the minimum relative # strain rate increase is reached, abort if ((T_max[-2] - T_max[-1] < delta_T_min) & diff --git a/interfaces/cython/cantera/examples/onedim/flamespeed_sensitivity.py b/interfaces/cython/cantera/examples/onedim/flamespeed_sensitivity.py index a4ee73601e..b477f3e55b 100644 --- a/interfaces/cython/cantera/examples/onedim/flamespeed_sensitivity.py +++ b/interfaces/cython/cantera/examples/onedim/flamespeed_sensitivity.py @@ -24,7 +24,7 @@ f.set_refine_criteria(ratio=3, slope=0.07, curve=0.14) f.solve(loglevel=1, auto=True) -print('\nmixture-averaged flamespeed = {:7f} m/s\n'.format(f.u[0])) +print('\nmixture-averaged flamespeed = {:7f} m/s\n'.format(f.velocity[0])) # Use the adjoint method to calculate sensitivities sens = f.get_flame_speed_reaction_sensitivities() diff --git a/interfaces/cython/cantera/examples/onedim/ion_free_flame.py b/interfaces/cython/cantera/examples/onedim/ion_free_flame.py index 0ab8fc12df..6dcf715736 100644 --- a/interfaces/cython/cantera/examples/onedim/ion_free_flame.py +++ b/interfaces/cython/cantera/examples/onedim/ion_free_flame.py @@ -31,7 +31,7 @@ f.save('CH4_adiabatic.xml', 'ion', 'solution with ionized gas transport') f.show_solution() -print('mixture-averaged flamespeed = {0:7f} m/s'.format(f.u[0])) +print('mixture-averaged flamespeed = {0:7f} m/s'.format(f.velocity[0])) # write the velocity, temperature, density, and mole fractions to a CSV file f.write_csv('CH4_adiabatic.csv', quiet=False) diff --git a/interfaces/cython/cantera/examples/onedim/premixed_counterflow_twin_flame.py b/interfaces/cython/cantera/examples/onedim/premixed_counterflow_twin_flame.py index 3bf8d05bf5..72039209ce 100644 --- a/interfaces/cython/cantera/examples/onedim/premixed_counterflow_twin_flame.py +++ b/interfaces/cython/cantera/examples/onedim/premixed_counterflow_twin_flame.py @@ -29,12 +29,12 @@ def derivative(x, y): def computeStrainRates(oppFlame): # Compute the derivative of axial velocity to obtain normal strain rate - strainRates = derivative(oppFlame.grid, oppFlame.u) + strainRates = derivative(oppFlame.grid, oppFlame.velocity) # Obtain the location of the max. strain rate upstream of the pre-heat zone. # This is the characteristic strain rate maxStrLocation = abs(strainRates).argmax() - minVelocityPoint = oppFlame.u[:maxStrLocation].argmin() + minVelocityPoint = oppFlame.velocity[:maxStrLocation].argmin() # Characteristic Strain Rate = K strainRatePoint = abs(strainRates[:minVelocityPoint]).argmax() @@ -130,15 +130,17 @@ def solveOpposedFlame(oppFlame, massFlux=0.12, loglevel=1, # Axial Velocity Plot plt.subplot(1, 2, 1) - plt.plot(oppFlame.grid, oppFlame.u, 'r', lw=2) + plt.plot(oppFlame.grid, oppFlame.velocity, 'r', lw=2) plt.xlim(oppFlame.grid[0], oppFlame.grid[-1]) plt.xlabel('Distance (m)') plt.ylabel('Axial Velocity (m/s)') # Identify the point where the strain rate is calculated - plt.plot(oppFlame.grid[strainRatePoint], oppFlame.u[strainRatePoint], 'gs') + plt.plot(oppFlame.grid[strainRatePoint], + oppFlame.velocity[strainRatePoint], 'gs') plt.annotate('Strain-Rate point', - xy=(oppFlame.grid[strainRatePoint], oppFlame.u[strainRatePoint]), + xy=(oppFlame.grid[strainRatePoint], + oppFlame.velocity[strainRatePoint]), xytext=(0.001, 0.1), arrowprops={'arrowstyle': '->'}) diff --git a/interfaces/cython/cantera/onedim.py b/interfaces/cython/cantera/onedim.py index 9594701b1a..b0fb79c764 100644 --- a/interfaces/cython/cantera/onedim.py +++ b/interfaces/cython/cantera/onedim.py @@ -170,8 +170,23 @@ def T(self): def u(self): """ Array containing the velocity [m/s] normal to the flame at each point. + + .. deprecated:: 2.5 + + To be deprecated with version 2.5, and removed thereafter. + Replaced by property `velocity`. + """ + warnings.warn("To be removed after Cantera 2.5. " + "Replaced by property 'velocity'", + DeprecationWarning) + return self.profile(self.flame, 'velocity') + + @property + def velocity(self): + """ + Array containing the velocity [m/s] normal to the flame at each point. """ - return self.profile(self.flame, 'u') + return self.profile(self.flame, 'velocity') @property def V(self): @@ -272,7 +287,7 @@ def write_csv(self, filename, species='X', quiet=True): z = self.grid T = self.T - u = self.u + u = self.velocity V = self.V with open(filename, 'w', newline='') as csvfile: @@ -303,8 +318,6 @@ def to_solution_array(self): for e in self._extra: if e == 'grid': val = self.grid - elif e == 'velocity': - val = self.profile(self.flame, 'u') elif e == 'gradient': val = self.profile(self.flame, 'V') else: @@ -380,8 +393,6 @@ def from_solution_array(self, arr, restore_boundaries=True): val = getattr(arr, e)[idx] if e in ['grid', 'qdot']: pass - elif e == 'velocity': - self.set_profile('u', xi, val) elif e == 'gradient': self.set_profile('V', xi, val) else: @@ -730,7 +741,7 @@ def set_initial_guess(self, locs=[0.0, 0.3, 0.5, 1.0], data=None, key=None): Yeq = self.gas.Y u1 = self.inlet.mdot/self.gas.density - self.set_profile('u', locs, [u0, u0, u1, u1]) + self.set_profile('velocity', locs, [u0, u0, u1, u1]) self.set_profile('T', locs, [T0, T0, Teq, Teq]) # Pick the location of the fixed temperature point, using an existing @@ -824,12 +835,12 @@ def get_flame_speed_reaction_sensitivities(self): """ def g(sim): - return sim.u[0] + return sim.velocity[0] Nvars = sum(D.n_components * D.n_points for D in self.domains) # Index of u[0] in the global solution vector - i_Su = self.inlet.n_components + self.flame.component_index('u') + i_Su = self.inlet.n_components + self.flame.component_index('velocity') dgdx = np.zeros(Nvars) dgdx[i_Su] = 1 @@ -856,13 +867,13 @@ def write_csv(self, filename, species='X', quiet=True): """ z = self.grid T = self.T - u = self.u + u = self.velocity V = self.V E = self.E with open(filename, 'w', newline='') as csvfile: writer = _csv.writer(csvfile) - writer.writerow(['z (m)', 'u (m/s)', 'V (1/s)', 'T (K)', + writer.writerow(['z (m)', 'velocity (m/s)', 'V (1/s)', 'T (K)', 'E (V/m)', 'rho (kg/m3)'] + self.gas.species_names) for n in range(self.flame.n_points): self.set_gas_state(n) @@ -972,7 +983,7 @@ def set_initial_guess(self): u1 = self.burner.mdot/self.gas.density locs = [0.0, 0.2, 1.0] - self.set_profile('u', locs, [u0, u1, u1]) + self.set_profile('velocity', locs, [u0, u1, u1]) self.set_profile('T', locs, [T0, Teq, Teq]) for n in range(self.gas.n_species): self.set_profile(self.gas.species_name(n), @@ -1162,7 +1173,7 @@ def set_initial_guess(self): T[-1] = T0o zrel = (zz - zz[0])/dz - self.set_profile('u', [0.0, 1.0], [u0f, -u0o]) + self.set_profile('velocity', [0.0, 1.0], [u0f, -u0o]) self.set_profile('V', [0.0, x0/dz, 1.0], [0.0, a, 0.0]) self.set_profile('T', zrel, T) for k,spec in enumerate(self.gas.species_names): @@ -1273,10 +1284,10 @@ def strain_rate(self, definition, fuel=None, oxidizer='O2', stoich=None): .. math:: a_{o} = \sqrt{-\frac{\Lambda}{\rho_{o}}} """ if definition == 'mean': - return - (self.u[-1] - self.u[0]) / self.grid[-1] + return - (self.velocity[-1] - self.velocity[0]) / self.grid[-1] elif definition == 'max': - return np.max(np.abs(np.gradient(self.u) / np.gradient(self.grid))) + return np.max(np.abs(np.gradient(self.velocity) / np.gradient(self.grid))) elif definition == 'stoichiometric': if fuel is None: @@ -1292,7 +1303,7 @@ def strain_rate(self, definition, fuel=None, oxidizer='O2', stoich=None): if 'C' in self.gas.element_names: stoich += self.gas.n_atoms(fuel, 'C') - d_u_d_z = np.gradient(self.u) / np.gradient(self.grid) + d_u_d_z = np.gradient(self.velocity) / np.gradient(self.grid) phi = (self.X[self.gas.species_index(fuel)] * stoich / np.maximum(self.X[self.gas.species_index(oxidizer)], 1e-20)) z_stoich = np.interp(-1., -phi, self.grid) @@ -1407,7 +1418,7 @@ def set_initial_guess(self, products='inlet'): [Y0[k], Y0[k]]) locs = np.array([0.0, 1.0]) - self.set_profile('u', locs, [u0, 0.0]) + self.set_profile('velocity', locs, [u0, 0.0]) self.set_profile('V', locs, [0.0, 0.0]) @@ -1498,7 +1509,7 @@ def set_initial_guess(self, equilibrate=True): # estimate stagnation point x0 = rhou*uu * dz / (rhou*uu + rhob*ub) - self.set_profile('u', [0.0, 1.0], [uu, -ub]) + self.set_profile('velocity', [0.0, 1.0], [uu, -ub]) self.set_profile('V', [0.0, x0/dz, 1.0], [0.0, a, 0.0]) @@ -1571,5 +1582,5 @@ def set_initial_guess(self): dz = zz[-1] - zz[0] a = 2 * uu / dz - self.set_profile('u', [0.0, 1.0], [uu, 0]) + self.set_profile('velocity', [0.0, 1.0], [uu, 0]) self.set_profile('V', [0.0, 1.0], [0.0, a]) diff --git a/interfaces/cython/cantera/test/test_onedim.py b/interfaces/cython/cantera/test/test_onedim.py index 3fc4110d9e..761be04b43 100644 --- a/interfaces/cython/cantera/test/test_onedim.py +++ b/interfaces/cython/cantera/test/test_onedim.py @@ -194,7 +194,7 @@ def test_auto_width2(self): self.sim.set_refine_criteria(ratio=4, slope=0.8, curve=0.8) self.sim.solve(refine_grid=True, auto=True, loglevel=0) - self.assertNear(self.sim.u[0], 17.02, 1e-1) + self.assertNear(self.sim.velocity[0], 17.02, 1e-1) self.assertLess(self.sim.grid[-1], 2.0) # grid should not be too wide @@ -241,7 +241,7 @@ def run_mix(self, phi, T, width, p, refine): rhou = self.sim.inlet.mdot # Check continuity - for rhou_j in self.sim.density * self.sim.u: + for rhou_j in self.sim.density * self.sim.velocity: self.assertNear(rhou_j, rhou, 1e-4) def test_solution_array_output(self): @@ -272,7 +272,7 @@ def test_restart(self): # Check continuity rhou = self.sim.inlet.mdot - for rhou_j in self.sim.density * self.sim.u: + for rhou_j in self.sim.density * self.sim.velocity: self.assertNear(rhou_j, rhou, 1e-4) def test_mixture_averaged_case1(self): @@ -309,15 +309,15 @@ def test_adjoint_sensitivities(self): # Forward sensitivities dk = 1e-4 - Su0 = self.sim.u[0] + Su0 = self.sim.velocity[0] for m in range(self.gas.n_reactions): self.gas.set_multiplier(1.0) # reset all multipliers self.gas.set_multiplier(1+dk, m) # perturb reaction m self.sim.solve(loglevel=0, refine_grid=False) - Suplus = self.sim.u[0] + Suplus = self.sim.velocity[0] self.gas.set_multiplier(1-dk, m) # perturb reaction m self.sim.solve(loglevel=0, refine_grid=False) - Suminus = self.sim.u[0] + Suminus = self.sim.velocity[0] fwd = (Suplus-Suminus)/(2*Su0*dk) self.assertNear(fwd, dSdk_adj[m], 5e-3) @@ -330,12 +330,12 @@ def test_multicomponent(self): self.create_sim(p, Tin, reactants) self.solve_fixed_T() self.solve_mix(ratio=5, slope=0.5, curve=0.3) - Su_mix = self.sim.u[0] + Su_mix = self.sim.velocity[0] # Multicomponent flame speed should be similar but not identical to # the mixture-averaged flame speed. self.solve_multi() - Su_multi = self.sim.u[0] + Su_multi = self.sim.velocity[0] self.assertFalse(self.sim.soret_enabled) self.assertNear(Su_mix, Su_multi, 5e-2) @@ -346,7 +346,7 @@ def test_multicomponent(self): self.sim.soret_enabled = True self.sim.solve(loglevel=0, refine_grid=True) self.assertTrue(self.sim.soret_enabled) - Su_soret = self.sim.u[0] + Su_soret = self.sim.velocity[0] self.assertNear(Su_multi, Su_soret, 2e-1) self.assertNotEqual(Su_multi, Su_soret) @@ -429,7 +429,7 @@ def test_save_restore(self): os.remove(filename) Y1 = self.sim.Y - u1 = self.sim.u + u1 = self.sim.velocity V1 = self.sim.V P1 = self.sim.P @@ -455,7 +455,7 @@ def test_save_restore(self): self.assertNear(P1, P2a) Y2 = self.sim.Y - u2 = self.sim.u + u2 = self.sim.velocity V2 = self.sim.V self.assertArrayNear(Y1, Y2) @@ -464,7 +464,7 @@ def test_save_restore(self): self.solve_fixed_T() Y3 = self.sim.Y - u3 = self.sim.u + u3 = self.sim.velocity V3 = self.sim.V # TODO: These tolereances seem too loose, but the tests fail on some @@ -478,7 +478,12 @@ def test_array_properties(self): for attr in ct.FlameBase.__dict__: if isinstance(ct.FlameBase.__dict__[attr], property): - getattr(self.sim, attr) + if attr == 'u': + msg = "Replaced by property 'velocity'" + with self.assertWarnsRegex(DeprecationWarning, msg): + getattr(self.sim, attr) + else: + getattr(self.sim, attr) def test_save_restore_add_species(self): reactants = 'H2:1.1, O2:1, AR:5' @@ -571,7 +576,7 @@ def test_refine_criteria(self): def test_replace_grid(self): self.create_sim(ct.one_atm, 300.0, 'H2:1.1, O2:1, AR:5') self.solve_fixed_T() - ub = self.sim.u[-1] + ub = self.sim.velocity[-1] # add some points to the grid grid = list(self.sim.grid) @@ -581,7 +586,7 @@ def test_replace_grid(self): self.sim.set_initial_guess() self.solve_fixed_T() - self.assertNear(self.sim.u[-1], ub, 1e-2) + self.assertNear(self.sim.velocity[-1], ub, 1e-2) def test_exceed_max_grid_points(self): self.create_sim(ct.one_atm, 400.0, 'H2:1.1, O2:1, AR:5') @@ -657,7 +662,7 @@ def test_mixture_averaged(self, saveReference=False): self.solve_mix() data = np.empty((self.sim.flame.n_points, self.gas.n_species + 4)) data[:,0] = self.sim.grid - data[:,1] = self.sim.u + data[:,1] = self.sim.velocity data[:,2] = self.sim.V data[:,3] = self.sim.T data[:,4:] = self.sim.Y.T @@ -696,7 +701,7 @@ def time_step_func(dt): data = np.empty((self.sim.flame.n_points, self.gas.n_species + 4)) data[:,0] = self.sim.grid - data[:,1] = self.sim.u + data[:,1] = self.sim.velocity data[:,2] = self.sim.V data[:,3] = self.sim.T data[:,4:] = self.sim.Y.T @@ -752,7 +757,7 @@ def test_mixture_averaged_rad(self, saveReference=False): self.solve_mix() data = np.empty((self.sim.flame.n_points, self.gas.n_species + 4)) data[:,0] = self.sim.grid - data[:,1] = self.sim.u + data[:,1] = self.sim.velocity data[:,2] = self.sim.V data[:,3] = self.sim.T data[:,4:] = self.sim.Y.T @@ -838,7 +843,7 @@ def test_mixture_averaged(self, saveReference=False): data = np.empty((sim.flame.n_points, gas.n_species + 4)) data[:,0] = sim.grid - data[:,1] = sim.u + data[:,1] = sim.velocity data[:,2] = sim.V data[:,3] = sim.T data[:,4:] = sim.Y.T @@ -927,7 +932,7 @@ def test_blowoff(self): sim.solve(loglevel=0, auto=True) # nonreacting solution self.assertNear(sim.T[-1], sim.T[0], 1e-6) - self.assertNear(sim.u[-1], sim.u[0], 1e-6) + self.assertNear(sim.velocity[-1], sim.velocity[0], 1e-6) self.assertArrayNear(sim.Y[:,0], sim.Y[:,-1], 1e-6, atol=1e-6) diff --git a/samples/cxx/flamespeed/flamespeed.cpp b/samples/cxx/flamespeed/flamespeed.cpp index d687671d7c..c6a219a101 100644 --- a/samples/cxx/flamespeed/flamespeed.cpp +++ b/samples/cxx/flamespeed/flamespeed.cpp @@ -101,7 +101,7 @@ int flamespeed(double phi) double uout = inlet.mdot()/rho_out; value = {uin, uin, uout, uout}; - flame.setInitialGuess("u",locs,value); + flame.setInitialGuess("velocity",locs,value); value = {temp, temp, Tad, Tad}; flame.setInitialGuess("T",locs,value); @@ -135,21 +135,24 @@ int flamespeed(double phi) bool refine_grid = true; flame.solve(loglevel,refine_grid); - double flameSpeed_mix = flame.value(flowdomain,flow.componentIndex("u"),0); + double flameSpeed_mix = flame.value(flowdomain, + flow.componentIndex("velocity"),0); print("Flame speed with mixture-averaged transport: {} m/s\n", flameSpeed_mix); // now switch to multicomponent transport flow.setTransport(*trmulti); flame.solve(loglevel, refine_grid); - double flameSpeed_multi = flame.value(flowdomain,flow.componentIndex("u"),0); + double flameSpeed_multi = flame.value(flowdomain, + flow.componentIndex("velocity"),0); print("Flame speed with multicomponent transport: {} m/s\n", flameSpeed_multi); // now enable Soret diffusion flow.enableSoret(true); flame.solve(loglevel, refine_grid); - double flameSpeed_full = flame.value(flowdomain,flow.componentIndex("u"),0); + double flameSpeed_full = flame.value(flowdomain, + flow.componentIndex("velocity"),0); print("Flame speed with multicomponent transport + Soret: {} m/s\n", flameSpeed_full); @@ -159,9 +162,12 @@ int flamespeed(double phi) "z (m)", "T (K)", "U (m/s)", "Y(CO)"); for (size_t n = 0; n < flow.nPoints(); n++) { Tvec.push_back(flame.value(flowdomain,flow.componentIndex("T"),n)); - COvec.push_back(flame.value(flowdomain,flow.componentIndex("CO"),n)); - CO2vec.push_back(flame.value(flowdomain,flow.componentIndex("CO2"),n)); - Uvec.push_back(flame.value(flowdomain,flow.componentIndex("u"),n)); + COvec.push_back(flame.value(flowdomain, + flow.componentIndex("CO"),n)); + CO2vec.push_back(flame.value(flowdomain, + flow.componentIndex("CO2"),n)); + Uvec.push_back(flame.value(flowdomain, + flow.componentIndex("velocity"),n)); zvec.push_back(flow.grid(n)); print("{:9.6f}\t{:8.3f}\t{:5.3f}\t{:7.5f}\n", flow.grid(n), Tvec[n], Uvec[n], COvec[n]); diff --git a/src/oneD/StFlow.cpp b/src/oneD/StFlow.cpp index e8faee3169..9259340305 100644 --- a/src/oneD/StFlow.cpp +++ b/src/oneD/StFlow.cpp @@ -574,7 +574,7 @@ string StFlow::componentName(size_t n) const { switch (n) { case 0: - return "u"; + return "velocity"; case 1: return "V"; case 2: @@ -595,6 +595,11 @@ string StFlow::componentName(size_t n) const size_t StFlow::componentIndex(const std::string& name) const { if (name=="u") { + warn_deprecated("StFlow::componentIndex", + "To be changed after Cantera 2.5. " + "Solution component 'u' renamed to 'velocity'"); + return 0; + } else if (name=="velocity") { return 0; } else if (name=="V") { return 1; From 005d9eb93bf36b160dff5fba8906424a2579003f Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Wed, 26 Feb 2020 16:34:59 -0600 Subject: [PATCH 16/29] [1D] Add unit test for simulation settings output --- interfaces/cython/cantera/test/test_onedim.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/interfaces/cython/cantera/test/test_onedim.py b/interfaces/cython/cantera/test/test_onedim.py index 761be04b43..eb28dccf6b 100644 --- a/interfaces/cython/cantera/test/test_onedim.py +++ b/interfaces/cython/cantera/test/test_onedim.py @@ -275,6 +275,22 @@ def test_restart(self): for rhou_j in self.sim.density * self.sim.velocity: self.assertNear(rhou_j, rhou, 1e-4) + def test_settings(self): + self.run_mix(phi=1.0, T=300, width=2.0, p=1.0, refine=False) + settings = self.sim.settings + + keys = ['type', 'transport_model', + 'energy_enabled', 'soret_enabled', 'radiation_enabled', + 'emissivity_left', 'emissivity_right', + 'fixed_temperature', + 'ratio', 'slope', 'curve', 'prune', + 'max_time_step_count', + 'max_grid_points', + 'steady_abstol', 'steady_reltol', + 'transient_abstol', 'transient_reltol'] + for k in keys: + self.assertIn(k, settings) + def test_mixture_averaged_case1(self): self.run_mix(phi=0.65, T=300, width=0.03, p=1.0, refine=True) From 61901670c610e444033f31d1e8f560df34d453c0 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Wed, 26 Feb 2020 17:15:12 -0600 Subject: [PATCH 17/29] [1D] Deprecate component name 'V' in favor of 'vGradient' --- .../examples/onedim/diffusion_flame_batch.py | 10 +++-- .../onedim/diffusion_flame_extinction.py | 6 ++- interfaces/cython/cantera/onedim.py | 37 ++++++++++++------- interfaces/cython/cantera/test/test_onedim.py | 20 +++++----- src/oneD/StFlow.cpp | 7 +++- 5 files changed, 50 insertions(+), 30 deletions(-) diff --git a/interfaces/cython/cantera/examples/onedim/diffusion_flame_batch.py b/interfaces/cython/cantera/examples/onedim/diffusion_flame_batch.py index 83a54c50c1..8ccdc1016d 100644 --- a/interfaces/cython/cantera/examples/onedim/diffusion_flame_batch.py +++ b/interfaces/cython/cantera/examples/onedim/diffusion_flame_batch.py @@ -113,8 +113,8 @@ def interrupt_extinction(t): # Update velocities f.set_profile('velocity', normalized_grid, f.velocity * rel_pressure_increase ** exp_u_p) - f.set_profile('V', normalized_grid, - f.V * rel_pressure_increase ** exp_V_p) + f.set_profile('vGradient', normalized_grid, + f.tangential_velocity_gradient * rel_pressure_increase ** exp_V_p) # Update pressure curvature f.set_profile('lambda', normalized_grid, f.L * rel_pressure_increase ** exp_lam_p) @@ -167,8 +167,10 @@ def interrupt_extinction(t): f.fuel_inlet.mdot *= strain_factor ** exp_mdot_a f.oxidizer_inlet.mdot *= strain_factor ** exp_mdot_a # Update velocities - f.set_profile('velocity', normalized_grid, f.velocity * strain_factor ** exp_u_a) - f.set_profile('V', normalized_grid, f.V * strain_factor ** exp_V_a) + f.set_profile('velocity', normalized_grid, + f.velocity * strain_factor ** exp_u_a) + f.set_profile('vGradient', normalized_grid, + f.tangential_velocity_gradient * strain_factor ** exp_V_a) # Update pressure curvature f.set_profile('lambda', normalized_grid, f.L * strain_factor ** exp_lam_a) try: diff --git a/interfaces/cython/cantera/examples/onedim/diffusion_flame_extinction.py b/interfaces/cython/cantera/examples/onedim/diffusion_flame_extinction.py index 5bd60abd10..23466ef3c0 100644 --- a/interfaces/cython/cantera/examples/onedim/diffusion_flame_extinction.py +++ b/interfaces/cython/cantera/examples/onedim/diffusion_flame_extinction.py @@ -109,8 +109,10 @@ f.fuel_inlet.mdot *= strain_factor ** exp_mdot_a f.oxidizer_inlet.mdot *= strain_factor ** exp_mdot_a # Update velocities - f.set_profile('velocity', normalized_grid, f.velocity * strain_factor ** exp_u_a) - f.set_profile('V', normalized_grid, f.V * strain_factor ** exp_V_a) + f.set_profile('velocity', normalized_grid, + f.velocity * strain_factor ** exp_u_a) + f.set_profile('vGradient', normalized_grid, + f.tangential_velocity_gradient * strain_factor ** exp_V_a) # Update pressure curvature f.set_profile('lambda', normalized_grid, f.L * strain_factor ** exp_lam_a) try: diff --git a/interfaces/cython/cantera/onedim.py b/interfaces/cython/cantera/onedim.py index b0fb79c764..d4fc5c750b 100644 --- a/interfaces/cython/cantera/onedim.py +++ b/interfaces/cython/cantera/onedim.py @@ -192,8 +192,23 @@ def velocity(self): def V(self): """ Array containing the tangential velocity gradient [1/s] at each point. + + .. deprecated:: 2.5 + + To be deprecated with version 2.5, and removed thereafter. + Replaced by property `tangential_velocity_gradient`. + """ + warnings.warn("To be removed after Cantera 2.5. " + "Replaced by property 'tangential_velocity_gradient'", + DeprecationWarning) + return self.profile(self.flame, 'vGradient') + + @property + def tangential_velocity_gradient(self): + """ + Array containing the tangential velocity gradient [1/s] at each point. """ - return self.profile(self.flame, 'V') + return self.profile(self.flame, 'vGradient') @property def L(self): @@ -288,7 +303,7 @@ def write_csv(self, filename, species='X', quiet=True): z = self.grid T = self.T u = self.velocity - V = self.V + V = self.tangential_velocity_gradient with open(filename, 'w', newline='') as csvfile: writer = _csv.writer(csvfile) @@ -309,7 +324,7 @@ def to_solution_array(self): The `SolutionArray` has the following ``extra`` entries: * ``grid``: grid point positions along the flame [m] * ``velocity``: normal velocity [m/s] - * ``gradient``: tangential velocity gradient [1/s] (if applicable) + * ``vGradient``: tangential velocity gradient [1/s] (if applicable) * ``lambda``: radial pressure gradient [N/m^4] (if applicable) * ``eField``: electric field strength (if applicable) """ @@ -318,8 +333,6 @@ def to_solution_array(self): for e in self._extra: if e == 'grid': val = self.grid - elif e == 'gradient': - val = self.profile(self.flame, 'V') else: val = self.profile(self.flame, e) extra[e] = np.hstack([np.nan, val, np.nan]) @@ -374,7 +387,7 @@ def from_solution_array(self, arr, restore_boundaries=True): The `SolutionArray` requires the following ``extra`` entries: * ``grid``: grid point positions along the flame [m] * ``velocity``: normal velocity [m/s] - * ``gradient``: tangential velocity gradient [1/s] (if applicable) + * ``vGradient``: tangential velocity gradient [1/s] (if applicable) * ``lambda``: radial pressure gradient [N/m^4] (if applicable) * ``eField``: electric field strength (if applicable) """ @@ -393,8 +406,6 @@ def from_solution_array(self, arr, restore_boundaries=True): val = getattr(arr, e)[idx] if e in ['grid', 'qdot']: pass - elif e == 'gradient': - self.set_profile('V', xi, val) else: self.set_profile(e, xi, val) @@ -868,7 +879,7 @@ def write_csv(self, filename, species='X', quiet=True): z = self.grid T = self.T u = self.velocity - V = self.V + V = self.tangential_velocity_gradient E = self.E with open(filename, 'w', newline='') as csvfile: @@ -1174,7 +1185,7 @@ def set_initial_guess(self): zrel = (zz - zz[0])/dz self.set_profile('velocity', [0.0, 1.0], [u0f, -u0o]) - self.set_profile('V', [0.0, x0/dz, 1.0], [0.0, a, 0.0]) + self.set_profile('vGradient', [0.0, x0/dz, 1.0], [0.0, a, 0.0]) self.set_profile('T', zrel, T) for k,spec in enumerate(self.gas.species_names): self.set_profile(spec, zrel, Y[:,k]) @@ -1419,7 +1430,7 @@ def set_initial_guess(self, products='inlet'): locs = np.array([0.0, 1.0]) self.set_profile('velocity', locs, [u0, 0.0]) - self.set_profile('V', locs, [0.0, 0.0]) + self.set_profile('vGradient', locs, [0.0, 0.0]) class CounterflowPremixedFlame(FlameBase): @@ -1510,7 +1521,7 @@ def set_initial_guess(self, equilibrate=True): x0 = rhou*uu * dz / (rhou*uu + rhob*ub) self.set_profile('velocity', [0.0, 1.0], [uu, -ub]) - self.set_profile('V', [0.0, x0/dz, 1.0], [0.0, a, 0.0]) + self.set_profile('vGradient', [0.0, x0/dz, 1.0], [0.0, a, 0.0]) class CounterflowTwinPremixedFlame(FlameBase): @@ -1583,4 +1594,4 @@ def set_initial_guess(self): a = 2 * uu / dz self.set_profile('velocity', [0.0, 1.0], [uu, 0]) - self.set_profile('V', [0.0, 1.0], [0.0, a]) + self.set_profile('vGradient', [0.0, 1.0], [0.0, a]) diff --git a/interfaces/cython/cantera/test/test_onedim.py b/interfaces/cython/cantera/test/test_onedim.py index eb28dccf6b..4951b4e1b4 100644 --- a/interfaces/cython/cantera/test/test_onedim.py +++ b/interfaces/cython/cantera/test/test_onedim.py @@ -446,7 +446,7 @@ def test_save_restore(self): Y1 = self.sim.Y u1 = self.sim.velocity - V1 = self.sim.V + V1 = self.sim.tangential_velocity_gradient P1 = self.sim.P self.sim.save(filename, 'test', loglevel=0) @@ -472,7 +472,7 @@ def test_save_restore(self): Y2 = self.sim.Y u2 = self.sim.velocity - V2 = self.sim.V + V2 = self.sim.tangential_velocity_gradient self.assertArrayNear(Y1, Y2) self.assertArrayNear(u1, u2) @@ -481,7 +481,7 @@ def test_save_restore(self): self.solve_fixed_T() Y3 = self.sim.Y u3 = self.sim.velocity - V3 = self.sim.V + V3 = self.sim.tangential_velocity_gradient # TODO: These tolereances seem too loose, but the tests fail on some # systems with tighter tolerances. @@ -494,8 +494,8 @@ def test_array_properties(self): for attr in ct.FlameBase.__dict__: if isinstance(ct.FlameBase.__dict__[attr], property): - if attr == 'u': - msg = "Replaced by property 'velocity'" + if attr in ['u', 'V']: + msg = "Replaced by property" with self.assertWarnsRegex(DeprecationWarning, msg): getattr(self.sim, attr) else: @@ -679,7 +679,7 @@ def test_mixture_averaged(self, saveReference=False): data = np.empty((self.sim.flame.n_points, self.gas.n_species + 4)) data[:,0] = self.sim.grid data[:,1] = self.sim.velocity - data[:,2] = self.sim.V + data[:,2] = self.sim.tangential_velocity_gradient data[:,3] = self.sim.T data[:,4:] = self.sim.Y.T @@ -718,7 +718,7 @@ def time_step_func(dt): data = np.empty((self.sim.flame.n_points, self.gas.n_species + 4)) data[:,0] = self.sim.grid data[:,1] = self.sim.velocity - data[:,2] = self.sim.V + data[:,2] = self.sim.tangential_velocity_gradient data[:,3] = self.sim.T data[:,4:] = self.sim.Y.T @@ -774,7 +774,7 @@ def test_mixture_averaged_rad(self, saveReference=False): data = np.empty((self.sim.flame.n_points, self.gas.n_species + 4)) data[:,0] = self.sim.grid data[:,1] = self.sim.velocity - data[:,2] = self.sim.V + data[:,2] = self.sim.tangential_velocity_gradient data[:,3] = self.sim.T data[:,4:] = self.sim.Y.T @@ -860,7 +860,7 @@ def test_mixture_averaged(self, saveReference=False): data = np.empty((sim.flame.n_points, gas.n_species + 4)) data[:,0] = sim.grid data[:,1] = sim.velocity - data[:,2] = sim.V + data[:,2] = sim.tangential_velocity_gradient data[:,3] = sim.T data[:,4:] = sim.Y.T @@ -881,7 +881,7 @@ def run_case(self, phi, T, width, P): sim.set_refine_criteria(ratio=6, slope=0.7, curve=0.8, prune=0.4) sim.solve(loglevel=0, auto=True) self.assertTrue(all(sim.T >= T - 1e-3)) - self.assertTrue(all(sim.V >= -1e-9)) + self.assertTrue(all(sim.tangential_velocity_gradient >= -1e-9)) return sim def test_solve_case1(self): diff --git a/src/oneD/StFlow.cpp b/src/oneD/StFlow.cpp index 9259340305..cc0a318d9d 100644 --- a/src/oneD/StFlow.cpp +++ b/src/oneD/StFlow.cpp @@ -576,7 +576,7 @@ string StFlow::componentName(size_t n) const case 0: return "velocity"; case 1: - return "V"; + return "vGradient"; case 2: return "T"; case 3: @@ -602,6 +602,11 @@ size_t StFlow::componentIndex(const std::string& name) const } else if (name=="velocity") { return 0; } else if (name=="V") { + warn_deprecated("StFlow::componentIndex", + "To be changed after Cantera 2.5. " + "Solution component 'V' renamed to 'vGradient'"); + return 1; + } else if (name=="vGradient") { return 1; } else if (name=="T") { return 2; From 139cd0aea2c1889887fd47238dae646823a73aec Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Thu, 27 Feb 2020 14:57:25 -0600 Subject: [PATCH 18/29] [1D] Improve test coverage and minor fixes --- interfaces/cython/cantera/_cantera.pxd | 1 + interfaces/cython/cantera/onedim.py | 6 ++-- interfaces/cython/cantera/onedim.pyx | 21 +++++++++----- interfaces/cython/cantera/test/test_onedim.py | 29 +++++++++++++++++-- src/oneD/StFlow.cpp | 2 ++ 5 files changed, 45 insertions(+), 14 deletions(-) diff --git a/interfaces/cython/cantera/_cantera.pxd b/interfaces/cython/cantera/_cantera.pxd index aa1540764e..ddffcd967a 100644 --- a/interfaces/cython/cantera/_cantera.pxd +++ b/interfaces/cython/cantera/_cantera.pxd @@ -763,6 +763,7 @@ cdef extern from "cantera/oneD/StFlow.h": cbool withSoret() void setFreeFlow() void setAxisymmetricFlow() + size_t componentIndex(string) except +translate_exception cdef extern from "cantera/oneD/IonFlow.h": diff --git a/interfaces/cython/cantera/onedim.py b/interfaces/cython/cantera/onedim.py index d4fc5c750b..87bb6d5136 100644 --- a/interfaces/cython/cantera/onedim.py +++ b/interfaces/cython/cantera/onedim.py @@ -337,7 +337,7 @@ def to_solution_array(self): val = self.profile(self.flame, e) extra[e] = np.hstack([np.nan, val, np.nan]) if self.radiation_enabled: - qdot = self.flame.radiative_heat_loss() + qdot = self.flame.radiative_heat_loss extra['qdot'] = np.hstack([np.nan, qdot, np.nan]) # consider inlet boundaries @@ -510,7 +510,7 @@ def write_hdf(self, filename, key=None, species='X', df.set_index('key') # store settings to HDF container file as a separate group - df.to_hdf(filename, key='settings', append=True) + df.to_hdf(filename, key='settings', format='table', append=True) def read_hdf(self, filename, key=None, restore_boundaries=True): """ @@ -536,7 +536,7 @@ def read_hdf(self, filename, key=None, restore_boundaries=True): @property def settings(self): """ Return a dictionary listing simulation settings """ - out = {'type': type(self).__name__} + out = {'configuration': type(self).__name__} out['transport_model'] = self.transport_model out['energy_enabled'] = self.energy_enabled out['soret_enabled'] = self.soret_enabled diff --git a/interfaces/cython/cantera/onedim.pyx b/interfaces/cython/cantera/onedim.pyx index d9c91f87d1..a9ffc26ba9 100644 --- a/interfaces/cython/cantera/onedim.pyx +++ b/interfaces/cython/cantera/onedim.pyx @@ -495,15 +495,16 @@ cdef class _FlowBase(Domain1D): def __set__(self, do_radiation): self.flow.enableRadiation(do_radiation) - def radiative_heat_loss(self): + property radiative_heat_loss: """ Return radiative heat loss (only non-zero if radiation is enabled). """ - cdef int j - cdef np.ndarray[np.double_t, ndim=1] data = np.empty(self.n_points) - for j in range(self.n_points): - data[j] = self.flow.radiativeHeatLoss(j) - return data + def __get__(self): + cdef int j + cdef np.ndarray[np.double_t, ndim=1] data = np.empty(self.n_points) + for j in range(self.n_points): + data[j] = self.flow.radiativeHeatLoss(j) + return data def set_free_flow(self): """ @@ -536,9 +537,9 @@ cdef class IdealGasFlow(_FlowBase): equations for the flow in a finite-height gap of infinite radial extent. The solution variables are: - *u* + *velocity* axial velocity - *V* + *tangential_velocity_gradient* radial velocity divided by radius *T* temperature @@ -561,6 +562,10 @@ cdef class IdealGasFlow(_FlowBase): gas = getIdealGasPhase(thermo) self.flow = new CxxStFlow(gas, thermo.n_species, 2) + def component_index(self, str name): + """Index of the component with name 'name'""" + return self.flow.componentIndex(stringify(name)) + cdef class IonFlow(_FlowBase): """ diff --git a/interfaces/cython/cantera/test/test_onedim.py b/interfaces/cython/cantera/test/test_onedim.py index 4951b4e1b4..23d8265c52 100644 --- a/interfaces/cython/cantera/test/test_onedim.py +++ b/interfaces/cython/cantera/test/test_onedim.py @@ -164,9 +164,29 @@ def test_fixed_temperature(self): reactants = 'H2:0.65, O2:0.5, AR:2' self.create_sim(p, Tin, reactants, width=0.0001) self.sim.set_initial_guess() - tfixed = 800. + zvec = self.sim.grid + tvec = self.sim.T + tfixed = 900. self.sim.fixed_temperature = tfixed - self.assertEqual(self.sim.fixed_temperature, tfixed) + zfixed = np.interp(tfixed, tvec, zvec) + self.assertNear(self.sim.fixed_temperature, tfixed) + self.assertNear(self.sim.fixed_temperature_location, zfixed) + + def test_deprecated(self): + Tin = 300 + p = ct.one_atm + reactants = 'H2:0.65, O2:0.5, AR:2' + self.create_sim(p, Tin, reactants, width=0.0001) + with self.assertWarnsRegex(DeprecationWarning, "Replaced by property"): + self.sim.flame.set_boundary_emissivities(0.5, 0.5) + with self.assertWarnsRegex(DeprecationWarning, "property 'velocity"): + self.sim.u + with self.assertWarnsRegex(DeprecationWarning, "property 'tangential"): + self.sim.V + with self.assertRaisesRegex(ct.CanteraError, "renamed to 'velocity"): + self.sim.flame.component_index('u') + with self.assertRaisesRegex(ct.CanteraError, "renamed to 'vGradient"): + self.sim.flame.component_index('V') def test_auto_width(self): Tin = 300 @@ -279,7 +299,7 @@ def test_settings(self): self.run_mix(phi=1.0, T=300, width=2.0, p=1.0, refine=False) settings = self.sim.settings - keys = ['type', 'transport_model', + keys = ['configuration', 'transport_model', 'energy_enabled', 'soret_enabled', 'radiation_enabled', 'emissivity_left', 'emissivity_right', 'fixed_temperature', @@ -778,6 +798,9 @@ def test_mixture_averaged_rad(self, saveReference=False): data[:,3] = self.sim.T data[:,4:] = self.sim.Y.T + qdot = self.sim.flame.radiative_heat_loss + self.assertEqual(len(qdot), self.sim.flame.n_points) + if saveReference: np.savetxt(referenceFile, data, '%11.6e', ', ') else: diff --git a/src/oneD/StFlow.cpp b/src/oneD/StFlow.cpp index cc0a318d9d..51e74d2385 100644 --- a/src/oneD/StFlow.cpp +++ b/src/oneD/StFlow.cpp @@ -620,6 +620,8 @@ size_t StFlow::componentIndex(const std::string& name) const return n; } } + throw CanteraError("StFlow1D::componentIndex", + "no component named " + name); } return npos; } From e586a69c105ac4bd548ce483eb9c025568deb733 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Thu, 27 Feb 2020 16:29:10 -0600 Subject: [PATCH 19/29] [1D] Implement setter for simulation settings --- interfaces/cython/cantera/onedim.py | 50 +++++++++++++++++-- interfaces/cython/cantera/test/test_onedim.py | 11 ++++ 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/interfaces/cython/cantera/onedim.py b/interfaces/cython/cantera/onedim.py index 87bb6d5136..1c1236c7a2 100644 --- a/interfaces/cython/cantera/onedim.py +++ b/interfaces/cython/cantera/onedim.py @@ -550,10 +550,10 @@ def settings(self): out['max_grid_points'] = self.get_max_grid_points(self.flame) # add tolerance settings - tols = {'steady_abstol': self.flame.steady_abstol(), - 'steady_reltol': self.flame.steady_reltol(), - 'transient_abstol': self.flame.transient_abstol(), - 'transient_reltol': self.flame.transient_reltol()} + tols = {'steady_reltol': self.flame.steady_reltol(), + 'steady_abstol': self.flame.steady_abstol(), + 'transient_reltol': self.flame.transient_reltol(), + 'transient_abstol': self.flame.transient_abstol()} comp = np.array(self.flame.component_names) for tname, tol in tols.items(): # add mode (most frequent tolerance setting) @@ -568,6 +568,48 @@ def settings(self): return out + @settings.setter + def settings(self, s): + # simple setters + attr = {'transport_model', + 'energy_enabled', 'soret_enabled', 'radiation_enabled', + 'fixed_temperature', + 'max_time_step_count', 'max_grid_points'} + attr = attr & set(s.keys()) + for key in attr: + self.__setattr__(key, s[key]) + + # boundary emissivities + if 'emissivity_left' in s or 'emissivity_right' in s: + epsilon = self.flame.boundary_emissivities + epsilon = (s.get('emissivity_left', epsilon[0]), + s.get('emissivity_right', epsilon[1])) + self.flame.boundary_emissivities = epsilon + + # refine criteria + refine = {k: v for k, v in s.items() + if k in ['ratio', 'slope', 'curve', 'prune']} + if refine: + self.set_refine_criteria(**refine) + + # tolerances + tols = ['steady_reltol', 'steady_abstol', + 'transient_reltol', 'transient_abstol'] + tols = [t for t in tols if t in s] + comp = np.array(self.flame.component_names) + for tname in tols: + mode = tname.split('_') + tol = s[tname] * np.ones(len(comp)) + for i, c in enumerate(comp): + key = '{}_{}'.format(tname, c) + if key in s: + tol[i] = s[key] + tol = {mode[1][:3]: tol} + if mode[0] == 'steady': + self.flame.set_steady_tolerances(**tol) + else: + self.flame.set_transient_tolerances(**tol) + def _load_restart_data(self, source, **kwargs): """ Load data for restart (called by set_initial_guess) """ if isinstance(source, SolutionArray): diff --git a/interfaces/cython/cantera/test/test_onedim.py b/interfaces/cython/cantera/test/test_onedim.py index 23d8265c52..4ca42eeb42 100644 --- a/interfaces/cython/cantera/test/test_onedim.py +++ b/interfaces/cython/cantera/test/test_onedim.py @@ -311,6 +311,17 @@ def test_settings(self): for k in keys: self.assertIn(k, settings) + changed = {'fixed_temperature': 900, + 'max_time_step_count': 100, + 'energy_enabled': False, + 'radiation_enabled': True, + 'transport_model': 'Multi'} + settings.update(changed) + + self.sim.settings = settings + for k, v in changed.items(): + self.assertEqual(getattr(self.sim, k), v) + def test_mixture_averaged_case1(self): self.run_mix(phi=0.65, T=300, width=0.03, p=1.0, refine=True) From 80f7a6013c37cf173d41bf5f645c78b04f986dff Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Thu, 27 Feb 2020 23:02:59 -0600 Subject: [PATCH 20/29] [1D] Restore simulation settings from HDF --- interfaces/cython/cantera/onedim.py | 62 +++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/interfaces/cython/cantera/onedim.py b/interfaces/cython/cantera/onedim.py index 1c1236c7a2..6aafd1d4e7 100644 --- a/interfaces/cython/cantera/onedim.py +++ b/interfaces/cython/cantera/onedim.py @@ -6,6 +6,7 @@ from .composite import Solution, SolutionArray import csv as _csv from math import erf +from os import path from email.utils import formatdate # avoid explicit dependence of cantera on pandas @@ -374,7 +375,8 @@ def to_solution_array(self): return arr - def from_solution_array(self, arr, restore_boundaries=True): + def from_solution_array(self, arr, restore_boundaries=True, + settings=None): """ Restore the solution vector from a Cantera `SolutionArray` object. @@ -383,6 +385,9 @@ def from_solution_array(self, arr, restore_boundaries=True): :param restore_boundaries: Boolean flag to indicate whether boundaries should be restored (default is ``True``) + :param settings: + dictionary containing simulation settings + (see `FlameBase.settings`) The `SolutionArray` requires the following ``extra`` entries: * ``grid``: grid point positions along the flame [m] @@ -433,6 +438,9 @@ def from_solution_array(self, arr, restore_boundaries=True): right.Y = arr[-1].Y right.mdot = -arr.velocity[-1] * arr[-1].density + if settings: + self.settings = settings + def to_pandas(self, species='X'): """ Return the solution vector as a `pandas.DataFrame`. @@ -448,7 +456,7 @@ def to_pandas(self, species='X'): cols = ('extra', 'T', 'D', species) return self.to_solution_array().to_pandas(cols=cols) - def from_pandas(self, df, restore_boundaries=True): + def from_pandas(self, df, restore_boundaries=True, settings=None): """ Restore the solution vector from a `pandas.DataFrame`. @@ -457,6 +465,9 @@ def from_pandas(self, df, restore_boundaries=True): :param restore_boundaries: Boolean flag to indicate whether boundaries should be restored (default is ``True``) + :param settings: + dictionary containing simulation settings + (see `FlameBase.settings`) This method is intendend for loading of data that were previously exported by `to_pandas`. The method uses `from_solution_array` and @@ -465,7 +476,8 @@ def from_pandas(self, df, restore_boundaries=True): """ arr = SolutionArray(self.gas, extra=self._extra) arr.from_pandas(df) - self.from_solution_array(arr, restore_boundaries=restore_boundaries) + self.from_solution_array(arr, restore_boundaries=restore_boundaries, + settings=settings) def write_hdf(self, filename, key=None, species='X', mode=None, complevel=None): @@ -493,8 +505,18 @@ def write_hdf(self, filename, key=None, species='X', PyTables. These packages can be installed using pip (`pandas` and `tables`) or conda (`pandas` and `pytables`). """ + # ensure key identifying HDF group within container is unique if not key: - key = type(self).__name__ + key = 'data' + if path.exists(filename) and mode != 'w': + with _pandas.HDFStore(filename) as hdf: + keys = hdf.keys() + count = sum([k.startswith(key, 1) for k in keys]) + if count: + key = '{}_{}'.format(key, count) + row = len(keys) - 1 + else: + row = 0 # save data cols = ('extra', 'T', 'D', species) @@ -507,12 +529,14 @@ def write_hdf(self, filename, key=None, species='X', df['date'] = formatdate(localtime=True) for key, val in self.settings.items(): df[key] = [val] - df.set_index('key') + df.index = _pandas.RangeIndex(start=row, stop=row+1, step=1) # store settings to HDF container file as a separate group - df.to_hdf(filename, key='settings', format='table', append=True) + df.to_hdf(filename, key='settings', format='table', + append=True) - def read_hdf(self, filename, key=None, restore_boundaries=True): + def read_hdf(self, filename, key=None, + restore_boundaries=True, restore_settings=True): """ Restore the solution vector from a HDF container file. @@ -523,15 +547,37 @@ def read_hdf(self, filename, key=None, restore_boundaries=True): :param restore_boundaries: Boolean flag to indicate whether boundaries should be restored (default is ``True``) + :param restore_settings: + Boolean flag to indicate whether simulation settings should be + restored (default is ``True``) The method imports data using `SolutionArray.read_hdf` via `from_solution_array` and requires working installations of pandas and PyTables. These packages can be installed using pip (`pandas` and `tables`) or conda (`pandas` and `pytables`). """ + if key is None: + with _pandas.HDFStore(filename) as hdf: + keys = hdf.keys() + key = [k.lstrip('/') for k in keys + if k != '/settings'][0] + + # retrieve data arr = SolutionArray(self.gas, extra=self._extra) arr.read_hdf(filename, key=key) - self.from_solution_array(arr, restore_boundaries=restore_boundaries) + + # retrieve settings + if restore_settings: + df = _pandas.read_hdf(filename, key='settings') + df = df.set_index('key') + series = df.loc[key] + settings = {k: v for k, v in series.items() + if k not in ['key', 'date']} + else: + settings = None + + self.from_solution_array(arr, restore_boundaries=restore_boundaries, + settings=settings) @property def settings(self): From 08f98023e71cf72279b2569a52c547d23b31c4fe Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Fri, 28 Feb 2020 09:07:53 -0600 Subject: [PATCH 21/29] [Examples] Add optional HDF output to adiabatic_flame.py --- .../cantera/examples/onedim/adiabatic_flame.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/interfaces/cython/cantera/examples/onedim/adiabatic_flame.py b/interfaces/cython/cantera/examples/onedim/adiabatic_flame.py index f3b0dbd05f..f0625a7a6b 100644 --- a/interfaces/cython/cantera/examples/onedim/adiabatic_flame.py +++ b/interfaces/cython/cantera/examples/onedim/adiabatic_flame.py @@ -6,6 +6,10 @@ """ import cantera as ct +try: + import pandas as pd +except: + pd = None # Simulation parameters p = ct.one_atm # pressure [Pa] @@ -29,7 +33,12 @@ f.solve(loglevel=loglevel, auto=True) # Solve with the energy equation enabled -f.save('h2_adiabatic.xml', 'mix', 'solution with mixture-averaged transport') +if pd: + f.write_hdf('h2_adiabatic.h5', key='mix', mode='w') +else: + f.save('h2_adiabatic.xml', 'mix', + 'solution with mixture-averaged transport') + f.show_solution() print('mixture-averaged flamespeed = {0:7f} m/s'.format(f.velocity[0])) @@ -38,7 +47,11 @@ f.solve(loglevel) # don't use 'auto' on subsequent solves f.show_solution() print('multicomponent flamespeed = {0:7f} m/s'.format(f.velocity[0])) -f.save('h2_adiabatic.xml', 'multi', 'solution with multicomponent transport') +if pd: + f.write_hdf('h2_adiabatic.h5', key='multi') +else: + f.save('h2_adiabatic.xml', 'multi', + 'solution with multicomponent transport') # write the velocity, temperature, density, and mole fractions to a CSV file f.write_csv('h2_adiabatic.csv', quiet=False) From b89886e41051ca9fd5ad3a5cb48a9b72088716ac Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Tue, 24 Mar 2020 18:20:01 -0500 Subject: [PATCH 22/29] [1D] Address review comments for FlameBase SolutionArray interface --- include/cantera/oneD/Domain1D.h | 2 +- include/cantera/oneD/Sim1D.h | 4 ++-- include/cantera/oneD/StFlow.h | 4 ++-- interfaces/cython/cantera/_cantera.pxd | 9 ++++----- interfaces/cython/cantera/onedim.py | 4 ++-- interfaces/cython/cantera/onedim.pyx | 10 +++------- src/oneD/Sim1D.cpp | 4 ++-- src/oneD/StFlow.cpp | 1 - 8 files changed, 16 insertions(+), 22 deletions(-) diff --git a/include/cantera/oneD/Domain1D.h b/include/cantera/oneD/Domain1D.h index 555d6cd7d5..4425141815 100644 --- a/include/cantera/oneD/Domain1D.h +++ b/include/cantera/oneD/Domain1D.h @@ -182,7 +182,7 @@ class Domain1D } //! index of component with name \a name. - size_t componentIndex(const std::string& name) const; + virtual size_t componentIndex(const std::string& name) const; void setBounds(size_t n, doublereal lower, doublereal upper) { m_min[n] = lower; diff --git a/include/cantera/oneD/Sim1D.h b/include/cantera/oneD/Sim1D.h index 583809b2f2..8ab374e26a 100644 --- a/include/cantera/oneD/Sim1D.h +++ b/include/cantera/oneD/Sim1D.h @@ -132,10 +132,10 @@ class Sim1D : public OneDim int setFixedTemperature(double t); //! Return temperature at the point used to fix the flame location - double getFixedTemperature(); + double fixedTemperature(); //! Return location of the point where temperature is fixed - double getFixedTemperatureLocation(); + double fixedTemperatureLocation(); /** * Set grid refinement criteria. If dom >= 0, then the settings diff --git a/include/cantera/oneD/StFlow.h b/include/cantera/oneD/StFlow.h index 8d5c9ff9f9..fef7d81a33 100644 --- a/include/cantera/oneD/StFlow.h +++ b/include/cantera/oneD/StFlow.h @@ -212,10 +212,10 @@ class StFlow : public Domain1D void setBoundaryEmissivities(double e_left, double e_right); //! Return emissivitiy at left boundary - double getLeftEmissivity() const { return m_epsilon_left; } + double leftEmissivity() const { return m_epsilon_left; } //! Return emissivitiy at right boundary - double getRightEmissivity() const { return m_epsilon_right; } + double rightEmissivity() const { return m_epsilon_right; } void fixTemperature(size_t j=npos); diff --git a/interfaces/cython/cantera/_cantera.pxd b/interfaces/cython/cantera/_cantera.pxd index ddffcd967a..79cb6edd99 100644 --- a/interfaces/cython/cantera/_cantera.pxd +++ b/interfaces/cython/cantera/_cantera.pxd @@ -754,8 +754,8 @@ cdef extern from "cantera/oneD/StFlow.h": double pressure() void setFixedTempProfile(vector[double]&, vector[double]&) void setBoundaryEmissivities(double, double) - double getLeftEmissivity() - double getRightEmissivity() + double leftEmissivity() + double rightEmissivity() void solveEnergyEqn() void fixTemperature() cbool doEnergy(size_t) @@ -763,7 +763,6 @@ cdef extern from "cantera/oneD/StFlow.h": cbool withSoret() void setFreeFlow() void setAxisymmetricFlow() - size_t componentIndex(string) except +translate_exception cdef extern from "cantera/oneD/IonFlow.h": @@ -819,8 +818,8 @@ cdef extern from "cantera/oneD/Sim1D.h": size_t maxGridPoints(size_t) except +translate_exception void setGridMin(int, double) except +translate_exception void setFixedTemperature(double) except +translate_exception - double getFixedTemperature() - double getFixedTemperatureLocation() + double fixedTemperature() + double fixedTemperatureLocation() void setInterrupt(CxxFunc1*) except +translate_exception void setTimeStepCallback(CxxFunc1*) void setSteadyCallback(CxxFunc1*) diff --git a/interfaces/cython/cantera/onedim.py b/interfaces/cython/cantera/onedim.py index 6aafd1d4e7..cf72d29131 100644 --- a/interfaces/cython/cantera/onedim.py +++ b/interfaces/cython/cantera/onedim.py @@ -177,7 +177,7 @@ def u(self): To be deprecated with version 2.5, and removed thereafter. Replaced by property `velocity`. """ - warnings.warn("To be removed after Cantera 2.5. " + warnings.warn("Property 'u' to be removed after Cantera 2.5. " "Replaced by property 'velocity'", DeprecationWarning) return self.profile(self.flame, 'velocity') @@ -199,7 +199,7 @@ def V(self): To be deprecated with version 2.5, and removed thereafter. Replaced by property `tangential_velocity_gradient`. """ - warnings.warn("To be removed after Cantera 2.5. " + warnings.warn("Property 'V' to be removed after Cantera 2.5. " "Replaced by property 'tangential_velocity_gradient'", DeprecationWarning) return self.profile(self.flame, 'vGradient') diff --git a/interfaces/cython/cantera/onedim.pyx b/interfaces/cython/cantera/onedim.pyx index a9ffc26ba9..b4522cbf62 100644 --- a/interfaces/cython/cantera/onedim.pyx +++ b/interfaces/cython/cantera/onedim.pyx @@ -481,7 +481,7 @@ cdef class _FlowBase(Domain1D): property boundary_emissivities: """ Set/get boundary emissivities. """ def __get__(self): - return self.flow.getLeftEmissivity(), self.flow.getRightEmissivity() + return self.flow.leftEmissivity(), self.flow.rightEmissivity() def __set__(self, tuple epsilon): if len(epsilon) == 2: self.flow.setBoundaryEmissivities(epsilon[0], epsilon[1]) @@ -562,10 +562,6 @@ cdef class IdealGasFlow(_FlowBase): gas = getIdealGasPhase(thermo) self.flow = new CxxStFlow(gas, thermo.n_species, 2) - def component_index(self, str name): - """Index of the component with name 'name'""" - return self.flow.componentIndex(stringify(name)) - cdef class IonFlow(_FlowBase): """ @@ -1175,7 +1171,7 @@ cdef class Sim1D: propagating flame. """ def __get__(self): - return self.sim.getFixedTemperature() + return self.sim.fixedTemperature() def __set__(self, T): self.sim.setFixedTemperature(T) @@ -1185,7 +1181,7 @@ cdef class Sim1D: propagating flame. """ def __get__(self): - return self.sim.getFixedTemperatureLocation() + return self.sim.fixedTemperatureLocation() def save(self, filename='soln.xml', name='solution', description='none', loglevel=1): diff --git a/src/oneD/Sim1D.cpp b/src/oneD/Sim1D.cpp index 202fd7a415..c1bf081bc2 100644 --- a/src/oneD/Sim1D.cpp +++ b/src/oneD/Sim1D.cpp @@ -511,7 +511,7 @@ int Sim1D::setFixedTemperature(double t) return np; } -double Sim1D::getFixedTemperature() +double Sim1D::fixedTemperature() { double t_fixed = -1.; for (size_t n = 0; n < nDomains(); n++) { @@ -524,7 +524,7 @@ double Sim1D::getFixedTemperature() return t_fixed; } -double Sim1D::getFixedTemperatureLocation() +double Sim1D::fixedTemperatureLocation() { double z_fixed = -1.; for (size_t n = 0; n < nDomains(); n++) { diff --git a/src/oneD/StFlow.cpp b/src/oneD/StFlow.cpp index 51e74d2385..4d9a7d5023 100644 --- a/src/oneD/StFlow.cpp +++ b/src/oneD/StFlow.cpp @@ -623,7 +623,6 @@ size_t StFlow::componentIndex(const std::string& name) const throw CanteraError("StFlow1D::componentIndex", "no component named " + name); } - return npos; } void StFlow::restore(const XML_Node& dom, doublereal* soln, int loglevel) From 69bc01e6e7f625525db28e66f842313adf31bbd6 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Tue, 24 Mar 2020 18:41:22 -0500 Subject: [PATCH 23/29] [1D] Clarify V replacement as spread_rate Replace the previous alternatives (tangential_velocity_gradient/vGradient) by a single property name (spread_rate). --- .../examples/onedim/diffusion_flame_batch.py | 8 ++--- .../onedim/diffusion_flame_extinction.py | 4 +-- interfaces/cython/cantera/onedim.py | 36 +++++++++---------- interfaces/cython/cantera/onedim.pyx | 2 +- interfaces/cython/cantera/test/test_onedim.py | 20 +++++------ src/oneD/StFlow.cpp | 6 ++-- 6 files changed, 38 insertions(+), 38 deletions(-) diff --git a/interfaces/cython/cantera/examples/onedim/diffusion_flame_batch.py b/interfaces/cython/cantera/examples/onedim/diffusion_flame_batch.py index 8ccdc1016d..488b0e45a3 100644 --- a/interfaces/cython/cantera/examples/onedim/diffusion_flame_batch.py +++ b/interfaces/cython/cantera/examples/onedim/diffusion_flame_batch.py @@ -113,8 +113,8 @@ def interrupt_extinction(t): # Update velocities f.set_profile('velocity', normalized_grid, f.velocity * rel_pressure_increase ** exp_u_p) - f.set_profile('vGradient', normalized_grid, - f.tangential_velocity_gradient * rel_pressure_increase ** exp_V_p) + f.set_profile('spread_rate', normalized_grid, + f.spread_rate * rel_pressure_increase ** exp_V_p) # Update pressure curvature f.set_profile('lambda', normalized_grid, f.L * rel_pressure_increase ** exp_lam_p) @@ -169,8 +169,8 @@ def interrupt_extinction(t): # Update velocities f.set_profile('velocity', normalized_grid, f.velocity * strain_factor ** exp_u_a) - f.set_profile('vGradient', normalized_grid, - f.tangential_velocity_gradient * strain_factor ** exp_V_a) + f.set_profile('spread_rate', normalized_grid, + f.spread_rate * strain_factor ** exp_V_a) # Update pressure curvature f.set_profile('lambda', normalized_grid, f.L * strain_factor ** exp_lam_a) try: diff --git a/interfaces/cython/cantera/examples/onedim/diffusion_flame_extinction.py b/interfaces/cython/cantera/examples/onedim/diffusion_flame_extinction.py index 23466ef3c0..df9d1e5075 100644 --- a/interfaces/cython/cantera/examples/onedim/diffusion_flame_extinction.py +++ b/interfaces/cython/cantera/examples/onedim/diffusion_flame_extinction.py @@ -111,8 +111,8 @@ # Update velocities f.set_profile('velocity', normalized_grid, f.velocity * strain_factor ** exp_u_a) - f.set_profile('vGradient', normalized_grid, - f.tangential_velocity_gradient * strain_factor ** exp_V_a) + f.set_profile('spread_rate', normalized_grid, + f.spread_rate * strain_factor ** exp_V_a) # Update pressure curvature f.set_profile('lambda', normalized_grid, f.L * strain_factor ** exp_lam_a) try: diff --git a/interfaces/cython/cantera/onedim.py b/interfaces/cython/cantera/onedim.py index cf72d29131..f5be69645a 100644 --- a/interfaces/cython/cantera/onedim.py +++ b/interfaces/cython/cantera/onedim.py @@ -197,19 +197,19 @@ def V(self): .. deprecated:: 2.5 To be deprecated with version 2.5, and removed thereafter. - Replaced by property `tangential_velocity_gradient`. + Replaced by property `spread_rate`. """ warnings.warn("Property 'V' to be removed after Cantera 2.5. " - "Replaced by property 'tangential_velocity_gradient'", + "Replaced by property 'spread_rate'", DeprecationWarning) - return self.profile(self.flame, 'vGradient') + return self.profile(self.flame, 'spread_rate') @property - def tangential_velocity_gradient(self): + def spread_rate(self): """ - Array containing the tangential velocity gradient [1/s] at each point. + Array containing the radial velocity divided by radius [1/s] at each point. """ - return self.profile(self.flame, 'vGradient') + return self.profile(self.flame, 'spread_rate') @property def L(self): @@ -304,7 +304,7 @@ def write_csv(self, filename, species='X', quiet=True): z = self.grid T = self.T u = self.velocity - V = self.tangential_velocity_gradient + V = self.spread_rate with open(filename, 'w', newline='') as csvfile: writer = _csv.writer(csvfile) @@ -325,7 +325,7 @@ def to_solution_array(self): The `SolutionArray` has the following ``extra`` entries: * ``grid``: grid point positions along the flame [m] * ``velocity``: normal velocity [m/s] - * ``vGradient``: tangential velocity gradient [1/s] (if applicable) + * ``spread_rate``: tangential velocity gradient [1/s] (if applicable) * ``lambda``: radial pressure gradient [N/m^4] (if applicable) * ``eField``: electric field strength (if applicable) """ @@ -392,7 +392,7 @@ def from_solution_array(self, arr, restore_boundaries=True, The `SolutionArray` requires the following ``extra`` entries: * ``grid``: grid point positions along the flame [m] * ``velocity``: normal velocity [m/s] - * ``vGradient``: tangential velocity gradient [1/s] (if applicable) + * ``spread_rate``: tangential velocity gradient [1/s] (if applicable) * ``lambda``: radial pressure gradient [N/m^4] (if applicable) * ``eField``: electric field strength (if applicable) """ @@ -967,7 +967,7 @@ def write_csv(self, filename, species='X', quiet=True): z = self.grid T = self.T u = self.velocity - V = self.tangential_velocity_gradient + V = self.spread_rate E = self.E with open(filename, 'w', newline='') as csvfile: @@ -1164,7 +1164,7 @@ def __init__(self, gas, grid=None, width=None): class CounterflowDiffusionFlame(FlameBase): """ A counterflow diffusion flame """ __slots__ = ('fuel_inlet', 'flame', 'oxidizer_inlet') - _extra = ('grid', 'velocity', 'gradient', 'lambda') + _extra = ('grid', 'velocity', 'spread_rate', 'lambda') def __init__(self, gas, grid=None, width=None): """ @@ -1273,7 +1273,7 @@ def set_initial_guess(self): zrel = (zz - zz[0])/dz self.set_profile('velocity', [0.0, 1.0], [u0f, -u0o]) - self.set_profile('vGradient', [0.0, x0/dz, 1.0], [0.0, a, 0.0]) + self.set_profile('spread_rate', [0.0, x0/dz, 1.0], [0.0, a, 0.0]) self.set_profile('T', zrel, T) for k,spec in enumerate(self.gas.species_names): self.set_profile(spec, zrel, Y[:,k]) @@ -1443,7 +1443,7 @@ def mixture_fraction(self, m): class ImpingingJet(FlameBase): """An axisymmetric flow impinging on a surface at normal incidence.""" __slots__ = ('inlet', 'flame', 'surface') - _extra = ('grid', 'velocity', 'gradient', 'lambda') + _extra = ('grid', 'velocity', 'spread_rate', 'lambda') def __init__(self, gas, grid=None, width=None, surface=None): """ @@ -1518,13 +1518,13 @@ def set_initial_guess(self, products='inlet'): locs = np.array([0.0, 1.0]) self.set_profile('velocity', locs, [u0, 0.0]) - self.set_profile('vGradient', locs, [0.0, 0.0]) + self.set_profile('spread_rate', locs, [0.0, 0.0]) class CounterflowPremixedFlame(FlameBase): """ A premixed counterflow flame """ __slots__ = ('reactants', 'flame', 'products') - _extra = ('grid', 'velocity', 'gradient', 'lambda') + _extra = ('grid', 'velocity', 'spread_rate', 'lambda') def __init__(self, gas, grid=None, width=None): """ @@ -1609,7 +1609,7 @@ def set_initial_guess(self, equilibrate=True): x0 = rhou*uu * dz / (rhou*uu + rhob*ub) self.set_profile('velocity', [0.0, 1.0], [uu, -ub]) - self.set_profile('vGradient', [0.0, x0/dz, 1.0], [0.0, a, 0.0]) + self.set_profile('spread_rate', [0.0, x0/dz, 1.0], [0.0, a, 0.0]) class CounterflowTwinPremixedFlame(FlameBase): @@ -1618,7 +1618,7 @@ class CounterflowTwinPremixedFlame(FlameBase): shooting into each other. """ __slots__ = ('reactants', 'flame', 'products') - _extra = ('grid', 'velocity', 'gradient', 'lambda') + _extra = ('grid', 'velocity', 'spread_rate', 'lambda') def __init__(self, gas, grid=None, width=None): """ @@ -1682,4 +1682,4 @@ def set_initial_guess(self): a = 2 * uu / dz self.set_profile('velocity', [0.0, 1.0], [uu, 0]) - self.set_profile('vGradient', [0.0, 1.0], [0.0, a]) + self.set_profile('spread_rate', [0.0, 1.0], [0.0, a]) diff --git a/interfaces/cython/cantera/onedim.pyx b/interfaces/cython/cantera/onedim.pyx index b4522cbf62..016ab8137c 100644 --- a/interfaces/cython/cantera/onedim.pyx +++ b/interfaces/cython/cantera/onedim.pyx @@ -539,7 +539,7 @@ cdef class IdealGasFlow(_FlowBase): *velocity* axial velocity - *tangential_velocity_gradient* + *spread_rate* radial velocity divided by radius *T* temperature diff --git a/interfaces/cython/cantera/test/test_onedim.py b/interfaces/cython/cantera/test/test_onedim.py index 4ca42eeb42..64764498c8 100644 --- a/interfaces/cython/cantera/test/test_onedim.py +++ b/interfaces/cython/cantera/test/test_onedim.py @@ -181,11 +181,11 @@ def test_deprecated(self): self.sim.flame.set_boundary_emissivities(0.5, 0.5) with self.assertWarnsRegex(DeprecationWarning, "property 'velocity"): self.sim.u - with self.assertWarnsRegex(DeprecationWarning, "property 'tangential"): + with self.assertWarnsRegex(DeprecationWarning, "property 'spread"): self.sim.V with self.assertRaisesRegex(ct.CanteraError, "renamed to 'velocity"): self.sim.flame.component_index('u') - with self.assertRaisesRegex(ct.CanteraError, "renamed to 'vGradient"): + with self.assertRaisesRegex(ct.CanteraError, "renamed to 'spread_rate"): self.sim.flame.component_index('V') def test_auto_width(self): @@ -477,7 +477,7 @@ def test_save_restore(self): Y1 = self.sim.Y u1 = self.sim.velocity - V1 = self.sim.tangential_velocity_gradient + V1 = self.sim.spread_rate P1 = self.sim.P self.sim.save(filename, 'test', loglevel=0) @@ -503,7 +503,7 @@ def test_save_restore(self): Y2 = self.sim.Y u2 = self.sim.velocity - V2 = self.sim.tangential_velocity_gradient + V2 = self.sim.spread_rate self.assertArrayNear(Y1, Y2) self.assertArrayNear(u1, u2) @@ -512,7 +512,7 @@ def test_save_restore(self): self.solve_fixed_T() Y3 = self.sim.Y u3 = self.sim.velocity - V3 = self.sim.tangential_velocity_gradient + V3 = self.sim.spread_rate # TODO: These tolereances seem too loose, but the tests fail on some # systems with tighter tolerances. @@ -710,7 +710,7 @@ def test_mixture_averaged(self, saveReference=False): data = np.empty((self.sim.flame.n_points, self.gas.n_species + 4)) data[:,0] = self.sim.grid data[:,1] = self.sim.velocity - data[:,2] = self.sim.tangential_velocity_gradient + data[:,2] = self.sim.spread_rate data[:,3] = self.sim.T data[:,4:] = self.sim.Y.T @@ -749,7 +749,7 @@ def time_step_func(dt): data = np.empty((self.sim.flame.n_points, self.gas.n_species + 4)) data[:,0] = self.sim.grid data[:,1] = self.sim.velocity - data[:,2] = self.sim.tangential_velocity_gradient + data[:,2] = self.sim.spread_rate data[:,3] = self.sim.T data[:,4:] = self.sim.Y.T @@ -805,7 +805,7 @@ def test_mixture_averaged_rad(self, saveReference=False): data = np.empty((self.sim.flame.n_points, self.gas.n_species + 4)) data[:,0] = self.sim.grid data[:,1] = self.sim.velocity - data[:,2] = self.sim.tangential_velocity_gradient + data[:,2] = self.sim.spread_rate data[:,3] = self.sim.T data[:,4:] = self.sim.Y.T @@ -894,7 +894,7 @@ def test_mixture_averaged(self, saveReference=False): data = np.empty((sim.flame.n_points, gas.n_species + 4)) data[:,0] = sim.grid data[:,1] = sim.velocity - data[:,2] = sim.tangential_velocity_gradient + data[:,2] = sim.spread_rate data[:,3] = sim.T data[:,4:] = sim.Y.T @@ -915,7 +915,7 @@ def run_case(self, phi, T, width, P): sim.set_refine_criteria(ratio=6, slope=0.7, curve=0.8, prune=0.4) sim.solve(loglevel=0, auto=True) self.assertTrue(all(sim.T >= T - 1e-3)) - self.assertTrue(all(sim.tangential_velocity_gradient >= -1e-9)) + self.assertTrue(all(sim.spread_rate >= -1e-9)) return sim def test_solve_case1(self): diff --git a/src/oneD/StFlow.cpp b/src/oneD/StFlow.cpp index 4d9a7d5023..df9b1e7d65 100644 --- a/src/oneD/StFlow.cpp +++ b/src/oneD/StFlow.cpp @@ -576,7 +576,7 @@ string StFlow::componentName(size_t n) const case 0: return "velocity"; case 1: - return "vGradient"; + return "spread_rate"; case 2: return "T"; case 3: @@ -604,9 +604,9 @@ size_t StFlow::componentIndex(const std::string& name) const } else if (name=="V") { warn_deprecated("StFlow::componentIndex", "To be changed after Cantera 2.5. " - "Solution component 'V' renamed to 'vGradient'"); + "Solution component 'V' renamed to 'spread_rate'"); return 1; - } else if (name=="vGradient") { + } else if (name=="spread_rate") { return 1; } else if (name=="T") { return 2; From ca11cbfd54f5a0b6de8ea136ae4001eac03528b0 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Wed, 25 Mar 2020 15:42:53 -0500 Subject: [PATCH 24/29] [1D] Make flow type accessible from Python --- interfaces/cython/cantera/_cantera.pxd | 1 + interfaces/cython/cantera/onedim.pyx | 8 ++++++++ interfaces/cython/cantera/test/test_onedim.py | 8 ++++++++ 3 files changed, 17 insertions(+) diff --git a/interfaces/cython/cantera/_cantera.pxd b/interfaces/cython/cantera/_cantera.pxd index 79cb6edd99..dd12c9de19 100644 --- a/interfaces/cython/cantera/_cantera.pxd +++ b/interfaces/cython/cantera/_cantera.pxd @@ -763,6 +763,7 @@ cdef extern from "cantera/oneD/StFlow.h": cbool withSoret() void setFreeFlow() void setAxisymmetricFlow() + string flowType() cdef extern from "cantera/oneD/IonFlow.h": diff --git a/interfaces/cython/cantera/onedim.pyx b/interfaces/cython/cantera/onedim.pyx index 016ab8137c..ef1ebde9e5 100644 --- a/interfaces/cython/cantera/onedim.pyx +++ b/interfaces/cython/cantera/onedim.pyx @@ -521,6 +521,14 @@ cdef class _FlowBase(Domain1D): """ self.flow.setAxisymmetricFlow() + property flow_type: + """ + Return the type of flow domain being represented, either "Free Flame" or + "Axisymmetric Stagnation". + """ + def __get__(self): + return pystr(self.flow.flowType()) + cdef CxxIdealGasPhase* getIdealGasPhase(ThermoPhase phase) except *: if pystr(phase.thermo.type()) != "IdealGas": diff --git a/interfaces/cython/cantera/test/test_onedim.py b/interfaces/cython/cantera/test/test_onedim.py index 64764498c8..7a1ee64116 100644 --- a/interfaces/cython/cantera/test/test_onedim.py +++ b/interfaces/cython/cantera/test/test_onedim.py @@ -157,6 +157,14 @@ def solve_multi(self): self.assertEqual(self.sim.transport_model, 'Multi') + def test_flow_type(self): + # Solve with the energy equation disabled + Tin = 300 + p = ct.one_atm + reactants = 'H2:0.65, O2:0.5, AR:2' + self.create_sim(p, Tin, reactants, width=0.0001) + self.assertEqual(self.sim.flame.flow_type, 'Free Flame') + def test_fixed_temperature(self): # test setting of fixed temperature Tin = 300 From 490bf8f8dcd0645efabc89c14a82a3202c830bc3 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Wed, 25 Mar 2020 15:29:44 -0500 Subject: [PATCH 25/29] [1D] Streamline approach for restarting simulations from existing data * Add ability to restart from existing data to remaining FlameBase derived objects. * Add logic to prevent multiple solution attempts for auto solver when restart data is provided. --- interfaces/cython/cantera/onedim.py | 200 +++++++++++++++++++++------ interfaces/cython/cantera/onedim.pyx | 19 ++- 2 files changed, 170 insertions(+), 49 deletions(-) diff --git a/interfaces/cython/cantera/onedim.py b/interfaces/cython/cantera/onedim.py index f5be69645a..61b4123005 100644 --- a/interfaces/cython/cantera/onedim.py +++ b/interfaces/cython/cantera/onedim.py @@ -72,6 +72,68 @@ def get_refine_criteria(self): """ return super().get_refine_criteria(self.flame) + def set_initial_guess(self, *args, **kwargs): + """ + Set the initial guess for the solution, and load restart data if + provided. Derived classes extend this function to set approximations + for the temperature and composition profiles. + """ + self._set_initial_guess(*args, **kwargs) + data = kwargs.get('data') + if data: + # load restart data into SolutionArray + if isinstance(data, SolutionArray): + # already a solution array + arr = data + elif isinstance(data, str): + # data source identifies a HDF file + arr = SolutionArray(self.gas, extra=self._extra) + key = kwargs.get('key') + arr.read_hdf(data, key=key) + else: + # data source is a pandas DataFrame + arr = SolutionArray(self.gas, extra=self._extra) + arr.from_pandas(data) + + # get left and right boundaries + left = self.domains[0] + right = self.domains[2] + + if isinstance(left, Inlet1D) and isinstance(right, Inlet1D): + # find stagnation plane + i = np.flatnonzero(self.velocity > 0)[-1] + + # adjust temperatures + T = arr.T + xi = arr.grid[1:-1] + T[1:-1] += (left.T - T[0]) * (1 - xi) + (right.T - T[-1]) * xi + arr.TP = T, self.P + + # adjust velocities + u = arr.velocity + + self.gas.TPY = left.T, self.P, left.Y + u[:i] = u[:i] * left.mdot / self.gas.density / u[0] + + self.gas.TPY = right.T, self.P, right.Y + u[i:] = - u[i:] * right.mdot / self.gas.density / u[-1] + + arr.velocity = u + + elif isinstance(left, Inlet1D): + # adjust temperatures + arr.TP = arr.T + left.T - arr.T[0], self.P + + # adjust velocities + if self.flame.flow_type != "Free Flame": + self.gas.TPY = left.T, self.P, left.Y + u0 = left.mdot/self.gas.density + arr.velocity *= u0 / arr.velocity[0] + + self.from_solution_array(arr, restore_boundaries=False) + else: + return None + def set_profile(self, component, locations, values): """ Set an initial estimate for a profile of one component. @@ -656,22 +718,6 @@ def settings(self, s): else: self.flame.set_transient_tolerances(**tol) - def _load_restart_data(self, source, **kwargs): - """ Load data for restart (called by set_initial_guess) """ - if isinstance(source, SolutionArray): - # already a solution array - return source - elif isinstance(source, str): - # source identifies a HDF file - arr = SolutionArray(self.gas, extra=self._extra) - arr.read_hdf(source, **kwargs) - return arr - else: - # source is a pandas DataFrame - arr = SolutionArray(self.gas, extra=self._extra) - arr.from_pandas(source) - return arr - def _trim(docstring): """Remove block indentation from a docstring.""" @@ -791,9 +837,10 @@ def __init__(self, gas, grid=None, width=None): def set_initial_guess(self, locs=[0.0, 0.3, 0.5, 1.0], data=None, key=None): """ - Set the initial guess for the solution. The adiabatic flame + Set the initial guess for the solution. By default, the adiabatic flame temperature and equilibrium composition are computed for the inlet gas - composition. + composition. Alternatively, a previously calculated result can be + supplied as an initial guess. :param locs: A list of four locations to define the temperature and mass fraction @@ -811,14 +858,10 @@ def set_initial_guess(self, locs=[0.0, 0.3, 0.5, 1.0], data=None, key=None): Group identifier within a HDF container file (only used in combination with HDF restart data). """ - super().set_initial_guess() + super().set_initial_guess(data=data, key=key) if data: - data = self._load_restart_data(data, key=key) - data.TP = data.T + self.inlet.T - data.T[0], self.P - self.from_solution_array(data, restore_boundaries=False) - # set fixed temperature - Tmid = .75 * data.T[0] + .25 * data.T[-1] + Tmid = .75 * self.T[0] + .25 * self.T[-1] i = np.flatnonzero(data.T < Tmid)[-1] self.fixed_temperature = data.T[i] @@ -1060,15 +1103,30 @@ def __init__(self, gas, grid=None, width=None): self.burner.T = gas.T self.burner.X = gas.X - def set_initial_guess(self): + def set_initial_guess(self, data=None, key=None): """ - Set the initial guess for the solution. The adiabatic flame + Set the initial guess for the solution. By default, the adiabatic flame temperature and equilibrium composition are computed for the burner gas composition. The temperature profile rises linearly in the first 20% of the flame to Tad, then is flat. The mass fraction profiles are - set similarly. + set similarly. Alternatively, a previously calculated result can be + supplied as an initial guess. + + :param data: + Restart data, which are typically based on an earlier simulation + result. Restart data may be specified using a SolutionArray, + pandas' DataFrame, or a saved HDF container file. Note that restart + data do not overwrite boundary conditions. DataFrame and HDF input + require working installations of pandas and PyTables. These packages + can be installed using pip (`pandas` and `tables`) or conda + (`pandas` and `pytables`). + :param key: + Group identifier within a HDF container file (only used in + combination with HDF restart data). """ - super().set_initial_guess() + super().set_initial_guess(data=data, key=key) + if data: + return self.gas.TPY = self.burner.T, self.P, self.burner.Y Y0 = self.burner.Y @@ -1198,13 +1256,27 @@ def __init__(self, gas, grid=None, width=None): super().__init__((self.fuel_inlet, self.flame, self.oxidizer_inlet), gas, grid) - def set_initial_guess(self): - """ - Set the initial guess for the solution. The initial guess is generated - by assuming infinitely-fast chemistry. + def set_initial_guess(self, data=None, key=None): """ + Set the initial guess for the solution. By default, the initial guess is + generated by assuming infinitely-fast chemistry. Alternatively, a + previously calculated result can be supplied as an initial guess. - super().set_initial_guess() + :param data: + Restart data, which are typically based on an earlier simulation + result. Restart data may be specified using a SolutionArray, + pandas' DataFrame, or a saved HDF container file. Note that restart + data do not overwrite boundary conditions. DataFrame and HDF input + require working installations of pandas and PyTables. These packages + can be installed using pip (`pandas` and `tables`) or conda + (`pandas` and `pytables`). + :param key: + Group identifier within a HDF container file (only used in + combination with HDF restart data). + """ + super().set_initial_guess(data=data, key=key) + if data: + return moles = lambda el: (self.gas.elemental_mass_fraction(el) / self.gas.atomic_weight(el)) @@ -1486,14 +1558,29 @@ def __init__(self, gas, grid=None, width=None, surface=None): self.inlet.T = gas.T self.inlet.X = gas.X - def set_initial_guess(self, products='inlet'): + def set_initial_guess(self, products='inlet', data=None, key=None): """ Set the initial guess for the solution. If products = 'equil', then the equilibrium composition at the adiabatic flame temperature will be used to form the initial guess. Otherwise the inlet composition will - be used. + be used. Alternatively, a previously calculated result can be supplied + as an initial guess. + + :param data: + Restart data, which are typically based on an earlier simulation + result. Restart data may be specified using a SolutionArray, + pandas' DataFrame, or a saved HDF container file. Note that restart + data do not overwrite boundary conditions. DataFrame and HDF input + require working installations of pandas and PyTables. These packages + can be installed using pip (`pandas` and `tables`) or conda + (`pandas` and `pytables`). + :param key: + Group identifier within a HDF container file (only used in + combination with HDF restart data). """ - super().set_initial_guess(products=products) + super().set_initial_guess(data=data, key=key, products=products) + if data: + return Y0 = self.inlet.Y T0 = self.inlet.T @@ -1561,15 +1648,30 @@ def __init__(self, gas, grid=None, width=None): # Setting X needs to be deferred until linked to the flow domain self.reactants.X = gas.X - def set_initial_guess(self, equilibrate=True): + def set_initial_guess(self, equilibrate=True, data=None, key=None): """ Set the initial guess for the solution. If `equilibrate` is True, then the products composition and temperature will be set to the equilibrium state of the reactants mixture. - """ + Alternatively, a previously calculated result can be supplied as an + initial guess. - super().set_initial_guess(equilibrate=equilibrate) + :param data: + Restart data, which are typically based on an earlier simulation + result. Restart data may be specified using a SolutionArray, + pandas' DataFrame, or a saved HDF container file. Note that restart + data do not overwrite boundary conditions. DataFrame and HDF input + require working installations of pandas and PyTables. These packages + can be installed using pip (`pandas` and `tables`) or conda + (`pandas` and `pytables`). + :param key: + Group identifier within a HDF container file (only used in + combination with HDF restart data). + """ + super().set_initial_guess(data=data, key=key, equilibrate=equilibrate) + if data: + return Yu = self.reactants.Y Tu = self.reactants.T @@ -1655,11 +1757,25 @@ def __init__(self, gas, grid=None, width=None): # Setting X needs to be deferred until linked to the flow domain self.reactants.X = gas.X - def set_initial_guess(self): + def set_initial_guess(self, data=None, key=None): """ - Set the initial guess for the solution. + Set the initial guess for the solution based on an equiibrium solution. + Alternatively, a previously calculated result can be supplied as an + initial guess. + + :param data: + Restart data, which are typically based on an earlier simulation + result. Restart data may be specified using a SolutionArray, + pandas' DataFrame, or a saved HDF container file. Note that restart + data do not overwrite boundary conditions. DataFrame and HDF input + require working installations of pandas and PyTables. These packages + can be installed using pip (`pandas` and `tables`) or conda + (`pandas` and `pytables`). + :param key: + Group identifier within a HDF container file (only used in + combination with HDF restart data). """ - super().set_initial_guess() + super().set_initial_guess(data=data, key=key) Yu = self.reactants.Y Tu = self.reactants.T diff --git a/interfaces/cython/cantera/onedim.pyx b/interfaces/cython/cantera/onedim.pyx index ef1ebde9e5..0935f5a6e6 100644 --- a/interfaces/cython/cantera/onedim.pyx +++ b/interfaces/cython/cantera/onedim.pyx @@ -868,11 +868,9 @@ cdef class Sim1D: def __set__(self, nmax): self.sim.setMaxTimeStepCount(nmax) - def set_initial_guess(self, *args, **kwargs): + def _set_initial_guess(self, *args, **kwargs): """ - Set the initial guess for the solution. Derived classes extend this - function to set approximations for the temperature and composition - profiles. + Store arguments for initial guess and prepare storage for solution. """ self._initial_guess_args = args self._initial_guess_kwargs = kwargs @@ -958,7 +956,13 @@ cdef class Sim1D: flow_domains = [D for D in self.domains if isinstance(D, _FlowBase)] zmin = [D.grid[0] for D in flow_domains] zmax = [D.grid[-1] for D in flow_domains] - nPoints = [len(flow_domains[0].grid), 12, 24, 48] + + # 'data' entry is used for restart + data = self._initial_guess_kwargs.get('data') + if data: + nPoints = [len(flow_domains[0].grid)] + else: + nPoints = [len(flow_domains[0].grid), 12, 24, 48] for N in nPoints: for i,D in enumerate(flow_domains): @@ -968,8 +972,9 @@ cdef class Sim1D: if N != len(D.grid): D.grid = np.linspace(zmin[i], zmax[i], N) - self.set_initial_guess(*self._initial_guess_args, - **self._initial_guess_kwargs) + if not data: + self.set_initial_guess(*self._initial_guess_args, + **self._initial_guess_kwargs) # Try solving with energy enabled, which usually works log('Solving on {} point grid with energy equation enabled', N) From 27adf629777a2750ac3a6a1a6b5441b45ff977ed Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Wed, 25 Mar 2020 17:22:28 -0500 Subject: [PATCH 26/29] [1D] Add unit tests for remaining restart cases --- interfaces/cython/cantera/test/test_onedim.py | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/interfaces/cython/cantera/test/test_onedim.py b/interfaces/cython/cantera/test/test_onedim.py index 7a1ee64116..5fc912538e 100644 --- a/interfaces/cython/cantera/test/test_onedim.py +++ b/interfaces/cython/cantera/test/test_onedim.py @@ -795,6 +795,22 @@ def test_extinction_case6(self): def test_extinction_case7(self): self.run_extinction(mdot_fuel=0.2, mdot_ox=2.0, T_ox=600, width=0.2, P=0.05) + def test_restart(self): + self.run_extinction(mdot_fuel=0.5, mdot_ox=3.0, T_ox=300, width=0.018, P=1.0) + + arr = self.sim.to_solution_array() + + self.create_sim(mdot_fuel=5.5, mdot_ox=3.3, T_ox=400, width=0.018, + p=ct.one_atm*1.1) + self.sim.set_initial_guess(data=arr) + self.sim.solve(loglevel=0, auto=True) + + # Check inlet + mdot = self.sim.density * self.sim.velocity + self.assertNear(mdot[0], self.sim.fuel_inlet.mdot, 1e-4) + self.assertNear(self.sim.T[0], self.sim.fuel_inlet.T, 1e-4) + self.assertNear(mdot[-1], -self.sim.oxidizer_inlet.mdot, 1e-4) + def test_mixture_averaged_rad(self, saveReference=False): referenceFile = pjoin(self.test_data_dir, 'DiffusionFlameTest-h2-mix-rad.csv') self.create_sim(p=ct.one_atm) @@ -941,6 +957,20 @@ def test_solve_case4(self): def test_solve_case5(self): self.run_case(phi=2.0, T=300, width=0.2, P=0.2) + def test_restart(self): + sim = self.run_case(phi=2.0, T=300, width=0.2, P=0.2) + + arr = sim.to_solution_array() + sim.reactants.mdot *= 1.1 + sim.products.mdot *= 1.1 + sim.set_initial_guess(data=arr) + sim.solve(loglevel=0, auto=True) + + # Check inlet / outlet + mdot = sim.density * sim.velocity + self.assertNear(mdot[0], sim.reactants.mdot, 1e-4) + self.assertNear(mdot[-1], -sim.products.mdot, 1e-4) + class TestBurnerFlame(utilities.CanteraTest): def solve(self, phi, T, width, P): @@ -993,6 +1023,24 @@ def test_blowoff(self): self.assertNear(sim.velocity[-1], sim.velocity[0], 1e-6) self.assertArrayNear(sim.Y[:,0], sim.Y[:,-1], 1e-6, atol=1e-6) + def test_restart(self): + gas = ct.Solution('h2o2.cti') + gas.set_equivalence_ratio(0.4, 'H2', 'O2:1.0, AR:5') + gas.TP = 300, ct.one_atm + sim = ct.BurnerFlame(gas=gas, width=0.1) + sim.burner.mdot = 1.2 + sim.set_refine_criteria(ratio=3, slope=0.3, curve=0.5, prune=0) + sim.solve(loglevel=0, auto=True) + + arr = sim.to_solution_array() + sim.burner.mdot = 1.1 + sim.set_initial_guess(data=arr) + sim.solve(loglevel=0, auto=True) + + # Check continuity + rhou = sim.burner.mdot + for rhou_j in sim.density * sim.velocity: + self.assertNear(rhou_j, rhou, 1e-4) class TestImpingingJet(utilities.CanteraTest): def run_reacting_surface(self, xch4, tsurf, mdot, width): @@ -1045,10 +1093,26 @@ def solve(self, phi, T, width, P): sim.reactants.mdot = gas.density * axial_velocity sim.solve(loglevel=0, auto=True) self.assertGreater(sim.T[-1], T + 100) + return sim def test_case1(self): self.solve(phi=0.4, T=300, width=0.05, P=0.1) + def test_restart(self): + sim = self.solve(phi=0.4, T=300, width=0.05, P=0.1) + + arr = sim.to_solution_array() + axial_velocity = 2.2 + sim.reactants.mdot *= 1.1 + sim.reactants.T = sim.reactants.T + 100 + sim.set_initial_guess(data=arr) + sim.solve(loglevel=0, auto=True) + + # Check inlet + mdot = sim.density * sim.velocity + self.assertNear(mdot[0], sim.reactants.mdot, 1e-4) + self.assertNear(sim.T[0], sim.reactants.T, 1e-4) + class TestIonFreeFlame(utilities.CanteraTest): def test_ion_profile(self): From 874e1511f75d768178fade05384001473bf06c4a Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Fri, 27 Mar 2020 13:35:28 -0500 Subject: [PATCH 27/29] [Thermo] Fix newline in SolutionArray.write_csv Prevent extra (i.e. double) newline in CSV output on Windows. --- interfaces/cython/cantera/composite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interfaces/cython/cantera/composite.py b/interfaces/cython/cantera/composite.py index 87f6b121af..2c126e08a0 100644 --- a/interfaces/cython/cantera/composite.py +++ b/interfaces/cython/cantera/composite.py @@ -865,7 +865,7 @@ def write_csv(self, filename, cols=None, *args, **kwargs): only with 1D `SolutionArray` objects. """ data, labels = self.collect_data(cols=cols, *args, **kwargs) - with open(filename, 'w') as outfile: + with open(filename, 'w', newline='') as outfile: writer = _csv.writer(outfile) writer.writerow(labels) for row in data: From b567742e14407a3aa17f3a02caafa50f26f93c60 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Thu, 26 Mar 2020 22:30:17 -0500 Subject: [PATCH 28/29] [1D] Write CSV output using SolutionArray.write_csv --- .../examples/onedim/adiabatic_flame.py | 1 + interfaces/cython/cantera/onedim.py | 50 +++---------------- interfaces/cython/cantera/test/test_onedim.py | 9 ++-- 3 files changed, 13 insertions(+), 47 deletions(-) diff --git a/interfaces/cython/cantera/examples/onedim/adiabatic_flame.py b/interfaces/cython/cantera/examples/onedim/adiabatic_flame.py index f0625a7a6b..75890d8135 100644 --- a/interfaces/cython/cantera/examples/onedim/adiabatic_flame.py +++ b/interfaces/cython/cantera/examples/onedim/adiabatic_flame.py @@ -8,6 +8,7 @@ import cantera as ct try: import pandas as pd + import tables except: pd = None diff --git a/interfaces/cython/cantera/onedim.py b/interfaces/cython/cantera/onedim.py index 61b4123005..d9bfd3fce3 100644 --- a/interfaces/cython/cantera/onedim.py +++ b/interfaces/cython/cantera/onedim.py @@ -4,7 +4,6 @@ import numpy as np from ._cantera import * from .composite import Solution, SolutionArray -import csv as _csv from math import erf from os import path from email.utils import formatdate @@ -363,19 +362,9 @@ def write_csv(self, filename, species='X', quiet=True): mole fractions or ``Y`` for mass fractions. """ - z = self.grid - T = self.T - u = self.velocity - V = self.spread_rate - - with open(filename, 'w', newline='') as csvfile: - writer = _csv.writer(csvfile) - writer.writerow(['z (m)', 'u (m/s)', 'V (1/s)', - 'T (K)', 'rho (kg/m3)'] + self.gas.species_names) - for n in range(self.flame.n_points): - self.set_gas_state(n) - writer.writerow([z[n], u[n], V[n], T[n], self.gas.density] + - list(getattr(self.gas, species))) + # save data + cols = ('extra', 'T', 'D', species) + self.to_solution_array().write_csv(filename, cols=cols) if not quiet: print("Solution saved to '{0}'.".format(filename)) @@ -542,7 +531,7 @@ def from_pandas(self, df, restore_boundaries=True, settings=None): settings=settings) def write_hdf(self, filename, key=None, species='X', - mode=None, complevel=None): + mode=None, complevel=None, quiet=True): """ Write the solution vector to a HDF container file. Note that it is possible to write multiple data entries to a single HDF container file. @@ -597,6 +586,9 @@ def write_hdf(self, filename, key=None, species='X', df.to_hdf(filename, key='settings', format='table', append=True) + if not quiet: + print("Solution saved to '{0}'.".format(filename)) + def read_hdf(self, filename, key=None, restore_boundaries=True, restore_settings=True): """ @@ -997,34 +989,6 @@ def perturb(sim, i, dp): class IonFlameBase(FlameBase): - def write_csv(self, filename, species='X', quiet=True): - """ - Write the velocity, temperature, density, electric potential, - electric field strength, and species profiles to a CSV file. - :param filename: - Output file name - :param species: - Attribute to use obtaining species profiles, e.g. ``X`` for - mole fractions or ``Y`` for mass fractions. - """ - z = self.grid - T = self.T - u = self.velocity - V = self.spread_rate - E = self.E - - with open(filename, 'w', newline='') as csvfile: - writer = _csv.writer(csvfile) - writer.writerow(['z (m)', 'velocity (m/s)', 'V (1/s)', 'T (K)', - 'E (V/m)', 'rho (kg/m3)'] + self.gas.species_names) - for n in range(self.flame.n_points): - self.set_gas_state(n) - writer.writerow([z[n], u[n], V[n], T[n], E[n], self.gas.density] + - list(getattr(self.gas, species))) - - if not quiet: - print("Solution saved to '{0}'.".format(filename)) - @property def electric_field_enabled(self): """ Get/Set whether or not to solve the Poisson's equation.""" diff --git a/interfaces/cython/cantera/test/test_onedim.py b/interfaces/cython/cantera/test/test_onedim.py index 5fc912538e..530af7e01f 100644 --- a/interfaces/cython/cantera/test/test_onedim.py +++ b/interfaces/cython/cantera/test/test_onedim.py @@ -603,11 +603,12 @@ def test_write_csv(self): self.create_sim(2e5, 350, 'H2:1.0, O2:2.0', mech='h2o2.xml') self.sim.write_csv(filename) - data = np.genfromtxt(filename, delimiter=',', skip_header=1) - self.assertArrayNear(data[:,0], self.sim.grid) - self.assertArrayNear(data[:,3], self.sim.T) + data = ct.SolutionArray(self.gas) + data.read_csv(filename) + self.assertArrayNear(data.grid[1:], self.sim.grid) + self.assertArrayNear(data.T[1:], self.sim.T) k = self.gas.species_index('H2') - self.assertArrayNear(data[:,5+k], self.sim.X[k,:]) + self.assertArrayNear(data.X[1:,k], self.sim.X[k,:]) def test_refine_criteria_boundscheck(self): self.create_sim(ct.one_atm, 300.0, 'H2:1.1, O2:1, AR:5') From 7b161e97d7c6044990d6a7cbf5d877dd9c9eec50 Mon Sep 17 00:00:00 2001 From: Ingmar Schoegl Date: Sun, 29 Mar 2020 00:12:44 -0500 Subject: [PATCH 29/29] [1D] Address review comments --- interfaces/cython/cantera/onedim.py | 203 +++++++----------- interfaces/cython/cantera/onedim.pyx | 12 +- interfaces/cython/cantera/test/test_onedim.py | 1 - src/oneD/Sim1D.cpp | 5 +- 4 files changed, 88 insertions(+), 133 deletions(-) diff --git a/interfaces/cython/cantera/onedim.py b/interfaces/cython/cantera/onedim.py index d9bfd3fce3..f28e966b87 100644 --- a/interfaces/cython/cantera/onedim.py +++ b/interfaces/cython/cantera/onedim.py @@ -71,67 +71,86 @@ def get_refine_criteria(self): """ return super().get_refine_criteria(self.flame) - def set_initial_guess(self, *args, **kwargs): + def set_initial_guess(self, *args, data=None, key=None, **kwargs): """ Set the initial guess for the solution, and load restart data if provided. Derived classes extend this function to set approximations for the temperature and composition profiles. + + :param data: + Restart data, which are typically based on an earlier simulation + result. Restart data may be specified using a `SolutionArray`, + pandas' DataFrame, or previously saved CSV or HDF container files. + Note that restart data do not overwrite boundary conditions. + DataFrame and HDF input require working installations of pandas and + PyTables. These packages can be installed using pip (`pandas` and + `tables`) or conda (`pandas` and `pytables`). + :param key: + Group identifier within a HDF container file (only used in + combination with HDF restart data). """ - self._set_initial_guess(*args, **kwargs) - data = kwargs.get('data') - if data: - # load restart data into SolutionArray - if isinstance(data, SolutionArray): - # already a solution array - arr = data - elif isinstance(data, str): + super().set_initial_guess(*args, data=data, key=key, **kwargs) + if not data: + return + + # load restart data into SolutionArray + if isinstance(data, SolutionArray): + # already a solution array + arr = data + elif isinstance(data, str): + if data.endswith('.hdf5') or data.endswith('.h5'): # data source identifies a HDF file arr = SolutionArray(self.gas, extra=self._extra) - key = kwargs.get('key') arr.read_hdf(data, key=key) - else: - # data source is a pandas DataFrame + elif data.endswith('.csv'): + # data source identifies a CSV file arr = SolutionArray(self.gas, extra=self._extra) - arr.from_pandas(data) + arr.read_csv(data) + else: + raise ValueError( + "'{}' does not identify CSV or HDF file.".format(data) + ) + else: + # data source is a pandas DataFrame + arr = SolutionArray(self.gas, extra=self._extra) + arr.from_pandas(data) - # get left and right boundaries - left = self.domains[0] - right = self.domains[2] + # get left and right boundaries + left = self.domains[0] + right = self.domains[2] - if isinstance(left, Inlet1D) and isinstance(right, Inlet1D): - # find stagnation plane - i = np.flatnonzero(self.velocity > 0)[-1] + if isinstance(left, Inlet1D) and isinstance(right, Inlet1D): + # find stagnation plane + i = np.flatnonzero(self.velocity > 0)[-1] - # adjust temperatures - T = arr.T - xi = arr.grid[1:-1] - T[1:-1] += (left.T - T[0]) * (1 - xi) + (right.T - T[-1]) * xi - arr.TP = T, self.P + # adjust temperatures + T = arr.T + xi = arr.grid[1:-1] + T[1:-1] += (left.T - T[0]) * (1 - xi) + (right.T - T[-1]) * xi + arr.TP = T, self.P - # adjust velocities - u = arr.velocity + # adjust velocities + u = arr.velocity - self.gas.TPY = left.T, self.P, left.Y - u[:i] = u[:i] * left.mdot / self.gas.density / u[0] + self.gas.TPY = left.T, self.P, left.Y + u[:i] = u[:i] * left.mdot / self.gas.density / u[0] - self.gas.TPY = right.T, self.P, right.Y - u[i:] = - u[i:] * right.mdot / self.gas.density / u[-1] + self.gas.TPY = right.T, self.P, right.Y + u[i:] = - u[i:] * right.mdot / self.gas.density / u[-1] - arr.velocity = u + arr.velocity = u - elif isinstance(left, Inlet1D): - # adjust temperatures - arr.TP = arr.T + left.T - arr.T[0], self.P + elif isinstance(left, Inlet1D): + # adjust temperatures + arr.TP = arr.T + left.T - arr.T[0], self.P - # adjust velocities - if self.flame.flow_type != "Free Flame": - self.gas.TPY = left.T, self.P, left.Y - u0 = left.mdot/self.gas.density - arr.velocity *= u0 / arr.velocity[0] + # adjust velocities + if self.flame.flow_type != "Free Flame": + self.gas.TPY = left.T, self.P, left.Y + u0 = left.mdot/self.gas.density + arr.velocity *= u0 / arr.velocity[0] - self.from_solution_array(arr, restore_boundaries=False) - else: - return None + self.from_solution_array(arr, restore_boundaries=False) def set_profile(self, component, locations, values): """ @@ -268,7 +287,8 @@ def V(self): @property def spread_rate(self): """ - Array containing the radial velocity divided by radius [1/s] at each point. + Array containing the tangential velocity gradient [1/s] (e.g. radial + velocity divided by radius) at each point. """ return self.profile(self.flame, 'spread_rate') @@ -663,7 +683,7 @@ def settings(self): # add values deviating from mode ix = np.logical_not(np.isclose(tol, values[ix])) - out.update({'{}_{}'.format(tname, c) + out.update({'{}_{}'.format(tname, c): t for c, t in zip(comp[ix], tol[ix])}) return out @@ -677,7 +697,7 @@ def settings(self, s): 'max_time_step_count', 'max_grid_points'} attr = attr & set(s.keys()) for key in attr: - self.__setattr__(key, s[key]) + setattr(self, key, s[key]) # boundary emissivities if 'emissivity_left' in s or 'emissivity_right' in s: @@ -832,23 +852,13 @@ def set_initial_guess(self, locs=[0.0, 0.3, 0.5, 1.0], data=None, key=None): Set the initial guess for the solution. By default, the adiabatic flame temperature and equilibrium composition are computed for the inlet gas composition. Alternatively, a previously calculated result can be - supplied as an initial guess. + supplied as an initial guess via 'data' and 'key' inputs (see + `FlameBase.set_initial_guess`). :param locs: A list of four locations to define the temperature and mass fraction profiles. Profiles rise linearly between the second and third location. Locations are given as a fraction of the entire domain - :param data: - Restart data, which are typically based on an earlier simulation - result. Restart data may be specified using a SolutionArray, - pandas' DataFrame, or a saved HDF container file. Note that restart - data do not overwrite boundary conditions. DataFrame and HDF input - require working installations of pandas and PyTables. These packages - can be installed using pip (`pandas` and `tables`) or conda - (`pandas` and `pytables`). - :param key: - Group identifier within a HDF container file (only used in - combination with HDF restart data). """ super().set_initial_guess(data=data, key=key) if data: @@ -1074,19 +1084,8 @@ def set_initial_guess(self, data=None, key=None): gas composition. The temperature profile rises linearly in the first 20% of the flame to Tad, then is flat. The mass fraction profiles are set similarly. Alternatively, a previously calculated result can be - supplied as an initial guess. - - :param data: - Restart data, which are typically based on an earlier simulation - result. Restart data may be specified using a SolutionArray, - pandas' DataFrame, or a saved HDF container file. Note that restart - data do not overwrite boundary conditions. DataFrame and HDF input - require working installations of pandas and PyTables. These packages - can be installed using pip (`pandas` and `tables`) or conda - (`pandas` and `pytables`). - :param key: - Group identifier within a HDF container file (only used in - combination with HDF restart data). + supplied as an initial guess via 'data' and 'key' inputs (see + `FlameBase.set_initial_guess`). """ super().set_initial_guess(data=data, key=key) if data: @@ -1222,21 +1221,10 @@ def __init__(self, gas, grid=None, width=None): def set_initial_guess(self, data=None, key=None): """ - Set the initial guess for the solution. By default, the initial guess is - generated by assuming infinitely-fast chemistry. Alternatively, a - previously calculated result can be supplied as an initial guess. - - :param data: - Restart data, which are typically based on an earlier simulation - result. Restart data may be specified using a SolutionArray, - pandas' DataFrame, or a saved HDF container file. Note that restart - data do not overwrite boundary conditions. DataFrame and HDF input - require working installations of pandas and PyTables. These packages - can be installed using pip (`pandas` and `tables`) or conda - (`pandas` and `pytables`). - :param key: - Group identifier within a HDF container file (only used in - combination with HDF restart data). + Set the initial guess for the solution. By default, the initial guess + is generated by assuming infinitely-fast chemistry. Alternatively, a + previously calculated result can be supplied as an initial guess via + 'data' and 'key' inputs (see `FlameBase.set_initial_guess`). """ super().set_initial_guess(data=data, key=key) if data: @@ -1528,19 +1516,8 @@ def set_initial_guess(self, products='inlet', data=None, key=None): the equilibrium composition at the adiabatic flame temperature will be used to form the initial guess. Otherwise the inlet composition will be used. Alternatively, a previously calculated result can be supplied - as an initial guess. - - :param data: - Restart data, which are typically based on an earlier simulation - result. Restart data may be specified using a SolutionArray, - pandas' DataFrame, or a saved HDF container file. Note that restart - data do not overwrite boundary conditions. DataFrame and HDF input - require working installations of pandas and PyTables. These packages - can be installed using pip (`pandas` and `tables`) or conda - (`pandas` and `pytables`). - :param key: - Group identifier within a HDF container file (only used in - combination with HDF restart data). + as an initial guess via 'data' and 'key' inputs (see + `FlameBase.set_initial_guess`). """ super().set_initial_guess(data=data, key=key, products=products) if data: @@ -1619,19 +1596,8 @@ def set_initial_guess(self, equilibrate=True, data=None, key=None): If `equilibrate` is True, then the products composition and temperature will be set to the equilibrium state of the reactants mixture. Alternatively, a previously calculated result can be supplied as an - initial guess. - - :param data: - Restart data, which are typically based on an earlier simulation - result. Restart data may be specified using a SolutionArray, - pandas' DataFrame, or a saved HDF container file. Note that restart - data do not overwrite boundary conditions. DataFrame and HDF input - require working installations of pandas and PyTables. These packages - can be installed using pip (`pandas` and `tables`) or conda - (`pandas` and `pytables`). - :param key: - Group identifier within a HDF container file (only used in - combination with HDF restart data). + initial guess via 'data' and 'key' inputs (see + `FlameBase.set_initial_guess`). """ super().set_initial_guess(data=data, key=key, equilibrate=equilibrate) if data: @@ -1725,19 +1691,8 @@ def set_initial_guess(self, data=None, key=None): """ Set the initial guess for the solution based on an equiibrium solution. Alternatively, a previously calculated result can be supplied as an - initial guess. - - :param data: - Restart data, which are typically based on an earlier simulation - result. Restart data may be specified using a SolutionArray, - pandas' DataFrame, or a saved HDF container file. Note that restart - data do not overwrite boundary conditions. DataFrame and HDF input - require working installations of pandas and PyTables. These packages - can be installed using pip (`pandas` and `tables`) or conda - (`pandas` and `pytables`). - :param key: - Group identifier within a HDF container file (only used in - combination with HDF restart data). + initial guess via 'data' and 'key' inputs (see + `FlameBase.set_initial_guess`). """ super().set_initial_guess(data=data, key=key) diff --git a/interfaces/cython/cantera/onedim.pyx b/interfaces/cython/cantera/onedim.pyx index 0935f5a6e6..78334db643 100644 --- a/interfaces/cython/cantera/onedim.pyx +++ b/interfaces/cython/cantera/onedim.pyx @@ -473,9 +473,9 @@ cdef class _FlowBase(Domain1D): To be deprecated with version 2.5, and removed thereafter. Replaced by property `boundary_emissivities`. """ - warnings.warn("To be removed after Cantera 2.5. " - "Replaced by property 'boundary_emissivities'", - DeprecationWarning) + warnings.warn("Method 'set_boundary_emissivities' to be removed after " + "Cantera 2.5. Replaced by property " + "'boundary_emissivities'", DeprecationWarning) self.boundary_emissivities = e_left, e_right property boundary_emissivities: @@ -868,7 +868,7 @@ cdef class Sim1D: def __set__(self, nmax): self.sim.setMaxTimeStepCount(nmax) - def _set_initial_guess(self, *args, **kwargs): + def set_initial_guess(self, *args, **kwargs): """ Store arguments for initial guess and prepare storage for solution. """ @@ -1173,8 +1173,8 @@ cdef class Sim1D: To be deprecated with version 2.5, and removed thereafter. Replaced by property `fixed_temperature`. """ - warnings.warn("To be removed after Cantera 2.5. " - "Replaced by property 'fixed_temperature'", + warnings.warn("Method 'set_fixed_temperature' to be removed after " + "Cantera 2.5. Replaced by property 'fixed_temperature'", DeprecationWarning) self.fixed_temperature = T diff --git a/interfaces/cython/cantera/test/test_onedim.py b/interfaces/cython/cantera/test/test_onedim.py index 530af7e01f..c2934847c6 100644 --- a/interfaces/cython/cantera/test/test_onedim.py +++ b/interfaces/cython/cantera/test/test_onedim.py @@ -158,7 +158,6 @@ def solve_multi(self): self.assertEqual(self.sim.transport_model, 'Multi') def test_flow_type(self): - # Solve with the energy equation disabled Tin = 300 p = ct.one_atm reactants = 'H2:0.65, O2:0.5, AR:2' diff --git a/src/oneD/Sim1D.cpp b/src/oneD/Sim1D.cpp index c1bf081bc2..70972062b5 100644 --- a/src/oneD/Sim1D.cpp +++ b/src/oneD/Sim1D.cpp @@ -12,6 +12,7 @@ #include "cantera/numerics/funcs.h" #include "cantera/base/xml.h" #include "cantera/numerics/Func1.h" +#include using namespace std; @@ -513,7 +514,7 @@ int Sim1D::setFixedTemperature(double t) double Sim1D::fixedTemperature() { - double t_fixed = -1.; + double t_fixed = std::numeric_limits::quiet_NaN(); for (size_t n = 0; n < nDomains(); n++) { StFlow* d = dynamic_cast(&domain(n)); if (d && d->domainType() == cFreeFlow && d->m_tfixed > 0) { @@ -526,7 +527,7 @@ double Sim1D::fixedTemperature() double Sim1D::fixedTemperatureLocation() { - double z_fixed = -1.; + double z_fixed = std::numeric_limits::quiet_NaN(); for (size_t n = 0; n < nDomains(); n++) { StFlow* d = dynamic_cast(&domain(n)); if (d && d->domainType() == cFreeFlow && d->m_tfixed > 0) {