diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 864db7add52..8b060ca8147 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -37,7 +37,7 @@ jobs: - name: Upgrade pip run: python3 -m pip install -U pip setuptools wheel - name: Install Python dependencies - run: python3 -m pip install ruamel.yaml scons numpy cython h5py + run: python3 -m pip install ruamel.yaml scons numpy cython h5py pandas - name: Build Cantera run: python3 `which scons` build -j2 - name: Test Cantera @@ -65,7 +65,7 @@ jobs: - name: Upgrade pip run: python3 -m pip install -U pip setuptools wheel - name: Install Python dependencies - run: python3 -m pip install ruamel.yaml scons numpy cython h5py + run: python3 -m pip install ruamel.yaml scons numpy cython h5py pandas - name: Build Cantera run: python3 `which scons` build -j2 - name: Test Cantera @@ -95,7 +95,7 @@ jobs: - name: Upgrade pip run: python3 -m pip install -U pip setuptools wheel - name: Install Python dependencies - run: python3 -m pip install ruamel.yaml scons numpy cython h5py + run: python3 -m pip install ruamel.yaml scons numpy cython h5py pandas - name: Build Cantera run: | python3 `which scons` build blas_lapack_libs=lapack,blas coverage=y \ @@ -166,6 +166,7 @@ jobs: python3-pip python3-setuptools libsundials-serial-dev liblapack-dev \ libblas-dev - name: Install Python dependencies + # Don't include Pandas here due to install errors run: | sudo -H /usr/bin/python3 -m pip install ruamel.yaml cython h5py - name: Build Cantera @@ -173,6 +174,44 @@ jobs: - name: Test Cantera run: scons test + run-examples: + name: Run the Python examples using bash + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v2 + name: Checkout the repository + with: + submodules: recursive + - name: Setup Python + uses: actions/setup-python@v1 + with: + python-version: '3.8' + architecture: x64 + - name: Install Apt dependencies + run: sudo apt-get install libboost-dev gfortran graphviz liblapack-dev libblas-dev + - name: Upgrade pip + run: python3 -m pip install -U pip setuptools wheel + - name: Install Python dependencies + run: python3 -m pip install ruamel.yaml scons numpy cython h5py pandas matplotlib scipy + - name: Build Cantera + run: python3 `which scons` build -j2 + - name: Run the examples + # See https://unix.stackexchange.com/a/392973 for an explanation of the -exec part + run: | + find interfaces/cython/cantera/examples -type f -iname "*.py" \ + -exec sh -c 'for n; do echo "$n" | tee -a results.txt && python3 "$n" >> results.txt || exit 1; done' sh {} + + env: + PYTHONPATH: build/python + PYTHONWARNINGS: error + MPLBACKEND: Agg + - name: Save the results file for inspection + uses: actions/upload-artifact@v2 + with: + path: results.txt + # Run this step if the job was successful or failed, but not if it was cancelled + # Using always() would run this step if the job was cancelled as well. + if: failure() || success() + multiple-sundials: name: Sundials ${{ matrix.sundials-ver }} runs-on: ubuntu-latest @@ -196,7 +235,7 @@ jobs: shell: bash -l {0} run: | conda install -q sundials=${{ matrix.sundials-ver}} scons numpy ruamel_yaml \ - cython libboost fmt eigen yaml-cpp h5py + cython libboost fmt eigen yaml-cpp h5py pandas - name: Build Cantera run: | scons build extra_inc_dirs=$CONDA_PREFIX/include:$CONDA_PREFIX/include/eigen3 \ @@ -226,7 +265,7 @@ jobs: run: python3 -m pip install -U pip setuptools wheel - name: Install Python dependencies run: | - python3 -m pip install ruamel.yaml scons numpy h5py; + python3 -m pip install ruamel.yaml scons numpy h5py pandas; python3 -m pip install https://github.com/cython/cython/archive/master.zip --install-option='--no-cython-compile'; - name: Build Cantera run: python3 `which scons` build blas_lapack_libs=lapack,blas python_package='full' -j2 @@ -270,7 +309,7 @@ jobs: - name: Install Python dependencies run: | python -m pip install -U pip setuptools - python -m pip install scons pypiwin32 numpy ruamel.yaml cython h5py + python -m pip install scons pypiwin32 numpy ruamel.yaml cython h5py pandas - name: Build Cantera run: | scons build -j2 boost_inc_dir=%BOOST_ROOT_1_69_0% debug=n VERBOSE=y python_package=full ^ diff --git a/SConstruct b/SConstruct index 276f9b1e0be..a718ca61f31 100644 --- a/SConstruct +++ b/SConstruct @@ -1229,9 +1229,26 @@ env['python_cmd_esc'] = quoted(env['python_cmd']) # Python Package Settings python_min_version = LooseVersion('3.5') +# The string is used to set python_requires in setup.py.in +env['py_min_ver_str'] = str(python_min_version) # Note: cython_min_version is redefined below if the Python version is 3.8 or higher cython_min_version = LooseVersion('0.23') -numpy_min_test_version = LooseVersion('1.8.1') +numpy_min_version = LooseVersion('1.12.0') + +# We choose ruamel.yaml 0.15.34 as the minimum version +# since it is the highest version available in the Ubuntu +# 18.04 repositories and seems to work. Older versions such as +# 0.13.14 on CentOS7 and 0.10.23 on Ubuntu 16.04 raise an exception +# that they are missing the RoundTripRepresenter +ruamel_min_version = LooseVersion('0.15.34') + +# Check for the minimum ruamel.yaml version, 0.15.34, at install and test +# time. The check happens at install and test time because ruamel.yaml is +# only required to run the Python interface, not to build it. +check_for_ruamel_yaml = any( + target in COMMAND_LINE_TARGETS + for target in ["install", "test", "test-python-convert"] +) # Handle the version-specific Python package options python_options = (('package', 'default'), @@ -1281,6 +1298,25 @@ if env['python_package'] != 'none': if err: print(err) """) + expected_output_lines = 3 + if check_for_ruamel_yaml: + ru_script = textwrap.dedent("""\ + try: + import ruamel_yaml as yaml + print(yaml.__version__) + except ImportError as ru_err: + try: + from ruamel import yaml + print(yaml.__version__) + except ImportError as ru_err_2: + print('0.0.0') + err += str(ru_err) + '\\n' + err += str(ru_err_2) + '\\n' + """).splitlines() + s = script.splitlines() + s[-2:-2] = ru_script + script = "\n".join(s) + expected_output_lines = 4 try: info = getCommandOutput(env['python_cmd'], '-c', script).splitlines() @@ -1299,6 +1335,22 @@ if env['python_package'] != 'none': python_version = LooseVersion(info[0]) numpy_version = LooseVersion(info[1]) cython_version = LooseVersion(info[2]) + if check_for_ruamel_yaml: + ruamel_yaml_version = LooseVersion(info[3]) + if ruamel_yaml_version == LooseVersion("0.0.0"): + print("ERROR: ruamel.yaml was not found. {} or newer is " + "required".format(ruamel_min_version)) + sys.exit(1) + elif ruamel_yaml_version < ruamel_min_version: + print("ERROR: ruamel.yaml is an incompatible version: Found " + "{}, but {} or newer is required.".format( + ruamel_yaml_version, ruamel_min_version)) + sys.exit(1) + + if len(info) > expected_output_lines: + print("WARNING: Unexpected output while checking Python " + "dependency versions:") + print('| ' + '\n| '.join(info[expected_output_lines:])) if warn_no_python: if env['python_package'] == 'default': @@ -1309,20 +1361,18 @@ if env['python_package'] != 'none': print('ERROR: Could not execute the Python interpreter {!r}'.format( env['python_cmd'])) sys.exit(1) - elif env['python_package'] == 'minimal': - print('INFO: Building the minimal Python package for Python {}'.format(python_version)) + elif python_version < python_min_version: + msg = ("{}: Python version is incompatible. Found {} but {} " + "or newer is required") + if env["python_package"] in ("minimal", "full"): + print(msg.format("ERROR", python_version, python_min_version)) + sys.exit(1) + elif env["python_package"] == "default": + print(msg.format("WARNING", python_version, python_min_version)) + env["python_package"] = "none" else: warn_no_full_package = False - if len(info) > 3: - print("WARNING: Unexpected output while checking Python / Numpy / Cython versions:") - print('| ' + '\n| '.join(info[3:])) - - if python_version < python_min_version: - print("WARNING: Python version is incompatible with the full Python module: " - "Found {0} but {1} or newer is required".format( - python_version, python_min_version)) - warn_no_full_package = True - elif python_version >= LooseVersion("3.8"): + if python_version >= LooseVersion("3.8"): # Reset the minimum Cython version if the Python version is 3.8 or higher # Due to internal changes in the CPython API, more recent versions of # Cython are necessary to build for Python 3.8. There is nothing Cantera @@ -1335,10 +1385,11 @@ if env['python_package'] != 'none': if numpy_version == LooseVersion('0.0.0'): print("NumPy not found.") warn_no_full_package = True - elif numpy_version < numpy_min_test_version: - print("WARNING: The installed version of Numpy is not tested and " - "support is not guaranteed. Found {0} but {1} or newer is preferred".format( - numpy_version, numpy_min_test_version)) + elif numpy_version < numpy_min_version: + print("WARNING: NumPy is an incompatible version: " + "Found {0} but {1} or newer is required".format( + numpy_version, numpy_min_version)) + warn_no_full_package = True else: print('INFO: Using NumPy version {0}.'.format(numpy_version)) @@ -1348,20 +1399,20 @@ if env['python_package'] != 'none': elif cython_version < cython_min_version: print("WARNING: Cython is an incompatible version: " "Found {0} but {1} or newer is required.".format( - cython_version, cython_min_version)) + cython_version, cython_min_version)) warn_no_full_package = True else: print('INFO: Using Cython version {0}.'.format(cython_version)) if warn_no_full_package: + msg = ('{}: Unable to build the full Python package because compatible ' + 'versions of Python, Numpy, and Cython could not be found.') if env['python_package'] == 'default': - print('WARNING: Unable to build the full Python package because compatible ' - 'versions of Python, Numpy, and Cython could not be found.') + print(msg.format("WARNING")) print('INFO: Building the minimal Python package for Python {}'.format(python_version)) env['python_package'] = 'minimal' else: - print('ERROR: Unable to build the full Python package because compatible ' - 'versions of Python, Numpy, and Cython could not be found.') + print(msg.format("ERROR")) sys.exit(1) else: print('INFO: Building the full Python package for Python {0}'.format(python_version)) @@ -1410,6 +1461,7 @@ env['debian'] = any(name.endswith('dist-packages') for name in sys.path) # Directories where things will be after actually being installed. These # variables are the ones that are used to populate header files, scripts, etc. +env['prefix'] = os.path.normpath(env['prefix']) env['ct_installroot'] = env['prefix'] env['ct_libdir'] = pjoin(env['prefix'], env['libdirname']) env['ct_bindir'] = pjoin(env['prefix'], 'bin') @@ -1781,15 +1833,18 @@ def postInstallMessage(target, source, env): """.format(**env_dict)) if os.name != 'nt': + env['setup_cantera'] = pjoin(env['ct_bindir'], 'setup_cantera') + env['setup_cantera_csh'] = pjoin(env['ct_bindir'], 'setup_cantera.csh') install_message += textwrap.dedent(""" + Setup scripts to configure the environment for Cantera are at: - setup script (bash) {ct_bindir!s}/setup_cantera - setup script (csh/tcsh) {ct_bindir!s}/setup_cantera.csh + setup script (bash) {setup_cantera!s} + setup script (csh/tcsh) {setup_cantera_csh!s} It is recommended that you run the script for your shell by typing: - source {ct_bindir!s}/setup_cantera + source {setup_cantera!s} before using Cantera, or else include its contents in your shell login script. """.format(**env_dict)) diff --git a/data/sofc.yaml b/data/sofc.yaml index cbe0b95e7c9..2421ca57172 100644 --- a/data/sofc.yaml +++ b/data/sofc.yaml @@ -5,7 +5,7 @@ description: |- species thermochemistry ARE NOT REAL VALUES - they are chosen only for the purposes of this example. - Defines bulk (i.e., 3D) phases - a gas, a metal, and an oxide. + Defines bulk (that is, 3D) phases - a gas, a metal, and an oxide. The gas contains only the minimum number of species needed to model operation on hydrogen. The species definitions are imported from gri30.yaml. The initial composition is set to hydrogen + 5% water, but @@ -26,14 +26,14 @@ description: |- charge balances on the metal, then we would require a more complex model. Note that there is no work function for this metal. - Note: the "const_cp" species thermo model is used throughout this + Note: the 'const_cp' species thermo model is used throughout this file (with the exception of the gaseous species, which use NASA - polynomials imported from gri30.cti). The const_cp model assumes a + polynomials imported from gri30.yaml). The const_cp model assumes a constant specific heat, which by default is zero. Parameters that - can be specified are cp0, t0, h0, and s0. If omitted, t0 = 300 K, h0 - = 0, and s0 = 0. The thermo properties are computed as follows: h = - h0 + cp0*(t - t0), s = s0 + cp0*ln(t/t0). For work at a single - temperature, it is sufficient to specify only h0. + can be specified are cp0, t0, h0, and s0. If omitted, t0 = 300 K, + h0 = 0, and s0 = 0. The thermo properties are computed as follows: + h = h0 + cp0*(t - t0), s = s0 + cp0*ln(t/t0). + For work at a single temperature, it is sufficient to specify only h0. The 'oxide_bulk' phase is a very simple model for the bulk phase. We only consider the oxygen sublattice. The only species we define are a @@ -41,7 +41,7 @@ description: |- required input, but is not used here, so may be set arbitrarily. The vacancy will be modeled as truly vacant - it contains no atoms, - has no charge, and has zero enthalpy and entropy. This is different + has no charge, and has zero enthalpy and entropy. This is different from the usual convention in which the vacancy properties are are expressed relative to the perfect crystal lattice. For example, in the usual convention, an oxygen vacancy has charge +2. But the @@ -65,7 +65,7 @@ description: |- 'mol' and 'cm' were specified in the units directive below as the units for quantity and length, respectively. 2. The 'reactions' field specifies that all reaction entries in this file - that have are in the 'metal_surface-reactions' field are reactions belonging + that are in the 'metal_surface-reactions' field are reactions belonging to this surface mechanism. On the oxide surface, we consider four species: @@ -74,11 +74,11 @@ description: |- 3. OH'(ox) - a surface hydroxyl with charge -1 4. H2O(ox) - physisorbed neutral water - The 'tpb' phase is th etriple phase boundary between the metal, oxide, and gas. A - single species is specified, but it is not used, since all reactions - only involve species on either side of the tpb. Note that the site - density is in mol/cm. But since no reactions involve TPB species, - this parameter is unused. + The triple phase boundary (TPB) between the metal, oxide, and gas is + specified in the 'tpb' phase. A single species is specified, but it + is not used, since all reactions only involve species on either side + of the TPB. Note that the site density is in mol/cm. But since no + reactions involve TPB species, this parameter is unused. generator: cti2yaml cantera-version: 2.5.0a3 diff --git a/include/cantera/base/AnyMap.inl.h b/include/cantera/base/AnyMap.inl.h index fcf5c1abc69..1346e17d5d9 100644 --- a/include/cantera/base/AnyMap.inl.h +++ b/include/cantera/base/AnyMap.inl.h @@ -26,7 +26,7 @@ const T &AnyValue::as() const { if (m_value->type() == typeid(void)) { // Values that have not been set are of type 'void' throw InputFileError("AnyValue::as", *this, - "Key '{}' not found", m_key); + "Key '{}' not found or contains no value", m_key); } else { throw InputFileError("AnyValue::as", *this, "Key '{}' contains a '{}',\nnot a '{}'", @@ -48,7 +48,7 @@ T &AnyValue::as() { if (m_value->type() == typeid(void)) { // Values that have not been set are of type 'void' throw InputFileError("AnyValue::as", *this, - "Key '{}' not found", m_key); + "Key '{}' not found or contains no value", m_key); } else { throw InputFileError("AnyValue::as", *this, "Key '{}' contains a '{}',\nnot a '{}'", diff --git a/include/cantera/kinetics/RateCoeffMgr.h b/include/cantera/kinetics/RateCoeffMgr.h index 8449e1b9601..4f89f464a74 100644 --- a/include/cantera/kinetics/RateCoeffMgr.h +++ b/include/cantera/kinetics/RateCoeffMgr.h @@ -43,10 +43,10 @@ class Rate1 /** * Update the concentration-dependent parts of the rate coefficient, if any. - * Used by class SurfaceArrhenius to compute coverage-dependent * + * Used by class SurfaceArrhenius to compute coverage-dependent * modifications to the Arrhenius parameters. The array c should contain * whatever data the particular rate coefficient class needs to update its - * rates. Note that this method does not return anything. To get the + * rates. Note that this method does not return anything. To get the * updated rates, method update must be called after the call to update_C. */ void update_C(const doublereal* c) { diff --git a/include/cantera/kinetics/RxnRates.h b/include/cantera/kinetics/RxnRates.h index 05e2ac3c36b..7b179bd88f5 100644 --- a/include/cantera/kinetics/RxnRates.h +++ b/include/cantera/kinetics/RxnRates.h @@ -162,7 +162,7 @@ class SurfaceArrhenius } /** - * Update the value the rate constant. + * Update the value of the rate constant. * * This function returns the actual value of the rate constant. It can be * safely called for negative values of the pre-exponential factor. diff --git a/include/cantera/kinetics/StoichManager.h b/include/cantera/kinetics/StoichManager.h index 9077ed11edd..66c2a138bae 100644 --- a/include/cantera/kinetics/StoichManager.h +++ b/include/cantera/kinetics/StoichManager.h @@ -40,7 +40,7 @@ namespace Cantera * \f] * * where \f$ \nu^{(p)_{k,i}} \f$ is the product-side stoichiometric - * coefficient of species \a k in reaction \a i. This could be done be + * coefficient of species \a k in reaction \a i. This could be done by * straightforward matrix multiplication, but would be inefficient, since most * of the matrix elements of \f$ \nu^{(p)}_{k,i} \f$ are zero. We could do * better by using sparse-matrix algorithms to compute this product. @@ -61,12 +61,12 @@ namespace Cantera * computing quantities that can be written as matrix multiplies. * * They are designed to explicitly unroll loops over species or reactions for - * Operations on reactions that require knowing the reaction stoichiometry. + * operations on reactions that require knowing the reaction stoichiometry. * * This module consists of class StoichManager, and classes C1, C2, and C3. * Classes C1, C2, and C3 handle operations involving one, two, or three * species, respectively, in a reaction. Instances are instantiated with a - * reaction number, and n species numbers (n = 1 for C1, etc.). All three + * reaction number, and n species numbers (n = 1 for C1, etc.). All three * classes have the same interface. * * These classes are designed for use by StoichManager, and the operations diff --git a/include/cantera/oneD/Boundary1D.h b/include/cantera/oneD/Boundary1D.h index 82dc4e68062..f1a7807b92a 100644 --- a/include/cantera/oneD/Boundary1D.h +++ b/include/cantera/oneD/Boundary1D.h @@ -299,6 +299,10 @@ class ReactingSurf1D : public Boundary1D m_enabled = docov; } + bool coverageEnabled() { + return m_enabled; + } + virtual std::string componentName(size_t n) const; virtual void init(); diff --git a/include/cantera/thermo/EdgePhase.h b/include/cantera/thermo/EdgePhase.h index 22d982a48f0..2825fde0a58 100644 --- a/include/cantera/thermo/EdgePhase.h +++ b/include/cantera/thermo/EdgePhase.h @@ -22,8 +22,8 @@ namespace Cantera * thermodynamic object. * * All of the equations and formulations carry through from SurfPhase to this - * EdgePhase object. It should be noted however, that dimensional object with - * length dimensions, have their dimensions reduced by one. + * EdgePhase object. It should be noted that dimensional quantities with + * dimensions including a length have their dimensions reduced by one. * * @ingroup thermoprops */ diff --git a/interfaces/cython/SConscript b/interfaces/cython/SConscript index adcff5c3965..4e8791fa3dc 100644 --- a/interfaces/cython/SConscript +++ b/interfaces/cython/SConscript @@ -83,14 +83,13 @@ localenv.Depends(ext, localenv['cantera_staticlib']) for f in (mglob(localenv, 'cantera', 'py') + mglob(localenv, 'cantera/test', 'py') + - mglob(localenv, 'cantera/examples/tutorial', 'py') + - mglob(localenv, 'cantera/examples/equilibrium', 'py') + mglob(localenv, 'cantera/examples/kinetics', 'py') + - mglob(localenv, 'cantera/examples/transport', 'py') + - mglob(localenv, 'cantera/examples/reactors', 'py') + + mglob(localenv, 'cantera/examples/multiphase', 'py') + mglob(localenv, 'cantera/examples/onedim', 'py') + + mglob(localenv, 'cantera/examples/reactors', 'py') + mglob(localenv, 'cantera/examples/surface_chemistry', 'py') + - mglob(localenv, 'cantera/examples/misc', 'py')): + mglob(localenv, 'cantera/examples/thermo', 'py') + + mglob(localenv, 'cantera/examples/transport', 'py')): localenv.Depends(mod, f) # Determine installation path and install the Python module diff --git a/interfaces/cython/cantera/__init__.py b/interfaces/cython/cantera/__init__.py index 546801ac29c..023fa3883fd 100644 --- a/interfaces/cython/cantera/__init__.py +++ b/interfaces/cython/cantera/__init__.py @@ -7,6 +7,7 @@ from .liquidvapor import * from .onedim import * from .utils import * +import cantera.interrupts # Helps with standalone packaging (PyInstaller etc.) import os import sys diff --git a/interfaces/cython/cantera/_cantera.pxd b/interfaces/cython/cantera/_cantera.pxd index 9a8dcf5eeb8..26ff56dcd3c 100644 --- a/interfaces/cython/cantera/_cantera.pxd +++ b/interfaces/cython/cantera/_cantera.pxd @@ -757,6 +757,7 @@ cdef extern from "cantera/oneD/Boundary1D.h": CxxRreactingSurf1D() void setKineticsMgr(CxxInterfaceKinetics*) except +translate_exception void enableCoverageEquations(cbool) except +translate_exception + cbool coverageEnabled() cdef extern from "cantera/oneD/StFlow.h": diff --git a/interfaces/cython/cantera/ck2yaml.py b/interfaces/cython/cantera/ck2yaml.py index 5644539569f..c037da40ad0 100644 --- a/interfaces/cython/cantera/ck2yaml.py +++ b/interfaces/cython/cantera/ck2yaml.py @@ -43,13 +43,11 @@ included in the YAML output. """ -from collections import defaultdict, OrderedDict import logging import os.path import sys import numpy as np import re -import itertools import getopt import textwrap from email.utils import formatdate @@ -59,6 +57,21 @@ except ImportError: from ruamel import yaml +# yaml.version_info is a tuple with the three parts of the version +yaml_version = yaml.version_info +# We choose ruamel.yaml 0.15.34 as the minimum version +# since it is the highest version available in the Ubuntu +# 18.04 repositories and seems to work. Older versions such as +# 0.13.14 on CentOS7 and 0.10.23 on Ubuntu 16.04 raise an exception +# that they are missing the RoundTripRepresenter +yaml_min_version = (0, 15, 34) +if yaml_version < yaml_min_version: + raise RuntimeError( + "The minimum supported version of ruamel.yaml is 0.15.34. If you " + "installed ruamel.yaml from your operating system's package manager, " + "please install an updated version using pip or conda." + ) + BlockMap = yaml.comments.CommentedMap logger = logging.getLogger(__name__) diff --git a/interfaces/cython/cantera/composite.py b/interfaces/cython/cantera/composite.py index f1172f9180a..b614bf67cc9 100644 --- a/interfaces/cython/cantera/composite.py +++ b/interfaces/cython/cantera/composite.py @@ -447,6 +447,7 @@ class SolutionArray: # From Transport 'viscosity', 'electrical_conductivity', 'thermal_conductivity', ] + _strings = ['phase_of_matter'] _n_species = [ # from ThermoPhase 'Y', 'X', 'concentrations', 'partial_molar_enthalpies', @@ -955,14 +956,22 @@ def read_csv(self, filename): using `restore_data`. This method allows for recreation of data previously exported by `write_csv`. """ - # read data block and header separately - data = np.genfromtxt(filename, skip_header=1, delimiter=',') - labels = np.genfromtxt(filename, max_rows=1, delimiter=',', dtype=str) - - data_dict = OrderedDict() - for i, label in enumerate(labels): - data_dict[label] = data[:, i] - + if np.lib.NumpyVersion(np.__version__) < "1.14.0": + # bytestring needs to be converted for columns containing strings + data = np.genfromtxt(filename, delimiter=',', + dtype=None, names=True) + data_dict = OrderedDict() + for label in data.dtype.names: + if data[label].dtype.type == np.bytes_: + data_dict[label] = data[label].astype('U') + else: + data_dict[label] = data[label] + else: + # the 'encoding' parameter introduced with NumPy 1.14 simplifies import + data = np.genfromtxt(filename, delimiter=',', + dtype=None, names=True, encoding=None) + data_dict = OrderedDict({label: data[label] + for label in data.dtype.names}) self.restore_data(data_dict) def to_pandas(self, cols=None, *args, **kwargs): @@ -1115,8 +1124,12 @@ def write_hdf(self, filename, *args, cols=None, group=None, subgroup=None, # store SolutionArray data for key, val in self._meta.items(): dgroup.attrs[key] = val - for header, col in data.items(): - dgroup.create_dataset(header, data=col, **hdf_kwargs) + for header, value in data.items(): + if value.dtype.type == np.str_: + dgroup.create_dataset(header, data=value.astype('S'), + **hdf_kwargs) + else: + dgroup.create_dataset(header, data=value, **hdf_kwargs) return group @@ -1204,7 +1217,11 @@ def strip_ext(source): # load data data = OrderedDict() for name, value in dgroup.items(): - if name != 'phase': + if name == 'phase': + continue + elif value.dtype.type == np.bytes_: + data[name] = np.array(value).astype('U') + else: data[name] = np.array(value) self.restore_data(data) @@ -1306,6 +1323,12 @@ def _make_functions(): def empty_scalar(self): return np.empty(self._shape) + def empty_strings(self): + # The maximum length of strings assigned by built-in methods is + # currently limited to 50 characters; an attempt to assign longer + # character arrays will result in truncated strings. + return np.empty(self._shape, dtype='U50') + def empty_species(self): return np.empty(self._shape + (self._phase.n_selected_species,)) @@ -1332,6 +1355,9 @@ def getter(self): for name in SolutionArray._scalar: setattr(SolutionArray, name, make_prop(name, empty_scalar, Solution)) + for name in SolutionArray._strings: + setattr(SolutionArray, name, make_prop(name, empty_strings, Solution)) + for name in SolutionArray._n_species: setattr(SolutionArray, name, make_prop(name, empty_species, Solution)) diff --git a/interfaces/cython/cantera/cti2yaml.py b/interfaces/cython/cantera/cti2yaml.py index 2967e224864..20252a98545 100644 --- a/interfaces/cython/cantera/cti2yaml.py +++ b/interfaces/cython/cantera/cti2yaml.py @@ -24,6 +24,21 @@ except ImportError: from ruamel import yaml +# yaml.version_info is a tuple with the three parts of the version +yaml_version = yaml.version_info +# We choose ruamel.yaml 0.15.34 as the minimum version +# since it is the highest version available in the Ubuntu +# 18.04 repositories and seems to work. Older versions such as +# 0.13.14 on CentOS7 and 0.10.23 on Ubuntu 16.04 raise an exception +# that they are missing the RoundTripRepresenter +yaml_min_version = (0, 15, 34) +if yaml_version < yaml_min_version: + raise RuntimeError( + "The minimum supported version of ruamel.yaml is 0.15.34. If you " + "installed ruamel.yaml from your operating system's package manager, " + "please install an updated version using pip or conda." + ) + def _printerr(*args): # All debug and error output should go to stderr diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 4eff5255877..aac78ead829 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -24,12 +24,27 @@ from typing import Any, Dict, Union, Iterable, Optional, List, Tuple from typing import TYPE_CHECKING +import numpy as np + try: import ruamel_yaml as yaml # type: ignore except ImportError: from ruamel import yaml -import numpy as np +# yaml.version_info is a tuple with the three parts of the version +yaml_version = yaml.version_info +# We choose ruamel.yaml 0.15.34 as the minimum version +# since it is the highest version available in the Ubuntu +# 18.04 repositories and seems to work. Older versions such as +# 0.13.14 on CentOS7 and 0.10.23 on Ubuntu 16.04 raise an exception +# that they are missing the RoundTripRepresenter +yaml_min_version = (0, 15, 34) +if yaml_version < yaml_min_version: + raise RuntimeError( + "The minimum supported version of ruamel.yaml is 0.15.34. If you " + "installed ruamel.yaml from your operating system's package manager, " + "please install an updated version using pip or conda." + ) if TYPE_CHECKING: # This is available in the built-in typing module in Python 3.8 diff --git a/interfaces/cython/cantera/examples/kinetics/extract_submechanism.py b/interfaces/cython/cantera/examples/kinetics/extract_submechanism.py index aa52aefea0d..19a7c9b0d81 100644 --- a/interfaces/cython/cantera/examples/kinetics/extract_submechanism.py +++ b/interfaces/cython/cantera/examples/kinetics/extract_submechanism.py @@ -14,7 +14,8 @@ import cantera as ct import matplotlib.pyplot as plt -all_species = ct.Species.listFromFile('gri30.yaml') +input_file = 'gri30.yaml' +all_species = ct.Species.listFromFile(input_file) species = [] # Filter species @@ -36,7 +37,8 @@ print('Species: {0}'.format(', '.join(S.name for S in species))) # Filter reactions, keeping only those that only involve the selected species -all_reactions = ct.Reaction.listFromFile('gri30.yaml') +ref_phase = ct.Solution(thermo='ideal-gas', kinetics='gas', species=all_species) +all_reactions = ct.Reaction.listFromFile(input_file, ref_phase) reactions = [] print('\nReactions:') @@ -51,8 +53,8 @@ print(R.equation) print('\n') -gas1 = ct.Solution('gri30.yaml') -gas2 = ct.Solution(thermo='IdealGas', kinetics='GasKinetics', +gas1 = ct.Solution(input_file) +gas2 = ct.Solution(thermo='ideal-gas', kinetics='gas', species=species, reactions=reactions) diff --git a/interfaces/cython/cantera/examples/kinetics/reaction_path.py b/interfaces/cython/cantera/examples/kinetics/reaction_path.py index 629ffda619e..544f84c9000 100644 --- a/interfaces/cython/cantera/examples/kinetics/reaction_path.py +++ b/interfaces/cython/cantera/examples/kinetics/reaction_path.py @@ -9,8 +9,9 @@ Requires: cantera >= 2.5.0 """ -import os +from subprocess import run import sys +from pathlib import Path import cantera as ct @@ -33,14 +34,14 @@ dot_file = 'rxnpath.dot' img_file = 'rxnpath.png' -img_path = os.path.join(os.getcwd(), img_file) +img_path = Path.cwd().joinpath(img_file) diagram.write_dot(dot_file) print(diagram.get_data()) -print("Wrote graphviz input file to '{0}'.".format(os.path.join(os.getcwd(), dot_file))) +print("Wrote graphviz input file to '{0}'.".format(Path.cwd().joinpath(dot_file))) -os.system('dot {0} -Tpng -o{1} -Gdpi=200'.format(dot_file, img_file)) +run('dot {0} -Tpng -o{1} -Gdpi=200'.format(dot_file, img_file).split()) print("Wrote graphviz output file to '{0}'.".format(img_path)) if "-view" in sys.argv: diff --git a/interfaces/cython/cantera/examples/onedim/diffusion_flame.py b/interfaces/cython/cantera/examples/onedim/diffusion_flame.py index e910e74633d..66906e9cfc1 100644 --- a/interfaces/cython/cantera/examples/onedim/diffusion_flame.py +++ b/interfaces/cython/cantera/examples/onedim/diffusion_flame.py @@ -41,7 +41,7 @@ f.oxidizer_inlet.T = tin_o # Set the boundary emissivities -f.set_boundary_emissivities(0.0, 0.0) +f.boundary_emissivities = 0.0, 0.0 # Turn radiation off f.radiation_enabled = False diff --git a/interfaces/cython/cantera/examples/onedim/diffusion_flame_extinction.py b/interfaces/cython/cantera/examples/onedim/diffusion_flame_extinction.py index 14b84313ad3..ac168b0d7b3 100644 --- a/interfaces/cython/cantera/examples/onedim/diffusion_flame_extinction.py +++ b/interfaces/cython/cantera/examples/onedim/diffusion_flame_extinction.py @@ -130,13 +130,12 @@ f.solve(loglevel=0) except ct.CanteraError as e: print('Error: Did not converge at n =', n, e) - if np.max(f.T) > temperature_limit_extinction: + if not np.isclose(np.max(f.T), temperature_limit_extinction): # Flame is still burning, so proceed to next strain rate n_last_burning = n if hdf_output: group = 'extinction/{0:04d}'.format(n) - f.write_hdf(file_name, group=group, quiet=False, - description='extinction iteration'.format(n)) + f.write_hdf(file_name, group=group, quiet=True) else: file_name = 'extinction_{0:04d}.xml'.format(n) f.save(os.path.join(data_directory, file_name), @@ -145,19 +144,32 @@ ', reaction mechanism ' + reaction_mechanism) T_max.append(np.max(f.T)) a_max.append(np.max(np.abs(np.gradient(f.velocity) / np.gradient(f.grid)))) + print('Flame burning at alpha = {:8.4F}. Proceeding to the next iteration, ' + 'with delta_alpha = {}'.format(alpha[-1], delta_alpha)) + elif ((T_max[-2] - T_max[-1] < delta_T_min) and (delta_alpha < delta_alpha_min)): # 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) & - (delta_alpha < delta_alpha_min)): - print('Flame extinguished at n = {0}.'.format(n), - 'Abortion criterion satisfied.') - break + # strain rate increase is reached, save the last, non-burning, solution + # to the output file and break the loop + T_max.append(np.max(f.T)) + a_max.append(np.max(np.abs(np.gradient(f.velocity) / np.gradient(f.grid)))) + if hdf_output: + group = 'extinction/{0:04d}'.format(n) + f.write_hdf(file_name, group=group, quiet=True) + else: + file_name = 'extinction_{0:04d}.xml'.format(n) + f.save(os.path.join(data_directory, file_name), name='solution', loglevel=0) + print('Flame extinguished at alpha = {0:8.4F}.'.format(alpha[-1]), + 'Abortion criterion satisfied.') + break else: # Procedure if flame extinguished but abortion criterion is not satisfied - print('Flame extinguished at n = {0}. Restoring n = {1} with ' - 'alpha = {2}'.format(n, n_last_burning, alpha[n_last_burning])) # Reduce relative strain rate increase delta_alpha = delta_alpha / delta_alpha_factor + + print('Flame extinguished at alpha = {0:8.4F}. Restoring alpha = {1:8.4F} and ' + 'trying delta_alpha = {2}'.format( + alpha[-1], alpha[n_last_burning], delta_alpha)) + # Restore last burning solution if hdf_output: group = 'extinction/{0:04d}'.format(n_last_burning) @@ -168,7 +180,15 @@ name='solution', loglevel=0) -# Print some parameters at the extinction point +# Print some parameters at the extinction point, after restoring the last burning +# solution +if hdf_output: + group = 'extinction/{0:04d}'.format(n_last_burning) + f.read_hdf(file_name, group=group) +else: + file_name = 'extinction_{0:04d}.xml'.format(n_last_burning) + f.restore(os.path.join(data_directory, file_name), + name='solution', loglevel=0) print('----------------------------------------------------------------------') print('Parameters at the extinction point:') print('Pressure p={0} bar'.format(f.P / 1e5)) diff --git a/interfaces/cython/cantera/examples/onedim/flame_fixed_T.py b/interfaces/cython/cantera/examples/onedim/flame_fixed_T.py index 7811da192a6..fd2bc54a7ed 100644 --- a/interfaces/cython/cantera/examples/onedim/flame_fixed_T.py +++ b/interfaces/cython/cantera/examples/onedim/flame_fixed_T.py @@ -7,6 +7,7 @@ import cantera as ct import numpy as np +from pathlib import Path ################################################################ # parameter values @@ -38,7 +39,9 @@ # read temperature vs. position data from a file. # The file is assumed to have one z, T pair per line, separated by a comma. -zloc, tvalues = np.genfromtxt('tdata.dat', delimiter=',', comments='#').T +# The data file must be stored in the same folder as this script. +data_file = Path(__file__).parent.joinpath('tdata.dat') +zloc, tvalues = np.genfromtxt(str(data_file), delimiter=',', comments='#').T zloc /= max(zloc) # set the temperature profile to the values read in diff --git a/interfaces/cython/cantera/examples/onedim/ion_burner_flame.py b/interfaces/cython/cantera/examples/onedim/ion_burner_flame.py index cb7c744a559..e480dffc91c 100644 --- a/interfaces/cython/cantera/examples/onedim/ion_burner_flame.py +++ b/interfaces/cython/cantera/examples/onedim/ion_burner_flame.py @@ -9,7 +9,7 @@ p = ct.one_atm tburner = 600.0 reactants = 'CH4:1.0, O2:2.0, N2:7.52' # premixed gas composition -width = 0.5 # m +width = 0.05 # m loglevel = 1 # amount of diagnostic output (0 to 5) gas = ct.Solution('gri30_ion.yaml') diff --git a/interfaces/cython/cantera/examples/reactors/combustor.py b/interfaces/cython/cantera/examples/reactors/combustor.py index 538ea35d579..b67e66a5fb1 100644 --- a/interfaces/cython/cantera/examples/reactors/combustor.py +++ b/interfaces/cython/cantera/examples/reactors/combustor.py @@ -73,12 +73,9 @@ def mdot(t): states.append(combustor.thermo.state, tres=residence_time) residence_time *= 0.9 # decrease the residence time for the next iteration -# Heat release rate [W/m^3] -Q = - np.sum(states.net_production_rates * states.partial_molar_enthalpies, axis=1) - # Plot results f, ax1 = plt.subplots(1, 1) -ax1.plot(states.tres, Q, '.-', color='C0') +ax1.plot(states.tres, states.heat_release_rate, '.-', color='C0') ax2 = ax1.twinx() ax2.plot(states.tres[:-1], states.T[:-1], '.-', color='C1') ax1.set_xlabel('residence time [s]') diff --git a/interfaces/cython/cantera/examples/reactors/fuel_injection.py b/interfaces/cython/cantera/examples/reactors/fuel_injection.py index ef49f12917d..142662da845 100644 --- a/interfaces/cython/cantera/examples/reactors/fuel_injection.py +++ b/interfaces/cython/cantera/examples/reactors/fuel_injection.py @@ -15,6 +15,7 @@ # Use a reduced n-dodecane mechanism with PAH formation pathways gas = ct.Solution('nDodecane_Reitz.yaml', 'nDodecane_IG') +gas.case_sensitive_species_names = True # Create a Reservoir for the fuel inlet, set to pure dodecane gas.TPX = 300, 20*ct.one_atm, 'c12h26:1.0' @@ -59,31 +60,40 @@ def fuel_mdot(t): tprev = tnow states.append(r.thermo.state, t=tnow) -# nice names for species -labels = { +# nice names for species, including PAH species that can be considered +# as precursors to soot formation +species_aliases = { + 'o2': 'O$_2$', + 'h2o': 'H$_2$O', + 'co': 'CO', + 'co2': 'CO$_2$', + 'h2': 'H$_2$', + 'ch4': 'CH$_4$' +} +for name, alias in species_aliases.items(): + gas.add_species_alias(name, alias) + +pah_aliases = { 'A1c2h': 'phenylacetylene', 'A1c2h3': 'styrene', 'A1': 'benzene', 'A2': 'naphthalene', 'A2r5': 'acenaphthylene', 'A3': 'phenanthrene', - 'A4': 'pyrene', - 'o2': 'O$_2$', - 'h2o': 'H$_2$O', - 'co2': 'CO$_2$', - 'h2': 'H$_2$', - 'ch4': 'CH$_4$' + 'A4': 'pyrene' } +for name, alias in pah_aliases.items(): + gas.add_species_alias(name, alias) -# Plot the concentrations of some species of interest, including PAH species -# which can be considered as precursors to soot formation. +# Plot the concentrations of species of interest f, ax = plt.subplots(1, 2) -for s in ['o2', 'h2o', 'co2', 'CO', 'h2', 'ch4']: - ax[0].plot(states.t, states(s).X, label=labels.get(s, s)) +for s in species_aliases.values(): + ax[0].plot(states.t, states(s).X, label=s) + +for s in pah_aliases.values(): + ax[1].plot(states.t, states(s).X, label=s) -for s in ['A1c2h', 'A1c2h3', 'A2r5', 'A1', 'A2', 'A3', 'A4']: - ax[1].plot(states.t, states(s).X, label=labels[s]) for a in ax: a.legend(loc='best') a.set_xlabel('time [s]') diff --git a/interfaces/cython/cantera/examples/reactors/ic_engine.py b/interfaces/cython/cantera/examples/reactors/ic_engine.py index 13b75c8fefb..c1260cfd0ee 100644 --- a/interfaces/cython/cantera/examples/reactors/ic_engine.py +++ b/interfaces/cython/cantera/examples/reactors/ic_engine.py @@ -191,6 +191,7 @@ def ca_ticks(t): t = states.t # pressure and temperature +xticks = np.arange(0, 0.18, 0.02) fig, ax = plt.subplots(nrows=2) ax[0].plot(t, states.P / 1.e5) ax[0].set_ylabel('$p$ [bar]') @@ -199,7 +200,8 @@ def ca_ticks(t): ax[1].plot(t, states.T) ax[1].set_ylabel('$T$ [K]') ax[1].set_xlabel(r'$\phi$ [deg]') -ax[1].set_xticklabels(ca_ticks(ax[1].get_xticks())) +ax[1].set_xticks(xticks) +ax[1].set_xticklabels(ca_ticks(xticks)) plt.show() # p-V diagram @@ -224,7 +226,8 @@ def ca_ticks(t): ax.legend(loc=0) ax.set_ylabel('[kW]') ax.set_xlabel(r'$\phi$ [deg]') -ax.set_xticklabels(ca_ticks(ax.get_xticks())) +ax.set_xticks(xticks) +ax.set_xticklabels(ca_ticks(xticks)) plt.show() # gas composition @@ -236,7 +239,8 @@ def ca_ticks(t): ax.legend(loc=0) ax.set_ylabel('$X_i$ [-]') ax.set_xlabel(r'$\phi$ [deg]') -ax.set_xticklabels(ca_ticks(ax.get_xticks())) +ax.set_xticks(xticks) +ax.set_xticklabels(ca_ticks(xticks)) plt.show() ###################################################################### @@ -245,22 +249,21 @@ def ca_ticks(t): # heat release Q = trapz(states.heat_release_rate * states.V, t) -print('{:45s}{:>4.1f} kW'.format('Heat release rate per cylinder (estimate):', - Q / t[-1] / 1000.)) +output_str = '{:45s}{:>4.1f} {}' +print(output_str.format('Heat release rate per cylinder (estimate):', + Q / t[-1] / 1000., 'kW')) # expansion power W = trapz(states.dWv_dt, t) -print('{:45s}{:>4.1f} kW'.format('Expansion power per cylinder (estimate):', - W / t[-1] / 1000.)) +print(output_str.format('Expansion power per cylinder (estimate):', + W / t[-1] / 1000., 'kW')) # efficiency eta = W / Q -print('{:45s}{:>4.1f} %'.format('Efficiency (estimate):', - eta * 100.)) +print(output_str.format('Efficiency (estimate):', eta * 100., '%')) # CO emissions MW = states.mean_molecular_weight CO_emission = trapz(MW * states.mdot_out * states('CO').X[:, 0], t) CO_emission /= trapz(MW * states.mdot_out, t) -print('{:45s}{:>4.1f} ppm'.format('CO emission (estimate):', - CO_emission * 1.e6)) +print(output_str.format('CO emission (estimate):', CO_emission * 1.e6, 'ppm')) diff --git a/interfaces/cython/cantera/examples/reactors/periodic_cstr.py b/interfaces/cython/cantera/examples/reactors/periodic_cstr.py index 2c7ed63f14b..5e1d4f99e02 100644 --- a/interfaces/cython/cantera/examples/reactors/periodic_cstr.py +++ b/interfaces/cython/cantera/examples/reactors/periodic_cstr.py @@ -82,9 +82,16 @@ network.advance(t) states.append(cstr.thermo.state, t=t) +aliases = {'H2': 'H$_2$', 'O2': 'O$_2$', 'H2O': 'H$_2$O'} +for name, alias in aliases.items(): + gas.add_species_alias(name, alias) + if __name__ == '__main__': print(__doc__) plt.figure(1) - plt.plot(states.t, states('H2', 'O2', 'H2O').Y) - plt.title('Mass Fractions') + for spc in aliases.values(): + plt.plot(states.t, states(spc).Y, label=spc) + plt.legend(loc='upper right') + plt.xlabel('time [s]') + plt.ylabel('mass fraction') plt.show() diff --git a/interfaces/cython/cantera/examples/reactors/piston.py b/interfaces/cython/cantera/examples/reactors/piston.py index e53c08b9e14..4967d7bc39a 100644 --- a/interfaces/cython/cantera/examples/reactors/piston.py +++ b/interfaces/cython/cantera/examples/reactors/piston.py @@ -53,8 +53,8 @@ def v(t): net = ct.ReactorNet([r1, r2]) -states1 = ct.SolutionArray(r1.thermo, extra=['t', 'v']) -states2 = ct.SolutionArray(r2.thermo, extra=['t', 'v']) +states1 = ct.SolutionArray(r1.thermo, extra=['t', 'volume']) +states2 = ct.SolutionArray(r2.thermo, extra=['t', 'volume']) for n in range(200): time = (n+1)*0.001 @@ -63,8 +63,8 @@ def v(t): print(fmt.format(time, r1.T, r2.T, r1.volume, r2.volume, r1.volume + r2.volume, r2.thermo['CO'].X[0])) - states1.append(r1.thermo.state, t=1000*time, v=r1.volume) - states2.append(r2.thermo.state, t=1000*time, v=r2.volume) + states1.append(r1.thermo.state, t=1000*time, volume=r1.volume) + states2.append(r2.thermo.state, t=1000*time, volume=r2.volume) # plot the results if matplotlib is installed. if '--plot' in sys.argv: @@ -74,8 +74,8 @@ def v(t): plt.xlabel('Time (ms)') plt.ylabel('Temperature (K)') plt.subplot(2, 2, 2) - plt.plot(states1.t, states1.v, '-', states2.t, states2.v, 'r-', - states1.t, states1.v + states2.v, 'g-') + plt.plot(states1.t, states1.volume, '-', states2.t, states2.volume, 'r-', + states1.t, states1.volume + states2.volume, 'g-') plt.xlabel('Time (ms)') plt.ylabel('Volume (m3)') plt.subplot(2, 2, 3) diff --git a/interfaces/cython/cantera/examples/reactors/reactor2.py b/interfaces/cython/cantera/examples/reactors/reactor2.py index 0b33ffe9a56..168136016ab 100644 --- a/interfaces/cython/cantera/examples/reactors/reactor2.py +++ b/interfaces/cython/cantera/examples/reactors/reactor2.py @@ -27,8 +27,8 @@ # First create each gas needed, and a reactor or reservoir for each one. # create an argon gas object and set its state -ar = ct.Solution('argon.yaml') -ar.TP = 1000.0, 20.0 * ct.one_atm +ar = ct.Solution('air.yaml') +ar.TPX = 1000.0, 20.0 * ct.one_atm, "AR:1" # create a reactor to represent the side of the cylinder filled with argon r1 = ct.IdealGasReactor(ar) @@ -113,4 +113,4 @@ plt.tight_layout() plt.show() else: - print("""To view a plot of these results, run this script with the option -plot""") + print("To view a plot of these results, run this script with the option --plot") diff --git a/interfaces/cython/cantera/examples/surface_chemistry/catalytic_combustion.py b/interfaces/cython/cantera/examples/surface_chemistry/catalytic_combustion.py index d75e20cabed..8b3566e2164 100644 --- a/interfaces/cython/cantera/examples/surface_chemistry/catalytic_combustion.py +++ b/interfaces/cython/cantera/examples/surface_chemistry/catalytic_combustion.py @@ -64,7 +64,7 @@ # grid sim = ct.ImpingingJet(gas=gas, width=width, surface=surf_phase) -# Objects of class StagnationFlow have members that represent the gas inlet +# Objects of class ImpingingJet have members that represent the gas inlet # ('inlet') and the surface ('surface'). Set some parameters of these objects. sim.inlet.mdot = mdot sim.inlet.T = tinlet @@ -74,7 +74,7 @@ # Show the initial solution estimate sim.show_solution() -# Solving problems with stiff chemistry coulpled to flow can require a +# Solving problems with stiff chemistry coupled to flow can require a # sequential approach where solutions are first obtained for simpler problems # and used as the initial guess for more difficult problems. diff --git a/interfaces/cython/cantera/examples/surface_chemistry/diamond_cvd.py b/interfaces/cython/cantera/examples/surface_chemistry/diamond_cvd.py index 0b75c08d87e..ecf7d2301b4 100644 --- a/interfaces/cython/cantera/examples/surface_chemistry/diamond_cvd.py +++ b/interfaces/cython/cantera/examples/surface_chemistry/diamond_cvd.py @@ -9,7 +9,7 @@ role in diamond CVD, and this example computes the growth rate and surface coverages as a function of [H] at the surface for fixed temperature and [CH3]. -Requires: cantera >= 2.5.0, pandas >= 0.21.0, matplotlib >= 2.0 +Requires: cantera >= 2.5.0, pandas >= 0.25.0, matplotlib >= 2.0 """ import csv @@ -58,19 +58,13 @@ import pandas as pd data = pd.read_csv('diamond.csv') - plt.figure() - plt.plot(data['H mole Fraction'], data['Growth Rate (microns/hour)']) + data.plot(x="H mole Fraction", y="Growth Rate (microns/hour)", legend=False) plt.xlabel('H Mole Fraction') plt.ylabel('Growth Rate (microns/hr)') plt.show() - plt.figure() - for name in data: - if name.startswith(('H mole', 'Growth')): - continue - plt.plot(data['H mole Fraction'], data[name], label=name) - - plt.legend() + names = [name for name in data.columns if not name.startswith(('H mole', 'Growth'))] + data.plot(x='H mole Fraction', y=names, legend=True) plt.xlabel('H Mole Fraction') plt.ylabel('Coverage') plt.show() diff --git a/interfaces/cython/cantera/examples/surface_chemistry/sofc.py b/interfaces/cython/cantera/examples/surface_chemistry/sofc.py index 883dd074eca..5877ff1a0c3 100644 --- a/interfaces/cython/cantera/examples/surface_chemistry/sofc.py +++ b/interfaces/cython/cantera/examples/surface_chemistry/sofc.py @@ -1,7 +1,7 @@ """ A simple model of a solid oxide fuel cell. -Unlike most SOFC models, this model does not use semi-empirical Butler- Volmer +Unlike most SOFC models, this model does not use semi-empirical Butler-Volmer kinetics for the charge transfer reactions, but uses elementary, reversible reactions obeying mass-action kinetics for all reactions, including charge transfer. As this script will demonstrate, this approach allows computing the @@ -120,14 +120,16 @@ def anode_curr(E): # get the species net production rates due to the anode-side TPB reaction # mechanism. The production rate array has the values for the neighbor # species in the order listed in the .yaml file, followed by the tpb phase. - # Since the first neighbor phase is the bulk metal, species 0 is the - # electron. + # The kinetics_species_index method finds the index of the species, + # accounting for the possibility of species being in different orders in the + # arrays. w = tpb_a.net_production_rates + electron_index = tpb_a.kinetics_species_index('electron') # the sign convention is that the current is positive when - # electrons are being delivered to the anode - i.e. it is positive + # electrons are being delivered to the anode - that is, it is positive # for fuel cell operation. - return ct.faraday * w[0] * TPB_length_per_area + return ct.faraday * w[electron_index] * TPB_length_per_area ##################################################################### @@ -167,13 +169,15 @@ def cathode_curr(E): # get the species net production rates due to the cathode-side TPB # reaction mechanism. The production rate array has the values for the # neighbor species in the order listed in the .yaml file, followed by the - # tpb phase. Since the first neighbor phase is the bulk metal, species 0 - # is the electron. + # tpb phase. The kinetics_species_index method finds the index of the species, + # accounting for the possibility of species being in different orders in the + # arrays. w = tpb_c.net_production_rates + electron_index = tpb_c.kinetics_species_index('electron') # the sign convention is that the current is positive when electrons are - # being drawn from the cathode (i.e, negative production rate). - return -ct.faraday * w[0] * TPB_length_per_area + # being drawn from the cathode (that is, negative production rate). + return -ct.faraday * w[electron_index] * TPB_length_per_area # initialization diff --git a/interfaces/cython/cantera/examples/thermo/sound_speed.py b/interfaces/cython/cantera/examples/thermo/sound_speed.py index 7b9c269bb66..8fd8f7a5ca1 100644 --- a/interfaces/cython/cantera/examples/thermo/sound_speed.py +++ b/interfaces/cython/cantera/examples/thermo/sound_speed.py @@ -8,7 +8,7 @@ import math -def equilSoundSpeeds(gas, rtol=1.0e-6, maxiter=5000): +def equilSoundSpeeds(gas, rtol=1.0e-6, max_iter=5000): """ Returns a tuple containing the equilibrium and frozen sound speeds for a gas with an equilibrium composition. The gas is first set to an @@ -17,7 +17,7 @@ def equilSoundSpeeds(gas, rtol=1.0e-6, maxiter=5000): """ # set the gas to equilibrium at its current T and P - gas.equilibrate('TP', rtol=rtol, maxiter=maxiter) + gas.equilibrate('TP', rtol=rtol, max_iter=max_iter) # save properties s0 = gas.s @@ -35,7 +35,7 @@ def equilSoundSpeeds(gas, rtol=1.0e-6, maxiter=5000): afrozen = math.sqrt((p1 - p0)/(gas.density - r0)) # now equilibrate the gas holding S and P constant - gas.equilibrate('SP', rtol=rtol, maxiter=maxiter) + gas.equilibrate('SP', rtol=rtol, max_iter=max_iter) # equilibrium sound speed aequil = math.sqrt((p1 - p0)/(gas.density - r0)) diff --git a/interfaces/cython/cantera/examples/transport/multiprocessing_viscosity.py b/interfaces/cython/cantera/examples/transport/multiprocessing_viscosity.py index 4f23ae0b318..835a9178896 100644 --- a/interfaces/cython/cantera/examples/transport/multiprocessing_viscosity.py +++ b/interfaces/cython/cantera/examples/transport/multiprocessing_viscosity.py @@ -55,15 +55,14 @@ def parallel(mech, predicate, nProcs, nTemps): """ P = ct.one_atm X = 'CH4:1.0, O2:1.0, N2:3.76' - pool = multiprocessing.Pool(processes=nProcs, - initializer=init_process, - initargs=(mech,)) - - y = pool.map(predicate, - zip(itertools.repeat(mech), - np.linspace(300, 900, nTemps), - itertools.repeat(P), - itertools.repeat(X))) + with multiprocessing.Pool( + processes=nProcs, initializer=init_process, initargs=(mech,) + ) as pool: + y = pool.map(predicate, + zip(itertools.repeat(mech), + np.linspace(300, 900, nTemps), + itertools.repeat(P), + itertools.repeat(X))) return y diff --git a/interfaces/cython/cantera/onedim.py b/interfaces/cython/cantera/onedim.py index a5e2c2e3345..3232f4e1466 100644 --- a/interfaces/cython/cantera/onedim.py +++ b/interfaces/cython/cantera/onedim.py @@ -2,8 +2,8 @@ # at https://cantera.org/license.txt for license and copyright information. from math import erf -from os import path from email.utils import formatdate +import warnings import numpy as np from ._cantera import * @@ -51,7 +51,7 @@ def other_components(self, domain=None): if isinstance(dom, Inlet1D): return tuple([e for e in self._other if e not in {'grid', 'lambda', 'eField'}]) - elif isinstance(dom, IdealGasFlow): + elif isinstance(dom, (IdealGasFlow, IonFlow)): return self._other else: return () @@ -253,7 +253,27 @@ def radiation_enabled(self, enable): self.flame.radiation_enabled = enable def set_boundary_emissivities(self, e_left, e_right): - self.flame.set_boundary_emissivities(e_left, e_right) + """ + .. deprecated:: 2.5 + + To be deprecated with version 2.5, and removed thereafter. + Replaced by property `boundary_emissivities`. + """ + warnings.warn("Method 'set_boundary_emissivities' to be removed after " + "Cantera 2.5. Replaced by property " + "'boundary_emissivities'", DeprecationWarning) + self.flame.boundary_emissivities = e_left, e_right + + @property + def boundary_emissivities(self): + """ Set/get boundary emissivities. """ + return self.flame.boundary_emissivities + + @boundary_emissivities.setter + def boundary_emissivities(self, epsilon): + if len(epsilon) != 2: + raise ValueError("Boundary emissivities must both be set at the same time.") + self.flame.boundary_emissivities = epsilon[0], epsilon[1] @property def grid(self): diff --git a/interfaces/cython/cantera/onedim.pyx b/interfaces/cython/cantera/onedim.pyx index 86f81dea246..97de224169a 100644 --- a/interfaces/cython/cantera/onedim.pyx +++ b/interfaces/cython/cantera/onedim.pyx @@ -432,6 +432,8 @@ cdef class ReactingSurface1D(Boundary1D): """Controls whether or not to solve the surface coverage equations.""" def __set__(self, value): self.surf.enableCoverageEquations(value) + def __get__(self): + return self.surf.coverageEnabled() cdef class _FlowBase(Domain1D): @@ -578,10 +580,10 @@ cdef class _FlowBase(Domain1D): def __get__(self): return self.flow.leftEmissivity(), self.flow.rightEmissivity() 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.") + if len(epsilon) != 2: + raise ValueError('Setting the boundary emissivities requires a ' + 'tuple of length 2.') + self.flow.setBoundaryEmissivities(epsilon[0], epsilon[1]) property radiation_enabled: """ Determines whether or not to include radiative heat transfer """ diff --git a/interfaces/cython/cantera/reactionpath.pyx b/interfaces/cython/cantera/reactionpath.pyx index afd1929f8a4..d23f1789843 100644 --- a/interfaces/cython/cantera/reactionpath.pyx +++ b/interfaces/cython/cantera/reactionpath.pyx @@ -1,6 +1,8 @@ # This file is part of Cantera. See License.txt in the top-level directory or # at https://cantera.org/license.txt for license and copyright information. +from pathlib import Path + cdef class ReactionPathDiagram: def __cinit__(self, *args, **kwargs): self._log = new CxxStringStream() @@ -158,7 +160,7 @@ cdef class ReactionPathDiagram: Write the reaction path diagram formatted for use by Graphviz's 'dot' program to the file named *filename*. """ - open(filename, 'wb').write(self.get_dot().encode('utf-8')) + Path(filename).write_text(self.get_dot()) def get_data(self): """ diff --git a/interfaces/cython/cantera/test/test_composite.py b/interfaces/cython/cantera/test/test_composite.py index fa75296afef..9c001c0448a 100644 --- a/interfaces/cython/cantera/test/test_composite.py +++ b/interfaces/cython/cantera/test/test_composite.py @@ -156,9 +156,19 @@ def test_write_csv(self): b = ct.SolutionArray(self.gas) b.read_csv(outfile) - self.assertTrue(np.allclose(states.T, b.T)) - self.assertTrue(np.allclose(states.P, b.P)) - self.assertTrue(np.allclose(states.X, b.X)) + self.assertArrayNear(states.T, b.T) + self.assertArrayNear(states.P, b.P) + self.assertArrayNear(states.X, b.X) + + def test_write_csv_str_column(self): + states = ct.SolutionArray(self.gas, 3, extra={'spam': 'eggs'}) + + outfile = pjoin(self.test_work_dir, 'solutionarray.csv') + states.write_csv(outfile) + + b = ct.SolutionArray(self.gas, extra={'spam'}) + b.read_csv(outfile) + self.assertEqual(list(states.spam), list(b.spam)) @utilities.unittest.skipIf(isinstance(_pandas, ImportError), "pandas is not installed") def test_to_pandas(self): @@ -190,11 +200,11 @@ def test_write_hdf(self): b = ct.SolutionArray(self.gas) attr = b.read_hdf(outfile) - self.assertTrue(np.allclose(states.T, b.T)) - self.assertTrue(np.allclose(states.P, b.P)) - self.assertTrue(np.allclose(states.X, b.X)) - self.assertTrue(np.allclose(states.foo, b.foo)) - self.assertTrue(np.allclose(states.bar, b.bar)) + self.assertArrayNear(states.T, b.T) + self.assertArrayNear(states.P, b.P) + self.assertArrayNear(states.X, b.X) + self.assertArrayNear(states.foo, b.foo) + self.assertArrayNear(states.bar, b.bar) self.assertEqual(b.meta['spam'], 'eggs') self.assertEqual(b.meta['hello'], 'world') self.assertEqual(attr['foobar'], 'spam and eggs') @@ -217,7 +227,18 @@ def test_write_hdf(self): states.write_hdf(outfile, group='foo/bar/baz') c.read_hdf(outfile, group='foo/bar/baz') - self.assertTrue(np.allclose(states.T, c.T)) + self.assertArrayNear(states.T, c.T) + + def test_write_hdf_str_column(self): + states = ct.SolutionArray(self.gas, 3, extra={'spam': 'eggs'}) + + outfile = pjoin(self.test_work_dir, 'solutionarray.h5') + states.write_hdf(outfile, mode='w') + + b = ct.SolutionArray(self.gas, extra={'spam'}) + b.read_hdf(outfile) + self.assertEqual(list(states.spam), list(b.spam)) + class TestRestoreIdealGas(utilities.CanteraTest): """ Test restoring of the IdealGas class """ @@ -258,8 +279,8 @@ def check(a, b, atol=None): # skip concentrations b = ct.SolutionArray(self.gas) b.restore_data({'T': data['T'], 'density': data['density']}) - self.assertTrue(np.allclose(a.T, b.T)) - self.assertTrue(np.allclose(a.density, b.density)) + self.assertArrayNear(a.T, b.T) + self.assertArrayNear(a.density, b.density) self.assertFalse(np.allclose(a.X, b.X)) # wrong data shape @@ -328,7 +349,7 @@ def check(a, b, atol=None): b = ct.SolutionArray(self.gas) b.restore_data(data) check(a, b) - self.assertTrue(len(b._extra) == 0) + self.assertEqual(len(b._extra), 0) class TestRestorePureFluid(utilities.CanteraTest): diff --git a/interfaces/cython/cantera/test/test_purefluid.py b/interfaces/cython/cantera/test/test_purefluid.py index c9c39b84b7c..73501d2b27b 100644 --- a/interfaces/cython/cantera/test/test_purefluid.py +++ b/interfaces/cython/cantera/test/test_purefluid.py @@ -81,16 +81,11 @@ def test_set_Q(self): self.water.Q = 0.3 def test_X_deprecated(self): - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") + with self.assertWarnsRegex(DeprecationWarning, "after Cantera 2.5"): X = self.water.X + with self.assertWarnsRegex(DeprecationWarning, "after Cantera 2.5"): self.water.TX = 300, 1 - self.assertEqual(len(w), 2) - for warning in w: - self.assertTrue(issubclass(warning.category, DeprecationWarning)) - self.assertIn("after Cantera 2.5", str(warning.message)) - def test_set_minmax(self): self.water.TP = self.water.min_temp, 101325 self.assertNear(self.water.T, self.water.min_temp) diff --git a/interfaces/cython/cantera/test/test_reactor.py b/interfaces/cython/cantera/test/test_reactor.py index 8ec141d58c0..c0d6314e3dd 100644 --- a/interfaces/cython/cantera/test/test_reactor.py +++ b/interfaces/cython/cantera/test/test_reactor.py @@ -150,17 +150,9 @@ def test_timestepping(self): dt_max = 0.07 t = tStart - with warnings.catch_warnings(record=True) as w: - - # cause all warnings to always be triggered. - warnings.simplefilter("always") + with self.assertWarnsRegex(DeprecationWarning, "after Cantera 2.5"): self.net.set_max_time_step(dt_max) - self.assertEqual(len(w), 1) - self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) - self.assertIn("To be removed after Cantera 2.5. ", - str(w[-1].message)) - self.net.max_time_step = dt_max self.assertEqual(self.net.max_time_step, dt_max) self.net.set_initial_time(tStart) @@ -291,10 +283,10 @@ def integrate(limit_H2 = None, apply=True): n_advance_negative = integrate(-1.0) n_advance_override = integrate(.001, False) - self.assertTrue(n_advance_coarse > n_baseline) - self.assertTrue(n_advance_fine > n_advance_coarse) - self.assertTrue(n_advance_negative == n_baseline) - self.assertTrue(n_advance_override == n_baseline) + self.assertGreater(n_advance_coarse, n_baseline) + self.assertGreater(n_advance_fine, n_advance_coarse) + self.assertEqual(n_advance_negative, n_baseline) + self.assertEqual(n_advance_override, n_baseline) def test_heat_transfer1(self): # Connected reactors reach thermal equilibrium after some time @@ -623,28 +615,12 @@ def test_valve_deprecations(self): valve = ct.Valve(self.r1, self.r2) k = 2e-5 - with warnings.catch_warnings(record=True) as w: - - # cause all warnings to always be triggered. - warnings.simplefilter("always") + with self.assertWarnsRegex(DeprecationWarning, "after Cantera 2.5"): valve.set_valve_coeff(k) - self.assertTrue(len(w) == 1) - self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) - self.assertTrue("To be removed after Cantera 2.5. " - in str(w[-1].message)) - - with warnings.catch_warnings(record=True) as w: - - # cause all warnings to always be triggered. - warnings.simplefilter("always") + with self.assertWarnsRegex(DeprecationWarning, "after Cantera 2.5"): valve.set_valve_function(lambda t: t>.01) - self.assertTrue(len(w) == 1) - self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) - self.assertTrue("To be removed after Cantera 2.5. " - in str(w[-1].message)) - def test_valve_errors(self): self.make_reactors() res = ct.Reservoir() @@ -718,17 +694,9 @@ def test_pressure_controller_deprecations(self): p = ct.PressureController(self.r1, self.r2, master=mfc, K=0.5) - with warnings.catch_warnings(record=True) as w: - - # cause all warnings to always be triggered. - warnings.simplefilter("always") + with self.assertWarnsRegex(DeprecationWarning, "after Cantera 2.5"): p.set_pressure_coeff(2.) - self.assertTrue(len(w) == 1) - self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) - self.assertTrue("To be removed after Cantera 2.5. " - in str(w[-1].message)) - def test_pressure_controller_errors(self): self.make_reactors() res = ct.Reservoir(self.gas1) diff --git a/interfaces/cython/cantera/test/test_thermo.py b/interfaces/cython/cantera/test/test_thermo.py index 7e67c84ad7f..b8471842b83 100644 --- a/interfaces/cython/cantera/test/test_thermo.py +++ b/interfaces/cython/cantera/test/test_thermo.py @@ -423,31 +423,15 @@ def test_name(self): def test_phase(self): self.assertEqual(self.phase.name, 'ohmech') - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") + with self.assertWarnsRegex(DeprecationWarning, "after Cantera 2.5"): self.assertEqual(self.phase.ID, 'ohmech') - self.assertEqual(len(w), 1) - self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) - self.assertIn("To be removed after Cantera 2.5. ", - str(w[-1].message)) - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") + with self.assertWarnsRegex(DeprecationWarning, "after Cantera 2.5"): self.phase.ID = 'something' - self.assertEqual(self.phase.name, 'something') - self.assertEqual(len(w), 1) - self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) - self.assertIn("To be removed after Cantera 2.5. ", - str(w[-1].message)) - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") + self.assertEqual(self.phase.name, 'something') + + with self.assertWarnsRegex(FutureWarning, "Keyword 'name' replaces 'phaseid'"): gas = ct.Solution('h2o2.cti', phaseid='ohmech') - self.assertEqual(len(w), 1) - self.assertTrue(issubclass(w[-1].category, FutureWarning)) - self.assertIn("Keyword 'name' replaces 'phaseid'", - str(w[-1].message)) def test_badLength(self): X = np.zeros(5) @@ -1243,7 +1227,7 @@ def test_modify_thermo_invalid(self): def test_alias(self): self.gas.add_species_alias('H2', 'hydrogen') - self.assertTrue(self.gas.species_index('hydrogen') == 0) + self.assertEqual(self.gas.species_index('hydrogen'), 0) self.gas.X = 'hydrogen:.5, O2:.5' self.assertNear(self.gas.X[0], 0.5) with self.assertRaisesRegex(ct.CanteraError, 'Invalid alias'): @@ -1254,13 +1238,13 @@ def test_alias(self): def test_isomers(self): gas = ct.Solution('nDodecane_Reitz.yaml') iso = gas.find_isomers({'C':4, 'H':9, 'O':2}) - self.assertTrue(len(iso) == 2) + self.assertEqual(len(iso), 2) iso = gas.find_isomers('C:4, H:9, O:2') - self.assertTrue(len(iso) == 2) + self.assertEqual(len(iso), 2) iso = gas.find_isomers({'C':7, 'H':15}) - self.assertTrue(len(iso) == 1) + self.assertEqual(len(iso), 1) iso = gas.find_isomers({'C':7, 'H':16}) - self.assertTrue(len(iso) == 0) + self.assertEqual(len(iso), 0) class TestSpeciesThermo(utilities.CanteraTest): @@ -1306,7 +1290,7 @@ def test_coeffs(self): self.assertEqual(st.max_temp, 3500) self.assertEqual(st.reference_pressure, 101325) self.assertArrayNear(self.h2o_coeffs, st.coeffs) - self.assertTrue(st.n_coeffs == len(st.coeffs)) + self.assertEqual(st.n_coeffs, len(st.coeffs)) self.assertTrue(st._check_n_coeffs(st.n_coeffs)) def test_nasa9_load(self): @@ -1502,7 +1486,7 @@ def test_stringify_bad(self): def test_case_sensitive_names(self): gas = ct.Solution('h2o2.xml') self.assertFalse(gas.case_sensitive_species_names) - self.assertTrue(gas.species_index('h2') == 0) + self.assertEqual(gas.species_index('h2'), 0) gas.X = 'h2:.5, o2:.5' self.assertNear(gas.X[0], 0.5) gas.Y = 'h2:.5, o2:.5' @@ -1787,6 +1771,16 @@ def test_purefluid(self): states.TP = np.linspace(400, 500, 5), 101325 self.assertArrayNear(states.Q.squeeze(), np.ones(5)) + def test_phase_of_matter(self): + water = ct.Water() + states = ct.SolutionArray(water, 5) + T = [300, 500, water.critical_temperature*2, 300] + P = [101325, 101325, 101325, water.critical_pressure*2] + states[:4].TP = T, P + states[4].TQ = 300, .4 + pom = ['liquid', 'gas', 'supercritical', 'supercritical', 'liquid-gas-mix'] + self.assertEqual(list(states.phase_of_matter), pom) + def test_purefluid_getters(self): N = 11 water = ct.Water() @@ -1840,7 +1834,7 @@ def test_sort(self): states.sort('T') self.assertFalse((states.t[1:] - states.t[:-1] > 0).all()) self.assertTrue((states.T[1:] - states.T[:-1] > 0).all()) - self.assertTrue(np.allclose(states.P, P)) + self.assertArrayNear(states.P, P) states.sort('T', reverse=True) self.assertTrue((states.T[1:] - states.T[:-1] < 0).all()) diff --git a/interfaces/cython/setup.py.in b/interfaces/cython/setup.py.in index 59d63041604..85e9a01f748 100644 --- a/interfaces/cython/setup.py.in +++ b/interfaces/cython/setup.py.in @@ -60,7 +60,7 @@ setup( author="Raymond Speth", author_email="speth@mit.edu", url="https://cantera.org", - packages = [ + packages=[ 'cantera', 'cantera.data', 'cantera.test', @@ -92,12 +92,12 @@ setup( 'Programming Language :: Python :: 3.8', 'Topic :: Scientific/Engineering :: Chemistry', ], - package_data = { + package_data={ 'cantera.data': ['*.*', '*/*.*'], 'cantera.test.data': ['*.*', '*/*.*'], 'cantera.examples': ['*/*.*'], 'cantera': ["@py_extension@", '*.pxd'], }, zip_safe=False, - python_requires=">=3.5", + python_requires=">=@py_min_ver_str@", ) diff --git a/samples/cxx/combustor/output_0_blessed.txt b/samples/cxx/combustor/output_0_blessed.txt deleted file mode 100644 index e8884c1b698..00000000000 --- a/samples/cxx/combustor/output_0_blessed.txt +++ /dev/null @@ -1,6 +0,0 @@ -Adding reactor (none) -Initializing reactor network. -Reactor 0: 55 variables. - 0 sensitivity params. -Number of equations: 55 -Maximum time step: 6 diff --git a/samples/cxx/kinetics1/output_0_blessed.txt b/samples/cxx/kinetics1/output_0_blessed.txt deleted file mode 100644 index f8173091698..00000000000 --- a/samples/cxx/kinetics1/output_0_blessed.txt +++ /dev/null @@ -1,116 +0,0 @@ -Constant-pressure ignition of a hydrogen/oxygen/nitrogen mixture -beginning at T = 1001 K and P = 1 atm. -Adding reactor (none) -Initializing reactor network. -Reactor 0: 55 variables. - 0 sensitivity params. -Number of equations: 55 -Maximum time step: 1e-05 -time = 1e-05 s -time = 2e-05 s -time = 3e-05 s -time = 4e-05 s -time = 5e-05 s -time = 6e-05 s -time = 7e-05 s -time = 8e-05 s -time = 9e-05 s -time = 0.0001 s -time = 0.00011 s -time = 0.00012 s -time = 0.00013 s -time = 0.00014 s -time = 0.00015 s -time = 0.00016 s -time = 0.00017 s -time = 0.00018 s -time = 0.00019 s -time = 0.0002 s -time = 0.00021 s -time = 0.00022 s -time = 0.00023 s -time = 0.00024 s -time = 0.00025 s -time = 0.00026 s -time = 0.00027 s -time = 0.00028 s -time = 0.00029 s -time = 0.0003 s -time = 0.00031 s -time = 0.00032 s -time = 0.00033 s -time = 0.00034 s -time = 0.00035 s -time = 0.00036 s -time = 0.00037 s -time = 0.00038 s -time = 0.00039 s -time = 0.0004 s -time = 0.00041 s -time = 0.00042 s -time = 0.00043 s -time = 0.00044 s -time = 0.00045 s -time = 0.00046 s -time = 0.00047 s -time = 0.00048 s -time = 0.00049 s -time = 0.0005 s -time = 0.00051 s -time = 0.00052 s -time = 0.00053 s -time = 0.00054 s -time = 0.00055 s -time = 0.00056 s -time = 0.00057 s -time = 0.00058 s -time = 0.00059 s -time = 0.0006 s -time = 0.00061 s -time = 0.00062 s -time = 0.00063 s -time = 0.00064 s -time = 0.00065 s -time = 0.00066 s -time = 0.00067 s -time = 0.00068 s -time = 0.00069 s -time = 0.0007 s -time = 0.00071 s -time = 0.00072 s -time = 0.00073 s -time = 0.00074 s -time = 0.00075 s -time = 0.00076 s -time = 0.00077 s -time = 0.00078 s -time = 0.00079 s -time = 0.0008 s -time = 0.00081 s -time = 0.00082 s -time = 0.00083 s -time = 0.00084 s -time = 0.00085 s -time = 0.00086 s -time = 0.00087 s -time = 0.00088 s -time = 0.00089 s -time = 0.0009 s -time = 0.00091 s -time = 0.00092 s -time = 0.00093 s -time = 0.00094 s -time = 0.00095 s -time = 0.00096 s -time = 0.00097 s -time = 0.00098 s -time = 0.00099 s -time = 0.001 s - Tfinal = 2663.78 - time = 1.22 - number of residual function evaluations = 1929 - time per evaluation = 0.000632452 - -Output files: - kin1.csv (Excel CSV file) - kin1.dat (Tecplot data file) diff --git a/samples/f77/SConscript b/samples/f77/SConscript index 0cd943c15d3..eb589c89b83 100644 --- a/samples/f77/SConscript +++ b/samples/f77/SConscript @@ -10,7 +10,8 @@ samples = [('ctlib', ['ctlib.f']), ('isentropic', ['isentropic.f'])] ftn_demo = localenv.Object('demo_ftnlib.cpp', - CPPPATH=['#include', localenv['boost_inc_dir']]) + CPPPATH=['#include', localenv['boost_inc_dir'], + localenv['extra_inc_dirs']]) for program_name, fortran_sources in samples: buildSample(localenv.Program, program_name, fortran_sources + ftn_demo, diff --git a/src/base/AnyMap.cpp b/src/base/AnyMap.cpp index af4b215162e..541763e9795 100644 --- a/src/base/AnyMap.cpp +++ b/src/base/AnyMap.cpp @@ -190,7 +190,12 @@ struct convert { if (node.IsScalar()) { // Scalar nodes are int, doubles, or strings std::string nodestr = node.as(); - if (isInt(nodestr)) { + if (node.Tag() == "!") { + // Prevent quoted strings from being implicitly converted to + // numeric types, e.g. the quoted YAML string '12345' should not + // be interpreted as an integer + target = nodestr; + } else if (isInt(nodestr)) { try { target = node.as(); } catch (YAML::BadConversion&) { @@ -245,6 +250,9 @@ struct convert { } else if (node.IsMap()) { target = node.as(); return true; + } else if (node.IsNull()) { + target = Empty; + return true; } return false; } diff --git a/src/oneD/Boundary1D.cpp b/src/oneD/Boundary1D.cpp index fdb336cc311..170956a8851 100644 --- a/src/oneD/Boundary1D.cpp +++ b/src/oneD/Boundary1D.cpp @@ -679,12 +679,10 @@ void ReactingSurf1D::eval(size_t jg, double* xg, double* rg, size_t ioffset = m_kin->kineticsSpeciesIndex(0, m_surfindex); if (m_enabled) { - double maxx = -1.0; for (size_t k = 0; k < m_nsp; k++) { r[k] = m_work[k + ioffset] * m_sphase->size(k) * rs0; r[k] -= rdt*(x[k] - prevSoln(k,0)); diag[k] = 1; - maxx = std::max(x[k], maxx); } r[0] = 1.0 - sum; diag[0] = 0; @@ -707,9 +705,17 @@ void ReactingSurf1D::eval(size_t jg, double* xg, double* rg, double* xb = x - nc; rb[c_offset_T] = xb[c_offset_T] - m_temp; // specified T size_t nSkip = m_flow_left->rightExcessSpecies(); + size_t l_offset = 0; + ThermoPhase* left_thermo = &m_flow_left->phase(); + for (size_t nth = 0; nth < m_kin->nPhases(); nth++) { + if (&m_kin->thermo(nth) == left_thermo) { + l_offset = m_kin->kineticsSpeciesIndex(0, nth); + break; + } + } for (size_t nl = 0; nl < m_left_nsp; nl++) { if (nl != nSkip) { - rb[c_offset_Y+nl] += m_work[nl]*mwleft[nl]; + rb[c_offset_Y+nl] += m_work[nl + l_offset]*mwleft[nl]; } } } diff --git a/src/zeroD/Reactor.cpp b/src/zeroD/Reactor.cpp index 85fd70a3520..da3f985312b 100644 --- a/src/zeroD/Reactor.cpp +++ b/src/zeroD/Reactor.cpp @@ -296,9 +296,10 @@ double Reactor::evalSurfaces(double t, double* ydot) ydot[loc] = sum; loc += nk; + size_t bulkloc = kin->kineticsSpeciesIndex(m_thermo->speciesName(0)); double wallarea = S->area(); for (size_t k = 0; k < m_nsp; k++) { - m_sdot[k] += m_work[k]*wallarea; + m_sdot[k] += m_work[bulkloc + k] * wallarea; mdot_surf += m_sdot[k] * mw[k]; } } diff --git a/test/data/ch4_ion.yaml b/test/data/ch4_ion.yaml index 40f09a57862..eaadccf8499 100644 --- a/test/data/ch4_ion.yaml +++ b/test/data/ch4_ion.yaml @@ -1,102 +1,186 @@ +generator: cti2yaml +cantera-version: 2.5.0a4 +date: Mon, 10 Aug 2020 12:55:00 +0000 +input-files: [ch4_ion.cti] + units: {length: cm, quantity: mol, activation-energy: cal/mol} phases: - name: gas thermo: ideal-gas + elements: [O, H, C, N, E] + species: + - gri30.yaml/species: [H, O, OH, HO2, H2O2, C, CH, CH2, CH2(S), CH3, HCO, + CH2O, CH3O] + - species: [H2, O2, H2O, CH4, CO, CO2, N2, HCO+, H3O+, E, O2-] + skip-undeclared-third-bodies: true + kinetics: gas + reactions: + - gri30.yaml/reactions: declared-species + - reactions: declared-species transport: ionized-gas - species: [O2, H2O, CO2, N2, H3O+, E, O2-] - state: {T: 300, P: 1 atm} + state: + T: 300.0 + P: 1.01325e+05 species: +- name: H2 + composition: {H: 2} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [2.34433112, 7.98052075e-03, -1.9478151e-05, 2.01572094e-08, -7.37611761e-12, + -917.935173, 0.683010238] + - [3.3372792, -4.94024731e-05, 4.99456778e-07, -1.79566394e-10, 2.00255376e-14, + -950.158922, -3.20502331] + transport: + model: gas + geometry: linear + diameter: 2.92 + well-depth: 38.0 + polarizability: 0.455 + rotational-relaxation: 280.0 + note: TPIS78 - name: O2 composition: {O: 2} thermo: model: NASA7 - temperature-ranges: [200, 1000, 3500] + temperature-ranges: [200.0, 1000.0, 3500.0] data: - - [3.78245636E+00, -2.99673416E-03, 9.84730201E-06, -9.68129509E-09, - 3.24372837E-12, -1.06394356E+03, 3.65767573E+00] - - [3.28253784E+00, 1.48308754E-03, -7.57966669E-07, 2.09470555E-10, - -2.16717794E-14, -1.08845772E+03, 5.45323129E+00] + - [3.78245636, -2.99673416e-03, 9.84730201e-06, -9.68129509e-09, 3.24372837e-12, + -1063.94356, 3.65767573] + - [3.28253784, 1.48308754e-03, -7.57966669e-07, 2.09470555e-10, -2.16717794e-14, + -1088.45772, 5.45323129] transport: model: gas geometry: linear diameter: 3.46 - well-depth: 107.40 + well-depth: 107.4 polarizability: 1.131 - rotational-relaxation: 3.80 - note: TPIS89 - + rotational-relaxation: 3.8 + note: TPIS89 - name: H2O composition: {H: 2, O: 1} thermo: model: NASA7 - temperature-ranges: [200, 1000, 3500] + temperature-ranges: [200.0, 1000.0, 3500.0] data: - - [4.19864056E+00, -2.03643410E-03, 6.52040211E-06, -5.48797062E-09, - 1.77197817E-12, -3.02937267E+04, -8.49032208E-01] - - [3.03399249E+00, 2.17691804E-03, -1.64072518E-07, -9.70419870E-11, - 1.68200992E-14, -3.00042971E+04, 4.96677010E+00] + - [4.19864056, -2.0364341e-03, 6.52040211e-06, -5.48797062e-09, 1.77197817e-12, + -3.02937267e+04, -0.849032208] + - [3.03399249, 2.17691804e-03, -1.64072518e-07, -9.7041987e-11, 1.68200992e-14, + -3.00042971e+04, 4.9667701] transport: model: gas geometry: nonlinear - diameter: 2.60 - well-depth: 572.40 + diameter: 2.6 + well-depth: 572.4 dipole: 1.84 polarizability: 1.053 - rotational-relaxation: 4.00 - note: L 8/89 - + rotational-relaxation: 4.0 + note: L 8/89 +- name: CH4 + composition: {C: 1, H: 4} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [5.14987613, -0.0136709788, 4.91800599e-05, -4.84743026e-08, 1.66693956e-11, + -1.02466476e+04, -4.64130376] + - [0.074851495, 0.0133909467, -5.73285809e-06, 1.22292535e-09, -1.0181523e-13, + -9468.34459, 18.437318] + transport: + model: gas + geometry: nonlinear + diameter: 3.75 + well-depth: 141.4 + polarizability: 2.6 + rotational-relaxation: 13.0 + note: L 8/88 +- name: CO + composition: {C: 1, O: 1} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [3.57953347, -6.1035368e-04, 1.01681433e-06, 9.07005884e-10, -9.04424499e-13, + -1.4344086e+04, 3.50840928] + - [2.71518561, 2.06252743e-03, -9.98825771e-07, 2.30053008e-10, -2.03647716e-14, + -1.41518724e+04, 7.81868772] + transport: + model: gas + geometry: linear + diameter: 3.65 + well-depth: 98.1 + polarizability: 1.95 + rotational-relaxation: 1.8 + note: TPIS79 - name: CO2 composition: {C: 1, O: 2} thermo: model: NASA7 - temperature-ranges: [200, 1000, 3500] + temperature-ranges: [200.0, 1000.0, 3500.0] data: - - [2.35677352E+00, 8.98459677E-03, -7.12356269E-06, 2.45919022E-09, - -1.43699548E-13, -4.83719697E+04, 9.90105222E+00] - - [3.85746029E+00, 4.41437026E-03, -2.21481404E-06, 5.23490188E-10, - -4.72084164E-14, -4.87591660E+04, 2.27163806E+00] + - [2.35677352, 8.98459677e-03, -7.12356269e-06, 2.45919022e-09, -1.43699548e-13, + -4.83719697e+04, 9.90105222] + - [3.85746029, 4.41437026e-03, -2.21481404e-06, 5.23490188e-10, -4.72084164e-14, + -4.8759166e+04, 2.27163806] transport: model: gas geometry: linear diameter: 3.76 - well-depth: 244.00 + well-depth: 244.0 polarizability: 2.65 - rotational-relaxation: 2.10 - note: L 7/88 - + rotational-relaxation: 2.1 + note: L 7/88 - name: N2 composition: {N: 2} thermo: model: NASA7 - temperature-ranges: [300, 1000, 5000] + temperature-ranges: [300.0, 1000.0, 5000.0] data: - - [3.29867700E+00, 1.40824040E-03, -3.96322200E-06, 5.64151500E-09, - -2.44485400E-12, -1.02089990E+03, 3.95037200E+00] - - [2.92664000E+00, 1.48797680E-03, -5.68476000E-07, 1.00970380E-10, - -6.75335100E-15, -9.22797700E+02, 5.98052800E+00] + - [3.298677, 1.4082404e-03, -3.963222e-06, 5.641515e-09, -2.444854e-12, + -1020.8999, 3.950372] + - [2.92664, 1.4879768e-03, -5.68476e-07, 1.0097038e-10, -6.753351e-15, + -922.7977, 5.980528] transport: model: gas geometry: linear diameter: 3.62 well-depth: 97.53 polarizability: 1.76 - rotational-relaxation: 4.00 + rotational-relaxation: 4.0 dispersion-coefficient: 2.995 quadrupole-polarizability: 3.602 - note: 121286 - + note: '121286' +- name: HCO+ + composition: {H: 1, C: 1, O: 1, E: -1} + thermo: + model: NASA7 + temperature-ranges: [300.0, 1000.0, 5000.0] + data: + - [2.4739736, 8.671559e-03, -1.00315e-05, 6.7170527e-09, -1.7872674e-12, + 9.9146608e+04, 8.17571187] + - [3.741188, 3.3441517e-03, -1.2397121e-06, 2.1189388e-10, -1.370415e-14, + 9.8884078e+04, 2.07861357] + transport: + model: gas + geometry: linear + diameter: 3.59 + well-depth: 498.0 + polarizability: 1.356 + dispersion-coefficient: 0.416 + note: J12/70 - name: H3O+ composition: {H: 3, O: 1, E: -1} thermo: model: NASA7 - temperature-ranges: [298.15, 1000, 6000] + temperature-ranges: [298.15, 1000.0, 6000.0] data: - - [3.79295270E+00, -9.10854000E-04, 1.16363549E-05, -1.21364887E-08, - 4.26159663E-12, 7.07512401E+04, 1.47156856E+00] - - [2.49647716E+00, 5.72844920E-03, -1.83953281E-06, 2.73577439E-10, - -1.54093985E-14, 7.09729113E+04, 7.45850779E+00] + - [3.7929527, -9.10854e-04, 1.16363549e-05, -1.21364887e-08, 4.26159663e-12, + 7.07512401e+04, 1.47156856] + - [2.49647716, 5.7284492e-03, -1.83953281e-06, 2.73577439e-10, -1.54093985e-14, + 7.09729113e+04, 7.45850779] transport: model: gas geometry: nonlinear @@ -104,39 +188,50 @@ species: well-depth: 106.2 dipole: 1.417 polarizability: 0.897 - rotational-relaxation: 0.0 - note: TPIS89 - + note: TPIS89 - name: O2- composition: {E: 1, O: 2} thermo: model: NASA7 - temperature-ranges: [298.15, 1000.00, 6000] + temperature-ranges: [298.15, 1000.0, 6000.0] data: - - [3.66442522E+00, -9.28741138E-04, 6.45477082E-06, -7.7470338E-09, - 2.93332662E-12, -6.87076983E+03, 4.35140681E+00] - - [3.95666294E+00, 5.98141823E-04, -2.12133905E-07, 3.63267581E-11, - -2.24989228E-15, -7.06287229E+03, 2.27871017E+00] + - [3.66442522, -9.28741138e-04, 6.45477082e-06, -7.7470338e-09, 2.93332662e-12, + -6870.76983, 4.35140681] + - [3.95666294, 5.98141823e-04, -2.12133905e-07, 3.63267581e-11, -2.24989228e-15, + -7062.87229, 2.27871017] transport: model: gas geometry: linear diameter: 3.33 well-depth: 136.5 polarizability: 1.424 - note: L4/89 - + note: L4/89 - name: E composition: {E: 1} thermo: model: NASA7 - temperature-ranges: [200, 3000] + temperature-ranges: [200.0, 1000.0, 6000.0] data: - - [2.5E+00, 0.0, 0.0, 0.0, 0.0, -7.45375000E+02, -1.17246902E+01] + - [2.5, 0.0, 0.0, 0.0, 0.0, -745.375, -11.7246902] + - [2.5, 0.0, 0.0, 0.0, 0.0, -745.375, -11.7246902] transport: model: gas geometry: atom diameter: 2.05 well-depth: 145.0 polarizability: 0.667 - rotational-relaxation: 0.0 - note: gas L10/92 + note: gas L10/92 + +reactions: +- equation: CH + O => HCO+ + E # Reaction 1 + rate-constant: {A: 2.51e+11, b: 0.0, Ea: 1700} +- equation: HCO+ + H2O => H3O+ + CO # Reaction 2 + rate-constant: {A: 1.51e+15, b: 0.0, Ea: 0.0} +- equation: H3O+ + E => H2O + H # Reaction 3 + rate-constant: {A: 2.29e+18, b: -0.5, Ea: 0.0} +- equation: H3O+ + E => OH + H + H # Reaction 4 + rate-constant: {A: 7.95e+21, b: -1.4, Ea: 0.0} +- equation: H3O+ + E => H2 + OH # Reaction 5 + rate-constant: {A: 1.25e+19, b: -0.5, Ea: 0.0} +- equation: H3O+ + E => O + H2 + H # Reaction 6 + rate-constant: {A: 6.0e+17, b: -0.3, Ea: 0.0} diff --git a/test/general/test_containers.cpp b/test/general/test_containers.cpp index 5b47ab6b5fe..ec95d9b15df 100644 --- a/test/general/test_containers.cpp +++ b/test/general/test_containers.cpp @@ -40,6 +40,19 @@ TEST(AnyValue, getMapWhere_initial_list) EXPECT_EQ(m["data"].getMapWhere("a", "baz")["x"].asInt(), 4); } +TEST(AnyValue, numeric_yaml_string) +{ + AnyMap m = AnyMap::fromYamlString( + "data: {a: '12345', b: '123.45', 'c': 12345, 'd': 123.45}"); + + EXPECT_THROW(m["data"]["a"].asInt(), CanteraError); + EXPECT_EQ(m["data"]["a"].asString(), "12345"); + EXPECT_THROW(m["data"]["b"].asDouble(), CanteraError); + EXPECT_EQ(m["data"]["b"].asString(), "123.45"); + EXPECT_EQ(m["data"]["c"].asInt(), 12345); + EXPECT_EQ(m["data"]["d"].asDouble(), 123.45); +} + TEST(AnyValue, getMapWhere_initial_map) { AnyMap m = AnyMap::fromYamlString( @@ -292,6 +305,23 @@ TEST(AnyMap, iterators) EXPECT_TRUE(std::find(keys.begin(), keys.end(), "bar") != keys.end()); } +TEST(AnyMap, null_values) +{ + AnyMap m = AnyMap::fromYamlString( + "{a: 1, b: ~, c: , d: 5}" + ); + EXPECT_EQ(m.size(), (size_t) 4); + EXPECT_TRUE(m.at("c").is()); + + try { + m.at("b").asString(); + FAIL(); + } catch (CanteraError& err) { + EXPECT_THAT(err.what(), + testing::HasSubstr("Key 'b' not found or contains no value")); + } +} + TEST(AnyMap, loadYaml) { AnyMap m = AnyMap::fromYamlString(