Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose C++ Units to Python #1076

Merged
merged 22 commits into from
Sep 16, 2021
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c2f96bc
[Units] Shorten Units::str output
ischoegl Jul 17, 2021
3ba2bb1
[Python] Expose rate coefficient and standard concentration units
ischoegl Jul 17, 2021
4f9a80d
[Units] Ensure factor is not rounded in Units::str
ischoegl Jul 18, 2021
670ad7d
[Python] Implement Python API for Units class
ischoegl Jul 18, 2021
957899a
[Units] Enable detection of factor when constructing from string
ischoegl Jul 18, 2021
289dad1
[Units] Only display units in Units::str
ischoegl Jul 18, 2021
1099410
[Units] Prevent invalid temperature and current scales
ischoegl Jul 18, 2021
5b90f5f
[Units] Implement UnitSystem::defaults()
ischoegl Jul 18, 2021
3440502
[Units] Expose Units::factor to Python
ischoegl Jul 19, 2021
664acdb
[Units] Fix activation energy units in UnitSystem::defaults
ischoegl Jul 19, 2021
be8ce65
[Python] Create API for UnitSystem
ischoegl Jul 18, 2021
7452a93
[Units] Set YamlWriter UnitSystem directly
ischoegl Jul 26, 2021
e3655d1
[Units] Simplify Units::str()
ischoegl Jul 26, 2021
4365740
[Units] Implement shortened Units::unit_str without factor
ischoegl Sep 3, 2021
a8b56fd
[Unittest] Add unit test for Units::str output
ischoegl Jul 17, 2021
88eb302
[Unittest] Add tests for UnitSystem.defaults
ischoegl Jul 18, 2021
9825a69
[Python] Disable Units constructors with non-unity conversion factors
ischoegl Sep 3, 2021
fd218b1
[Python] Shorten UnitSystem.__repr__
ischoegl Sep 5, 2021
4a448e8
[YamlWriter] Set UnitSystem directly with YamlWriter::setUnitSystem
ischoegl Sep 5, 2021
24fcfeb
[Units] Update checks for unity conversion factor
ischoegl Sep 10, 2021
d45482b
[Units] Improve docstrings
ischoegl Sep 10, 2021
1844332
Simplify implementation of UnitSystem::defaults
speth Sep 15, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions include/cantera/base/Units.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ class Units
Units& operator*=(const Units& other);

//! Provide a string representation of these Units
//! @param leading_one print '1' if no units are in the numerator
std::string unit_str(bool leading_one=true) const;

//! Provide a string representation of these Units that includes the
//! conversion factor
ischoegl marked this conversation as resolved.
Show resolved Hide resolved
std::string str() const;

//! Raise these Units to a power, changing both the conversion factor and
Expand Down Expand Up @@ -109,6 +114,9 @@ class UnitSystem
//! recognize an optional argument with a default value)
UnitSystem() : UnitSystem({}) {}

//! Return default units used by the unit system
std::map<std::string, std::string> defaults() const;

//! Set the default units to convert from when explicit units are not
//! provided. Defaults can be set for mass, length, time, quantity, energy,
//! and pressure. Conversion using the pressure or energy units is done only
Expand Down
6 changes: 6 additions & 0 deletions include/cantera/base/YamlWriter.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ class YamlWriter
//! corresponding units supported by the UnitSystem class.
void setUnits(const std::map<std::string, std::string>& units={});

//! Set the units to be used in the output file. Dimensions not specified
//! will use Cantera's defaults.
//! @param units A UnitSystem object specifying dimensions (mass, length, time,
//! quantity, pressure, energy, activation-energy).
void setUnitSystem(const UnitSystem& units=UnitSystem());

protected:
std::vector<shared_ptr<Solution>> m_phases;

Expand Down
28 changes: 27 additions & 1 deletion interfaces/cython/cantera/_cantera.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,20 @@ cdef extern from "cantera/base/xml.h" namespace "Cantera":
XML_Node* findID(string)
int nChildren()

cdef extern from "cantera/base/Units.h" namespace "Cantera":
cdef cppclass CxxUnits "Cantera::Units":
CxxUnits()
CxxUnits(CxxUnits)
CxxUnits(string) except +translate_exception
string unit_str()
string str()
double factor()

cdef cppclass CxxUnitSystem "Cantera::UnitSystem":
CxxUnitSystem()
stdmap[string, string] defaults()
void setDefaults(stdmap[string, string]&) except +translate_exception

cdef extern from "cantera/base/AnyMap.h" namespace "Cantera":
cdef cppclass CxxAnyValue "Cantera::AnyValue"

Expand Down Expand Up @@ -208,6 +222,7 @@ cdef extern from "cantera/thermo/ThermoPhase.h" namespace "Cantera":
size_t stateSize()
void saveState(size_t, double*) except +translate_exception
void restoreState(size_t, double*) except +translate_exception
CxxUnits standardConcentrationUnits() except +translate_exception

# initialization
void addUndefinedElements() except +translate_exception
Expand Down Expand Up @@ -420,6 +435,7 @@ cdef extern from "cantera/kinetics/Reaction.h" namespace "Cantera":
cbool allow_nonreactant_orders
cbool allow_negative_orders
cbool usesLegacy()
CxxUnits rate_units

cdef cppclass CxxElementaryReaction2 "Cantera::ElementaryReaction2" (CxxReaction):
CxxElementaryReaction2()
Expand Down Expand Up @@ -632,7 +648,7 @@ cdef extern from "cantera/base/YamlWriter.h" namespace "Cantera":
void toYamlFile(string&) except +translate_exception
void setPrecision(int)
void skipUserDefined(cbool)
void setUnits(stdmap[string, string]&) except +translate_exception
void setUnitSystem(CxxUnitSystem) except +translate_exception
ischoegl marked this conversation as resolved.
Show resolved Hide resolved

cdef extern from "cantera/equil/MultiPhase.h" namespace "Cantera":
cdef cppclass CxxMultiPhase "Cantera::MultiPhase":
Expand Down Expand Up @@ -1108,6 +1124,14 @@ ctypedef void (*transportMethod2d)(CxxTransport*, size_t, double*) except +trans
ctypedef void (*kineticsMethod1d)(CxxKinetics*, double*) except +translate_exception

# classes
cdef class Units:
cdef CxxUnits units
@staticmethod
cdef copy(CxxUnits)

cdef class UnitSystem:
cdef CxxUnitSystem unitsystem

cdef class SpeciesThermo:
cdef shared_ptr[CxxSpeciesThermo] _spthermo
cdef CxxSpeciesThermo* spthermo
Expand Down Expand Up @@ -1199,6 +1223,8 @@ cdef class DustyGasTransport(Transport):
cdef class YamlWriter:
cdef shared_ptr[CxxYamlWriter] _writer
cdef CxxYamlWriter* writer
@staticmethod
cdef CxxUnitSystem _get_unitsystem(UnitSystem units)

cdef class Mixture:
cdef CxxMultiPhase* mix
Expand Down
1 change: 1 addition & 0 deletions interfaces/cython/cantera/_cantera.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ from ._cantera cimport *
include "utils.pyx"
include "constants.pyx"
include "func1.pyx"
include "units.pyx"

include "base.pyx"
include "speciesthermo.pyx"
Expand Down
4 changes: 2 additions & 2 deletions interfaces/cython/cantera/base.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,8 @@ cdef class _SolutionBase:
Additional ThermoPhase / Solution objects to be included in the
output file
:param units:
A dictionary of the units to be used for each dimension. See
`YamlWriter.output_units`.
A `UnitSystem` object or dictionary of the units to be used for
each dimension. See `YamlWriter.output_units`.
:param precision:
For output floating point values, the maximum number of digits to
the right of the decimal point. The default is 15 digits.
Expand Down
6 changes: 6 additions & 0 deletions interfaces/cython/cantera/reaction.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,12 @@ cdef class Reaction:
def __get__(self):
return self.reaction.usesLegacy()

property rate_coeff_units:
"""Get reaction rate coefficient units"""
def __get__(self):
cdef CxxUnits rate_units = self.reaction.rate_units
return Units.copy(rate_units)


cdef class Arrhenius:
r"""
Expand Down
26 changes: 25 additions & 1 deletion interfaces/cython/cantera/test/test_composite.py
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ def test_yaml_simple(self):
gas2.forward_rate_constants)
self.assertArrayNear(gas.mix_diff_coeffs, gas2.mix_diff_coeffs)

def test_yaml_outunits(self):
def test_yaml_outunits1(self):
gas = ct.Solution('h2o2.yaml')
gas.TPX = 500, ct.one_atm, 'H2: 1.0, O2: 1.0'
gas.equilibrate('HP')
Expand All @@ -604,6 +604,30 @@ def test_yaml_outunits(self):
gas2.forward_rate_constants)
self.assertArrayNear(gas.mix_diff_coeffs, gas2.mix_diff_coeffs)

def test_yaml_outunits2(self):
gas = ct.Solution('h2o2.yaml')
gas.TPX = 500, ct.one_atm, 'H2: 1.0, O2: 1.0'
gas.equilibrate('HP')
gas.TP = 1500, ct.one_atm
units = {'length': 'cm', 'quantity': 'mol', 'energy': 'cal'}
system = ct.UnitSystem(units)
gas.write_yaml('h2o2-generated.yaml', units=system)
generated = utilities.load_yaml("h2o2-generated.yaml")
original = utilities.load_yaml(self.cantera_data_path / "h2o2.yaml")

for r1, r2 in zip(original['reactions'], generated['reactions']):
if 'rate-constant' in r1:
self.assertNear(r1['rate-constant']['A'], r2['rate-constant']['A'])
self.assertNear(r1['rate-constant']['Ea'], r2['rate-constant']['Ea'])

gas2 = ct.Solution("h2o2-generated.yaml")
self.assertArrayNear(gas.concentrations, gas2.concentrations)
self.assertArrayNear(gas.partial_molar_enthalpies,
gas2.partial_molar_enthalpies)
self.assertArrayNear(gas.forward_rate_constants,
gas2.forward_rate_constants)
self.assertArrayNear(gas.mix_diff_coeffs, gas2.mix_diff_coeffs)

def test_yaml_surface(self):
gas = ct.Solution('ptcombust.yaml', 'gas')
surf = ct.Interface('ptcombust.yaml', 'Pt_surf', [gas])
Expand Down
57 changes: 57 additions & 0 deletions interfaces/cython/cantera/test/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,63 @@

from cantera._cantera import _py_to_any_to_py


class TestUnitSystem(utilities.CanteraTest):

def test_default(self):
units = ct.UnitSystem().units
checks = {
"activation-energy": "J/kmol",
"current": "A",
"energy": "J",
"length": "m",
"mass": "kg",
"pressure": "Pa",
"quantity": "kmol",
"temperature": "K",
"time": "s",
}
for dim, unit in units.items():
self.assertIn(dim, checks)
self.assertEqual(checks[dim], unit)

def test_cgs(self):
system = ct.UnitSystem({
"length": "cm", "mass": "g", "time": "s",
"quantity": "mol", "pressure": "dyn/cm^2", "energy": "erg",
"activation-energy": "cal/mol"})
units = system.units
checks = {
"activation-energy": "cal/mol",
"current": "A",
"energy": "erg",
"length": "cm",
"mass": "g",
"pressure": "dyn/cm^2",
"quantity": "mol",
"temperature": "K",
"time": "s",
}
for dim, unit in units.items():
self.assertIn(dim, checks)
self.assertEqual(checks[dim], unit)

def test_activation_energy(self):
system = ct.UnitSystem({"activation-energy": "eV"})
units = system.units
self.assertEqual(units["activation-energy"], "eV")

system = ct.UnitSystem({"activation-energy": "K"})
units = system.units
self.assertEqual(units["activation-energy"], "K")

def test_raises(self):
with self.assertRaisesRegex(ct.CanteraError, "non-unity conversion factor"):
ct.UnitSystem({"temperature": "2 K"})
with self.assertRaisesRegex(ct.CanteraError, "non-unity conversion factor"):
ct.UnitSystem({"current": "2 A"})


class TestPyToAnyValue(utilities.CanteraTest):

def check_conversion(self, value, check_type=None):
Expand Down
6 changes: 6 additions & 0 deletions interfaces/cython/cantera/thermo.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -1570,6 +1570,12 @@ cdef class ThermoPhase(_SolutionBase):
def __set__(self, double value):
self.thermo.setElectricPotential(value)

property standard_concentration_units:
"""Get standard concentration units for this phase."""
def __get__(self):
cdef CxxUnits units = self.thermo.standardConcentrationUnits()
return Units.copy(units)


cdef class InterfacePhase(ThermoPhase):
""" A class representing a surface or edge phase"""
Expand Down
93 changes: 93 additions & 0 deletions interfaces/cython/cantera/units.pyx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# 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.

cdef class Units:
"""
A representation of the units associated with a dimensional quantity.

This class is a light-weight interface to internal Cantera capabilities that are
used for converting quantities between unit systems and checking for dimensional
consistency. Internally, `Units` objects are mainly used within the `UnitSystem`
class to convert values from a user-specified Unit system to Cantera's base units
(SI + kmol).

The Python API handles display of `Units` that do not require a conversion factor,
with other functions not enabled.
"""
def __cinit__(self, name=None):
if name:
self.units = CxxUnits(stringify(name))
if abs(self.units.factor() - 1.) > np.nextafter(0, 1):
raise ValueError(
"The creation of 'Units' objects that require a conversion "
"factor\nwith respect to Cantera's default unit system is not "
f"supported:\ninput '{name}' is equivalent to "
f"'{pystr(self.units.str())}' where the conversion factor is "
f"'{self.units.factor()}'")

def __repr__(self):
if abs(self.units.factor() - 1.) < np.nextafter(0, 1):
ischoegl marked this conversation as resolved.
Show resolved Hide resolved
return f"<Units({pystr(self.units.unit_str())}) at {id(self):0x}>"
return f"<Units({pystr(self.units.str())}) at {id(self):0x}>"

@staticmethod
cdef copy(CxxUnits other):
"""Copy a C++ Units object to a Python object."""
cdef Units units = Units()
units.units = CxxUnits(other)
return units


cdef class UnitSystem:
"""
Unit conversion utility

Provides functions for converting dimensional values from a given unit system.
The main use is for converting values specified in input files to Cantera's
native unit system, which is SI units except for the use of kmol as the base
unit of quantity, i.e. kilogram, meter, second, kelvin, ampere, and kmol.

Special functions for converting activation energies allow these values to be
expressed as either energy per quantity, energy (e.g. eV), or temperature by
applying a factor of the Avogadro number or the gas constant where needed.
ischoegl marked this conversation as resolved.
Show resolved Hide resolved

The default unit system used by Cantera is SI+kmol::

ct.UnitSystem({
"length": "m", "mass": "kg", "time": "s",
"quantity": "kmol", "pressure": "Pa", "energy": "J",
"temperature": "K", "current": "A", "activation-energy": "J/kmol"})

A CGS+mol unit system with activation energy units of cal/mol is created as::

ct.UnitSystem({
"length": "cm", "mass": "g", "time": "s",
"quantity": "mol", "pressure": "dyn/cm^2", "energy": "erg",
"temperature": "K", "current": "A", "activation-energy": "cal/mol"})

Defaults for dimensions not specified will be left unchanged. Accordingly,
the default unit system is retrieved as::

ct.UnitSystem()
"""
def __cinit__(self, units=None):
self.unitsystem = CxxUnitSystem()
if units:
self.units = units

def __repr__(self):
return f"<UnitSystem at {id(self):0x}>"

property units:
"""
Units used by the unit system
"""
def __get__(self):
cdef stdmap[string, string] cxxunits = self.unitsystem.defaults()
cdef pair[string, string] item
return {pystr(item.first): pystr(item.second) for item in cxxunits}
def __set__(self, units):
cdef stdmap[string, string] cxxunits
for dimension, unit in units.items():
cxxunits[stringify(dimension)] = stringify(unit)
self.unitsystem.setDefaults(cxxunits)
16 changes: 10 additions & 6 deletions interfaces/cython/cantera/yamlwriter.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,19 @@ cdef class YamlWriter:
will use Cantera's defaults.

:param units:
A map where keys are dimensions (mass, length, time, quantity,
pressure, energy, activation-energy), and the values are
A `UnitSystem` object or map where keys are dimensions (mass, length, time,
quantity, pressure, energy, activation-energy), and the values are
corresponding units such as kg, mm, s, kmol, Pa, cal, and eV.
"""
def __set__(self, units):
cdef stdmap[string, string] cxxunits
for dimension, unit in units.items():
cxxunits[stringify(dimension)] = stringify(unit)
self.writer.setUnits(cxxunits)
if not isinstance(units, UnitSystem):
units = UnitSystem(units)
cdef CxxUnitSystem cxxunits = YamlWriter._get_unitsystem(units)
self.writer.setUnitSystem(cxxunits)

@staticmethod
cdef CxxUnitSystem _get_unitsystem(UnitSystem units):
return units.unitsystem

def __reduce__(self):
raise NotImplementedError('YamlWriter object is not picklable')
Expand Down
Loading