Skip to content

Commit

Permalink
Refactor setter and getter templates for units
Browse files Browse the repository at this point in the history
* To allow setting properties on the upstream classes, __setattr__ is
  implemented and checks whether the attribute is defined on this
  class. This requires setting the phase instance directly into the
  __dict__ attribute to avoid a recursion error.
* Class inheritance is not used here because it is not easy to set
  attributes on the super class. By design, super() does not allow
  setting properties, only getting. Furthermore, since the base
  PureFluid class is defined in Cython, the attributes of that class
  cannot be set by subclasses at all, again by design.
* The PureFluid class implemented here does not inherit from the
  Solution class implemented here because eventually this Solution class
  will include methods and properties related to kinetics and transport,
  which are not implemented for PureFluid. The base PureFluid class is a
  subclass of ThermoPhase only, whereas the base Solution class is a
  subclass of ThermoPhase, Kinetics, and Transport.
* To reflect the distinction between Solution and ThermoPhase, the
  template variables are renamed.
* Several methods of the PureFluid are getters only, despite having
  three properties. These attributes are fixed here. TPQ is the only
  three-property attribute with a setter.
* Setter methods now raise a CanteraError if the unit conversion to base
  units fails due to an AttributeError.
  • Loading branch information
bryanwweber authored and speth committed Apr 25, 2023
1 parent 24e7f0e commit 09faa1e
Show file tree
Hide file tree
Showing 2 changed files with 175 additions and 97 deletions.
182 changes: 138 additions & 44 deletions interfaces/cython/SConscript
Expand Up @@ -135,62 +135,83 @@ UNITS = {
"reference_pressure": '"Pa"', "thermal_expansion_coeff": '"1/K"'
}

getter_properties = [
"density_mass", "density_mole", "enthalpy_mass", "enthalpy_mole", "entropy_mass",
"entropy_mole", "int_energy_mass", "int_energy_mole", "volume_mass", "volume_mole",
"gibbs_mass", "gibbs_mole", "cp_mass", "cp_mole", "cv_mass", "cv_mole", "P",
"P_sat", "T", "T_sat", "atomic_weight", "chemical_potentials", "concentrations",
"critical_pressure", "critical_temperature", "electric_potential",
"electrochemical_potentials", "isothermal_compressibility", "max_temp",
"mean_molecular_weight", "min_temp", "molecular_weights", "partial_molar_cp",
"partial_molar_enthalpies", "partial_molar_entropies", "partial_molar_int_energies",
"partial_molar_volumes", "reference_pressure", "thermal_expansion_coeff", "cp",
"cv", "density", "h", "s", "g", "u", "v"
]

getter_template = Template("""
@property
def ${name}(self):
return Q_(self._phase.${name}, ${units})
""")

getter_properties = [
"density_mass", "density_mole", "enthalpy_mass", "enthalpy_mole", "entropy_mass",
"entropy_mole", "int_energy_mass", "int_energy_mole", "volume_mass", "volume_mole",
"gibbs_mass", "gibbs_mole", "cp_mass", "cp_mole", "cv_mass", "cv_mole", "P", "P_sat", "T",
"T_sat", "atomic_weight", "chemical_potentials", "concentrations", "critical_pressure",
"critical_temperature", "electric_potential", "electrochemical_potentials",
"isothermal_compressibility", "max_temp", "mean_molecular_weight", "min_temp",
"molecular_weights", "partial_molar_cp", "partial_molar_enthalpies",
"partial_molar_entropies", "partial_molar_int_energies", "partial_molar_volumes",
"reference_pressure", "thermal_expansion_coeff", "cp", "cv", "density",
"h", "s", "g", "u", "v"
]

getter_string = "".join(
getter_template.substitute(name=name, units=UNITS[name]) for name in getter_properties
)
thermophase_getters = []
for name in getter_properties:
thermophase_getters.append(getter_template.substitute(name=name, units=UNITS[name]))

setter_template = Template("""
setter_2_template = Template("""
@property
def ${name}(self):
${n0}, ${n1} = self._phase.${name}
return Q_(${n0}, ${u0}), Q_(${n1}, ${u1})
@${name}.setter
def ${name}(self, value):
${n0} = value[0].to(${u0}) if value[0] is not None else self.${n0}
${n1} = value[1].to(${u1}) if value[1] is not None else self.${n1}
try:
${n0} = value[0].to(${u0}) if value[0] is not None else self.${n0}
${n1} = value[1].to(${u1}) if value[1] is not None else self.${n1}
except AttributeError as e:
# The 'to' attribute missing means this isn't a pint Quantity
if "'to'" in str(e):
raise CanteraError(
f"Values ({value}) must be instances of pint.Quantity classes"
) from None
else:
raise
self._phase.${name} = ${n0}.magnitude, ${n1}.magnitude
""")

setter_properties = ["TP", "DP", "HP", "SP", "SV", "TD", "UV"]
pf_setter_properties = ["PQ", "TQ", "PV", "SH", "ST", "TH", "TV", "UP", "VH"]
tp_setter_2_properties = ["TP", "DP", "HP", "SP", "SV", "TD", "UV"]
pf_setter_2_properties = ["PQ", "TQ", "PV", "SH", "ST", "TH", "TV", "UP", "VH"]

setter_string = "".join(
setter_template.substitute(name=name, n0=name[0], u0=UNITS[name[0]], n1=name[1], u1=UNITS[name[1]]) for name in setter_properties
)
pf_setter_string = "".join(
setter_template.substitute(name=name, n0=name[0], u0=UNITS[name[0]], n1=name[1], u1=UNITS[name[1]]) for name in pf_setter_properties
)
thermophase_2_setters = []
for name in tp_setter_2_properties:
d = dict(name=name, n0=name[0], u0=UNITS[name[0]], n1=name[1], u1=UNITS[name[1]])
thermophase_2_setters.append(setter_2_template.substitute(d))

setter1_template = Template("""
purefluid_2_setters = []
for name in pf_setter_2_properties:
d = dict(name=name, n0=name[0], u0=UNITS[name[0]], n1=name[1], u1=UNITS[name[1]])
purefluid_2_setters.append(setter_2_template.substitute(d))

setter_3_template = Template("""
@property
def ${name}(self):
${n0}, ${n1}, ${n2} = self._phase.${name}
return Q_(${n0}, ${u0}), Q_(${n1}, ${u1}), Q_(${n2}, ${u2})
@${name}.setter
def ${name}(self, value):
${n0} = value[0].to(${u0}) if value[0] is not None else self.${n0}
${n1} = value[1].to(${u1}) if value[1] is not None else self.${n1}
try:
${n0} = value[0].to(${u0}) if value[0] is not None else self.${n0}
${n1} = value[1].to(${u1}) if value[1] is not None else self.${n1}
except AttributeError as e:
# The 'to' attribute missing means this isn't a pint Quantity
if "'to'" in str(e):
raise CanteraError(
f"Values ({value}) must be instances of pint.Quantity classes"
) from None
else:
raise
if value[2] is not None:
try:
${n2} = value[2].to(${u2}).magnitude
Expand All @@ -201,24 +222,97 @@ setter1_template = Template("""
self._phase.${name} = ${n0}.magnitude, ${n1}.magnitude, ${n2}
""")

setter1_properties = [
"TPX", "TPY", "DPX", "DPY", "HPX", "HPY", "SPX", "SPY", "SVX", "SVY",
"TDX", "TDY", "UVX", "UVY"
tp_setter_3_properties = [
"TPX", "TPY", "DPX", "DPY", "HPX", "HPY", "SPX", "SPY", "SVX", "SVY", "TDX", "TDY",
"UVX", "UVY"
]
pf_setter1_properties = ["TPQ", "DPQ", "HPQ", "SPQ", "SVQ", "TDQ", "UVQ"]

setter1_string = "".join(
setter1_template.substitute(name=name, n0=name[0], u0=UNITS[name[0]], n1=name[1], u1=UNITS[name[1]], n2=name[2], u2=UNITS[name[2]]) for name in setter1_properties
)
pf_setter1_string = "".join(
setter1_template.substitute(name=name, n0=name[0], u0=UNITS[name[0]], n1=name[1], u1=UNITS[name[1]], n2=name[2], u2=UNITS[name[2]]) for name in pf_setter1_properties
)
thermophase_3_setters = []
for name in tp_setter_3_properties:
d = dict(name=name, n0=name[0], u0=UNITS[name[0]], n1=name[1], u1=UNITS[name[1]],
n2=name[2], u2=UNITS[name[2]])
thermophase_3_setters.append(setter_3_template.substitute(d))

getter_3_template = Template("""
@property
def ${name}(self):
${n0}, ${n1}, ${n2} = self._phase.${name}
return Q_(${n0}, ${u0}), Q_(${n1}, ${u1}), Q_(${n2}, ${u2})
""")

pf_getter_3_properties = ["DPQ", "HPQ", "SPQ", "SVQ", "TDQ", "UVQ"]

purefluid_3_getters = []
for name in pf_getter_3_properties:
d = dict(name=name, n0=name[0], u0=UNITS[name[0]], n1=name[1], u1=UNITS[name[1]],
n2=name[2], u2=UNITS[name[2]])
purefluid_3_getters.append(getter_3_template.substitute(d))


solution_properties = "".join([getter_string, setter_string, setter1_string])
pf_properties = "".join([pf_setter_string, pf_setter1_string])
def recursive_join(*args, joiner=""):
result = ""
for arg in args:
result = result + joiner.join(arg)
return result


thermophase_properties = recursive_join(thermophase_getters, thermophase_2_setters,
thermophase_3_setters)
purefluid_properties = recursive_join(purefluid_2_setters, purefluid_3_getters)

common_properties = """
def __getattr__(self, name):
return getattr(self._phase, name)
def __setattr__(self, name, value):
if name in dir(self):
object.__setattr__(self, name, value)
else:
setattr(self._phase, name, value)
@property
def basis_units(self):
if self._phase.basis == "mass":
return "kg"
else:
return "kmol"
@property
def X(self):
X = self._phase.X
return Q_(X, "dimensionless")
@X.setter
def X(self, value):
if value is not None:
try:
X = value.to("dimensionless").magnitude
except AttributeError:
X = value
else:
X = self.X.magnitude
self._phase.X = X
@property
def Y(self):
Y = self._phase.Y
return Q_(Y, "dimensionless")
@Y.setter
def Y(self, value):
if value is not None:
try:
Y = value.to("dimensionless").magnitude
except AttributeError:
Y = value
else:
Y = self.Y.magnitude
self._phase.Y = Y
"""

localenv["solution_properties"] = solution_properties.strip()
localenv["purefluid_properties"] = pf_properties.strip()
localenv["common_properties"] = common_properties.strip()
localenv["thermophase_properties"] = thermophase_properties.strip()
localenv["purefluid_properties"] = purefluid_properties.strip()
units = localenv.SubstFile("cantera/units/solution.py", "cantera/units/solution.py.in")
localenv.Depends(mod, units)

Expand Down
90 changes: 37 additions & 53 deletions interfaces/cython/cantera/units/solution.py.in
@@ -1,69 +1,29 @@
from .. import Solution as _Solution, PureFluid as _PureFluid
from .. import Solution as _Solution, PureFluid as _PureFluid, CanteraError

from pint import UnitRegistry
units = UnitRegistry()
Q_ = units.Quantity

__all__ = (
"units", "Q_", "Solution", "PureFluid", "Heptane", "CarbonDioxide",
"Hfc134a", "Hydrogen", "Methane", "Nitrogen", "Oxygen", "Water")
__all__ = ("units", "Q_", "Solution", "PureFluid", "Heptane", "CarbonDioxide",
"Hfc134a", "Hydrogen", "Methane", "Nitrogen", "Oxygen", "Water",
"CanteraError")


class Solution:
def __init__(self, infile, phasename=""):
self._phase = _Solution(infile, phasename)
def __init__(self, infile, name=""):
self.__dict__["_phase"] = _Solution(infile, name)

@property
def basis_units(self):
if self._phase.basis == "mass":
return "kg"
else:
return "kmol"
@common_properties@

def __getattr__(self, name):
return getattr(self._phase, name)
@thermophase_properties@

def __setattr__(self, name, value):
return super(Solution, self).__setattr__(name, value)

@property
def X(self):
X = self._phase.X
return Q_(X, "dimensionless")

@X.setter
def X(self, value):
if value is not None:
try:
X = value.to("dimensionless").magnitude
except AttributeError:
X = value
else:
X = self.X.magnitude
self._phase.X = X
class PureFluid:
def __init__(self, infile, name=""):
self.__dict__["_phase"] = _PureFluid(infile, name)

@property
def Y(self):
Y = self._phase.Y
return Q_(Y, "dimensionless")

@Y.setter
def Y(self, value):
if value is not None:
try:
Y = value.to("dimensionless").magnitude
except AttributeError:
Y = value
else:
Y = self.Y.magnitude
self._phase.Y = Y

@solution_properties@


class PureFluid(Solution):
def __init__(self, infile, phasename=""):
self._phase = _PureFluid(infile, phasename)
@common_properties@

@property
def Q(self):
Expand All @@ -76,11 +36,35 @@ class PureFluid(Solution):
try:
Q = value.to("dimensionless").magnitude
except AttributeError:
Q = value
raise CanteraError(
"Values must be instances of pint.Quantity classes"
) from None
else:
Q = self.Q.magnitude
self._phase.Q = Q

@thermophase_properties@

@property
def TPQ(self):
T, P, Q = self._phase.TPQ
return Q_(T, "K"), Q_(P, "Pa"), Q_(Q, "dimensionless")

@TPQ.setter
def TPQ(self, value):
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
try:
T = T.to("K")
P = P.to("Pa")
Q = Q.to("dimensionless")
except AttributeError:
raise CanteraError(
"Values must be instances of pint.Quantity classes"
) from None
self._phase.TPQ = T.magnitude, P.magnitude, Q.magnitude

@purefluid_properties@


Expand Down

0 comments on commit 09faa1e

Please sign in to comment.