diff --git a/include/cantera/base/Solution.h b/include/cantera/base/Solution.h new file mode 100644 index 0000000000..bf886a95dc --- /dev/null +++ b/include/cantera/base/Solution.h @@ -0,0 +1,70 @@ +//! @file Solution.h + +// 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. + +#ifndef CT_SOLUTION_H +#define CT_SOLUTION_H + +#include "cantera/base/ctexceptions.h" + +namespace Cantera +{ + +class ThermoPhase; +class Kinetics; +class Transport; + +//! A container class holding managers for all pieces defining a phase +class Solution : public std::enable_shared_from_this +{ +private: + Solution(); + +public: + ~Solution() {} + Solution(const Solution&) = delete; + Solution& operator=(const Solution&) = delete; + + static shared_ptr create() { + return shared_ptr( new Solution ); + } + + //! Return the name of this Solution object + std::string name() const; + + //! Set the name of this Solution object + void setName(const std::string& name); + + //! Set the ThermoPhase object + void setThermoPhase(shared_ptr thermo); + + //! Set the Kinetics object + void setKinetics(shared_ptr kinetics); + + //! Set the Transport object + void setTransport(shared_ptr transport); + + //! Accessor for the ThermoPhase object + ThermoPhase& thermo() { + return *m_thermo; + } + + //! Accessor for the Kinetics object + Kinetics& kinetics() { + return *m_kinetics; + } + + //! Accessor for the Transport object + Transport& transport() { + return *m_transport; + } + +protected: + shared_ptr m_thermo; //!< ThermoPhase manager + shared_ptr m_kinetics; //!< Kinetics manager + shared_ptr m_transport; //!< Transport manager +}; + +} +#endif diff --git a/include/cantera/kinetics/Kinetics.h b/include/cantera/kinetics/Kinetics.h index 17b232d4b8..57b2853954 100644 --- a/include/cantera/kinetics/Kinetics.h +++ b/include/cantera/kinetics/Kinetics.h @@ -19,6 +19,8 @@ namespace Cantera { +class Solution; + /** * @defgroup chemkinetics Chemical Kinetics */ @@ -814,6 +816,11 @@ class Kinetics void selectPhase(const doublereal* data, const thermo_t* phase, doublereal* phase_data); + //! Set root Solution holding all phase information + virtual void setRoot(std::shared_ptr root) { + m_root = root; + } + protected: //! Cache for saved calculations within each Kinetics object. ValueCache m_cache; @@ -935,6 +942,9 @@ class Kinetics //! @see skipUndeclaredThirdBodies() bool m_skipUndeclaredThirdBodies; + + //! reference to Solution + std::weak_ptr m_root; }; } diff --git a/include/cantera/thermo/Phase.h b/include/cantera/thermo/Phase.h index a82e69d784..48635dc4c0 100644 --- a/include/cantera/thermo/Phase.h +++ b/include/cantera/thermo/Phase.h @@ -29,6 +29,8 @@ namespace Cantera * support thermodynamic calculations (see \ref thermoprops). */ +class Solution; + //! Class Phase is the base class for phases of matter, managing the species and //! elements in a phase, as well as the independent variables of temperature, //! mass density, species mass/mole fraction, and other generalized forces and @@ -66,15 +68,11 @@ namespace Cantera * operate on a state vector, which is in general of length (2 + nSpecies()). * The first two entries of the state vector are temperature and density. * - * A species name may be referred to via three methods: - * - * - "speciesName" - * - "PhaseId:speciesName" - * - "phaseName:speciesName" - * . - * - * The first two methods of naming may not yield a unique species within - * complicated assemblies of %Cantera Phases. + * A species name is referred to via speciesName(), which is unique within a + * given phase. Note that within multiphase mixtures (MultiPhase()), both a + * phase name/index as well as species name are required to access information + * about a species in a particular phase. For surfaces, the species names are + * unique among the phases. * * @todo * - Make the concept of saving state vectors more general, so that it can @@ -122,30 +120,31 @@ class Phase */ void setXMLdata(XML_Node& xmlPhase); - /*! @name Name and ID - * Class Phase contains two strings that identify a phase. The ID is the - * value of the ID attribute of the XML phase node that is used to - * initialize a phase when it is read. The name field is also initialized - * to the value of the ID attribute of the XML phase node. + /*! @name Name + * Class Phase uses the string name to identify a phase. The name is the + * value of the corresponding key in the phase map (in YAML), name (in + * CTI), or id (in XML) that is used to initialize a phase when it is read. * * However, the name field may be changed to another value during the - * course of a calculation. For example, if a phase is located in two - * places, but has the same constitutive input, the IDs of the two phases - * will be the same, but the names of the two phases may be different. - * - * It is an error to have two phases in a single problem with the same name - * and ID (or the name from one phase being the same as the id of - * another phase). Thus, it is expected that there is a 1-1 correspondence - * between names and unique phases within a Cantera problem. + * course of a calculation. For example, if duplicates of a phase object + * are instantiated and used in multiple places (e.g. a ReactorNet), they + * will have the same constitutive input, i.e. the names of the phases will + * be the same. Note that this is not a problem for Cantera internally; + * however, a user may want to rename phase objects in order to clarify. */ //!@{ //! Return the string id for the phase. + /*! + * @deprecated To be removed after Cantera 2.5. + */ std::string id() const; //! Set the string id for the phase. /*! * @param id String id of the phase + * + * @deprecated To be removed after Cantera 2.5. */ void setID(const std::string& id); @@ -758,6 +757,11 @@ class Phase m_caseSensitiveSpecies = cflag; } + //! Set root Solution holding all phase information + virtual void setRoot(std::shared_ptr root) { + m_root = root; + } + protected: //! Cached for saved calculations within each ThermoPhase. /*! @@ -870,6 +874,9 @@ class Phase //! Entropy at 298.15 K and 1 bar of stable state pure elements (J kmol-1) vector_fp m_entropy298; + + //! reference to Solution + std::weak_ptr m_root; }; } diff --git a/include/cantera/transport/TransportBase.h b/include/cantera/transport/TransportBase.h index 9447fd6770..664d47d2a4 100644 --- a/include/cantera/transport/TransportBase.h +++ b/include/cantera/transport/TransportBase.h @@ -74,6 +74,8 @@ const VelocityBasis VB_SPECIES_2 = 2; const VelocityBasis VB_SPECIES_3 = 3; //@} +class Solution; + //! Base class for transport property managers. /*! * All classes that compute transport properties for a single phase derive from @@ -654,6 +656,11 @@ class Transport */ virtual void setThermo(thermo_t& thermo); + //! Set root Solution holding all phase information + virtual void setRoot(std::shared_ptr root) { + m_root = root; + } + protected: //! Enable the transport object for use. /*! @@ -680,6 +687,9 @@ class Transport //! Velocity basis from which diffusion velocities are computed. //! Defaults to the mass averaged basis = -2 int m_velocityBasis; + + //! reference to Solution + std::weak_ptr m_root; }; } diff --git a/interfaces/cython/cantera/_cantera.pxd b/interfaces/cython/cantera/_cantera.pxd index a37104633f..02a1fb1213 100644 --- a/interfaces/cython/cantera/_cantera.pxd +++ b/interfaces/cython/cantera/_cantera.pxd @@ -120,6 +120,19 @@ cdef extern from "cantera/thermo/Species.h" namespace "Cantera": cdef shared_ptr[CxxSpecies] CxxNewSpecies "newSpecies" (CxxAnyMap&) except +translate_exception cdef vector[shared_ptr[CxxSpecies]] CxxGetSpecies "getSpecies" (CxxAnyValue&) except +translate_exception + +cdef extern from "cantera/base/Solution.h" namespace "Cantera": + cdef cppclass CxxSolution "Cantera::Solution": + CxxSolution() + string name() + void setName(string) + void setThermoPhase(shared_ptr[CxxThermoPhase]) + void setKinetics(shared_ptr[CxxKinetics]) + void setTransport(shared_ptr[CxxTransport]) + + cdef shared_ptr[CxxSolution] CxxNewSolution "Cantera::Solution::create" () + + cdef extern from "cantera/thermo/ThermoPhase.h" namespace "Cantera": cdef cppclass CxxThermoPhase "Cantera::ThermoPhase": CxxThermoPhase() @@ -127,10 +140,6 @@ cdef extern from "cantera/thermo/ThermoPhase.h" namespace "Cantera": # miscellaneous string type() string report(cbool, double) except +translate_exception - string name() - void setName(string) - string id() - void setID(string) double minTemp() except +translate_exception double maxTemp() except +translate_exception double refPressure() except +translate_exception @@ -928,6 +937,8 @@ cdef class GasTransportData: cdef _assign(self, shared_ptr[CxxTransportData] other) cdef class _SolutionBase: + cdef shared_ptr[CxxSolution] _base + cdef CxxSolution* base cdef shared_ptr[CxxThermoPhase] _thermo cdef CxxThermoPhase* thermo cdef shared_ptr[CxxKinetics] _kinetics diff --git a/interfaces/cython/cantera/base.pyx b/interfaces/cython/cantera/base.pyx index f2284c1af9..6444a4c56d 100644 --- a/interfaces/cython/cantera/base.pyx +++ b/interfaces/cython/cantera/base.pyx @@ -1,18 +1,40 @@ # 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 collections import defaultdict as _defaultdict + cdef class _SolutionBase: - def __cinit__(self, infile='', phaseid='', phases=(), origin=None, + def __cinit__(self, infile='', name='', adjacent=(), origin=None, source=None, yaml=None, thermo=None, species=(), kinetics=None, reactions=(), **kwargs): + + if 'phaseid' in kwargs: + if name is not '': + raise AttributeError('duplicate specification of phase name') + + warnings.warn("Keyword 'name' replaces 'phaseid'", + FutureWarning) + name = kwargs['phaseid'] + + if 'phases' in kwargs: + if len(adjacent)>0: + raise AttributeError( + 'duplicate specification of adjacent phases') + + warnings.warn("Keyword 'adjacent' replaces 'phases'", + FutureWarning) + adjacent = kwargs['phases'] + # Shallow copy of an existing Solution (for slicing support) cdef _SolutionBase other if origin is not None: other = <_SolutionBase?>origin + self.base = other.base self.thermo = other.thermo self.kinetics = other.kinetics self.transport = other.transport + self._base = other._base self._thermo = other._thermo self._kinetics = other._kinetics self._transport = other._transport @@ -21,17 +43,26 @@ cdef class _SolutionBase: self._selected_species = other._selected_species.copy() return + # Assign base and set managers to NULL + self._base = CxxNewSolution() + self.base = self._base.get() + self.thermo = NULL + self.kinetics = NULL + self.transport = NULL + + # Parse inputs if infile.endswith('.yml') or infile.endswith('.yaml') or yaml: - self._init_yaml(infile, phaseid, phases, yaml) + self._init_yaml(infile, name, adjacent, yaml) elif infile or source: - self._init_cti_xml(infile, phaseid, phases, source) + self._init_cti_xml(infile, name, adjacent, source) elif thermo and species: - self._init_parts(thermo, species, kinetics, phases, reactions) + self._init_parts(thermo, species, kinetics, adjacent, reactions) else: raise ValueError("Arguments are insufficient to define a phase") # Initialization of transport is deferred to Transport.__init__ - self.transport = NULL + self.base.setThermoPhase(self._thermo) + self.base.setKinetics(self._kinetics) self._selected_species = np.ndarray(0, dtype=np.integer) @@ -39,7 +70,37 @@ cdef class _SolutionBase: if isinstance(self, Transport): assert self.transport is not NULL - def _init_yaml(self, infile, phaseid, phases, source): + name = kwargs.get('name') + if name is not None: + self.name = name + + property name: + """ + The name assigned to this object. The default value corresponds + to the CTI/XML/YAML input file phase entry. + """ + def __get__(self): + return pystr(self.base.name()) + + def __set__(self, name): + self.base.setName(stringify(name)) + + property composite: + """ + Returns tuple of thermo/kinetics/transport models associated with + this SolutionBase object. + """ + def __get__(self): + thermo = None if self.thermo == NULL \ + else pystr(self.thermo.type()) + kinetics = None if self.kinetics == NULL \ + else pystr(self.kinetics.kineticsType()) + transport = None if self.transport == NULL \ + else pystr(self.transport.transportType()) + + return thermo, kinetics, transport + + def _init_yaml(self, infile, name, adjacent, source): """ Instantiate a set of new Cantera C++ objects from a YAML phase definition @@ -51,7 +112,7 @@ cdef class _SolutionBase: root = AnyMapFromYamlString(stringify(source)) phaseNode = root[stringify("phases")].getMapWhere(stringify("name"), - stringify(phaseid)) + stringify(name)) # Thermo if isinstance(self, ThermoPhase): @@ -66,7 +127,7 @@ cdef class _SolutionBase: if isinstance(self, Kinetics): v.push_back(self.thermo) - for phase in phases: + for phase in adjacent: # adjacent bulk phases for a surface phase v.push_back(phase.thermo) self._kinetics = newKinetics(v, phaseNode, root) @@ -74,7 +135,7 @@ cdef class _SolutionBase: else: self.kinetics = NULL - def _init_cti_xml(self, infile, phaseid, phases, source): + def _init_cti_xml(self, infile, name, adjacent, source): """ Instantiate a set of new Cantera C++ objects from a CTI or XML phase definition @@ -86,8 +147,8 @@ cdef class _SolutionBase: # Get XML data cdef XML_Node* phaseNode - if phaseid: - phaseNode = rootNode.findID(stringify(phaseid)) + if name: + phaseNode = rootNode.findID(stringify(name)) else: phaseNode = rootNode.findByName(stringify('phase')) if phaseNode is NULL: @@ -106,7 +167,7 @@ cdef class _SolutionBase: if isinstance(self, Kinetics): v.push_back(self.thermo) - for phase in phases: + for phase in adjacent: # adjacent bulk phases for a surface phase v.push_back(phase.thermo) self.kinetics = newKineticsMgr(deref(phaseNode), v) @@ -114,7 +175,7 @@ cdef class _SolutionBase: else: self.kinetics = NULL - def _init_parts(self, thermo, species, kinetics, phases, reactions): + def _init_parts(self, thermo, species, kinetics, adjacent, reactions): """ Instantiate a set of new Cantera C++ objects based on a string defining the model type and a list of Species objects. @@ -136,7 +197,8 @@ cdef class _SolutionBase: self.kinetics = CxxNewKinetics(stringify(kinetics)) self._kinetics.reset(self.kinetics) self.kinetics.addPhase(deref(self.thermo)) - for phase in phases: + for phase in adjacent: + # adjacent bulk phases for a surface phase self.kinetics.addPhase(deref(phase.thermo)) self.kinetics.init() self.kinetics.skipUndeclaredThirdBodies(True) diff --git a/interfaces/cython/cantera/ck2yaml.py b/interfaces/cython/cantera/ck2yaml.py index 43bef62a1f..39df856bfd 100644 --- a/interfaces/cython/cantera/ck2yaml.py +++ b/interfaces/cython/cantera/ck2yaml.py @@ -12,7 +12,7 @@ [--thermo=] [--transport=] [--surface=] - [--id=] + [--name=] [--output=] [--permissive] [-d | --debug] @@ -32,7 +32,8 @@ 'surface'. The '--permissive' option allows certain recoverable parsing errors (e.g. -duplicate transport data) to be ignored. +duplicate transport data) to be ignored. The '--name=' option +is used to override default phase names (i.e. 'gas'). """ from collections import defaultdict, OrderedDict @@ -1797,7 +1798,7 @@ def parse_transport_data(self, lines, filename, line_offset): if speciesName in self.species_dict: if len(data) != 7: raise InputError('Unable to parse line {} of {}:\n"""\n{}"""\n' - '6 transport parameters expected, but found {}.', + '6 transport parameters expected, but found {}.', line_offset + i, filename, original_line, len(data)-1) if self.species_dict[speciesName].transport is None: @@ -2024,9 +2025,9 @@ def convert_mech(input_file, thermo_file=None, transport_file=None, surface_file def main(argv): - longOptions = ['input=', 'thermo=', 'transport=', 'surface=', 'id=', + longOptions = ['input=', 'thermo=', 'transport=', 'surface=', 'name=', 'output=', 'permissive', 'help', 'debug', 'quiet', - 'no-validate'] + 'no-validate', 'id='] try: optlist, args = getopt.getopt(argv, 'dh', longOptions) @@ -2054,7 +2055,13 @@ def main(argv): quiet = '--quiet' in options transport_file = options.get('--transport') surface_file = options.get('--surface') - phase_name = options.get('--id', 'gas') + + if '--id' in options: + phase_name = options.get('--id', 'gas') + logging.warning("\nFutureWarning: " + "option '--id=...' is superseded by '--name=...'") + else: + phase_name = options.get('--name', 'gas') if not input_file and not thermo_file: print('At least one of the arguments "--input=..." or "--thermo=..."' diff --git a/interfaces/cython/cantera/composite.py b/interfaces/cython/cantera/composite.py index 196dc48e79..dcae86607f 100644 --- a/interfaces/cython/cantera/composite.py +++ b/interfaces/cython/cantera/composite.py @@ -23,28 +23,42 @@ class Solution(ThermoPhase, Kinetics, Transport): The most common way to instantiate `Solution` objects is by using a phase definition, species and reactions defined in an input file:: - gas = ct.Solution('gri30.cti') + gas = ct.Solution('gri30.yaml') - If an input file defines multiple phases, the phase *name* (in CTI) or *id* - (in XML) can be used to specify the desired phase:: + If an input file defines multiple phases, the corresponding key in the + *phases* map (in YAML), *name* (in CTI), or *id* (in XML) can be used + to specify the desired phase via the ``name`` keyword argument of + the constructor:: - gas = ct.Solution('diamond.cti', 'gas') - diamond = ct.Solution('diamond.cti', 'diamond') + gas = ct.Solution('diamond.yaml', name='gas') + diamond = ct.Solution('diamond.yaml', name='diamond') + + The name of the `Solution` object defaults to the *phase* identifier + specified in the input file. Upon initialization of a 'Solution' object, + a custom name can assigned via:: + + gas.name = 'my_custom_name' `Solution` objects can also be constructed using `Species` and `Reaction` objects which can themselves either be imported from input files or defined directly in Python:: - spec = ct.Species.listFromFile('gri30.cti') - rxns = ct.Reaction.listFromFile('gri30.cti') + spec = ct.Species.listFromFile('gri30.yaml') + rxns = ct.Reaction.listFromFile('gri30.yaml') gas = ct.Solution(thermo='IdealGas', kinetics='GasKinetics', - species=spec, reactions=rxns) + species=spec, reactions=rxns, name='my_custom_name') where the ``thermo`` and ``kinetics`` keyword arguments are strings specifying the thermodynamic and kinetics model, respectively, and ``species`` and ``reactions`` keyword arguments are lists of `Species` and `Reaction` objects, respectively. + Types of underlying models that form the composite `Solution` object are + queried using the ``thermo_model``, ``kinetics_model`` and + ``transport_model`` attributes; further, the ``composite`` attribute is a + shorthand returning a tuple containing the types of the three contitutive + models. + For non-trivial uses cases of this functionality, see the examples `extract_submechanism.py `_ and `mechanism_reduction.py `_. @@ -57,7 +71,8 @@ class Solution(ThermoPhase, Kinetics, Transport): ideal_gas(name='gas', elements='O H Ar', species='gri30: all', reactions='gri30: all', - options=['skip_undeclared_elements', 'skip_undeclared_species', 'skip_undeclared_third_bodies'], + options=['skip_undeclared_elements', 'skip_undeclared_species', + 'skip_undeclared_third_bodies'], initial_state=state(temperature=300, pressure=101325))''' gas = ct.Solution(source=cti_def) """ @@ -74,11 +89,12 @@ class Interface(InterfacePhase, InterfaceKinetics): To construct an `Interface` object, adjacent bulk phases which participate in reactions need to be created and then passed in as a list in the - ``phases`` argument to the constructor:: + ``adjacent`` argument to the constructor:: - gas = ct.Solution('diamond.cti', 'gas') - diamond = ct.Solution('diamond.cti', 'diamond') - diamond_surf = ct.Interface('diamond.cti', 'diamond_100', [gas, diamond]) + gas = ct.Solution('diamond.yaml', name='gas') + diamond = ct.Solution('diamond.yaml', name='diamond') + diamond_surf = ct.Interface('diamond.yaml', name='diamond_100', + adjacent=[gas, diamond]) """ __slots__ = ('_phase_indices',) @@ -89,7 +105,6 @@ class DustyGas(ThermoPhase, Kinetics, DustyGasTransport): The only transport properties computed are the multicomponent diffusion coefficients. The model does not compute viscosity or thermal conductivity. - """ __slots__ = () @@ -297,7 +312,7 @@ class SolutionArray: with shapes described in the same way as Numpy arrays. All of the states can be set in a single call:: - >>> gas = ct.Solution('gri30.cti') + >>> gas = ct.Solution('gri30.yaml') >>> states = ct.SolutionArray(gas, (6, 10)) >>> T = np.linspace(300, 1000, 10) # row vector >>> P = ct.one_atm * np.linspace(0.1, 5.0, 6)[:,np.newaxis] # column vector diff --git a/interfaces/cython/cantera/kinetics.pyx b/interfaces/cython/cantera/kinetics.pyx index 44ae9281f0..05ddf9fdff 100644 --- a/interfaces/cython/cantera/kinetics.pyx +++ b/interfaces/cython/cantera/kinetics.pyx @@ -26,6 +26,13 @@ cdef class Kinetics(_SolutionBase): a reaction mechanism. """ + property kinetics_model: + """ + Return type of kinetics. + """ + def __get__(self): + return pystr(self.kinetics.kineticsType()) + property n_total_species: """ Total number of species in all phases participating in the kinetics @@ -364,13 +371,13 @@ cdef class InterfaceKinetics(Kinetics): A kinetics manager for heterogeneous reaction mechanisms. The reactions are assumed to occur at an interface between bulk phases. """ - def __init__(self, infile='', phaseid='', phases=(), *args, **kwargs): - super().__init__(infile, phaseid, phases, *args, **kwargs) + def __init__(self, infile='', name='', adjacent=(), *args, **kwargs): + super().__init__(infile, name, adjacent, *args, **kwargs) if pystr(self.kinetics.kineticsType()) not in ("Surf", "Edge"): raise TypeError("Underlying Kinetics class is not of the correct type.") self._phase_indices = {} - for phase in [self] + list(phases): + for phase in [self] + list(adjacent): i = self.kinetics.phaseIndex(stringify(phase.name)) self._phase_indices[phase] = i self._phase_indices[phase.name] = i diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 1d46402323..0d5c7707e3 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -571,8 +571,8 @@ def setUpClass(cls): def checkConversion(self, basename, cls=ct.Solution, ctiphases=(), yamlphases=(), **kwargs): - ctiPhase = cls(basename + '.cti', phases=ctiphases, **kwargs) - yamlPhase = cls(basename + '.yaml', phases=yamlphases, **kwargs) + ctiPhase = cls(basename + '.cti', adjacent=ctiphases, **kwargs) + yamlPhase = cls(basename + '.yaml', adjacent=yamlphases, **kwargs) self.assertEqual(ctiPhase.element_names, yamlPhase.element_names) self.assertEqual(ctiPhase.species_names, yamlPhase.species_names) @@ -660,7 +660,7 @@ def test_ptcombust(self): Path(self.test_work_dir).joinpath('ptcombust.yaml')) ctiGas, yamlGas = self.checkConversion('ptcombust') ctiSurf, yamlSurf = self.checkConversion('ptcombust', ct.Interface, - phaseid='Pt_surf', ctiphases=[ctiGas], yamlphases=[yamlGas]) + name='Pt_surf', ctiphases=[ctiGas], yamlphases=[yamlGas]) self.checkKinetics(ctiGas, yamlGas, [500, 1200], [1e4, 3e5]) self.checkThermo(ctiSurf, yamlSurf, [400, 800, 1600]) @@ -670,16 +670,16 @@ def test_sofc(self): cti2yaml.convert(Path(self.cantera_data).joinpath('sofc.cti'), Path(self.test_work_dir).joinpath('sofc.yaml')) ctiGas, yamlGas = self.checkConversion('sofc') - ctiMetal, yamlMetal = self.checkConversion('sofc', phaseid='metal') - ctiOxide, yamlOxide = self.checkConversion('sofc', phaseid='oxide_bulk') + ctiMetal, yamlMetal = self.checkConversion('sofc', name='metal') + ctiOxide, yamlOxide = self.checkConversion('sofc', name='oxide_bulk') ctiMSurf, yamlMSurf = self.checkConversion('sofc', ct.Interface, - phaseid='metal_surface', ctiphases=[ctiGas, ctiMetal], + name='metal_surface', ctiphases=[ctiGas, ctiMetal], yamlphases=[yamlGas, yamlMetal]) ctiOSurf, yamlOSurf = self.checkConversion('sofc', ct.Interface, - phaseid='oxide_surface', ctiphases=[ctiGas, ctiOxide], + name='oxide_surface', ctiphases=[ctiGas, ctiOxide], yamlphases=[yamlGas, yamlOxide]) cti_tpb, yaml_tpb = self.checkConversion('sofc', ct.Interface, - phaseid='tpb', ctiphases=[ctiMetal, ctiMSurf, ctiOSurf], + name='tpb', ctiphases=[ctiMetal, ctiMSurf, ctiOSurf], yamlphases=[yamlMetal, yamlMSurf, yamlOSurf]) self.checkThermo(ctiMSurf, yamlMSurf, [900, 1000, 1100]) @@ -694,7 +694,7 @@ def test_liquidvapor(self): Path(self.test_work_dir).joinpath('liquidvapor.yaml')) for name in ['water', 'nitrogen', 'methane', 'hydrogen', 'oxygen', 'hfc134a', 'carbondioxide', 'heptane']: - ctiPhase, yamlPhase = self.checkConversion('liquidvapor', phaseid=name) + ctiPhase, yamlPhase = self.checkConversion('liquidvapor', name=name) self.checkThermo(ctiPhase, yamlPhase, [1.3 * ctiPhase.min_temp, 0.7 * ctiPhase.max_temp]) @@ -717,10 +717,10 @@ def test_Redlich_Kwong_ndodecane(self): def test_diamond(self): cti2yaml.convert(Path(self.cantera_data).joinpath('diamond.cti'), Path(self.test_work_dir).joinpath('diamond.yaml')) - ctiGas, yamlGas = self.checkConversion('diamond', phaseid='gas') - ctiSolid, yamlSolid = self.checkConversion('diamond', phaseid='diamond') + ctiGas, yamlGas = self.checkConversion('diamond', name='gas') + ctiSolid, yamlSolid = self.checkConversion('diamond', name='diamond') ctiSurf, yamlSurf = self.checkConversion('diamond', - ct.Interface, phaseid='diamond_100', ctiphases=[ctiGas, ctiSolid], + ct.Interface, name='diamond_100', ctiphases=[ctiGas, ctiSolid], yamlphases=[yamlGas, yamlSolid]) self.checkThermo(ctiSolid, yamlSolid, [300, 500]) self.checkThermo(ctiSurf, yamlSurf, [330, 490]) @@ -730,16 +730,16 @@ def test_lithium_ion_battery(self): cti2yaml.convert(Path(self.cantera_data).joinpath('lithium_ion_battery.cti'), Path(self.test_work_dir).joinpath('lithium_ion_battery.yaml')) name = 'lithium_ion_battery' - ctiAnode, yamlAnode = self.checkConversion(name, phaseid='anode') - ctiCathode, yamlCathode = self.checkConversion(name, phaseid='cathode') - ctiMetal, yamlMetal = self.checkConversion(name, phaseid='electron') - ctiElyt, yamlElyt = self.checkConversion(name, phaseid='electrolyte') + ctiAnode, yamlAnode = self.checkConversion(name, name='anode') + ctiCathode, yamlCathode = self.checkConversion(name, name='cathode') + ctiMetal, yamlMetal = self.checkConversion(name, name='electron') + ctiElyt, yamlElyt = self.checkConversion(name, name='electrolyte') ctiAnodeInt, yamlAnodeInt = self.checkConversion(name, - phaseid='edge_anode_electrolyte', + name='edge_anode_electrolyte', ctiphases=[ctiAnode, ctiMetal, ctiElyt], yamlphases=[yamlAnode, yamlMetal, yamlElyt]) ctiCathodeInt, yamlCathodeInt = self.checkConversion(name, - phaseid='edge_cathode_electrolyte', + name='edge_cathode_electrolyte', ctiphases=[ctiCathode, ctiMetal, ctiElyt], yamlphases=[yamlCathode, yamlMetal, yamlElyt]) diff --git a/interfaces/cython/cantera/test/test_kinetics.py b/interfaces/cython/cantera/test/test_kinetics.py index 4638566458..58d0637eda 100644 --- a/interfaces/cython/cantera/test/test_kinetics.py +++ b/interfaces/cython/cantera/test/test_kinetics.py @@ -185,7 +185,7 @@ def test_surface(self): surf2 = ct.Interface(thermo='Surface', kinetics='interface', species=surf_species, reactions=reactions, - phases=[gas]) + adjacent=[gas]) surf1.site_density = surf2.site_density = 5e-9 gas.TP = surf2.TP = surf1.TP = 900, 2*ct.one_atm surf2.concentrations = surf1.concentrations @@ -1032,7 +1032,7 @@ def test_interface(self): self.assertNear(r1.coverage_deps['H(S)'][2], -6e6) surf2 = ct.Interface(thermo='Surface', species=surf_species, - kinetics='interface', reactions=[r1], phases=[gas]) + kinetics='interface', reactions=[r1], adjacent=[gas]) surf2.site_density = surf1.site_density surf1.coverages = surf2.coverages = 'PT(S):0.7, H(S):0.3' diff --git a/interfaces/cython/cantera/test/test_reactor.py b/interfaces/cython/cantera/test/test_reactor.py index 9e788cb5bb..71cc77634e 100644 --- a/interfaces/cython/cantera/test/test_reactor.py +++ b/interfaces/cython/cantera/test/test_reactor.py @@ -929,9 +929,9 @@ def create_reactors(self, add_Q=False, add_mdot=False, add_surf=False): self.gas.TPX = 900, 25*ct.one_atm, 'CO:0.5, H2O:0.2' self.gas1 = ct.Solution('gri30.xml') - self.gas1.ID = 'gas' + self.gas1.name = 'gas' self.gas2 = ct.Solution('gri30.xml') - self.gas2.ID = 'gas' + self.gas2.name = 'gas' resGas = ct.Solution('gri30.xml') solid = ct.Solution('diamond.xml', 'diamond') diff --git a/interfaces/cython/cantera/test/test_thermo.py b/interfaces/cython/cantera/test/test_thermo.py index 8ce4d846e6..ff20e3cdc8 100644 --- a/interfaces/cython/cantera/test/test_thermo.py +++ b/interfaces/cython/cantera/test/test_thermo.py @@ -7,11 +7,29 @@ import cantera as ct from . import utilities +import warnings + class TestThermoPhase(utilities.CanteraTest): def setUp(self): self.phase = ct.Solution('h2o2.xml') + def test_base_attributes(self): + self.assertIsInstance(self.phase.name, str) + self.assertIsInstance(self.phase.thermo_model, str) + self.assertIsInstance(self.phase.kinetics_model, str) + self.assertIsInstance(self.phase.transport_model, str) + self.assertIsInstance(self.phase.composite, tuple) + self.assertEqual(len(self.phase.composite), 3) + self.assertEqual(self.phase.composite, + (self.phase.thermo_model, + self.phase.kinetics_model, + self.phase.transport_model)) + self.phase.name = 'spam' + self.assertEqual(self.phase.name, 'spam') + with self.assertRaises(AttributeError): + self.phase.type = 'eggs' + def test_phases(self): self.assertEqual(self.phase.n_phases, 1) @@ -299,18 +317,35 @@ def test_default_report(self): self.assertNotIn(name, report) def test_name(self): - self.assertEqual(self.phase.name, 'ohmech') - self.phase.name = 'something' self.assertEqual(self.phase.name, 'something') self.assertIn('something', self.phase.report()) - def test_ID(self): - self.assertEqual(self.phase.ID, 'ohmech') - - self.phase.ID = 'something' - self.assertEqual(self.phase.ID, 'something') + def test_phase(self): self.assertEqual(self.phase.name, 'ohmech') + warnings.simplefilter("always") + + with warnings.catch_warnings(record=True) as w: + 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: + 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: + 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) @@ -844,8 +879,8 @@ class ImportTest(utilities.CanteraTest): """ Test the various ways of creating a Solution object """ - def check(self, gas, name, T, P, nSpec, nElem): - self.assertEqual(gas.name, name) + def check(self, gas, phase, T, P, nSpec, nElem): + self.assertEqual(gas.name, phase) self.assertNear(gas.T, T) self.assertNear(gas.P, P) self.assertEqual(gas.n_species, nSpec) @@ -1134,7 +1169,7 @@ def test_invalid(self): def test_wrap(self): st = self.gas.species('H2O').thermo - self.assertTrue(isinstance(st, ct.NasaPoly2)) + self.assertIsInstance(st, ct.NasaPoly2) for T in [300, 500, 900, 1200, 2000]: self.gas.TP = T, 101325 diff --git a/interfaces/cython/cantera/thermo.pyx b/interfaces/cython/cantera/thermo.pyx index 4980b69ecc..3d6260dcff 100644 --- a/interfaces/cython/cantera/thermo.pyx +++ b/interfaces/cython/cantera/thermo.pyx @@ -268,6 +268,13 @@ cdef class ThermoPhase(_SolutionBase): self.thermo_basis = mass_basis self._references = weakref.WeakKeyDictionary() + property thermo_model: + """ + Return thermodynamic model describing phase. + """ + def __get__(self): + return pystr(self.thermo.type()) + def report(self, show_thermo=True, float threshold=1e-14): """ Generate a report describing the thermodynamic state of this phase. To @@ -282,25 +289,24 @@ cdef class ThermoPhase(_SolutionBase): def __call__(self, *args, **kwargs): print(self.report(*args, **kwargs)) - property name: - """ - The name assigned to this phase. The default is taken from the CTI/XML - input file. - """ - def __get__(self): - return pystr(self.thermo.name()) - def __set__(self, name): - self.thermo.setName(stringify(name)) - property ID: """ - The ID of the phase. The default is taken from the CTI/XML input file. + The identifier of the object. The default value corresponds to the + CTI/XML/YAML input file phase entry. + + .. deprecated:: 2.5 + + To be deprecated with version 2.5, and removed thereafter. + Usage merged with `name`. """ def __get__(self): - return pystr(self.thermo.id()) + warnings.warn("To be removed after Cantera 2.5. " + "Use 'name' attribute instead", DeprecationWarning) + return pystr(self.base.name()) def __set__(self, id_): - self.thermo.setID(stringify(id_)) - + warnings.warn("To be removed after Cantera 2.5. " + "Use 'name' attribute instead", DeprecationWarning) + self.base.setName(stringify(id_)) property basis: """ diff --git a/interfaces/cython/cantera/transport.pyx b/interfaces/cython/cantera/transport.pyx index b6235b7dee..b9b0bb24e8 100644 --- a/interfaces/cython/cantera/transport.pyx +++ b/interfaces/cython/cantera/transport.pyx @@ -144,7 +144,9 @@ cdef class Transport(_SolutionBase): model = 'None' self.transport = newTransportMgr(stringify(model), self.thermo) self._transport.reset(self.transport) + super().__init__(*args, **kwargs) + self.base.setTransport(self._transport) property transport_model: """ diff --git a/src/base/Solution.cpp b/src/base/Solution.cpp new file mode 100644 index 0000000000..85e0f1877c --- /dev/null +++ b/src/base/Solution.cpp @@ -0,0 +1,58 @@ +/** + * @file Solution.cpp + * Definition file for class Solution. + */ + +// 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. + +#include "cantera/base/Solution.h" +#include "cantera/thermo/ThermoPhase.h" +#include "cantera/kinetics/Kinetics.h" +#include "cantera/transport/TransportBase.h" + +namespace Cantera +{ + +Solution::Solution() {} + +std::string Solution::name() const { + if (m_thermo) { + return m_thermo->name(); + } else { + throw CanteraError("Solution::name()", + "Requires associated 'ThermoPhase'"); + } +} + +void Solution::setName(const std::string& name) { + if (m_thermo) { + m_thermo->setName(name); + } else { + throw CanteraError("Solution::setName()", + "Requires associated 'ThermoPhase'"); + } +} + +void Solution::setThermoPhase(shared_ptr thermo) { + m_thermo = thermo; + if (m_thermo) { + m_thermo->setRoot(shared_from_this()); + } +} + +void Solution::setKinetics(shared_ptr kinetics) { + m_kinetics = kinetics; + if (m_kinetics) { + m_kinetics->setRoot(shared_from_this()); + } +} + +void Solution::setTransport(shared_ptr transport) { + m_transport = transport; + if (m_transport) { + m_transport->setRoot(shared_from_this()); + } +} + +} // namespace Cantera diff --git a/src/kinetics/Kinetics.cpp b/src/kinetics/Kinetics.cpp index 300110b57e..570f592f98 100644 --- a/src/kinetics/Kinetics.cpp +++ b/src/kinetics/Kinetics.cpp @@ -306,7 +306,7 @@ size_t Kinetics::kineticsSpeciesIndex(const std::string& nm, } for (size_t n = 0; n < m_thermo.size(); n++) { - string id = thermo(n).id(); + string id = thermo(n).name(); if (ph == id) { size_t k = thermo(n).speciesIndex(nm); if (k == npos) { @@ -451,7 +451,7 @@ void Kinetics::addPhase(thermo_t& thermo) m_rxnphase = nPhases(); } m_thermo.push_back(&thermo); - m_phaseindex[m_thermo.back()->id()] = nPhases(); + m_phaseindex[m_thermo.back()->name()] = nPhases(); resizeSpecies(); } @@ -608,7 +608,7 @@ shared_ptr Kinetics::reaction(size_t i) checkReactionIndex(i); return m_reactions[i]; } - + shared_ptr Kinetics::reaction(size_t i) const { checkReactionIndex(i); diff --git a/src/kinetics/importKinetics.cpp b/src/kinetics/importKinetics.cpp index 7d05355ff7..296eb6f35d 100644 --- a/src/kinetics/importKinetics.cpp +++ b/src/kinetics/importKinetics.cpp @@ -168,7 +168,7 @@ bool importKinetics(const XML_Node& phase, std::vector th, // loop over the supplied 'ThermoPhase' objects representing // phases, to find an object with the same id. for (size_t m = 0; m < th.size(); m++) { - if (th[m]->id() == phase_id) { + if (th[m]->name() == phase_id) { phase_ok = true; // if no phase with this id has been added to //the kinetics manager yet, then add this one @@ -176,7 +176,7 @@ bool importKinetics(const XML_Node& phase, std::vector th, k->addPhase(*th[m]); } } - msg += " "+th[m]->id(); + msg += " "+th[m]->name(); } if (!phase_ok) { throw CanteraError("importKinetics", @@ -254,7 +254,7 @@ bool checkElectrochemReaction(const XML_Node& p, Kinetics& kin, const XML_Node& size_t k = ph.speciesIndex(sp.first); double stoich = sp.second; for (size_t m = 0; m < phase_ids.size(); m++) { - if (phase_ids[m] == ph.id()) { + if (phase_ids[m] == ph.name()) { e_counter[m] += stoich * ph.charge(k); break; } @@ -267,7 +267,7 @@ bool checkElectrochemReaction(const XML_Node& p, Kinetics& kin, const XML_Node& size_t k = ph.speciesIndex(sp.first); double stoich = sp.second; for (size_t m = 0; m < phase_ids.size(); m++) { - if (phase_ids[m] == ph.id()) { + if (phase_ids[m] == ph.name()) { e_counter[m] -= stoich * ph.charge(k); break; } diff --git a/src/thermo/FixedChemPotSSTP.cpp b/src/thermo/FixedChemPotSSTP.cpp index a94a0e1056..62105fdf90 100644 --- a/src/thermo/FixedChemPotSSTP.cpp +++ b/src/thermo/FixedChemPotSSTP.cpp @@ -40,7 +40,6 @@ FixedChemPotSSTP::FixedChemPotSSTP(const std::string& Ename, doublereal val) : chemPot_(0.0) { std::string pname = Ename + "Fixed"; - setID(pname); setName(pname); setNDim(3); addElement(Ename); diff --git a/src/thermo/LatticeSolidPhase.cpp b/src/thermo/LatticeSolidPhase.cpp index 02293b85ba..a3ce57767f 100644 --- a/src/thermo/LatticeSolidPhase.cpp +++ b/src/thermo/LatticeSolidPhase.cpp @@ -347,7 +347,7 @@ void LatticeSolidPhase::setLatticeStoichiometry(const compositionMap& comp) } // Add in the lattice stoichiometry constraint for (size_t i = 1; i < m_lattice.size(); i++) { - string econ = fmt::format("LC_{}_{}", i, id()); + string econ = fmt::format("LC_{}_{}", i, name()); size_t m = addElement(econ, 0.0, 0, 0.0, CT_ELEM_TYPE_LATTICERATIO); size_t mm = nElements(); for (size_t k = 0; k < m_lattice[0]->nSpecies(); k++) { diff --git a/src/thermo/Phase.cpp b/src/thermo/Phase.cpp index 85798d5ec8..2c18552e38 100644 --- a/src/thermo/Phase.cpp +++ b/src/thermo/Phase.cpp @@ -68,12 +68,17 @@ void Phase::setXMLdata(XML_Node& xmlPhase) std::string Phase::id() const { + warn_deprecated("Phase::id", + "To be removed after Cantera 2.5. Usage merged with 'Phase::name'"); return m_id; } void Phase::setID(const std::string& id_) { + warn_deprecated("Phase::setID", + "To be removed after Cantera 2.5. Usage merged with 'Phase::setName'"); m_id = id_; + m_name = id_; } std::string Phase::name() const @@ -81,9 +86,10 @@ std::string Phase::name() const return m_name; } -void Phase::setName(const std::string& nm) +void Phase::setName(const std::string& name) { - m_name = nm; + m_name = name; + m_id = name; } size_t Phase::nElements() const @@ -206,6 +212,9 @@ size_t Phase::speciesIndex(const std::string& nameStr) const std::string pn; std::string sn = parseSpeciesName(nameStr, pn); if (pn == "" || pn == m_name || pn == m_id) { + warn_deprecated("Phase::speciesIndex()", + "Retrieval of species indices via 'PhaseId:speciesName' or " + "'phaseName:speciesName' to be removed after Cantera 2.5."); it = m_speciesIndices.find(nameStr); if (it != m_speciesIndices.end()) { return it->second; diff --git a/src/thermo/PureFluidPhase.cpp b/src/thermo/PureFluidPhase.cpp index 2382ac5119..5b77323500 100644 --- a/src/thermo/PureFluidPhase.cpp +++ b/src/thermo/PureFluidPhase.cpp @@ -64,7 +64,7 @@ void PureFluidPhase::initThermo() m_sub->setStdState(h0_RT*GasConstant*298.15/m_mw, s_R*GasConstant/m_mw, T0, p); debuglog("PureFluidPhase::initThermo: initialized phase " - +id()+"\n", m_verbose); + +name()+"\n", m_verbose); } void PureFluidPhase::setParametersFromXML(const XML_Node& eosdata) diff --git a/src/thermo/ThermoFactory.cpp b/src/thermo/ThermoFactory.cpp index 5d4c96f339..b152c55570 100644 --- a/src/thermo/ThermoFactory.cpp +++ b/src/thermo/ThermoFactory.cpp @@ -250,7 +250,6 @@ void importPhase(XML_Node& phase, ThermoPhase* th) th->setXMLdata(phase); // set the id attribute of the phase to the 'id' attribute in the XML tree. - th->setID(phase.id()); th->setName(phase.id()); // Number of spatial dimensions. Defaults to 3 (bulk phase) @@ -258,7 +257,7 @@ void importPhase(XML_Node& phase, ThermoPhase* th) int idim = intValue(phase["dim"]); if (idim < 1 || idim > 3) { throw CanteraError("importPhase", - "phase, " + th->id() + + "phase, " + th->name() + ", has unphysical number of dimensions: " + phase["dim"]); } th->setNDim(idim); @@ -274,7 +273,7 @@ void importPhase(XML_Node& phase, ThermoPhase* th) th->setParametersFromXML(eos); } else { throw CanteraError("importPhase", - " phase, " + th->id() + + " phase, " + th->name() + ", XML_Node does not have a \"thermo\" XML_Node"); } @@ -284,7 +283,7 @@ void importPhase(XML_Node& phase, ThermoPhase* th) vpss_ptr = dynamic_cast (th); if (vpss_ptr == 0) { throw CanteraError("importPhase", - "phase, " + th->id() + ", was VPSS, but dynamic cast failed"); + "phase, " + th->name() + ", was VPSS, but dynamic cast failed"); } } @@ -300,7 +299,7 @@ void importPhase(XML_Node& phase, ThermoPhase* th) vector sparrays = phase.getChildren("speciesArray"); if (ssConvention != cSS_CONVENTION_SLAVE && sparrays.empty()) { throw CanteraError("importPhase", - "phase, " + th->id() + ", has zero \"speciesArray\" XML nodes.\n" + "phase, " + th->name() + ", has zero \"speciesArray\" XML nodes.\n" + " There must be at least one speciesArray nodes " "with one or more species"); }