Skip to content

Commit 1612550

Browse files
authored
Merge pull request #153 from KingsburyLab/bugfix
Solution: fix auto charge balance bug
2 parents 83d1cac + 8bef6a4 commit 1612550

File tree

4 files changed

+67
-44
lines changed

4 files changed

+67
-44
lines changed

CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.1.2] - 2024-07-28
9+
10+
### Fixed
11+
12+
- `Solution`: Fix a bug in which setting `balance_charge` to `auto` when the initial
13+
composition was electroneutral would cause errors and/or improper charge balancing
14+
after `equilibrate` was called.
15+
816
## [1.1.1] - 2024-07-27
917

1018
### Fixed

src/pyEQL/engines.py

+5-14
Original file line numberDiff line numberDiff line change
@@ -229,10 +229,7 @@ def _setup_ppsol(self, solution: "Solution") -> None:
229229
d[key] = str(mol / solv_mass)
230230

231231
# tell PHREEQC which species to use for charge balance
232-
if (
233-
solution.balance_charge is not None
234-
and solution.balance_charge in solution.get_components_by_element()[el]
235-
):
232+
if solution.balance_charge is not None and solution._cb_species in solution.get_components_by_element()[el]:
236233
d[key] += " charge"
237234

238235
# create the PHREEQC solution object
@@ -698,18 +695,12 @@ def equilibrate(self, solution: "Solution") -> None:
698695
if charge_adjust != 0:
699696
logger.warning(
700697
"After equilibration, the charge balance of the solution was not electroneutral."
701-
f" {charge_adjust} eq of charge were added via {solution.balance_charge}"
698+
f" {charge_adjust} eq of charge were added via {solution._cb_species}"
702699
)
703700

704-
if solution.balance_charge is None:
705-
pass
706-
elif solution.balance_charge == "pH":
707-
solution.components["H+"] += charge_adjust
708-
elif solution.balance_charge == "pE":
709-
raise NotImplementedError
710-
else:
711-
z = solution.get_property(solution.balance_charge, "charge")
712-
solution.add_amount(solution.balance_charge, f"{charge_adjust/z} mol")
701+
if solution.balance_charge is not None:
702+
z = solution.get_property(solution._cb_species, "charge")
703+
solution.add_amount(solution._cb_species, f"{charge_adjust/z} mol")
713704

714705
# rescale the solvent mass to ensure the total mass of solution does not change
715706
# this is important because PHREEQC and the pyEQL database may use slightly different molecular

src/pyEQL/solution.py

+31-26
Original file line numberDiff line numberDiff line change
@@ -261,39 +261,44 @@ def __init__(
261261
for item in self._solutes:
262262
self.add_solute(*item)
263263

264-
# adjust the charge balance, if necessary
264+
# determine the species that will be used for charge balancing, when needed.
265+
# this is necessary to do even if the composition is already electroneutral,
266+
# because the appropriate species also needs to be passed to equilibrate
267+
# to keep from distorting the charge balance.
265268
cb = self.charge_balance
269+
if self.balance_charge is None:
270+
self._cb_species = None
271+
elif self.balance_charge == "pH":
272+
self._cb_species = "H[+1]"
273+
elif self.balance_charge == "pE":
274+
raise NotImplementedError("Balancing charge via redox (pE) is not yet implemented!")
275+
elif self.balance_charge == "auto":
276+
# add the most abundant ion of the opposite charge
277+
if cb <= 0:
278+
self._cb_species = max(self.cations, key=self.cations.get)
279+
elif cb > 0:
280+
self._cb_species = max(self.anions, key=self.anions.get)
281+
else:
282+
ions = set().union(*[self.cations, self.anions]) # all ions
283+
self._cb_species = self.balance_charge
284+
if self._cb_species not in ions:
285+
raise ValueError(
286+
f"Charge balancing species {self._cb_species} was not found in the solution!. "
287+
f"Species {ions} were found."
288+
)
289+
290+
# adjust charge balance, if necessary
266291
if not np.isclose(cb, 0, atol=1e-8) and self.balance_charge is not None:
267292
balanced = False
268293
self.logger.info(
269-
f"Solution is not electroneutral (C.B. = {cb} eq/L). Adding {balance_charge} to compensate."
294+
f"Solution is not electroneutral (C.B. = {cb} eq/L). Adding {self._cb_species} to compensate."
270295
)
271-
if self.balance_charge == "pH":
272-
self.components["H+"] += (
273-
-1 * cb * self.volume.to("L").magnitude
274-
) # if C.B. is negative, we need to add cations. H+ is 1 eq/mol
296+
z = self.get_property(self._cb_species, "charge")
297+
self.components[self._cb_species] += -1 * cb / z * self.volume.to("L").magnitude
298+
if np.isclose(self.charge_balance, 0, atol=1e-8):
275299
balanced = True
276-
elif self.balance_charge == "pE":
277-
raise NotImplementedError("Balancing charge via redox (pE) is not yet implemented!")
278-
else:
279-
ions = set().union(*[self.cations, self.anions]) # all ions
280-
if self.balance_charge == "auto":
281-
# add the most abundant ion of the opposite charge
282-
if cb <= 0:
283-
self.balance_charge = max(self.cations, key=self.cations.get)
284-
elif cb > 0:
285-
self.balance_charge = max(self.anions, key=self.anions.get)
286-
if self.balance_charge not in ions:
287-
raise ValueError(
288-
f"Charge balancing species {self.balance_charge} was not found in the solution!. "
289-
f"Species {ions} were found."
290-
)
291-
z = self.get_property(self.balance_charge, "charge")
292-
self.components[self.balance_charge] += -1 * cb / z * self.volume.to("L").magnitude
293-
balanced = True
294-
295300
if not balanced:
296-
warnings.warn(f"Unable to balance charge using species {self.balance_charge}")
301+
warnings.warn(f"Unable to balance charge using species {self._cb_species}")
297302

298303
@property
299304
def mass(self) -> Quantity:

tests/test_solution.py

+23-4
Original file line numberDiff line numberDiff line change
@@ -268,16 +268,35 @@ def test_charge_balance(s3, s5, s5_pH, s6, s6_Ca):
268268
volume="1 L",
269269
balance_charge="auto",
270270
)
271-
assert s.balance_charge == "Na[+1]"
271+
assert s.balance_charge == "auto"
272+
assert s._cb_species == "Na[+1]"
272273
assert np.isclose(s.charge_balance, 0, atol=1e-8)
273274
s.equilibrate()
274-
assert s.balance_charge == "Na[+1]"
275+
assert s.balance_charge == "auto"
276+
assert s._cb_species == "Na[+1]"
277+
assert np.isclose(s.charge_balance, 0, atol=1e-8)
275278

276279
s = Solution({"Na+": "2 mM", "Cl-": "1 mM"}, balance_charge="auto")
277-
assert s.balance_charge == "Cl[-1]"
280+
assert s.balance_charge == "auto"
281+
assert s._cb_species == "Cl[-1]"
278282
assert np.isclose(s.charge_balance, 0, atol=1e-8)
279283
s.equilibrate()
280-
assert s.balance_charge == "Cl[-1]"
284+
assert s.balance_charge == "auto"
285+
assert s._cb_species == "Cl[-1]"
286+
assert np.isclose(s.charge_balance, 0, atol=1e-8)
287+
288+
# check "auto" with an electroneutral solution
289+
s = Solution({"Na+": "2 mM", "Cl-": "2 mM"}, balance_charge="auto")
290+
assert s.balance_charge == "auto"
291+
assert s._cb_species == "Na[+1]"
292+
assert np.isclose(s.charge_balance, 0, atol=1e-8)
293+
s.equilibrate()
294+
assert s.balance_charge == "auto"
295+
assert s._cb_species == "Na[+1]"
296+
assert np.isclose(s.charge_balance, 0, atol=1e-8)
297+
298+
with pytest.raises(ValueError, match=r"Charge balancing species Zr\[\+4\] was not found"):
299+
s = Solution({"Na+": "2 mM", "Cl-": "2 mM"}, balance_charge="Zr[+4]")
281300

282301

283302
def test_alkalinity_hardness(s3, s5, s6):

0 commit comments

Comments
 (0)