From ed4b3a910d1f64c9a8346f8b99faae63232cf3ae Mon Sep 17 00:00:00 2001 From: Bryan Weber Date: Sun, 23 Apr 2023 10:47:34 -0600 Subject: [PATCH] Address a bunch of review comments --- doc/sphinx/conf.py | 36 +-------- doc/sphinx/cython/units.rst | 9 ++- .../cython/cantera/with_units/solution.py.in | 73 ++++++------------- samples/python/thermo/isentropic_units.py | 12 +-- samples/python/thermo/sound_speed_units.py | 5 +- 5 files changed, 43 insertions(+), 92 deletions(-) diff --git a/doc/sphinx/conf.py b/doc/sphinx/conf.py index 0186b1c1f..cf1625b3a 100644 --- a/doc/sphinx/conf.py +++ b/doc/sphinx/conf.py @@ -84,48 +84,18 @@ def setup(app): lines[i] = l.replace("*", r"\*") app.connect('autodoc-process-docstring', escape_splats) - # NAMES = [] - # DIRECTIVES = {} - - # def get_rst(app, what, name, obj, options, signature, - # return_annotation): - # if "with_units" not in name: - # return - # doc_indent = ' ' - # directive_indent = '' - # if what in ['method', 'attribute']: - # doc_indent += ' ' - # directive_indent += ' ' - # directive = '%s.. py:%s:: %s' % (directive_indent, what, name) - # if signature: # modules, attributes, ... don't have a signature - # directive += signature - # NAMES.append(name) - # rst = directive + '\n\n' + doc_indent + obj.__doc__ + '\n' - # DIRECTIVES[name] = rst - - # def write_new_docs(app, exception): - # txt = ['My module documentation'] - # txt.append('-----------------------\n') - # for name in NAMES: - # txt.append(DIRECTIVES[name]) - # # print('\n'.join(txt)) - # with open('../doc_new/generated.rst', 'w') as outfile: - # outfile.write('\n'.join(txt)) - - # app.connect('autodoc-process-signature', get_rst) - # app.connect('build-finished', write_new_docs) - autoclass_content = 'both' doxylink = { - 'ct': (os.path.abspath('../../build/docs/Cantera.tag'), - '../../doxygen/html/') + 'ct': (os.path.abspath('../../build/docs/Cantera.tag'), + '../../doxygen/html/') } intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), 'pandas': ('https://pandas.pydata.org/pandas-docs/stable/', None), 'numpy': ('https://numpy.org/doc/stable/', None), + 'pint': ('https://pint.readthedocs.io/en/stable/', None), } # Ensure that the primary domain is the Python domain, since we've added the diff --git a/doc/sphinx/cython/units.rst b/doc/sphinx/cython/units.rst index 8c8b28d28..ae70390fa 100644 --- a/doc/sphinx/cython/units.rst +++ b/doc/sphinx/cython/units.rst @@ -3,10 +3,17 @@ Python Interface With Units =========================== +This interface allows users to specify physical units associated with quantities. +To do so, this interface leverages the `pint `__ +library to provide consistent unit conversion. + +Solution with Units +------------------- + .. autoclass:: Solution PureFluid Phases With Units -=========================== +--------------------------- The following convenience classes are available to create `PureFluid ` objects with the indicated equation of state: diff --git a/interfaces/cython/cantera/with_units/solution.py.in b/interfaces/cython/cantera/with_units/solution.py.in index b8d391eb9..d6aa2993c 100644 --- a/interfaces/cython/cantera/with_units/solution.py.in +++ b/interfaces/cython/cantera/with_units/solution.py.in @@ -17,6 +17,12 @@ Q_ = units.Quantity def copy_doc(method): + """Decorator to copy docstrings from related methods in upstream classes. + + This decorator will copy the docstring from the same named method in the upstream + class, either `Solution` or `PureFluid`. The docstring in the method being + decorated is appended to the upstream documentation. + """ doc = getattr(method, "__doc__", None) or "" if isinstance(method, property): method = method.fget @@ -32,10 +38,6 @@ def copy_doc(method): class Solution: """ - A class for chemically-reacting solutions. Instances can be created to - represent any type of solution -- a mixture of gases, a liquid solution, or - a solid solution, for example. - This implementation of `Solution ` operates with units by using the `pint` library to convert between unit systems. All properties are assigned units in the standard MKS system that Cantera uses, substituting kmol @@ -50,51 +52,19 @@ class Solution: See the `pint documentation `__ for more information about using pint's ``Quantity`` classes. - 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.yaml') - - If an input file defines multiple phases, the corresponding key in the - ``phases`` map can be used to specify the desired phase via the ``name`` keyword - argument of the constructor:: - - 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' - - In addition, `Solution` objects can be constructed by passing the text of - the YAML phase definition in directly, using the ``yaml`` keyword - argument:: - - yaml_def = ''' - phases: - - name: gas - thermo: ideal-gas - kinetics: gas - elements: [O, H, Ar] - species: - - gri30.yaml/species: all - reactions: - - gri30.yaml/reactions: declared-species - skip-undeclared-elements: true - skip-undeclared-third-bodies: true - state: {T: 300, P: 1 atm} - ''' - gas = ct.Solution(yaml=yaml_def) + **Note:** This class is experimental. It only implements methods from `ThermoPhase`. + Methods from other classes are not yet supported. If you are interested in contributing + to this feature, please chime in on our enhancements issue: + ``__. """ def __init__(self, infile="", name="", *, yaml=None): self.__dict__["_phase"] = _Solution(infile, name, yaml=yaml) - @common_properties@ +@common_properties@ - @thermophase_properties@ +@thermophase_properties@ +Solution.__doc__ = f"{Solution.__doc__}\n{_Solution.__doc__}" class PureFluid: """ @@ -113,9 +83,9 @@ class PureFluid: about using pint's ``Quantity`` classes. """ def __init__(self, infile, name="", *, yaml=None, **kwargs): - self.__dict__["_phase"] = _PureFluid(infile, name, **kwargs) + self.__dict__["_phase"] = _PureFluid(infile, name, yaml=yaml, **kwargs) - @common_properties@ +@common_properties@ @property @copy_doc @@ -135,12 +105,12 @@ class PureFluid: f"Value {value!r} must be an instance of a pint.Quantity class" ) from None else: - raise # pragma: no cover + raise else: Q = self.Q.magnitude self._phase.Q = Q - @thermophase_properties@ +@thermophase_properties@ @property @copy_doc @@ -150,7 +120,6 @@ class PureFluid: @TPQ.setter def TPQ(self, value): - msg = "Value {value!r} must be an instance of a pint.Quantity class" T = value[0] if value[0] is not None else self.T P = value[1] if value[1] is not None else self.P Q = value[2] if value[2] is not None else self.Q @@ -159,12 +128,14 @@ class PureFluid: val.ito(unit) except AttributeError as e: if "'ito'" in str(e): - raise CanteraError(msg.format(value=val)) from None + raise CanteraError( + f"Value {val!r} must be an instance of a pint.Quantity class" + ) from None else: - raise # pragma: no cover + raise self._phase.TPQ = T.magnitude, P.magnitude, Q.magnitude - @purefluid_properties@ +@purefluid_properties@ PureFluid.__doc__ = f"{PureFluid.__doc__}\n{_PureFluid.__doc__}" diff --git a/samples/python/thermo/isentropic_units.py b/samples/python/thermo/isentropic_units.py index d0f3523d2..a5a1a4f65 100644 --- a/samples/python/thermo/isentropic_units.py +++ b/samples/python/thermo/isentropic_units.py @@ -1,5 +1,7 @@ """ -Isentropic, adiabatic flow example - calculate area ratio vs. Mach number curve +Isentropic, adiabatic flow example - calculate area ratio vs. Mach number curve. +Uses the pint library to include customized units in the calculation. + Requires: Cantera >= 3.0.0, pint Keywords: thermodynamics, compressible flow, units @@ -10,7 +12,7 @@ import numpy as np # This sets the default output format of the units to have 2 significant digits # and the units are printed with a Unicode font. See: -# https://pint.readthedocs.io/en/stable/formatting.html#unit-format-types +# https://pint.readthedocs.io/en/stable/user/formatting.html ctu.units.default_format = ".2F~P" @@ -18,8 +20,8 @@ def soundspeed(gas): """The speed of sound. Assumes an ideal gas.""" gamma = gas.cp / gas.cv - return np.sqrt(gamma * ctu.units.molar_gas_constant - * gas.T / gas.mean_molecular_weight).to("m/s") + specific_gas_constant = ctu.units.molar_gas_constant / gas.mean_molecular_weight + return np.sqrt(gamma * specific_gas_constant * gas.T).to("m/s") def isentropic(gas=None): @@ -45,7 +47,7 @@ def isentropic(gas=None): data = [] # compute values for a range of pressure ratios - p_range = np.logspace(-3, 0, 200) * p0 + p_range = np.logspace(-3, 0, 10) * p0 for p in p_range: # set the state using (p,s0) diff --git a/samples/python/thermo/sound_speed_units.py b/samples/python/thermo/sound_speed_units.py index aedf3bac5..7500ae6a3 100644 --- a/samples/python/thermo/sound_speed_units.py +++ b/samples/python/thermo/sound_speed_units.py @@ -1,5 +1,6 @@ """ -Compute the "equilibrium" and "frozen" sound speeds for a gas +Compute the "equilibrium" and "frozen" sound speeds for a gas. Uses the pint library to +include customized units in the calculation. Requires: Cantera >= 3.0.0, pint Keywords: thermodynamics, equilibrium, units @@ -57,7 +58,7 @@ def equilibrium_sound_speeds(gas, rtol=1.0e-6, max_iter=5000): if __name__ == "__main__": gas = ctu.Solution('gri30.yaml') gas.X = 'CH4:1.00, O2:2.0, N2:7.52' - T_range = np.linspace(80.33, 4760.33, 50) * ctu.units.degF + T_range = np.linspace(80, 4880, 25) * ctu.units.degF print("Temperature Equilibrium Sound Speed Frozen Sound Speed Frozen Sound Speed Check") for T in T_range: gas.TP = T, 1.0 * ctu.units.atm