Skip to content

Commit

Permalink
Merge pull request #129 from QSD-Group/get_property_issue100
Browse files Browse the repository at this point in the history
Update _components.py
  • Loading branch information
joyxyz1994 authored Jan 31, 2025
2 parents e45b7b4 + 55c6704 commit 38c0f7e
Show file tree
Hide file tree
Showing 14 changed files with 127 additions and 64 deletions.
43 changes: 22 additions & 21 deletions qsdsan/_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,8 @@ class Component(Chemical):
.. note::
[1] Element ratios like `i_C`, `i_N`, `i_P`, `i_K`, `i_Mg`, and `i_Ca` will
be calculated based on `formula` and `measured_as` if given; and the ratio
[1] Element contents like `i_C`, `i_N`, `i_P`, `i_K`, `i_Mg`, and `i_Ca` will
be calculated based on `formula` and `measured_as` if given; and the content value
will be 1 if the component is measured as this element.
[2] For fractions including `f_BOD5_COD`, `f_uBOD_COD`, and `f_Vmass_Totmass`,
Expand All @@ -195,7 +195,13 @@ class Component(Chemical):
[3] If no formula or MW is provided, then MW of this component is assumed to
1 to avoid ZeroDivisionError exception in calculation.
[4] Molar flowrate of a component, if not given, will always be calculated
using the mass flowrate data and the MW of this component. If `measured_as`
is not `None`, i.e., the mass flowrate data is interpreted in the `measured_as`
unit, the calculated molar flowrate would be inaccurate unless user adjusts
the MW value accordingly. However, this is irrelevant if the component itself
does not have a sensible MW anyway.
Examples
--------
`Component <https://qsdsan.readthedocs.io/en/latest/tutorials/2_Component.html>`_
Expand Down Expand Up @@ -237,7 +243,7 @@ def __new__(cls, ID, search_ID=None, formula=None, phase=None, measured_as=None,
self._degradability = degradability
self._organic = organic
self.description = description
self._measured_as = measured_as
self.measured_as = measured_as
self.i_mass = i_mass
self.i_C = i_C
self.i_N = i_N
Expand Down Expand Up @@ -337,12 +343,10 @@ def i_mass(self, i):
if self.measured_as in self.atoms:
i = 1/get_mass_frac(self.atoms)[self.measured_as]
elif self.measured_as == 'COD':
# chem_MW = molecular_weight(self.atoms)
chem_charge = charge_from_formula(self.formula)
Cr2O7 = - cod_test_stoichiometry(self.atoms, chem_charge)['Cr2O7-2']
cod = Cr2O7 * 1.5 * molecular_weight({'O':2})
try: i = self.chem_MW/cod
except: breakpoint()
i = self.chem_MW/cod
elif self.measured_as:
raise AttributeError(f'Must specify i_mass for component {self.ID} '
f'measured as {self.measured_as}.')
Expand Down Expand Up @@ -430,22 +434,14 @@ def measured_as(self, measured_as):
be automatically updated.
'''
if measured_as:
if measured_as == 'COD':
self._MW = molecular_weight({'O':2})
elif measured_as in self.atoms or 'i_'+measured_as in _num_component_properties:
self._MW = molecular_weight({measured_as:1})
else:
if not (measured_as in ('COD', *self.atoms) or 'i_'+measured_as in _num_component_properties):
raise AttributeError(f"Component {self.ID} must be measured as "
f"either COD or one of its constituent atoms, "
f"if not as itself.")
else:
# if self.atoms: self._MW = molecular_weight(self.atoms)
# else: self._MW = 1
self._MW = self.chem_MW

if self._measured_as != measured_as:
self._convert_i_attr(measured_as)

# self._MW = self.chem_MW
if hasattr(self, '_measured_as'):
if self._measured_as != measured_as:
self._convert_i_attr(measured_as)
self._measured_as = measured_as

formula = property(Chemical.formula.fget)
Expand Down Expand Up @@ -532,7 +528,12 @@ def i_COD(self):
return self._i_COD or 0.
@i_COD.setter
def i_COD(self, i):
if i is not None: self._i_COD = check_return_property('i_COD', i)
if i is not None:
i = check_return_property('i_COD', i)
if self._measured_as == 'COD' and i != 1:
warn(f'{self.ID} is measured as COD but `i_COD` is not 1, '
'`<WasteStream>.COD` will yield inaccurate result.')
self._i_COD = i
else:
if self.organic or self.formula in ('H2', 'O2', 'N2', 'NO2-', 'NO3-', 'H2S', 'S'):
if self.measured_as == 'COD': self._i_COD = 1.
Expand Down
92 changes: 77 additions & 15 deletions qsdsan/_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,13 +151,14 @@ def extend(self, components):
for component in components: self.append(component)


def compile(self, skip_checks=False):
def compile(self, skip_checks=False, ignore_inaccurate_molar_weight=False,
adjust_MW_to_measured_as=False):
'''Cast as a :class:`CompiledComponents` object.'''
components = tuple(self)
tmo._chemicals.prepare(components, skip_checks)
setattr(self, '__class__', CompiledComponents)

try: self._compile(components)
try: self._compile(components, ignore_inaccurate_molar_weight, adjust_MW_to_measured_as)
except Exception as error:
setattr(self, '__class__', Components)
setattr(self, '__dict__', {i.ID: i for i in components})
Expand All @@ -169,7 +170,9 @@ def compile(self, skip_checks=False):


def default_compile(self, lock_state_at='l',
soluble_ref='Urea', gas_ref='CO2', particulate_ref='Stearin'):
soluble_ref='Urea', gas_ref='CO2', particulate_ref='Stearin',
ignore_inaccurate_molar_weight=False,
adjust_MW_to_measured_as=False):
'''
Auto-fill of the missing properties of the components and compile,
boiling point (Tb) and molar volume (V) will be copied from the reference component,
Expand All @@ -187,20 +190,68 @@ def default_compile(self, lock_state_at='l',
Reference component (or chemical ID) for those with `particle_size` == 'Dissolved gas'.
particulate_ref : obj or str
Reference component (or chemical ID) for those with `particle_size` == 'Particulate'.
ignore_inaccurate_molar_weight : bool
Default is False, need to be manually set to True if having components
with `measured_as` attributes. This is to alert the users that
calculations for attributes using molecular weight will be inacurate,
unless all components have sensible MWs and `adjust_MW_to_measured_as`
is set to True.
adjust_MW_to_measured_as : bool
Default is False. Manually set it to True to adjust the MW data of
components with `measured_as` attributes and chemical formulas. This
is to enable correct calculations of component molar flows when possible.
For components without a sensible MW, their MWs will remain 1 by default.
This is pertinent for calculations of molar flows and any thermodynamic
property of a stream.
Examples
--------
>>> from qsdsan import Component, Components
>>> from qsdsan import Component, Components, Stream, set_thermo
>>> X = Component('X', phase='s', measured_as='COD', i_COD=0, description='Biomass',
... organic=True, particle_size='Particulate', degradability='Readily')
>>> X_inert = Component('X_inert', phase='s', description='Inert biomass', i_COD=0,
... organic=True, particle_size='Particulate', degradability='Undegradable')
>>> Substrate = Component('Substrate', phase='s', measured_as='COD', i_mass=18.3/300,
... organic=True, particle_size='Particulate', degradability='Readily')
>>> cmps = Components([X, X_inert, Substrate])
>>> cmps.default_compile()
>>> # As none of the components above has a chemical formula, i.e., no sensible MW,
>>> # simply set `ignore_inaccurate_molar_weight` to True to bypass error.
>>> cmps.default_compile(ignore_inaccurate_molar_weight=True)
>>> cmps
CompiledComponents([X, X_inert, Substrate])
>>> Ac = Component('Ac', search_ID='CH3COOH', particle_size='Soluble',
... degradability='Readily', organic=True)
>>> Ac_as_COD = Component('Ac_as_COD', search_ID='CH3COOH', measured_as='COD',
... particle_size='Soluble', degradability='Readily', organic=True)
>>> Acs = Components([Ac, Ac_as_COD])
>>> Acs.default_compile(ignore_inaccurate_molar_weight=True,
... adjust_MW_to_measured_as=False)
>>> set_thermo(Acs)
>>> # Create a WasteStream object with 1 kmol/hr of each acetic acid component,
>>> # knowing 1 mol acetate is equivalent to 2 mol of O2 demand
>>> s1 = Stream('s1', Ac=60.052, Ac_as_COD=2 * 32, units='kg/hr')
>>> s1.mass
sparse([60.052, 64. ])
>>> # However, the calculated molar flow is inaccurate because the MW for Ac_as_COD
>>> # is inconsistent with its `measured_as`. This also affects the
>>> # calculation of other thermodynamic properties.
>>> s1.mol
sparse([1. , 1.066])
>>> s1.vol
sparse([0.05 , 0.054])
>>> # To fix the molar flow calculation, simply set `adjust_MW_to_measured_as` to True when compile.
>>> Acs_MW_adjusted = Components([Ac, Ac_as_COD])
>>> Acs_MW_adjusted.default_compile(adjust_MW_to_measured_as=True)
>>> set_thermo(Acs_MW_adjusted)
>>> s2 = Stream('s2', Ac=60.052, Ac_as_COD=2 * 32, units='kg/hr')
>>> s2.mol
sparse([1., 1.])
>>> s2.vol
sparse([0.05, 0.05])
'''
isa = isinstance
get = getattr
Expand Down Expand Up @@ -261,7 +312,8 @@ def get_constant_V_model(ref_cmp, phase=''):
# Copy all remaining properties from water
cmp.copy_models_from(water)
for cmp in self: cmp.default()
self.compile()
self.compile(ignore_inaccurate_molar_weight=ignore_inaccurate_molar_weight,
adjust_MW_to_measured_as=adjust_MW_to_measured_as)


@classmethod
Expand Down Expand Up @@ -398,8 +450,8 @@ def load_default(cls, use_default_data=True, store_data=False, default_compile=T
new.append(H2O)

if default_compile:
new.default_compile(lock_state_at='', particulate_ref='NaCl')
new.compile()
new.default_compile(lock_state_at='', particulate_ref='NaCl', ignore_inaccurate_molar_weight=True)
new.compile(ignore_inaccurate_molar_weight=True)
# Add aliases
new.set_alias('H2O', 'Water')
# Pre-define groups
Expand Down Expand Up @@ -486,9 +538,9 @@ def append_combustion_components(components, alt_IDs={},
add_V_from_rho(cmps.P4O10, rho=2.39, rho_unit='g/mL') # http://www.chemspider.com/Chemical-Structure.14128.html
try:
for cmp in cmps: cmp.default()
cmps.compile()
cmps.compile(ignore_inaccurate_molar_weight=True)
except RuntimeError: # cannot compile due to missing properties
cmps.default_compile(**default_compile_kwargs)
cmps.default_compile(ignore_inaccurate_molar_weight=True, **default_compile_kwargs)
for k, v in aliases.items():
cmps.set_alias(k, v)
return cmps
Expand Down Expand Up @@ -611,22 +663,32 @@ def refresh_constants(self):
for i in _num_component_properties:
dct[i] = component_data_array(components, i)

def compile(self, skip_checks=False):
def compile(self, skip_checks=False, ignore_inaccurate_molar_weight=False):
'''Do nothing, :class:`CompiledComponents` have already been compiled.'''
pass


def _compile(self, components):
def _compile(self, components, ignore_inaccurate_molar_weight, adjust_MW_to_measured_as):
dct = self.__dict__
tuple_ = tuple # this speeds up the code
components = tuple_(dct.values())
CompiledChemicals._compile(self, components)
for component in components:
missing_properties = component.get_missing_properties(_key_component_properties)
if component.measured_as:
inaccurate = True
if component.formula and adjust_MW_to_measured_as:
component._MW = component.chem_MW / component.i_mass
inaccurate = False
if (not ignore_inaccurate_molar_weight) and inaccurate:
raise RuntimeError(f'{component} does not have a sensible molar weight. Set ignore_inaccurate_molar_weight=True to bypass this error.')
if not missing_properties: continue
missing = utils.repr_listed_values(missing_properties)
raise RuntimeError(f'{component} is missing key component-related properties ({missing}).')

if adjust_MW_to_measured_as:
dct['MW'] = component_data_array(components, 'MW')

for i in _num_component_properties:
dct[i] = component_data_array(components, i)

Expand All @@ -639,7 +701,7 @@ def _compile(self, components):
org = dct['org'] = np.asarray([int(cmp.organic) for cmp in components])
inorg = dct['inorg'] = np.ones_like(org) - org
ID_arr = dct['_ID_arr'] = np.asarray([i.ID for i in components])
dct['chem_MW'] = np.asarray([i.chem_MW for i in components])
dct['chem_MW'] = component_data_array(components, 'chem_MW')

# Inorganic degradable non-gas, incorrect
inorg_b = inorg * b * (s+c)
Expand All @@ -653,7 +715,7 @@ def subgroup(self, IDs):
'''Create a new subgroup of :class:`Component` objects.'''
components = self[IDs]
new = Components(components)
new.compile()
new.compile(ignore_inaccurate_molar_weight=True)
for i in new.IDs:
for j in self.get_aliases(i):
try: new.set_alias(i, j)
Expand All @@ -680,7 +742,7 @@ def indices(self, IDs):
def copy(self):
'''Return a copy.'''
copy = Components(self)
copy.compile()
copy.compile(ignore_inaccurate_molar_weight=True)
return copy


Expand Down
5 changes: 3 additions & 2 deletions qsdsan/processes/_adm1.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
C_mw = 12
N_mw = 14

def create_adm1_cmps(set_thermo=True):
def create_adm1_cmps(set_thermo=True, adjust_MW_to_measured_as=True):
cmps_all = Components.load_default()

# varies
Expand Down Expand Up @@ -169,7 +169,8 @@ def create_adm1_cmps(set_thermo=True):
S_ch4, S_IC, S_IN, S_I, X_c, X_ch, X_pr, X_li,
X_su, X_aa, X_fa, X_c4, X_pro, X_ac, X_h2, X_I,
S_cat, S_an, cmps_all.H2O])
cmps_adm1.default_compile()
cmps_adm1.default_compile(ignore_inaccurate_molar_weight=True,
adjust_MW_to_measured_as=adjust_MW_to_measured_as)
if set_thermo: settings.set_thermo(cmps_adm1)
return cmps_adm1

Expand Down
10 changes: 6 additions & 4 deletions qsdsan/processes/_adm1_p_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
N_mw = get_mw({'N':1})
P_mw = get_mw({'P':1})

def create_adm1_p_extension_cmps(set_thermo=True):
def create_adm1_p_extension_cmps(set_thermo=True, adjust_MW_to_measured_as=True):
c1 = create_adm1_cmps(False)
c2d = create_masm2d_cmps(False)
_c2 = create_asm2d_cmps(False)
Expand All @@ -75,7 +75,8 @@ def create_adm1_p_extension_cmps(set_thermo=True):
c2d.X_PHA, c2d.X_PP, c2d.X_PAO,
c2d.S_K, c2d.S_Mg, _c2.X_MeOH, _c2.X_MeP,
*others])
cmps_adm1p.default_compile()
cmps_adm1p.default_compile(ignore_inaccurate_molar_weight=True,
adjust_MW_to_measured_as=adjust_MW_to_measured_as)
if set_thermo: settings.set_thermo(cmps_adm1p)
return cmps_adm1p

Expand Down Expand Up @@ -546,7 +547,7 @@ def check_stoichiometric_parameters(self):
# =============================================================================


def create_adm1p_cmps(set_thermo=True):
def create_adm1p_cmps(set_thermo=True, adjust_MW_to_measured_as=True):
c1 = create_adm1_cmps(False)
c2d = create_masm2d_cmps(False)

Expand Down Expand Up @@ -602,7 +603,8 @@ def create_adm1p_cmps(set_thermo=True):
c2d.X_struv, c2d.X_newb, c2d.X_ACP, c2d.X_MgCO3,
c2d.X_AlOH, c2d.X_AlPO4, c2d.X_FeOH, c2d.X_FePO4,
c2d.S_Na, c2d.S_Cl, c2d.H2O])
cmps_adm1p.default_compile()
cmps_adm1p.default_compile(ignore_inaccurate_molar_weight=True,
adjust_MW_to_measured_as=adjust_MW_to_measured_as)
if set_thermo: settings.set_thermo(cmps_adm1p)
return cmps_adm1p

Expand Down
4 changes: 2 additions & 2 deletions qsdsan/processes/_asm1.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
_load_components = settings.get_default_chemicals

############# Components with default notation #############
def create_asm1_cmps(set_thermo=True):
def create_asm1_cmps(set_thermo=True, adjust_MW_to_measured_as=True):
cmps = Components.load_default()

S_I = cmps.S_U_Inf.copy('S_I')
Expand Down Expand Up @@ -77,7 +77,7 @@ def create_asm1_cmps(set_thermo=True):
cmps_asm1 = Components([S_I, S_S, X_I, X_S, X_BH, X_BA, X_P,
S_O, S_NO, S_NH, S_ND, X_ND, S_ALK,
cmps.S_N2, cmps.H2O])
cmps_asm1.compile()
cmps_asm1.compile(ignore_inaccurate_molar_weight=True, adjust_MW_to_measured_as=adjust_MW_to_measured_as)
if set_thermo: settings.set_thermo(cmps_asm1)

return cmps_asm1
Expand Down
10 changes: 6 additions & 4 deletions qsdsan/processes/_asm2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
_load_components = settings.get_default_chemicals

############# Components with default notation #############
def create_asm2d_cmps(set_thermo=True):
def create_asm2d_cmps(set_thermo=True, adjust_MW_to_measured_as=True):
cmps = Components.load_default()

S_A = cmps.S_Ac.copy('S_A')
Expand Down Expand Up @@ -69,12 +69,13 @@ def create_asm2d_cmps(set_thermo=True):
S_F, S_A, S_I, S_ALK, X_I, X_S, X_H, X_PAO, X_PP,
X_PHA, X_AUT, X_MeOH, X_MeP, cmps.H2O])

cmps_asm2d.compile()
cmps_asm2d.compile(ignore_inaccurate_molar_weight=True,
adjust_MW_to_measured_as=adjust_MW_to_measured_as)
if set_thermo: settings.set_thermo(cmps_asm2d)

return cmps_asm2d

def create_masm2d_cmps(set_thermo=True):
def create_masm2d_cmps(set_thermo=True, adjust_MW_to_measured_as=True):
c2d = create_asm2d_cmps(False)
ion_kwargs = dict(particle_size='Soluble',
degradability='Undegradable',
Expand Down Expand Up @@ -153,7 +154,8 @@ def create_masm2d_cmps(set_thermo=True):
cmps = Components([*solubles, S_IC, S_K, S_Mg, *particulates,
S_Ca, X_CaCO3, X_struv, X_newb, X_ACP, X_MgCO3,
X_AlOH, X_AlPO4, X_FeOH, X_FePO4, S_Na, S_Cl, H2O])
cmps.default_compile()
cmps.default_compile(ignore_inaccurate_molar_weight=True,
adjust_MW_to_measured_as=adjust_MW_to_measured_as)
if set_thermo: settings.set_thermo(cmps)

return cmps
Expand Down
Loading

0 comments on commit 38c0f7e

Please sign in to comment.