pyEQL 1.1.1__py3-none-any.whl → 1.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyEQL/engines.py +3 -24
- pyEQL/solution.py +79 -32
- {pyEQL-1.1.1.dist-info → pyEQL-1.1.3.dist-info}/METADATA +1 -1
- {pyEQL-1.1.1.dist-info → pyEQL-1.1.3.dist-info}/RECORD +9 -9
- {pyEQL-1.1.1.dist-info → pyEQL-1.1.3.dist-info}/AUTHORS.md +0 -0
- {pyEQL-1.1.1.dist-info → pyEQL-1.1.3.dist-info}/COPYING +0 -0
- {pyEQL-1.1.1.dist-info → pyEQL-1.1.3.dist-info}/LICENSE.txt +0 -0
- {pyEQL-1.1.1.dist-info → pyEQL-1.1.3.dist-info}/WHEEL +0 -0
- {pyEQL-1.1.1.dist-info → pyEQL-1.1.3.dist-info}/top_level.txt +0 -0
pyEQL/engines.py
CHANGED
|
@@ -229,10 +229,7 @@ class NativeEOS(EOS):
|
|
|
229
229
|
d[key] = str(mol / solv_mass)
|
|
230
230
|
|
|
231
231
|
# 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]:
|
|
236
233
|
d[key] += " charge"
|
|
237
234
|
|
|
238
235
|
# create the PHREEQC solution object
|
|
@@ -690,26 +687,8 @@ class NativeEOS(EOS):
|
|
|
690
687
|
)
|
|
691
688
|
|
|
692
689
|
# re-adjust charge balance for any missing species
|
|
693
|
-
# note that if balance_charge is set, it will have been passed to PHREEQC, so
|
|
694
|
-
|
|
695
|
-
charge_adjust = 0
|
|
696
|
-
for s in missing_species:
|
|
697
|
-
charge_adjust += -1 * solution.get_amount(s, "eq").magnitude
|
|
698
|
-
if charge_adjust != 0:
|
|
699
|
-
logger.warning(
|
|
700
|
-
"After equilibration, the charge balance of the solution was not electroneutral."
|
|
701
|
-
f" {charge_adjust} eq of charge were added via {solution.balance_charge}"
|
|
702
|
-
)
|
|
703
|
-
|
|
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")
|
|
690
|
+
# note that if balance_charge is set, it will have been passed to PHREEQC, so the only reason to re-adjust charge balance here is to account for any missing species.
|
|
691
|
+
solution._adjust_charge_balance()
|
|
713
692
|
|
|
714
693
|
# rescale the solvent mass to ensure the total mass of solution does not change
|
|
715
694
|
# this is important because PHREEQC and the pyEQL database may use slightly different molecular
|
pyEQL/solution.py
CHANGED
|
@@ -35,6 +35,7 @@ from pyEQL.utils import FormulaDict, create_water_substance, interpret_units, st
|
|
|
35
35
|
EQUIV_WT_CACO3 = ureg.Quantity(100.09 / 2, "g/mol")
|
|
36
36
|
# string to denote unknown oxidation states
|
|
37
37
|
UNKNOWN_OXI_STATE = "unk"
|
|
38
|
+
K_W = 1e-14 # ion product of water at 25 degC
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
class Solution(MSONable):
|
|
@@ -242,7 +243,7 @@ class Solution(MSONable):
|
|
|
242
243
|
|
|
243
244
|
# set the pH with H+ and OH-
|
|
244
245
|
self.add_solute("H+", str(10 ** (-1 * pH)) + "mol/L")
|
|
245
|
-
self.add_solute("OH-", str(10 ** (-1 *
|
|
246
|
+
self.add_solute("OH-", str(K_W / (10 ** (-1 * pH))) + "mol/L")
|
|
246
247
|
|
|
247
248
|
# populate the other solutes
|
|
248
249
|
self._solutes = solutes
|
|
@@ -261,39 +262,34 @@ class Solution(MSONable):
|
|
|
261
262
|
for item in self._solutes:
|
|
262
263
|
self.add_solute(*item)
|
|
263
264
|
|
|
264
|
-
#
|
|
265
|
+
# determine the species that will be used for charge balancing, when needed.
|
|
266
|
+
# this is necessary to do even if the composition is already electroneutral,
|
|
267
|
+
# because the appropriate species also needs to be passed to equilibrate
|
|
268
|
+
# to keep from distorting the charge balance.
|
|
265
269
|
cb = self.charge_balance
|
|
266
|
-
if
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
elif
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
|
270
|
+
if self.balance_charge is None:
|
|
271
|
+
self._cb_species = None
|
|
272
|
+
elif self.balance_charge == "pH":
|
|
273
|
+
self._cb_species = "H[+1]"
|
|
274
|
+
elif self.balance_charge == "pE":
|
|
275
|
+
raise NotImplementedError("Balancing charge via redox (pE) is not yet implemented!")
|
|
276
|
+
elif self.balance_charge == "auto":
|
|
277
|
+
# add the most abundant ion of the opposite charge
|
|
278
|
+
if cb <= 0:
|
|
279
|
+
self._cb_species = max(self.cations, key=self.cations.get)
|
|
280
|
+
elif cb > 0:
|
|
281
|
+
self._cb_species = max(self.anions, key=self.anions.get)
|
|
282
|
+
else:
|
|
283
|
+
ions = set().union(*[self.cations, self.anions]) # all ions
|
|
284
|
+
self._cb_species = self.balance_charge
|
|
285
|
+
if self._cb_species not in ions:
|
|
286
|
+
raise ValueError(
|
|
287
|
+
f"Charge balancing species {self._cb_species} was not found in the solution!. "
|
|
288
|
+
f"Species {ions} were found."
|
|
289
|
+
)
|
|
294
290
|
|
|
295
|
-
|
|
296
|
-
|
|
291
|
+
# adjust charge balance, if necessary
|
|
292
|
+
self._adjust_charge_balance()
|
|
297
293
|
|
|
298
294
|
@property
|
|
299
295
|
def mass(self) -> Quantity:
|
|
@@ -2288,6 +2284,57 @@ class Solution(MSONable):
|
|
|
2288
2284
|
|
|
2289
2285
|
return distance.to("nm")
|
|
2290
2286
|
|
|
2287
|
+
def _adjust_charge_balance(self, atol=1e-8) -> None:
|
|
2288
|
+
"""Helper method to adjust the charge balance of the Solution."""
|
|
2289
|
+
cb = self.charge_balance
|
|
2290
|
+
if not np.isclose(cb, 0, atol=atol):
|
|
2291
|
+
self.logger.info(f"Solution is not electroneutral (C.B. = {cb} eq/L).")
|
|
2292
|
+
if self.balance_charge is None:
|
|
2293
|
+
# Nothing to do.
|
|
2294
|
+
self.logger.info("balance_charge is None, so no charge balancing will be performed.")
|
|
2295
|
+
return
|
|
2296
|
+
|
|
2297
|
+
self.logger.info(
|
|
2298
|
+
f"Solution is not electroneutral (C.B. = {cb} eq/L). Adjusting {self._cb_species} to compensate."
|
|
2299
|
+
)
|
|
2300
|
+
|
|
2301
|
+
if self.balance_charge == "pH":
|
|
2302
|
+
# the charge imbalance associated with the H+ / OH- system can be expressed
|
|
2303
|
+
# as ([H+] - [OH-]) or ([H+] - K_W/[H+]). If we adjust H+, we also have to
|
|
2304
|
+
# adjust OH- to maintain water equilibrium.
|
|
2305
|
+
C_hplus = self.get_amount("H+", "mol/L").magnitude
|
|
2306
|
+
start_imbalance = C_hplus - K_W / C_hplus
|
|
2307
|
+
new_imbalance = start_imbalance - cb
|
|
2308
|
+
# calculate the new concentration of H+ that will balance the charge
|
|
2309
|
+
# solve H^2 - new_imbalance H - K_W = 0, so a=1, b=-new_imbalance, c=-K_W
|
|
2310
|
+
# check b^2 - 4ac; are there any real roots?
|
|
2311
|
+
if new_imbalance**2 - 4 * 1 * K_W < 0:
|
|
2312
|
+
self.logger.error("Cannot balance charge by adjusting pH. The imbalance is too large.")
|
|
2313
|
+
return
|
|
2314
|
+
new_hplus = max(
|
|
2315
|
+
[
|
|
2316
|
+
(new_imbalance + np.sqrt(new_imbalance**2 + 4 * 1 * K_W)) / 2,
|
|
2317
|
+
(new_imbalance - np.sqrt(new_imbalance**2 + 4 * 1 * K_W)) / 2,
|
|
2318
|
+
]
|
|
2319
|
+
)
|
|
2320
|
+
self.set_amount("H+", f"{new_hplus} mol/L")
|
|
2321
|
+
self.set_amount("OH-", f"{K_W/new_hplus} mol/L")
|
|
2322
|
+
assert np.isclose(self.charge_balance, 0, atol=atol), f"{self.charge_balance}"
|
|
2323
|
+
return
|
|
2324
|
+
|
|
2325
|
+
z = self.get_property(self._cb_species, "charge")
|
|
2326
|
+
try:
|
|
2327
|
+
self.add_amount(self._cb_species, f"{-1*cb/z} mol")
|
|
2328
|
+
return
|
|
2329
|
+
except ValueError:
|
|
2330
|
+
# if the concentration is negative, it must mean there is not enough present.
|
|
2331
|
+
# remove everything that's present and log an error.
|
|
2332
|
+
self.components[self._cb_species] = 0
|
|
2333
|
+
self.logger.error(
|
|
2334
|
+
f"There is not enough {self._cb_species} present to balance the charge. Try a different species."
|
|
2335
|
+
)
|
|
2336
|
+
return
|
|
2337
|
+
|
|
2291
2338
|
def _update_volume(self):
|
|
2292
2339
|
"""Recalculate the solution volume based on composition."""
|
|
2293
2340
|
self._volume = self._get_solvent_volume() + self._get_solute_volume()
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
pyEQL/__init__.py,sha256=OCp_PiQEPyVoi1VX0ursBzHJWN6nDS1Id6bTBOgqCYs,1999
|
|
2
2
|
pyEQL/activity_correction.py,sha256=eOixjgTd5hTrTRD5s6aPCCG12lAIH7-lRN0Z1qHu678,37151
|
|
3
|
-
pyEQL/engines.py,sha256=
|
|
3
|
+
pyEQL/engines.py,sha256=VbdQSPKlNehW96U1XxWYwjTy6k6WDpZZEx_Y4l3qZv4,34686
|
|
4
4
|
pyEQL/equilibrium.py,sha256=YCtoAJSgn1WC9NJnc3H4FTJdKQvogsvCuj7HqlKMtww,8307
|
|
5
5
|
pyEQL/functions.py,sha256=nc-Hc61MmW-ELBR1PByJvQnELxM7PZexMHbU_O5-Bnw,10584
|
|
6
6
|
pyEQL/salt_ion_match.py,sha256=0nCZXmeo67VqcyYWQpPx-81hjSvnsg8HFB3fIyfjW_k,4070
|
|
7
7
|
pyEQL/solute.py,sha256=no00Rc3tRfHmyht4wm2UXA1KZhKC45tWMO5QEkZY6yg,5140
|
|
8
|
-
pyEQL/solution.py,sha256=
|
|
8
|
+
pyEQL/solution.py,sha256=LwlfY6Q19QEcY8MJpP_mcJgEw0ohNaGfAqAiqhdPmU8,116022
|
|
9
9
|
pyEQL/utils.py,sha256=DWLtNm71qw5j4-jqBp5v3LssEjWgJnVvI6a_H60c5ic,6670
|
|
10
10
|
pyEQL/database/geothermal.dat,sha256=kksnfcBtWdOTpNn4CLXU1Mz16cwas2WuVKpuMU8CaVI,234230
|
|
11
11
|
pyEQL/database/llnl.dat,sha256=jN-a0kfUFbQlYMn2shTVRg1JX_ZhLa-tJ0lLw2YSpLU,751462
|
|
@@ -17,10 +17,10 @@ pyEQL/presets/rainwater.yaml,sha256=S0WHZNDfCJyjSSFxNFdkypjn2s3P0jJGCiYIxvi1ibA,
|
|
|
17
17
|
pyEQL/presets/seawater.yaml,sha256=oryc1CkhRz20RpWE6uiGiT93HoZnqlB0s-0PmBWr3-U,843
|
|
18
18
|
pyEQL/presets/urine.yaml,sha256=0Njtc-H1fFRo7UhquHdiSTT4z-8VZJ1utDCk02qk28M,679
|
|
19
19
|
pyEQL/presets/wastewater.yaml,sha256=jTTFBpmKxczaEtkCZb0xUULIPZt7wfC8eAJ6rthGnmw,502
|
|
20
|
-
pyEQL-1.1.
|
|
21
|
-
pyEQL-1.1.
|
|
22
|
-
pyEQL-1.1.
|
|
23
|
-
pyEQL-1.1.
|
|
24
|
-
pyEQL-1.1.
|
|
25
|
-
pyEQL-1.1.
|
|
26
|
-
pyEQL-1.1.
|
|
20
|
+
pyEQL-1.1.3.dist-info/AUTHORS.md,sha256=K9ZLhKFwZ2zLlFXwN62VuUYCpr5T6n4mOUCUHlytTUs,415
|
|
21
|
+
pyEQL-1.1.3.dist-info/COPYING,sha256=Ww2oUywfFTn242v9ksCgQdIVSpcMXJiKKePn0GFm25E,7649
|
|
22
|
+
pyEQL-1.1.3.dist-info/LICENSE.txt,sha256=2Zf1F7RzbpeposgIxUydpurqNCMoMgDi2gAB65_GjwQ,969
|
|
23
|
+
pyEQL-1.1.3.dist-info/METADATA,sha256=aDLyZrhXd2laVUiJ6drgt4RFSvlptOPxLEo9xrkaR40,6096
|
|
24
|
+
pyEQL-1.1.3.dist-info/WHEEL,sha256=Wyh-_nZ0DJYolHNn1_hMa4lM7uDedD_RGVwbmTjyItk,91
|
|
25
|
+
pyEQL-1.1.3.dist-info/top_level.txt,sha256=QMOaZjCAm_lS4Njsjh4L0B5aWnJFGQMYKhuH88CG1co,6
|
|
26
|
+
pyEQL-1.1.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|