From 800eb886b029197a2076a8a2275857bd8056d476 Mon Sep 17 00:00:00 2001 From: Ryan Date: Fri, 26 Jul 2024 12:38:09 -0400 Subject: [PATCH] add auto charge balancing --- CHANGELOG.md | 8 +++++++- src/pyEQL/solution.py | 26 ++++++++++++++++++-------- tests/test_solution.py | 17 +++++++++++++++++ 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89c95b2..d3acdaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.0.4] - 2024-07-26 +## [1.1.0] - 2024-07-26 ### Fixed @@ -13,6 +13,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 balancing. - Database: `size.radius_ionic` was missing units for `Ni[+2]` and `Cr[+3]`. Correct units have been added. +### Added + +- `Solution`: New automatic charge balancing method will automatically identify the majority (highest concentration) + cation or anion as appropriate (depending on the charge balance) for charge balancing. To use this mode, set + `balance_charge='auto'` when instantiating a `Solution`. + ### Changed - `Solution.add_amount`: This method will now add solutes that are absent from the Solution. Previously, calling, e.g., diff --git a/src/pyEQL/solution.py b/src/pyEQL/solution.py index f726a64..12320c7 100644 --- a/src/pyEQL/solution.py +++ b/src/pyEQL/solution.py @@ -92,11 +92,15 @@ def __init__( -7 to +14. The default value corresponds to a pE value typical of natural waters in equilibrium with the atmosphere. balance_charge: The strategy for balancing charge during init and equilibrium calculations. Valid options - are 'pH', which will adjust the solution pH to balance charge, 'pE' which will adjust the - redox equilibrium to balance charge, or the name of a dissolved species e.g. 'Ca+2' or 'Cl-' - that will be added/subtracted to balance charge. If set to None, no charge balancing will be - performed either on init or when equilibrate() is called. Note that in this case, equilibrate() - can distort the charge balance! + are + - 'pH', which will adjust the solution pH to balance charge, + - 'auto' which will use the majority cation or anion (i.e., that with the largest concentration) + as needed, + - 'pE' (not currently implemented) which will adjust the redox equilibrium to balance charge, or + the name of a dissolved species e.g. 'Ca+2' or 'Cl-' that will be added/subtracted to balance + charge. + - None (default), in which case no charge balancing will be performed either on init or when + equilibrate() is called. Note that in this case, equilibrate() can distort the charge balance! solvent: Formula of the solvent. Solvents other than water are not supported at this time. engine: Electrolyte modeling engine to use. See documentation for details on the available engines. database: path to a .json file (str or Path) or maggma Store instance that @@ -171,7 +175,7 @@ def __init__( self._pE = pE self._pH = pH self.pE = self._pE - if isinstance(balance_charge, str) and balance_charge not in ["pH", "pE"]: + if isinstance(balance_charge, str) and balance_charge not in ["pH", "pE", "auto"]: self.balance_charge = standardize_formula(balance_charge) else: self.balance_charge = balance_charge #: Standardized formula of the species used for charge balancing. @@ -273,13 +277,19 @@ def __init__( raise NotImplementedError("Balancing charge via redox (pE) is not yet implemented!") else: ions = set().union(*[self.cations, self.anions]) # all ions + if self.balance_charge == "auto": + # add the most abundant ion of the opposite charge + if cb < 0: + self.balance_charge = max(self.cations, key=self.cations.get) + elif cb >0: + self.balance_charge = max(self.anions, key=self.anions.get) if self.balance_charge not in ions: raise ValueError( f"Charge balancing species {self.balance_charge} was not found in the solution!. " f"Species {ions} were found." ) - z = self.get_property(balance_charge, "charge") - self.components[balance_charge] += -1 * cb / z * self.volume.to("L").magnitude + z = self.get_property(self.balance_charge, "charge") + self.components[self.balance_charge] += -1 * cb / z * self.volume.to("L").magnitude balanced = True if not balanced: diff --git a/tests/test_solution.py b/tests/test_solution.py index 3583d85..a13582c 100644 --- a/tests/test_solution.py +++ b/tests/test_solution.py @@ -255,6 +255,23 @@ def test_charge_balance(s3, s5, s5_pH, s6, s6_Ca): assert np.isclose(s6.charge_balance, -0.12) assert np.isclose(s6_Ca.charge_balance, 0, atol=1e-8) + # test auto charge balance + s=Solution( + [ + ["Ca+2", "1 mM"], # 2 meq/L + ["Mg+2", "5 mM"], # 10 meq/L + ["Na+1", "10 mM"], # 10 meq/L + ["Ag+1", "10 mM"], # no contribution to alk or hardness + ["CO3-2", "6 mM"], # no contribution to alk or hardness + ["SO4-2", "60 mM"], # -120 meq/L + ["Br-", "20 mM"], + ], # -20 meq/L + volume="1 L", + balance_charge="auto", + ) + assert s.balance_charge == 'Na[+1]' + assert np.isclose(s.charge_balance, 0, atol=1e-8) + def test_alkalinity_hardness(s3, s5, s6): assert np.isclose(s3.hardness, 0)