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

NativeEOS: ensure equilibrate does not change solvent mass #118

Merged
merged 3 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- `Solution`: Fixed an issue in which repeated calls to `equilibrate` when using `NativeEOS` or `PHREEQCEOS` would
change the mass of the `Solution` slightly. This was attributed to the fact that `pyEQL` and `PHREEQC` use slightly
different molecular weights for water.
- `Solution`: `get_total_amount` and related methods could fail when the oxidation state of an element was
unknown (e.g., 'Br') (Issue [#116](https://github.com/KingsburyLab/pyEQL/issues/116))

Expand Down
49 changes: 28 additions & 21 deletions src/pyEQL/engines.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
:license: LGPL, see LICENSE for more details.

"""

import os
import warnings
from abc import ABC, abstractmethod
Expand Down Expand Up @@ -189,7 +190,6 @@
# PHREEQC will assume 1 kg if not specified, there is also no direct way to specify volume, so we
# really have to specify the solvent mass in 1 liter of solution
"water": solv_mass,
"density": solution.density.to("g/mL").magnitude,
}
if solution.balance_charge == "pH":
d["pH"] = str(d["pH"]) + " charge"
Expand Down Expand Up @@ -234,9 +234,7 @@
self.ppsol = ppsol

def _destroy_ppsol(self):
"""
Remove the PhreeqPython solution from memory
"""
"""Remove the PhreeqPython solution from memory"""
if self.ppsol is not None:
self.ppsol.forget()
self.ppsol = None
Expand Down Expand Up @@ -370,9 +368,7 @@
)

logger.info(
"Calculated activity coefficient of species {} as {} based on salt {} using Pitzer model".format(
solute, activity_coefficient, salt
)
f"Calculated activity coefficient of species {solute} as {activity_coefficient} based on salt {salt} using Pitzer model"
)
molal = activity_coefficient

Expand Down Expand Up @@ -549,9 +545,7 @@
)

logger.info(
"Calculated osmotic coefficient of water as {} based on salt {} using Pitzer model".format(
osmotic_coefficient, item.formula
)
f"Calculated osmotic coefficient of water as {osmotic_coefficient} based on salt {item.formula} using Pitzer model"
)
effective_osmotic_sum += concentration * osmotic_coefficient

Expand Down Expand Up @@ -658,7 +652,9 @@
if self.ppsol is not None:
self.ppsol.forget()
self._setup_ppsol(solution)
# self._stored_comp = solution.components

# store the original solvent mass
orig_solvent_moles = solution.components[solution.solvent]

# use the output from PHREEQC to update the Solution composition
# the .species_moles attribute should return MOLES (not moles per ___)
Expand All @@ -679,17 +675,28 @@
)
for s in missing_species:
charge_adjust += -1 * solution.get_amount(s, "eq").magnitude
if charge_adjust != 0:
logger.warning(
"After equilibration, the charge balance of the solution was not electroneutral."
f" {charge_adjust} eq of charge were added via {solution.balance_charge}"
)

# re-adjust charge balance
if solution.balance_charge is None:
pass
elif solution.balance_charge == "pH":
solution.components["H+"] += charge_adjust
elif solution.balance_charge == "pE":
raise NotImplementedError
else:
z = solution.get_property(solution.balance_charge, "charge")
solution.add_amount(solution.balance_charge, f"{charge_adjust/z} mol")
# re-adjust charge balance
if solution.balance_charge is None:
pass
elif solution.balance_charge == "pH":
solution.components["H+"] += charge_adjust

Check warning on line 688 in src/pyEQL/engines.py

View check run for this annotation

Codecov / codecov/patch

src/pyEQL/engines.py#L688

Added line #L688 was not covered by tests
elif solution.balance_charge == "pE":
raise NotImplementedError
else:
z = solution.get_property(solution.balance_charge, "charge")
solution.add_amount(solution.balance_charge, f"{charge_adjust/z} mol")

# rescale the solvent mass to ensure the total mass of solution does not change
# this is important because PHREEQC and the pyEQL database may use slightly different molecular
# weights for water. Since water amount is passed to PHREEQC in kg but returned in moles, each
# call to equilibrate can thus result in a slight change in the Solution mass.
solution.components[solution.solvent] = orig_solvent_moles

def __deepcopy__(self, memo):
# custom deepcopy required because the PhreeqPython instance used by the Native and Phreeqc engines
Expand Down
21 changes: 16 additions & 5 deletions tests/test_phreeqc.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,33 +168,44 @@ def test_equilibrate(s1, s2, s5_pH, s6_Ca, caplog):
assert "H2(aq)" not in s1.components
orig_pH = s1.pH
orig_pE = s1.pE
orig_mass = s1.mass
orig_density = s2.density.magnitude
orig_solv_mass = s2.solvent_mass.magnitude
s1.equilibrate()
assert "H2(aq)" in s1.components
assert np.isclose(s1.charge_balance, 0, atol=1e-8)
assert np.isclose(s1.pH, orig_pH, atol=0.01)
assert np.isclose(s1.pE, orig_pE)
# assert np.isclose(s1.mass, orig_mass)
# assert np.isclose(s1.density.magnitude, orig_density)
# assert np.isclose(s1.solvent_mass.magnitude, orig_solv_mass)

assert "NaOH(aq)" not in s2.components
s2.equilibrate()
orig_pH = s2.pH
orig_pE = s2.pE
orig_density = s2.density.magnitude
orig_solv_mass = s2.solvent_mass.magnitude
orig_pH = s2.pH
orig_pE = s2.pE
orig_mass = s2.mass
s2.equilibrate()
assert "NaOH(aq)" in s2.components

# total element concentrations should be conserved after equilibrating
assert np.isclose(s2.get_total_amount("Na", "mol").magnitude, 8)
assert np.isclose(s2.get_total_amount("Cl", "mol").magnitude, 8)
assert np.isclose(s2.solvent_mass.magnitude, orig_solv_mass)
assert np.isclose(s2.density.magnitude, orig_density)
# the pH should drop due to hydrolysis of Ca+2
assert s2.pH < orig_pH
assert np.isclose(s2.pE, orig_pE)
assert np.isclose(s2.mass, orig_mass)
# this solution has balance_charge=None, therefore, the charge balance
# may be off after equilibration
assert not np.isclose(s2.charge_balance, 0, atol=1e-8)
assert np.isclose(s2.pH, orig_pH, atol=0.01)
assert np.isclose(s2.pE, orig_pE)
eq_Hplus = s2.components["H+"]
s2.balance_charge = "pH"
s2.equilibrate()
assert np.isclose(s2.charge_balance, 0, atol=1e-8)
assert s2.components["H+"] > eq_Hplus

# test log message if there is a species not present in the phreeqc database
s_zr = Solution({"Zr+4": "0.05 mol/kg", "Na+": "0.05 mol/kg", "Cl-": "0.1 mol/kg"}, engine="phreeqc")
Expand Down
Loading