pyEQL 0.15.1__py2.py3-none-any.whl → 1.0.1__py2.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/__init__.py +9 -1
- pyEQL/activity_correction.py +24 -21
- pyEQL/engines.py +56 -42
- pyEQL/equilibrium.py +18 -12
- pyEQL/functions.py +8 -161
- pyEQL/salt_ion_match.py +1 -0
- pyEQL/solute.py +3 -0
- pyEQL/solution.py +189 -763
- pyEQL/utils.py +3 -0
- {pyEQL-0.15.1.dist-info → pyEQL-1.0.1.dist-info}/METADATA +9 -10
- pyEQL-1.0.1.dist-info/RECORD +27 -0
- pyEQL/logging_system.py +0 -78
- pyEQL-0.15.1.dist-info/RECORD +0 -28
- {pyEQL-0.15.1.dist-info → pyEQL-1.0.1.dist-info}/AUTHORS.md +0 -0
- {pyEQL-0.15.1.dist-info → pyEQL-1.0.1.dist-info}/COPYING +0 -0
- {pyEQL-0.15.1.dist-info → pyEQL-1.0.1.dist-info}/LICENSE.txt +0 -0
- {pyEQL-0.15.1.dist-info → pyEQL-1.0.1.dist-info}/WHEEL +0 -0
- {pyEQL-0.15.1.dist-info → pyEQL-1.0.1.dist-info}/top_level.txt +0 -0
pyEQL/__init__.py
CHANGED
|
@@ -5,6 +5,8 @@ and performing chemical thermodynamics computations.
|
|
|
5
5
|
:copyright: 2013-2024 by Ryan S. Kingsbury
|
|
6
6
|
:license: LGPL, see LICENSE for more details.
|
|
7
7
|
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
8
10
|
from importlib.metadata import PackageNotFoundError, version # pragma: no cover
|
|
9
11
|
from importlib.resources import files
|
|
10
12
|
|
|
@@ -20,6 +22,12 @@ except PackageNotFoundError: # pragma: no cover
|
|
|
20
22
|
finally:
|
|
21
23
|
del version, PackageNotFoundError
|
|
22
24
|
|
|
25
|
+
# logging
|
|
26
|
+
logger = logging.getLogger("pyEQL")
|
|
27
|
+
logger.setLevel(logging.WARNING)
|
|
28
|
+
logger.addHandler(logging.NullHandler())
|
|
29
|
+
|
|
30
|
+
|
|
23
31
|
# Units handling
|
|
24
32
|
# per the pint documentation, it's important that pint and its associated Unit
|
|
25
33
|
# Registry are only instantiated once.
|
|
@@ -39,7 +47,7 @@ ureg.default_format = "P~"
|
|
|
39
47
|
|
|
40
48
|
# create a Store for the default database
|
|
41
49
|
json_db_file = files("pyEQL") / "database" / "pyeql_db.json"
|
|
42
|
-
IonDB = JSONStore(str(json_db_file), key="formula")
|
|
50
|
+
IonDB = JSONStore(str(json_db_file), key="formula", encoding="utf8")
|
|
43
51
|
# By calling connect on init, we get the expensive JSON reading operation out
|
|
44
52
|
# of the way. Subsequent calls to connect will bypass this and access the already-
|
|
45
53
|
# instantiated Store in memory, which should speed up instantiation of Solution objects.
|
pyEQL/activity_correction.py
CHANGED
|
@@ -12,14 +12,17 @@ are called from within the get_activity_coefficient method of the Solution class
|
|
|
12
12
|
:license: LGPL, see LICENSE for more details.
|
|
13
13
|
|
|
14
14
|
"""
|
|
15
|
-
import math
|
|
16
15
|
|
|
16
|
+
import logging
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
17
19
|
from pint import Quantity
|
|
18
20
|
|
|
19
21
|
from pyEQL import ureg
|
|
20
|
-
from pyEQL.logging_system import logger
|
|
21
22
|
from pyEQL.utils import create_water_substance
|
|
22
23
|
|
|
24
|
+
logger = logging.getLogger(f"pyEQL.{__name__}")
|
|
25
|
+
|
|
23
26
|
|
|
24
27
|
def _debye_parameter_B(temperature: str = "25 degC") -> Quantity:
|
|
25
28
|
r"""
|
|
@@ -111,11 +114,11 @@ def _debye_parameter_activity(temperature: str = "25 degC") -> "Quantity":
|
|
|
111
114
|
|
|
112
115
|
debyeparam = (
|
|
113
116
|
ureg.elementary_charge**3
|
|
114
|
-
* (2 *
|
|
115
|
-
/ (4 *
|
|
117
|
+
* (2 * np.pi * ureg.N_A * ureg.Quantity(water_substance.rho, "g/L")) ** 0.5
|
|
118
|
+
/ (4 * np.pi * ureg.epsilon_0 * water_substance.epsilon * ureg.boltzmann_constant * T) ** 1.5
|
|
116
119
|
)
|
|
117
120
|
|
|
118
|
-
logger.
|
|
121
|
+
logger.debug(rf"Computed Debye-Huckel Limiting Law Constant A^{{\gamma}} = {debyeparam} at {temperature}")
|
|
119
122
|
return debyeparam.to("kg ** 0.5 / mol ** 0.5")
|
|
120
123
|
|
|
121
124
|
|
|
@@ -149,7 +152,7 @@ def _debye_parameter_osmotic(temperature="25 degC"):
|
|
|
149
152
|
|
|
150
153
|
"""
|
|
151
154
|
output = 1 / 3 * _debye_parameter_activity(temperature)
|
|
152
|
-
logger.
|
|
155
|
+
logger.debug(f"Computed Debye-Huckel Limiting slope for osmotic coefficient A^phi = {output} at {temperature}")
|
|
153
156
|
return output.to("kg ** 0.5 /mol ** 0.5")
|
|
154
157
|
|
|
155
158
|
|
|
@@ -206,7 +209,7 @@ def _debye_parameter_volume(temperature="25 degC"):
|
|
|
206
209
|
if T.to("degC").magnitude != 25:
|
|
207
210
|
logger.warning("Debye-Huckel limiting slope for volume is approximate when T is not equal to 25 degC")
|
|
208
211
|
|
|
209
|
-
logger.
|
|
212
|
+
logger.debug(f"Computed Debye-Huckel Limiting Slope for volume A^V = {result} at {temperature}")
|
|
210
213
|
|
|
211
214
|
return result.to("cm ** 3 * kg ** 0.5 / mol ** 1.5")
|
|
212
215
|
|
|
@@ -245,7 +248,7 @@ def get_activity_coefficient_debyehuckel(ionic_strength, z=1, temperature="25 de
|
|
|
245
248
|
|
|
246
249
|
log_f = -_debye_parameter_activity(temperature) * z**2 * ionic_strength**0.5
|
|
247
250
|
|
|
248
|
-
return
|
|
251
|
+
return np.exp(log_f) * ureg.Quantity(1, "dimensionless")
|
|
249
252
|
|
|
250
253
|
|
|
251
254
|
def get_activity_coefficient_guntelberg(ionic_strength, z=1, temperature="25 degC"):
|
|
@@ -280,11 +283,9 @@ def get_activity_coefficient_guntelberg(ionic_strength, z=1, temperature="25 deg
|
|
|
280
283
|
if not ionic_strength.magnitude <= 0.1:
|
|
281
284
|
logger.warning("Ionic strength exceeds valid range of the Guntelberg approximation")
|
|
282
285
|
|
|
283
|
-
log_f = (
|
|
284
|
-
-_debye_parameter_activity(temperature) * z**2 * ionic_strength**0.5 / (1 + ionic_strength.magnitude**0.5)
|
|
285
|
-
)
|
|
286
|
+
log_f = -_debye_parameter_activity(temperature) * z**2 * ionic_strength**0.5 / (1 + ionic_strength.magnitude**0.5)
|
|
286
287
|
|
|
287
|
-
return
|
|
288
|
+
return np.exp(log_f) * ureg.Quantity(1, "dimensionless")
|
|
288
289
|
|
|
289
290
|
|
|
290
291
|
def get_activity_coefficient_davies(ionic_strength, z=1, temperature="25 degC"):
|
|
@@ -326,7 +327,7 @@ def get_activity_coefficient_davies(ionic_strength, z=1, temperature="25 degC"):
|
|
|
326
327
|
* (ionic_strength.magnitude**0.5 / (1 + ionic_strength.magnitude**0.5) - 0.2 * ionic_strength.magnitude)
|
|
327
328
|
)
|
|
328
329
|
|
|
329
|
-
return
|
|
330
|
+
return np.exp(log_f) * ureg.Quantity(1, "dimensionless")
|
|
330
331
|
|
|
331
332
|
|
|
332
333
|
def get_activity_coefficient_pitzer(
|
|
@@ -436,7 +437,7 @@ def get_activity_coefficient_pitzer(
|
|
|
436
437
|
b,
|
|
437
438
|
)
|
|
438
439
|
|
|
439
|
-
return
|
|
440
|
+
return np.exp(loggamma) * ureg.Quantity(1, "dimensionless")
|
|
440
441
|
|
|
441
442
|
|
|
442
443
|
def get_apparent_volume_pitzer(
|
|
@@ -532,7 +533,7 @@ def get_apparent_volume_pitzer(
|
|
|
532
533
|
(nu_cation + nu_anion)
|
|
533
534
|
* abs(z_cation * z_anion)
|
|
534
535
|
* (_debye_parameter_volume(temperature) / 2 / b)
|
|
535
|
-
*
|
|
536
|
+
* np.log(1 + b * ionic_strength**0.5)
|
|
536
537
|
)
|
|
537
538
|
|
|
538
539
|
third_term = (
|
|
@@ -565,7 +566,7 @@ def _pitzer_f1(x):
|
|
|
565
566
|
# return 0 if the input is 0
|
|
566
567
|
if x == 0:
|
|
567
568
|
return 0
|
|
568
|
-
return 2 * (1 - (1 + x) *
|
|
569
|
+
return 2 * (1 - (1 + x) * np.exp(-x)) / x**2
|
|
569
570
|
|
|
570
571
|
|
|
571
572
|
def _pitzer_f2(x):
|
|
@@ -585,7 +586,7 @@ def _pitzer_f2(x):
|
|
|
585
586
|
# return 0 if the input is 0
|
|
586
587
|
if x == 0:
|
|
587
588
|
return 0
|
|
588
|
-
return -2 * (1 - (1 + x + x**2 / 2) *
|
|
589
|
+
return -2 * (1 - (1 + x + x**2 / 2) * np.exp(-x)) / x**2
|
|
589
590
|
|
|
590
591
|
|
|
591
592
|
def _pitzer_B_MX(ionic_strength, alpha1, alpha2, beta0, beta1, beta2):
|
|
@@ -615,9 +616,7 @@ def _pitzer_B_MX(ionic_strength, alpha1, alpha2, beta0, beta1, beta2):
|
|
|
615
616
|
:func:`_pitzer_f1`
|
|
616
617
|
|
|
617
618
|
"""
|
|
618
|
-
coeff = (
|
|
619
|
-
beta0 + beta1 * _pitzer_f1(alpha1 * ionic_strength**0.5) + beta2 * _pitzer_f1(alpha2 * ionic_strength**0.5)
|
|
620
|
-
)
|
|
619
|
+
coeff = beta0 + beta1 * _pitzer_f1(alpha1 * ionic_strength**0.5) + beta2 * _pitzer_f1(alpha2 * ionic_strength**0.5)
|
|
621
620
|
return coeff.magnitude
|
|
622
621
|
|
|
623
622
|
|
|
@@ -693,6 +692,10 @@ def _pitzer_B_phi(ionic_strength, alpha1, alpha2, beta0, beta1, beta2):
|
|
|
693
692
|
and Representation with an Ion Interaction (Pitzer) Model.
|
|
694
693
|
Journal of Chemical & Engineering Data, 55(2), 830-838. doi:10.1021/je900487a
|
|
695
694
|
"""
|
|
695
|
+
import math
|
|
696
|
+
|
|
697
|
+
# TODO - for some reason this specific method requires the use of math.exp rather than np.exp. Using np.exp raises
|
|
698
|
+
# a dimensionalityerror.
|
|
696
699
|
return beta0 + beta1 * math.exp(-alpha1 * ionic_strength**0.5) + beta2 * math.exp(-alpha2 * ionic_strength**0.5)
|
|
697
700
|
|
|
698
701
|
|
|
@@ -773,7 +776,7 @@ def _pitzer_log_gamma(
|
|
|
773
776
|
-1
|
|
774
777
|
* abs(z_cation * z_anion)
|
|
775
778
|
* _debye_parameter_osmotic(temperature)
|
|
776
|
-
* (ionic_strength**0.5 / (1 + b * ionic_strength**0.5) + 2 / b *
|
|
779
|
+
* (ionic_strength**0.5 / (1 + b * ionic_strength**0.5) + 2 / b * np.log(1 + b * ionic_strength**0.5))
|
|
777
780
|
)
|
|
778
781
|
second_term = 2 * molality * nu_cation * nu_anion / (nu_cation + nu_anion) * (B_MX + B_phi)
|
|
779
782
|
third_term = 3 * molality**2 * (nu_cation * nu_anion) ** 1.5 / (nu_cation + nu_anion) * C_phi
|
pyEQL/engines.py
CHANGED
|
@@ -6,6 +6,7 @@ pyEQL engines for computing aqueous equilibria (e.g., speciation, redox, etc.).
|
|
|
6
6
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import logging
|
|
9
10
|
import os
|
|
10
11
|
import warnings
|
|
11
12
|
from abc import ABC, abstractmethod
|
|
@@ -14,13 +15,8 @@ from typing import Literal
|
|
|
14
15
|
|
|
15
16
|
from phreeqpython import PhreeqPython
|
|
16
17
|
|
|
17
|
-
# internal pyEQL imports
|
|
18
18
|
import pyEQL.activity_correction as ac
|
|
19
|
-
|
|
20
|
-
# import the parameters database
|
|
21
|
-
# the pint unit registry
|
|
22
19
|
from pyEQL import ureg
|
|
23
|
-
from pyEQL.logging_system import logger
|
|
24
20
|
from pyEQL.salt_ion_match import Salt
|
|
25
21
|
from pyEQL.utils import standardize_formula
|
|
26
22
|
|
|
@@ -28,6 +24,8 @@ from pyEQL.utils import standardize_formula
|
|
|
28
24
|
# PHREEQC will ignore others (e.g., 'Na(1)')
|
|
29
25
|
SPECIAL_ELEMENTS = ["S", "C", "N", "Cu", "Fe", "Mn"]
|
|
30
26
|
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
31
29
|
|
|
32
30
|
class EOS(ABC):
|
|
33
31
|
"""
|
|
@@ -49,7 +47,7 @@ class EOS(ABC):
|
|
|
49
47
|
solution: pyEQL Solution object
|
|
50
48
|
solute: str identifying the solute of interest
|
|
51
49
|
|
|
52
|
-
Returns
|
|
50
|
+
Returns:
|
|
53
51
|
Quantity: dimensionless quantity object
|
|
54
52
|
|
|
55
53
|
Raises:
|
|
@@ -64,7 +62,7 @@ class EOS(ABC):
|
|
|
64
62
|
Args:
|
|
65
63
|
solution: pyEQL Solution object
|
|
66
64
|
|
|
67
|
-
Returns
|
|
65
|
+
Returns:
|
|
68
66
|
Quantity: dimensionless molal scale osmotic coefficient
|
|
69
67
|
|
|
70
68
|
Raises:
|
|
@@ -79,7 +77,7 @@ class EOS(ABC):
|
|
|
79
77
|
Args:
|
|
80
78
|
solution: pyEQL Solution object
|
|
81
79
|
|
|
82
|
-
Returns
|
|
80
|
+
Returns:
|
|
83
81
|
Quantity: solute volume in L
|
|
84
82
|
|
|
85
83
|
Raises:
|
|
@@ -96,7 +94,7 @@ class EOS(ABC):
|
|
|
96
94
|
Args:
|
|
97
95
|
solution: pyEQL Solution object
|
|
98
96
|
|
|
99
|
-
Returns
|
|
97
|
+
Returns:
|
|
100
98
|
Nothing. The speciation of the Solution is modified in-place.
|
|
101
99
|
|
|
102
100
|
Raises:
|
|
@@ -175,9 +173,7 @@ class NativeEOS(EOS):
|
|
|
175
173
|
self._stored_comp = None
|
|
176
174
|
|
|
177
175
|
def _setup_ppsol(self, solution):
|
|
178
|
-
"""
|
|
179
|
-
Helper method to set up a PhreeqPython solution for subsequent analysis.
|
|
180
|
-
"""
|
|
176
|
+
"""Helper method to set up a PhreeqPython solution for subsequent analysis."""
|
|
181
177
|
self._stored_comp = solution.components.copy()
|
|
182
178
|
solv_mass = solution.solvent_mass.to("kg").magnitude
|
|
183
179
|
# inherit bulk solution properties
|
|
@@ -199,6 +195,16 @@ class NativeEOS(EOS):
|
|
|
199
195
|
# add the composition to the dict
|
|
200
196
|
# also, skip H and O
|
|
201
197
|
for el, mol in solution.get_el_amt_dict().items():
|
|
198
|
+
# CAUTION - care must be taken to avoid unintended behavior here. get_el_amt_dict() will return
|
|
199
|
+
# all distinct oxi states of each element present. If there are elements present whose oxi states
|
|
200
|
+
# are NOT recognized by PHREEQC (via SPECIAL_ELEMENTS) then the amount of only 1 oxi state will be
|
|
201
|
+
# entered into the composition dict. This can especially cause problems after equilibrate() has already
|
|
202
|
+
# been called once. For example, equilibrating a simple NaCl solution generates Cl species that are assigned
|
|
203
|
+
# various oxidations states, -1 mostly, but also 1, 2, and 3. Since the concentrations of everything
|
|
204
|
+
# except the -1 oxi state are tiny, this can result in Cl "disappearing" from the solution if
|
|
205
|
+
# equlibrate is called again. It also causes non-determinism, because the amount is taken from whatever
|
|
206
|
+
# oxi state happens to be iterated through last.
|
|
207
|
+
|
|
202
208
|
# strip off the oxi state
|
|
203
209
|
bare_el = el.split("(")[0]
|
|
204
210
|
if bare_el in SPECIAL_ELEMENTS:
|
|
@@ -210,7 +216,12 @@ class NativeEOS(EOS):
|
|
|
210
216
|
else:
|
|
211
217
|
key = bare_el
|
|
212
218
|
|
|
213
|
-
|
|
219
|
+
if key in d:
|
|
220
|
+
# when multiple oxi states for the same (non-SPECIAL) element are present, make sure to
|
|
221
|
+
# add all their amounts together
|
|
222
|
+
d[key] += str(mol / solv_mass)
|
|
223
|
+
else:
|
|
224
|
+
d[key] = str(mol / solv_mass)
|
|
214
225
|
|
|
215
226
|
# tell PHREEQC which species to use for charge balance
|
|
216
227
|
if (
|
|
@@ -318,7 +329,7 @@ class NativeEOS(EOS):
|
|
|
318
329
|
|
|
319
330
|
# show an error if no salt can be found that contains the solute
|
|
320
331
|
if salt is None:
|
|
321
|
-
logger.
|
|
332
|
+
logger.error("No salts found that contain solute %s. Returning unit activity coefficient." % solute)
|
|
322
333
|
return ureg.Quantity(1, "dimensionless")
|
|
323
334
|
|
|
324
335
|
# use the Pitzer model for higher ionic strength, if the parameters are available
|
|
@@ -367,15 +378,16 @@ class NativeEOS(EOS):
|
|
|
367
378
|
str(solution.temperature),
|
|
368
379
|
)
|
|
369
380
|
|
|
370
|
-
logger.
|
|
371
|
-
f"Calculated activity coefficient of species {solute} as {activity_coefficient} based on salt
|
|
381
|
+
logger.debug(
|
|
382
|
+
f"Calculated activity coefficient of species {solute} as {activity_coefficient} based on salt"
|
|
383
|
+
f" {salt} using Pitzer model"
|
|
372
384
|
)
|
|
373
385
|
molal = activity_coefficient
|
|
374
386
|
|
|
375
387
|
# for very low ionic strength, use the Debye-Huckel limiting law
|
|
376
388
|
elif solution.ionic_strength.magnitude <= 0.005:
|
|
377
|
-
logger.
|
|
378
|
-
"Ionic strength =
|
|
389
|
+
logger.debug(
|
|
390
|
+
f"Ionic strength = {solution.ionic_strength}. Using Debye-Huckel to calculate activity coefficient."
|
|
379
391
|
)
|
|
380
392
|
molal = ac.get_activity_coefficient_debyehuckel(
|
|
381
393
|
solution.ionic_strength,
|
|
@@ -385,8 +397,8 @@ class NativeEOS(EOS):
|
|
|
385
397
|
|
|
386
398
|
# use the Guntelberg approximation for 0.005 < I < 0.1
|
|
387
399
|
elif solution.ionic_strength.magnitude <= 0.1:
|
|
388
|
-
logger.
|
|
389
|
-
"Ionic strength =
|
|
400
|
+
logger.debug(
|
|
401
|
+
f"Ionic strength = {solution.ionic_strength}. Using Guntelberg to calculate activity coefficient."
|
|
390
402
|
)
|
|
391
403
|
molal = ac.get_activity_coefficient_guntelberg(
|
|
392
404
|
solution.ionic_strength,
|
|
@@ -396,9 +408,8 @@ class NativeEOS(EOS):
|
|
|
396
408
|
|
|
397
409
|
# use the Davies equation for 0.1 < I < 0.5
|
|
398
410
|
elif solution.ionic_strength.magnitude <= 0.5:
|
|
399
|
-
logger.
|
|
400
|
-
"Ionic strength =
|
|
401
|
-
% solution.ionic_strength
|
|
411
|
+
logger.debug(
|
|
412
|
+
f"Ionic strength = {solution.ionic_strength}. Using Davies equation to calculate activity coefficient."
|
|
402
413
|
)
|
|
403
414
|
molal = ac.get_activity_coefficient_davies(
|
|
404
415
|
solution.ionic_strength,
|
|
@@ -407,9 +418,9 @@ class NativeEOS(EOS):
|
|
|
407
418
|
)
|
|
408
419
|
|
|
409
420
|
else:
|
|
410
|
-
logger.
|
|
411
|
-
"Ionic strength too high to estimate activity for species
|
|
412
|
-
|
|
421
|
+
logger.error(
|
|
422
|
+
f"Ionic strength too high to estimate activity for species {solute}. Specify parameters for Pitzer "
|
|
423
|
+
"model. Returning unit activity coefficient"
|
|
413
424
|
)
|
|
414
425
|
|
|
415
426
|
molal = ureg.Quantity(1, "dimensionless")
|
|
@@ -479,11 +490,11 @@ class NativeEOS(EOS):
|
|
|
479
490
|
behavior on desalination calculations for mixed electrolyte solutions with comparison to seawater. Desalination 2013, 318, 34-47.
|
|
480
491
|
|
|
481
492
|
Examples:
|
|
482
|
-
>>> s1 = pyEQL.Solution(
|
|
493
|
+
>>> s1 = pyEQL.Solution({'Na+': '0.2 mol/kg', 'Cl-': '0.2 mol/kg'})
|
|
483
494
|
>>> s1.get_osmotic_coefficient()
|
|
484
495
|
<Quantity(0.923715281, 'dimensionless')>
|
|
485
496
|
|
|
486
|
-
>>> s1 = pyEQL.Solution(
|
|
497
|
+
>>> s1 = pyEQL.Solution({'Mg+2': '0.3 mol/kg', 'Cl-': '0.6 mol/kg'},temperature='30 degC')
|
|
487
498
|
>>> s1.get_osmotic_coefficient()
|
|
488
499
|
<Quantity(0.891409618, 'dimensionless')>
|
|
489
500
|
|
|
@@ -494,7 +505,7 @@ class NativeEOS(EOS):
|
|
|
494
505
|
molality_sum = 0
|
|
495
506
|
|
|
496
507
|
# loop through all the salts in the solution, calculate the osmotic
|
|
497
|
-
# coefficint for
|
|
508
|
+
# coefficint for each, and average them into an effective osmotic
|
|
498
509
|
# coefficient
|
|
499
510
|
for d in solution.get_salt_dict().values():
|
|
500
511
|
item = Salt(d["cation"], d["anion"])
|
|
@@ -544,17 +555,18 @@ class NativeEOS(EOS):
|
|
|
544
555
|
str(solution.temperature),
|
|
545
556
|
)
|
|
546
557
|
|
|
547
|
-
logger.
|
|
548
|
-
f"Calculated osmotic coefficient of water as {osmotic_coefficient} based on salt
|
|
558
|
+
logger.debug(
|
|
559
|
+
f"Calculated osmotic coefficient of water as {osmotic_coefficient} based on salt "
|
|
560
|
+
f"{item.formula} using Pitzer model"
|
|
549
561
|
)
|
|
550
562
|
effective_osmotic_sum += concentration * osmotic_coefficient
|
|
551
563
|
|
|
552
564
|
else:
|
|
553
|
-
logger.
|
|
554
|
-
"
|
|
555
|
-
|
|
565
|
+
logger.debug(
|
|
566
|
+
f"Returning unit osmotic coefficient for salt {item.formula} because Pitzer parameters are not"
|
|
567
|
+
"available in database."
|
|
556
568
|
)
|
|
557
|
-
effective_osmotic_sum += concentration *
|
|
569
|
+
effective_osmotic_sum += concentration * 1
|
|
558
570
|
|
|
559
571
|
try:
|
|
560
572
|
return effective_osmotic_sum / molality_sum
|
|
@@ -621,7 +633,7 @@ class NativeEOS(EOS):
|
|
|
621
633
|
|
|
622
634
|
pitzer_calc = True
|
|
623
635
|
|
|
624
|
-
logger.
|
|
636
|
+
logger.debug("Updated solution volume using Pitzer model for solute %s" % salt.formula)
|
|
625
637
|
|
|
626
638
|
# add the partial molar volume of any other solutes, except for water
|
|
627
639
|
# or the parent salt, which is already accounted for by the Pitzer parameters
|
|
@@ -637,12 +649,11 @@ class NativeEOS(EOS):
|
|
|
637
649
|
part_vol = solution.get_property(solute, "size.molar_volume")
|
|
638
650
|
if part_vol is not None:
|
|
639
651
|
solute_vol += part_vol * ureg.Quantity(mol, "mol")
|
|
640
|
-
logger.
|
|
652
|
+
logger.debug("Updated solution volume using direct partial molar volume for solute %s" % solute)
|
|
641
653
|
|
|
642
654
|
else:
|
|
643
655
|
logger.warning(
|
|
644
|
-
"
|
|
645
|
-
% solute
|
|
656
|
+
f"Volume of solute {solute} will be ignored because partial molar volume data are not available."
|
|
646
657
|
)
|
|
647
658
|
|
|
648
659
|
return solute_vol.to("L")
|
|
@@ -662,7 +673,6 @@ class NativeEOS(EOS):
|
|
|
662
673
|
solution.components[s] = mol
|
|
663
674
|
|
|
664
675
|
# make sure all species are accounted for
|
|
665
|
-
charge_adjust = 0
|
|
666
676
|
assert set(self._stored_comp.keys()) - set(solution.components.keys()) == set()
|
|
667
677
|
|
|
668
678
|
# log a message if any components were not touched by PHREEQC
|
|
@@ -673,6 +683,11 @@ class NativeEOS(EOS):
|
|
|
673
683
|
f"After equilibration, the amounts of species {missing_species} were not modified "
|
|
674
684
|
"by PHREEQC. These species are likely absent from its database."
|
|
675
685
|
)
|
|
686
|
+
|
|
687
|
+
# re-adjust charge balance for any missing species
|
|
688
|
+
# note that if balance_charge is set, it will have been passed to PHREEQC, so we only need to adjust
|
|
689
|
+
# for any missing species here.
|
|
690
|
+
charge_adjust = 0
|
|
676
691
|
for s in missing_species:
|
|
677
692
|
charge_adjust += -1 * solution.get_amount(s, "eq").magnitude
|
|
678
693
|
if charge_adjust != 0:
|
|
@@ -681,11 +696,10 @@ class NativeEOS(EOS):
|
|
|
681
696
|
f" {charge_adjust} eq of charge were added via {solution.balance_charge}"
|
|
682
697
|
)
|
|
683
698
|
|
|
684
|
-
# re-adjust charge balance
|
|
685
699
|
if solution.balance_charge is None:
|
|
686
700
|
pass
|
|
687
701
|
elif solution.balance_charge == "pH":
|
|
688
|
-
solution.components["H+"] += charge_adjust
|
|
702
|
+
solution.components["H+"] += charge_adjust.magnitude
|
|
689
703
|
elif solution.balance_charge == "pE":
|
|
690
704
|
raise NotImplementedError
|
|
691
705
|
else:
|
pyEQL/equilibrium.py
CHANGED
|
@@ -8,12 +8,15 @@ NOTE: these methods are not currently used but are here for the future.
|
|
|
8
8
|
:license: LGPL, see LICENSE for more details.
|
|
9
9
|
|
|
10
10
|
"""
|
|
11
|
+
|
|
11
12
|
# import libraries for scientific functions
|
|
12
|
-
import
|
|
13
|
+
import logging
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
13
16
|
|
|
14
|
-
# the pint unit registry
|
|
15
17
|
from pyEQL import ureg
|
|
16
|
-
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
17
20
|
|
|
18
21
|
# TODO - not used. Remove?
|
|
19
22
|
SPECIES_ALIAISES = {
|
|
@@ -49,7 +52,7 @@ def adjust_temp_pitzer(c1, c2, c3, c4, c5, temp, temp_ref=ureg.Quantity("298.15
|
|
|
49
52
|
return (
|
|
50
53
|
c1
|
|
51
54
|
+ c2 * (1 / temp + 1 / temp_ref)
|
|
52
|
-
+ c2 *
|
|
55
|
+
+ c2 * np.log(temp / temp_ref)
|
|
53
56
|
+ c3 * (temp - temp_ref)
|
|
54
57
|
+ c4 * (temp**2 - temp_ref**2)
|
|
55
58
|
+ c5 * (temp**-2 - temp_ref**-2)
|
|
@@ -89,15 +92,17 @@ def adjust_temp_vanthoff(equilibrium_constant, enthalpy, temperature, reference_
|
|
|
89
92
|
>>> adjust_temp_vanthoff(0.15,ureg.Quantity('-197.6 kJ/mol'),ureg.Quantity('42 degC')) #doctest: +ELLIPSIS
|
|
90
93
|
0.00203566...
|
|
91
94
|
"""
|
|
92
|
-
output = equilibrium_constant *
|
|
95
|
+
output = equilibrium_constant * np.exp(
|
|
93
96
|
enthalpy / ureg.R * (1 / reference_temperature.to("K") - 1 / temperature.to("K"))
|
|
94
97
|
)
|
|
95
98
|
|
|
96
|
-
logger.
|
|
99
|
+
logger.debug(
|
|
97
100
|
"Adjusted equilibrium constant K=%s from %s to %s degrees Celsius with Delta H = %s. Adjusted K = %s % equilibrium_constant,reference_temperature,temperature,enthalpy,output"
|
|
98
101
|
)
|
|
99
102
|
|
|
100
|
-
logger.
|
|
103
|
+
logger.info(
|
|
104
|
+
"Note that the Van't Hoff equation assumes enthalpy is independent of temperature over the range of interest"
|
|
105
|
+
)
|
|
101
106
|
return output
|
|
102
107
|
|
|
103
108
|
|
|
@@ -124,7 +129,7 @@ def adjust_temp_arrhenius(
|
|
|
124
129
|
TODO - add better reference
|
|
125
130
|
|
|
126
131
|
.. math::
|
|
127
|
-
ln(\frac{K2}{K1} = \frac{E_a}{R} ( \frac{1}{T_{1}} - {\frac{1}{T_2}} )
|
|
132
|
+
ln(\frac{K2}{K1}) = \frac{E_a}{R} ( \frac{1}{T_{1}} - {\frac{1}{T_2}} )
|
|
128
133
|
|
|
129
134
|
References:
|
|
130
135
|
http://chemwiki.ucdavis.edu/Physical_Chemistry/Kinetics/Reaction_Rates/Temperature_Dependence_of_Reaction_Rates/Arrhenius_Equation
|
|
@@ -133,12 +138,13 @@ def adjust_temp_arrhenius(
|
|
|
133
138
|
>>> adjust_temp_arrhenius(7,900*ureg.Quantity('kJ/mol'),37*ureg.Quantity('degC'),97*ureg.Quantity('degC')) #doctest: +ELLIPSIS
|
|
134
139
|
1.8867225...e-24
|
|
135
140
|
"""
|
|
136
|
-
output = rate_constant *
|
|
141
|
+
output = rate_constant * np.exp(
|
|
137
142
|
activation_energy / ureg.R * (1 / reference_temperature.to("K") - 1 / temperature.to("K"))
|
|
138
143
|
)
|
|
139
144
|
|
|
140
|
-
logger.
|
|
141
|
-
"Adjusted parameter
|
|
145
|
+
logger.debug(
|
|
146
|
+
f"Adjusted parameter {rate_constant} from {reference_temperature} to {temperature} degrees Celsius with"
|
|
147
|
+
f"Activation Energy = {activation_energy}s kJ/mol. Adjusted value = {output}"
|
|
142
148
|
)
|
|
143
149
|
|
|
144
150
|
return output
|
|
@@ -215,7 +221,7 @@ def alpha(n, pH, pKa_list):
|
|
|
215
221
|
|
|
216
222
|
# return the desired distribution factor
|
|
217
223
|
alpha = terms_list[n] / sum(terms_list)
|
|
218
|
-
logger.
|
|
224
|
+
logger.debug(
|
|
219
225
|
"Calculated %s-deprotonated acid distribution coefficient of %s for pKa=%s at pH %s % n,alpha,pKa_list,pH"
|
|
220
226
|
)
|
|
221
227
|
return alpha
|