Skip to content

Commit

Permalink
Solute: dataclass; rm redundant methods
Browse files Browse the repository at this point in the history
  • Loading branch information
rkingsbury committed Aug 1, 2023
1 parent ff691a2 commit 972579e
Show file tree
Hide file tree
Showing 7 changed files with 57 additions and 102 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## 0.6.0 (in progress)

- **BREAKING CHANGE** `Solute`: methods `get_formal_charge()`, `get_name()`, and `get_molecular_weight()` have been
replaced by direct access to the attributes `charge`, `formula`, and `mw`, respectively.
- **DEPRECATION NOTICE** `Solution`: new properties `pressure`, `temperature`, `pE`,
- `pH`, `mass`, `density`, `viscosity_dynamic`, `viscosity_kinematic`, `ionic_strength`,
- `conductivity`, `debye_length`, `bjerrum_length`, `alkalinity`, `hardness`,
Expand Down
8 changes: 4 additions & 4 deletions src/pyEQL/engines.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ def get_activity_coefficient(self, solution, solute):
)
molal = ac.get_activity_coefficient_debyehuckel(
solution.ionic_strength,
ion.get_formal_charge(),
ion.charge,
str(solution.temperature),
)

Expand All @@ -296,7 +296,7 @@ def get_activity_coefficient(self, solution, solute):
)
molal = ac.get_activity_coefficient_guntelberg(
solution.ionic_strength,
ion.get_formal_charge(),
ion.charge,
str(solution.temperature),
)

Expand All @@ -308,7 +308,7 @@ def get_activity_coefficient(self, solution, solute):
)
molal = ac.get_activity_coefficient_davies(
solution.ionic_strength,
ion.get_formal_charge(),
ion.charge,
str(solution.temperature),
)

Expand Down Expand Up @@ -562,7 +562,7 @@ def get_solute_volume(self, solution):
continue

if db.has_parameter(item, "partial_molar_volume"):
solute_vol += solute.get_parameter("partial_molar_volume") * solute.get_moles()
solute_vol += solute.get_parameter("partial_molar_volume") * solute.moles
logger.info("Updated solution volume using direct partial molar volume for solute %s" % item)

else:
Expand Down
4 changes: 2 additions & 2 deletions src/pyEQL/salt_ion_match.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,9 @@ def _sort_components(Solution, type="all"):
if type == "all":
formula_list.append(item)
elif type == "cations":
if Solution.get_solute(item).get_formal_charge() > 0:
if Solution.get_solute(item).charge > 0:
formula_list.append(item)
elif type == "anions" and Solution.get_solute(item).get_formal_charge() < 0:
elif type == "anions" and Solution.get_solute(item).charge < 0:
formula_list.append(item)

# populate a dictionary with formula:concentration pairs
Expand Down
78 changes: 19 additions & 59 deletions src/pyEQL/solute.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,24 @@
:license: LGPL, see LICENSE for more details.
"""
from dataclasses import dataclass

from pint import Quantity

# import the parameters database
# the pint unit registry
from pyEQL import paramsDB as db
from pyEQL import unit
from pyEQL.logging_system import logger


@dataclass
class Solute:
"""
represent each chemical species as an object containing its formal charge,
transport numbers, concentration, activity, etc.
"""

def __init__(self, formula, amount, volume, solvent_mass):
"""
Parameters
----------
Args:
formula : str
Chemical formula for the solute.
Charged species must contain a + or - and (for polyvalent solutes) a number representing the net charge (e.g. 'SO4-2').
Expand All @@ -39,7 +39,14 @@ def __init__(self, formula, amount, volume, solvent_mass):
The volume of the solution
solvent_mass : pint Quantity
The mass of solvent in the parent solution.
"""
"""

formula: str
amount: str
volume: Quantity
solvent_mass: Quantity

def __init__(self, formula, amount, volume, solvent_mass):
# import the chemical formula interpreter module
import pyEQL.chemical_formula as chem

Expand Down Expand Up @@ -87,54 +94,7 @@ def add_parameter(self, name, magnitude, units="", **kwargs):
import pyEQL.parameter as pm

newparam = pm.Parameter(name, magnitude, units, **kwargs)
db.add_parameter(self.get_name(), newparam)

def get_name(self):
"""
Return the name (formula) of the solute
Parameters
----------
None
Returns
-------
str
The chemical formula of the solute
"""
return self.formula

def get_formal_charge(self):
"""
Return the formal charge of the solute
Parameters
----------
None
Returns
-------
int
The formal charge of the solute
"""
return self.charge

def get_molecular_weight(self):
"""
Return the molecular weight of the solute
Parameters
----------
None
Returns
-------
Quantity
The molecular weight of the solute, in g/mol
"""
return self.mw
db.add_parameter(self.formula, newparam)

def get_moles(self):
"""
Expand Down Expand Up @@ -184,11 +144,11 @@ def set_moles(self, amount, volume, solvent_mass):
def __str__(self):
return (
"Species "
+ str(self.get_name())
+ str(self.formula)
+ " MW="
+ str(self.get_molecular_weight())
+ str(self.mw)
+ " Formal Charge="
+ str(self.get_formal_charge())
+ str(self.charge)
+ " Amount= "
+ str(self.get_moles())
+ str(self.moles)
)
57 changes: 25 additions & 32 deletions src/pyEQL/solution.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ def add_solute(self, formula, amount):

# add the new solute
new_solute = sol.Solute(formula, amount, self.get_volume(), self.get_solvent_mass())
self.components.update({new_solute.get_name(): new_solute})
self.components.update({new_solute.formula: new_solute})

# calculate the volume occupied by all the solutes
solute_vol = self._get_solute_volume()
Expand All @@ -198,14 +198,14 @@ def add_solute(self, formula, amount):
# adjust the amount of solvent
# density is returned in kg/m3 = g/L
target_mass = target_vol.magnitude * self.water_substance.rho * unit.Quantity("1 g")
mw = self.get_solvent().get_molecular_weight()
mw = self.get_solvent().mw
target_mol = target_mass / mw
self.get_solvent().moles = target_mol

else:
# add the new solute
new_solute = sol.Solute(formula, amount, self.get_volume(), self.get_solvent_mass())
self.components.update({new_solute.get_name(): new_solute})
self.components.update({new_solute.formula: new_solute})

# update the volume to account for the space occupied by all the solutes
# make sure that there is still solvent present in the first place
Expand All @@ -220,7 +220,7 @@ def add_solvent(self, formula, amount):
Same as add_solute but omits the need to pass solvent mass to pint
"""
new_solvent = sol.Solute(formula, amount, self.get_volume(), amount)
self.components.update({new_solvent.get_name(): new_solvent})
self.components.update({new_solvent.formula: new_solvent})

def get_solute(self, i):
"""
Expand Down Expand Up @@ -302,9 +302,9 @@ def get_solvent_mass(self):
"""
# return the total mass (kg) of the solvent
solvent = self.get_solvent()
mw = solvent.get_molecular_weight()
mw = solvent.mw

return solvent.get_moles().to("kg", "chem", mw=mw)
return solvent.moles.to("kg", "chem", mw=mw)

def get_volume(self):
"""
Expand Down Expand Up @@ -543,7 +543,7 @@ def viscosity_kinematic(self):
MW = self.mass / (self.get_moles_solvent() + self.get_total_moles_solute())

# get the MW of water
MW_w = self.get_solvent().get_molecular_weight()
MW_w = self.get_solvent().mw

# calculate the cation mole fraction
x_cat = self.get_amount(cation, "fraction")
Expand Down Expand Up @@ -602,7 +602,7 @@ def conductivity(self):
EC = 0 * unit("S/m")

for item in self.components:
z = abs(self.get_solute(item).get_formal_charge())
z = abs(self.get_solute(item).charge)
# ignore uncharged species
if z != 0:
# determine the value of the exponent alpha
Expand All @@ -616,7 +616,7 @@ def conductivity(self):
molar_cond = (
diffusion_coefficient
* (unit.e * unit.N_A) ** 2
* self.get_solute(item).get_formal_charge() ** 2
* self.get_solute(item).charge ** 2
/ (unit.R * self.temperature)
)

Expand Down Expand Up @@ -663,7 +663,7 @@ def ionic_strength(self) -> Quantity:
"""
ionic_strength = 0
for solute in self.components:
ionic_strength += 0.5 * self.get_amount(solute, "mol/kg") * self.components[solute].get_formal_charge() ** 2
ionic_strength += 0.5 * self.get_amount(solute, "mol/kg") * self.components[solute].charge ** 2

return ionic_strength

Expand Down Expand Up @@ -693,7 +693,7 @@ def charge_balance(self) -> float:
charge_balance = 0
F = (unit.e * unit.N_A).magnitude
for solute in self.components:
charge_balance += self.get_amount(solute, "mol").magnitude * self.components[solute].get_formal_charge() * F
charge_balance += self.get_amount(solute, "mol").magnitude * self.components[solute].charge * F

return charge_balance

Expand Down Expand Up @@ -744,10 +744,10 @@ def alkalinity(self):

for item in self.components:
if item in base_cations:
z = self.get_solute(item).get_formal_charge()
z = self.get_solute(item).charge
alkalinity += self.get_amount(item, "mol/L") * z
if item in acid_anions:
z = self.get_solute(item).get_formal_charge()
z = self.get_solute(item).charge
alkalinity -= self.get_amount(item, "mol/L") * z

# convert the alkalinity to mg/L as CaCO3
Expand Down Expand Up @@ -778,7 +778,7 @@ def hardness(self):
equiv_wt_CaCO3 = 100.09 / 2 * unit("g/mol")

for item in self.components:
z = self.get_solute(item).get_formal_charge()
z = self.get_solute(item).charge
if z > 1:
hardness += z * self.get_amount(item, "mol/L")

Expand Down Expand Up @@ -1013,8 +1013,8 @@ def get_amount(self, solute, units):
"""
# retrieve the number of moles of solute and its molecular weight
try:
moles = self.get_solute(solute).get_moles()
mw = self.get_solute(solute).get_molecular_weight()
moles = self.get_solute(solute).moles
mw = self.get_solute(solute).mw
# if the solute is not present in the solution, we'll get a KeyError
# In that case, the amount is zero
except KeyError:
Expand Down Expand Up @@ -1159,7 +1159,7 @@ def add_amount(self, solute, amount):
# volume in L, density in kg/m3 = g/L
target_mass = target_vol.magnitude * self.water_substance.rho * unit.Quantity("1 g")

mw = self.get_solvent().get_molecular_weight()
mw = self.get_solvent().mw
target_mol = target_mass / mw
self.get_solvent().moles = target_mol

Expand Down Expand Up @@ -1239,7 +1239,7 @@ def set_amount(self, solute, amount):

# adjust the amount of solvent
target_mass = target_vol.magnitude / 1000 * self.water_substance.rho * unit.Quantity("1 kg")
mw = self.get_solvent().get_molecular_weight()
mw = self.get_solvent().mw
target_mol = target_mass / mw
self.get_solvent().moles = target_mol

Expand Down Expand Up @@ -1288,7 +1288,7 @@ def get_total_moles_solute(self):
tot_mol = 0
for item in self.components:
if item != self.solvent_name:
tot_mol += self.components[item].get_moles()
tot_mol += self.components[item].moles
return tot_mol

def get_moles_solvent(self):
Expand Down Expand Up @@ -1663,7 +1663,7 @@ def get_transport_number(self, solute, activity_correction=False):
numerator = 0

for item in self.components:
z = self.get_solute(item).get_formal_charge()
z = self.get_solute(item).charge
term = self.get_property(item, "diffusion_coefficient") * z**2 * self.get_amount(item, "mol/L")

if activity_correction is True:
Expand Down Expand Up @@ -1725,12 +1725,7 @@ def get_molar_conductivity(self, solute):
"""
D = self.get_property(solute, "diffusion_coefficient")

molar_cond = (
D
* (unit.e * unit.N_A) ** 2
* self.get_solute(solute).get_formal_charge() ** 2
/ (unit.R * self.temperature)
)
molar_cond = D * (unit.e * unit.N_A) ** 2 * self.get_solute(solute).charge ** 2 / (unit.R * self.temperature)

logger.info(f"Computed molar conductivity as {molar_cond} from D = {D!s} at T={self.temperature}")

Expand Down Expand Up @@ -1767,9 +1762,7 @@ def get_mobility(self, solute):
"""
D = self.get_property(solute, "diffusion_coefficient")

mobility = (
unit.N_A * unit.e * abs(self.get_solute(solute).get_formal_charge()) * D / (unit.R * self.temperature)
)
mobility = unit.N_A * unit.e * abs(self.get_solute(solute).charge) * D / (unit.R * self.temperature)

logger.info(f"Computed ionic mobility as {mobility} from D = {D!s} at T={self.temperature}")

Expand Down Expand Up @@ -1826,7 +1819,7 @@ def get_property(self, solute, name):
if name == "partial_molar_volume":
# calculate the partial molar volume for water since it isn't in the database
if solute == "H2O":
vol = self.get_solute("H2O").get_molecular_weight() / self.water_substance.rho * unit.Quantity("1 g/L")
vol = self.get_solute("H2O").mw / self.water_substance.rho * unit.Quantity("1 g/L")

return vol.to("cm **3 / mol")

Expand Down Expand Up @@ -2044,15 +2037,15 @@ def list_concentrations(self, unit="mol/kg", decimals=4, type="all"):
print("Cation Concentrations:\n")
print("========================\n")
for item in self.components:
if self.components[item].get_formal_charge() > 0:
if self.components[item].charge > 0:
amount = self.get_amount(item, unit)
result_list.append([item, amount])
print(item + ":" + "\t {0:0.{decimals}f~}".format(amount, decimals=decimals))
elif type == "anions":
print("Anion Concentrations:\n")
print("========================\n")
for item in self.components:
if self.components[item].get_formal_charge() < 0:
if self.components[item].charge < 0:
amount = self.get_amount(item, unit)
result_list.append([item, amount])
print(item + ":" + "\t {0:0.{decimals}f~}".format(amount, decimals=decimals))
Expand Down
2 changes: 1 addition & 1 deletion tests/test_pyeql_volume_concentration.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def test_empty_solution_3(self):
# It should have exactly 1L volume
assert s1.get_volume().to("L").magnitude == 1.0
# the solvent should be water
assert s1.get_solvent().get_name() == "H2O"
assert s1.get_solvent().formula == "H2O"
# It should have 0.997 kg water mass
assert np.isclose(s1.get_solvent_mass().to("kg").magnitude, 0.9970415)
# the temperature should be 25 degC
Expand Down
Loading

0 comments on commit 972579e

Please sign in to comment.