pyEQL 1.0.0__py2.py3-none-any.whl → 1.0.2__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 +1 -1
- pyEQL/activity_correction.py +15 -11
- pyEQL/engines.py +30 -14
- pyEQL/equilibrium.py +6 -5
- pyEQL/functions.py +5 -4
- pyEQL/salt_ion_match.py +1 -1
- pyEQL/solute.py +1 -1
- pyEQL/solution.py +19 -18
- pyEQL/utils.py +38 -5
- {pyEQL-1.0.0.dist-info → pyEQL-1.0.2.dist-info}/METADATA +5 -4
- pyEQL-1.0.2.dist-info/RECORD +27 -0
- {pyEQL-1.0.0.dist-info → pyEQL-1.0.2.dist-info}/WHEEL +1 -1
- pyEQL-1.0.0.dist-info/RECORD +0 -27
- {pyEQL-1.0.0.dist-info → pyEQL-1.0.2.dist-info}/AUTHORS.md +0 -0
- {pyEQL-1.0.0.dist-info → pyEQL-1.0.2.dist-info}/COPYING +0 -0
- {pyEQL-1.0.0.dist-info → pyEQL-1.0.2.dist-info}/LICENSE.txt +0 -0
- {pyEQL-1.0.0.dist-info → pyEQL-1.0.2.dist-info}/top_level.txt +0 -0
pyEQL/__init__.py
CHANGED
|
@@ -47,7 +47,7 @@ ureg.default_format = "P~"
|
|
|
47
47
|
|
|
48
48
|
# create a Store for the default database
|
|
49
49
|
json_db_file = files("pyEQL") / "database" / "pyeql_db.json"
|
|
50
|
-
IonDB = JSONStore(str(json_db_file), key="formula")
|
|
50
|
+
IonDB = JSONStore(str(json_db_file), key="formula", encoding="utf8")
|
|
51
51
|
# By calling connect on init, we get the expensive JSON reading operation out
|
|
52
52
|
# of the way. Subsequent calls to connect will bypass this and access the already-
|
|
53
53
|
# instantiated Store in memory, which should speed up instantiation of Solution objects.
|
pyEQL/activity_correction.py
CHANGED
|
@@ -14,8 +14,8 @@ are called from within the get_activity_coefficient method of the Solution class
|
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
import logging
|
|
17
|
-
import math
|
|
18
17
|
|
|
18
|
+
import numpy as np
|
|
19
19
|
from pint import Quantity
|
|
20
20
|
|
|
21
21
|
from pyEQL import ureg
|
|
@@ -114,8 +114,8 @@ def _debye_parameter_activity(temperature: str = "25 degC") -> "Quantity":
|
|
|
114
114
|
|
|
115
115
|
debyeparam = (
|
|
116
116
|
ureg.elementary_charge**3
|
|
117
|
-
* (2 *
|
|
118
|
-
/ (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
|
|
119
119
|
)
|
|
120
120
|
|
|
121
121
|
logger.debug(rf"Computed Debye-Huckel Limiting Law Constant A^{{\gamma}} = {debyeparam} at {temperature}")
|
|
@@ -248,7 +248,7 @@ def get_activity_coefficient_debyehuckel(ionic_strength, z=1, temperature="25 de
|
|
|
248
248
|
|
|
249
249
|
log_f = -_debye_parameter_activity(temperature) * z**2 * ionic_strength**0.5
|
|
250
250
|
|
|
251
|
-
return
|
|
251
|
+
return np.exp(log_f) * ureg.Quantity(1, "dimensionless")
|
|
252
252
|
|
|
253
253
|
|
|
254
254
|
def get_activity_coefficient_guntelberg(ionic_strength, z=1, temperature="25 degC"):
|
|
@@ -285,7 +285,7 @@ def get_activity_coefficient_guntelberg(ionic_strength, z=1, temperature="25 deg
|
|
|
285
285
|
|
|
286
286
|
log_f = -_debye_parameter_activity(temperature) * z**2 * ionic_strength**0.5 / (1 + ionic_strength.magnitude**0.5)
|
|
287
287
|
|
|
288
|
-
return
|
|
288
|
+
return np.exp(log_f) * ureg.Quantity(1, "dimensionless")
|
|
289
289
|
|
|
290
290
|
|
|
291
291
|
def get_activity_coefficient_davies(ionic_strength, z=1, temperature="25 degC"):
|
|
@@ -327,7 +327,7 @@ def get_activity_coefficient_davies(ionic_strength, z=1, temperature="25 degC"):
|
|
|
327
327
|
* (ionic_strength.magnitude**0.5 / (1 + ionic_strength.magnitude**0.5) - 0.2 * ionic_strength.magnitude)
|
|
328
328
|
)
|
|
329
329
|
|
|
330
|
-
return
|
|
330
|
+
return np.exp(log_f) * ureg.Quantity(1, "dimensionless")
|
|
331
331
|
|
|
332
332
|
|
|
333
333
|
def get_activity_coefficient_pitzer(
|
|
@@ -437,7 +437,7 @@ def get_activity_coefficient_pitzer(
|
|
|
437
437
|
b,
|
|
438
438
|
)
|
|
439
439
|
|
|
440
|
-
return
|
|
440
|
+
return np.exp(loggamma) * ureg.Quantity(1, "dimensionless")
|
|
441
441
|
|
|
442
442
|
|
|
443
443
|
def get_apparent_volume_pitzer(
|
|
@@ -533,7 +533,7 @@ def get_apparent_volume_pitzer(
|
|
|
533
533
|
(nu_cation + nu_anion)
|
|
534
534
|
* abs(z_cation * z_anion)
|
|
535
535
|
* (_debye_parameter_volume(temperature) / 2 / b)
|
|
536
|
-
*
|
|
536
|
+
* np.log(1 + b * ionic_strength**0.5)
|
|
537
537
|
)
|
|
538
538
|
|
|
539
539
|
third_term = (
|
|
@@ -566,7 +566,7 @@ def _pitzer_f1(x):
|
|
|
566
566
|
# return 0 if the input is 0
|
|
567
567
|
if x == 0:
|
|
568
568
|
return 0
|
|
569
|
-
return 2 * (1 - (1 + x) *
|
|
569
|
+
return 2 * (1 - (1 + x) * np.exp(-x)) / x**2
|
|
570
570
|
|
|
571
571
|
|
|
572
572
|
def _pitzer_f2(x):
|
|
@@ -586,7 +586,7 @@ def _pitzer_f2(x):
|
|
|
586
586
|
# return 0 if the input is 0
|
|
587
587
|
if x == 0:
|
|
588
588
|
return 0
|
|
589
|
-
return -2 * (1 - (1 + x + x**2 / 2) *
|
|
589
|
+
return -2 * (1 - (1 + x + x**2 / 2) * np.exp(-x)) / x**2
|
|
590
590
|
|
|
591
591
|
|
|
592
592
|
def _pitzer_B_MX(ionic_strength, alpha1, alpha2, beta0, beta1, beta2):
|
|
@@ -692,6 +692,10 @@ def _pitzer_B_phi(ionic_strength, alpha1, alpha2, beta0, beta1, beta2):
|
|
|
692
692
|
and Representation with an Ion Interaction (Pitzer) Model.
|
|
693
693
|
Journal of Chemical & Engineering Data, 55(2), 830-838. doi:10.1021/je900487a
|
|
694
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.
|
|
695
699
|
return beta0 + beta1 * math.exp(-alpha1 * ionic_strength**0.5) + beta2 * math.exp(-alpha2 * ionic_strength**0.5)
|
|
696
700
|
|
|
697
701
|
|
|
@@ -772,7 +776,7 @@ def _pitzer_log_gamma(
|
|
|
772
776
|
-1
|
|
773
777
|
* abs(z_cation * z_anion)
|
|
774
778
|
* _debye_parameter_osmotic(temperature)
|
|
775
|
-
* (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))
|
|
776
780
|
)
|
|
777
781
|
second_term = 2 * molality * nu_cation * nu_anion / (nu_cation + nu_anion) * (B_MX + B_phi)
|
|
778
782
|
third_term = 3 * molality**2 * (nu_cation * nu_anion) ** 1.5 / (nu_cation + nu_anion) * C_phi
|
pyEQL/engines.py
CHANGED
|
@@ -47,7 +47,7 @@ class EOS(ABC):
|
|
|
47
47
|
solution: pyEQL Solution object
|
|
48
48
|
solute: str identifying the solute of interest
|
|
49
49
|
|
|
50
|
-
Returns
|
|
50
|
+
Returns:
|
|
51
51
|
Quantity: dimensionless quantity object
|
|
52
52
|
|
|
53
53
|
Raises:
|
|
@@ -62,7 +62,7 @@ class EOS(ABC):
|
|
|
62
62
|
Args:
|
|
63
63
|
solution: pyEQL Solution object
|
|
64
64
|
|
|
65
|
-
Returns
|
|
65
|
+
Returns:
|
|
66
66
|
Quantity: dimensionless molal scale osmotic coefficient
|
|
67
67
|
|
|
68
68
|
Raises:
|
|
@@ -77,7 +77,7 @@ class EOS(ABC):
|
|
|
77
77
|
Args:
|
|
78
78
|
solution: pyEQL Solution object
|
|
79
79
|
|
|
80
|
-
Returns
|
|
80
|
+
Returns:
|
|
81
81
|
Quantity: solute volume in L
|
|
82
82
|
|
|
83
83
|
Raises:
|
|
@@ -94,7 +94,7 @@ class EOS(ABC):
|
|
|
94
94
|
Args:
|
|
95
95
|
solution: pyEQL Solution object
|
|
96
96
|
|
|
97
|
-
Returns
|
|
97
|
+
Returns:
|
|
98
98
|
Nothing. The speciation of the Solution is modified in-place.
|
|
99
99
|
|
|
100
100
|
Raises:
|
|
@@ -139,7 +139,7 @@ class NativeEOS(EOS):
|
|
|
139
139
|
def __init__(
|
|
140
140
|
self,
|
|
141
141
|
phreeqc_db: Literal["vitens.dat", "wateq4f_PWN.dat", "pitzer.dat", "llnl.dat", "geothermal.dat"] = "llnl.dat",
|
|
142
|
-
):
|
|
142
|
+
) -> None:
|
|
143
143
|
"""
|
|
144
144
|
Args:
|
|
145
145
|
phreeqc_db: Name of the PHREEQC database file to use for solution thermodynamics
|
|
@@ -173,9 +173,7 @@ class NativeEOS(EOS):
|
|
|
173
173
|
self._stored_comp = None
|
|
174
174
|
|
|
175
175
|
def _setup_ppsol(self, solution):
|
|
176
|
-
"""
|
|
177
|
-
Helper method to set up a PhreeqPython solution for subsequent analysis.
|
|
178
|
-
"""
|
|
176
|
+
"""Helper method to set up a PhreeqPython solution for subsequent analysis."""
|
|
179
177
|
self._stored_comp = solution.components.copy()
|
|
180
178
|
solv_mass = solution.solvent_mass.to("kg").magnitude
|
|
181
179
|
# inherit bulk solution properties
|
|
@@ -197,6 +195,16 @@ class NativeEOS(EOS):
|
|
|
197
195
|
# add the composition to the dict
|
|
198
196
|
# also, skip H and O
|
|
199
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
|
+
|
|
200
208
|
# strip off the oxi state
|
|
201
209
|
bare_el = el.split("(")[0]
|
|
202
210
|
if bare_el in SPECIAL_ELEMENTS:
|
|
@@ -208,7 +216,12 @@ class NativeEOS(EOS):
|
|
|
208
216
|
else:
|
|
209
217
|
key = bare_el
|
|
210
218
|
|
|
211
|
-
|
|
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)
|
|
212
225
|
|
|
213
226
|
# tell PHREEQC which species to use for charge balance
|
|
214
227
|
if (
|
|
@@ -660,7 +673,6 @@ class NativeEOS(EOS):
|
|
|
660
673
|
solution.components[s] = mol
|
|
661
674
|
|
|
662
675
|
# make sure all species are accounted for
|
|
663
|
-
charge_adjust = 0
|
|
664
676
|
assert set(self._stored_comp.keys()) - set(solution.components.keys()) == set()
|
|
665
677
|
|
|
666
678
|
# log a message if any components were not touched by PHREEQC
|
|
@@ -671,6 +683,11 @@ class NativeEOS(EOS):
|
|
|
671
683
|
f"After equilibration, the amounts of species {missing_species} were not modified "
|
|
672
684
|
"by PHREEQC. These species are likely absent from its database."
|
|
673
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
|
|
674
691
|
for s in missing_species:
|
|
675
692
|
charge_adjust += -1 * solution.get_amount(s, "eq").magnitude
|
|
676
693
|
if charge_adjust != 0:
|
|
@@ -679,11 +696,10 @@ class NativeEOS(EOS):
|
|
|
679
696
|
f" {charge_adjust} eq of charge were added via {solution.balance_charge}"
|
|
680
697
|
)
|
|
681
698
|
|
|
682
|
-
# re-adjust charge balance
|
|
683
699
|
if solution.balance_charge is None:
|
|
684
700
|
pass
|
|
685
701
|
elif solution.balance_charge == "pH":
|
|
686
|
-
solution.components["H+"] += charge_adjust
|
|
702
|
+
solution.components["H+"] += charge_adjust.magnitude
|
|
687
703
|
elif solution.balance_charge == "pE":
|
|
688
704
|
raise NotImplementedError
|
|
689
705
|
else:
|
|
@@ -696,7 +712,7 @@ class NativeEOS(EOS):
|
|
|
696
712
|
# call to equilibrate can thus result in a slight change in the Solution mass.
|
|
697
713
|
solution.components[solution.solvent] = orig_solvent_moles
|
|
698
714
|
|
|
699
|
-
def __deepcopy__(self, memo):
|
|
715
|
+
def __deepcopy__(self, memo) -> "NativeEOS":
|
|
700
716
|
# custom deepcopy required because the PhreeqPython instance used by the Native and Phreeqc engines
|
|
701
717
|
# is not pickle-able.
|
|
702
718
|
import copy
|
|
@@ -720,7 +736,7 @@ class PhreeqcEOS(NativeEOS):
|
|
|
720
736
|
phreeqc_db: Literal[
|
|
721
737
|
"vitens.dat", "wateq4f_PWN.dat", "pitzer.dat", "llnl.dat", "geothermal.dat"
|
|
722
738
|
] = "phreeqc.dat",
|
|
723
|
-
):
|
|
739
|
+
) -> None:
|
|
724
740
|
"""
|
|
725
741
|
Args:
|
|
726
742
|
phreeqc_db: Name of the PHREEQC database file to use for solution thermodynamics
|
pyEQL/equilibrium.py
CHANGED
|
@@ -11,7 +11,8 @@ NOTE: these methods are not currently used but are here for the future.
|
|
|
11
11
|
|
|
12
12
|
# import libraries for scientific functions
|
|
13
13
|
import logging
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
15
16
|
|
|
16
17
|
from pyEQL import ureg
|
|
17
18
|
|
|
@@ -51,7 +52,7 @@ def adjust_temp_pitzer(c1, c2, c3, c4, c5, temp, temp_ref=ureg.Quantity("298.15
|
|
|
51
52
|
return (
|
|
52
53
|
c1
|
|
53
54
|
+ c2 * (1 / temp + 1 / temp_ref)
|
|
54
|
-
+ c2 *
|
|
55
|
+
+ c2 * np.log(temp / temp_ref)
|
|
55
56
|
+ c3 * (temp - temp_ref)
|
|
56
57
|
+ c4 * (temp**2 - temp_ref**2)
|
|
57
58
|
+ c5 * (temp**-2 - temp_ref**-2)
|
|
@@ -91,7 +92,7 @@ def adjust_temp_vanthoff(equilibrium_constant, enthalpy, temperature, reference_
|
|
|
91
92
|
>>> adjust_temp_vanthoff(0.15,ureg.Quantity('-197.6 kJ/mol'),ureg.Quantity('42 degC')) #doctest: +ELLIPSIS
|
|
92
93
|
0.00203566...
|
|
93
94
|
"""
|
|
94
|
-
output = equilibrium_constant *
|
|
95
|
+
output = equilibrium_constant * np.exp(
|
|
95
96
|
enthalpy / ureg.R * (1 / reference_temperature.to("K") - 1 / temperature.to("K"))
|
|
96
97
|
)
|
|
97
98
|
|
|
@@ -128,7 +129,7 @@ def adjust_temp_arrhenius(
|
|
|
128
129
|
TODO - add better reference
|
|
129
130
|
|
|
130
131
|
.. math::
|
|
131
|
-
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}} )
|
|
132
133
|
|
|
133
134
|
References:
|
|
134
135
|
http://chemwiki.ucdavis.edu/Physical_Chemistry/Kinetics/Reaction_Rates/Temperature_Dependence_of_Reaction_Rates/Arrhenius_Equation
|
|
@@ -137,7 +138,7 @@ def adjust_temp_arrhenius(
|
|
|
137
138
|
>>> adjust_temp_arrhenius(7,900*ureg.Quantity('kJ/mol'),37*ureg.Quantity('degC'),97*ureg.Quantity('degC')) #doctest: +ELLIPSIS
|
|
138
139
|
1.8867225...e-24
|
|
139
140
|
"""
|
|
140
|
-
output = rate_constant *
|
|
141
|
+
output = rate_constant * np.exp(
|
|
141
142
|
activation_energy / ureg.R * (1 / reference_temperature.to("K") - 1 / temperature.to("K"))
|
|
142
143
|
)
|
|
143
144
|
|
pyEQL/functions.py
CHANGED
|
@@ -7,7 +7,8 @@ pyEQL functions that take Solution objects as inputs or return Solution objects.
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import logging
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
11
12
|
|
|
12
13
|
from pyEQL import Solution, ureg
|
|
13
14
|
|
|
@@ -55,7 +56,7 @@ def gibbs_mix(solution1: Solution, solution2: Solution):
|
|
|
55
56
|
for solution in term_list:
|
|
56
57
|
for solute in solution.components:
|
|
57
58
|
if solution.get_amount(solute, "fraction") != 0:
|
|
58
|
-
term_list[solution] += solution.get_amount(solute, "mol") *
|
|
59
|
+
term_list[solution] += solution.get_amount(solute, "mol") * np.log(solution.get_activity(solute))
|
|
59
60
|
|
|
60
61
|
return (ureg.R * blend.temperature.to("K") * (term_list[blend] - term_list[concentrate] - term_list[dilute])).to(
|
|
61
62
|
"J"
|
|
@@ -102,7 +103,7 @@ def entropy_mix(solution1: Solution, solution2: Solution):
|
|
|
102
103
|
for solution in term_list:
|
|
103
104
|
for solute in solution.components:
|
|
104
105
|
if solution.get_amount(solute, "fraction") != 0:
|
|
105
|
-
term_list[solution] += solution.get_amount(solute, "mol") *
|
|
106
|
+
term_list[solution] += solution.get_amount(solute, "mol") * np.log(
|
|
106
107
|
solution.get_amount(solute, "fraction")
|
|
107
108
|
)
|
|
108
109
|
|
|
@@ -242,7 +243,7 @@ def donnan_eql(solution: Solution, fixed_charge: str):
|
|
|
242
243
|
|
|
243
244
|
return (act_cation_mem / act_cation_soln) ** (1 / z_cation) * (act_anion_soln / act_anion_mem) ** (
|
|
244
245
|
1 / z_anion
|
|
245
|
-
) -
|
|
246
|
+
) - np.exp(delta_pi * exp_term)
|
|
246
247
|
|
|
247
248
|
# solve the function above using one of scipy's nonlinear solvers
|
|
248
249
|
|
pyEQL/salt_ion_match.py
CHANGED
pyEQL/solute.py
CHANGED
pyEQL/solution.py
CHANGED
|
@@ -9,7 +9,6 @@ pyEQL Solution Class.
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import logging
|
|
12
|
-
import math
|
|
13
12
|
import os
|
|
14
13
|
import warnings
|
|
15
14
|
from functools import lru_cache
|
|
@@ -58,7 +57,7 @@ class Solution(MSONable):
|
|
|
58
57
|
database: str | Path | Store | None = None,
|
|
59
58
|
default_diffusion_coeff: float = 1.6106e-9,
|
|
60
59
|
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = "ERROR",
|
|
61
|
-
):
|
|
60
|
+
) -> None:
|
|
62
61
|
"""
|
|
63
62
|
Instantiate a Solution from a composition.
|
|
64
63
|
|
|
@@ -433,6 +432,10 @@ class Solution(MSONable):
|
|
|
433
432
|
activity = False) of the solute.
|
|
434
433
|
"""
|
|
435
434
|
try:
|
|
435
|
+
# TODO - for some reason this specific method requires the use of math.log10 rather than np.log10.
|
|
436
|
+
# Using np.exp raises ZeroDivisionError
|
|
437
|
+
import math
|
|
438
|
+
|
|
436
439
|
if activity is True:
|
|
437
440
|
return -1 * math.log10(self.get_activity(solute))
|
|
438
441
|
return -1 * math.log10(self.get_amount(solute, "mol/L").magnitude)
|
|
@@ -647,7 +650,7 @@ class Solution(MSONable):
|
|
|
647
650
|
x_cat = self.get_amount(salt.cation, "fraction").magnitude
|
|
648
651
|
|
|
649
652
|
# calculate the kinematic viscosity
|
|
650
|
-
nu =
|
|
653
|
+
nu = np.log(nu_w * MW_w / MW) + 15 * x_cat**2 + x_cat**3 * G_123 + 3 * x_cat * G_23 * (1 - 0.05 * x_cat)
|
|
651
654
|
|
|
652
655
|
return ureg.Quantity(np.exp(nu), "m**2 / s")
|
|
653
656
|
|
|
@@ -930,9 +933,7 @@ class Solution(MSONable):
|
|
|
930
933
|
:attr:`dielectric_constant`
|
|
931
934
|
|
|
932
935
|
"""
|
|
933
|
-
bjerrum_length = ureg.e**2 / (
|
|
934
|
-
4 * math.pi * self.dielectric_constant * ureg.epsilon_0 * ureg.k * self.temperature
|
|
935
|
-
)
|
|
936
|
+
bjerrum_length = ureg.e**2 / (4 * np.pi * self.dielectric_constant * ureg.epsilon_0 * ureg.k * self.temperature)
|
|
936
937
|
return bjerrum_length.to("nm")
|
|
937
938
|
|
|
938
939
|
@property
|
|
@@ -975,7 +976,7 @@ class Solution(MSONable):
|
|
|
975
976
|
partial_molar_volume_water = self.get_property(self.solvent, "size.molar_volume")
|
|
976
977
|
|
|
977
978
|
osmotic_pressure = (
|
|
978
|
-
-1 * ureg.R * self.temperature / partial_molar_volume_water *
|
|
979
|
+
-1 * ureg.R * self.temperature / partial_molar_volume_water * np.log(self.get_water_activity())
|
|
979
980
|
)
|
|
980
981
|
self.logger.debug(
|
|
981
982
|
f"Calculated osmotic pressure of solution as {osmotic_pressure} Pa at T= {self.temperature} degrees C"
|
|
@@ -1835,12 +1836,12 @@ class Solution(MSONable):
|
|
|
1835
1836
|
* ureg.Quantity(0.018015, "kg/mol")
|
|
1836
1837
|
* self.get_total_moles_solute()
|
|
1837
1838
|
/ self.solvent_mass
|
|
1838
|
-
/
|
|
1839
|
+
/ np.log(self.get_amount(self.solvent, "fraction"))
|
|
1839
1840
|
)
|
|
1840
1841
|
if scale == "fugacity":
|
|
1841
1842
|
return np.exp(
|
|
1842
1843
|
-molal_phi * ureg.Quantity(0.018015, "kg/mol") * self.get_total_moles_solute() / self.solvent_mass
|
|
1843
|
-
-
|
|
1844
|
+
- np.log(self.get_amount(self.solvent, "fraction"))
|
|
1844
1845
|
) * ureg.Quantity(1, "dimensionless")
|
|
1845
1846
|
|
|
1846
1847
|
raise ValueError("Invalid scale argument. Pass 'molal', 'rational', or 'fugacity'.")
|
|
@@ -1939,14 +1940,14 @@ class Solution(MSONable):
|
|
|
1939
1940
|
ureg.R
|
|
1940
1941
|
* self.temperature.to("K")
|
|
1941
1942
|
* self.get_amount(item, "mol")
|
|
1942
|
-
*
|
|
1943
|
+
* np.log(self.get_activity(item))
|
|
1943
1944
|
)
|
|
1944
1945
|
else:
|
|
1945
1946
|
E += (
|
|
1946
1947
|
ureg.R
|
|
1947
1948
|
* self.temperature.to("K")
|
|
1948
1949
|
* self.get_amount(item, "mol")
|
|
1949
|
-
*
|
|
1950
|
+
* np.log(self.get_amount(item, "fraction"))
|
|
1950
1951
|
)
|
|
1951
1952
|
# If we have a solute with zero concentration, we will get a ValueError
|
|
1952
1953
|
except ValueError:
|
|
@@ -2511,7 +2512,7 @@ class Solution(MSONable):
|
|
|
2511
2512
|
return loadfn(filename)
|
|
2512
2513
|
|
|
2513
2514
|
# arithmetic operations
|
|
2514
|
-
def __add__(self, other: Solution):
|
|
2515
|
+
def __add__(self, other: Solution) -> Solution:
|
|
2515
2516
|
"""
|
|
2516
2517
|
Solution addition: mix two solutions together.
|
|
2517
2518
|
|
|
@@ -2582,12 +2583,12 @@ class Solution(MSONable):
|
|
|
2582
2583
|
"this property is planned for a future release."
|
|
2583
2584
|
)
|
|
2584
2585
|
# calculate the new pH and pE (before reactions) by mixing
|
|
2585
|
-
mix_pH = -
|
|
2586
|
+
mix_pH = -np.log10(float(mix_species["H+"].split(" ")[0]) / mix_vol.to("L").magnitude)
|
|
2586
2587
|
|
|
2587
2588
|
# pE = -log[e-], so calculate the moles of e- in each solution and mix them
|
|
2588
2589
|
mol_e_self = 10 ** (-1 * self.pE) * self.volume.to("L").magnitude
|
|
2589
2590
|
mol_e_other = 10 ** (-1 * other.pE) * other.volume.to("L").magnitude
|
|
2590
|
-
mix_pE = -
|
|
2591
|
+
mix_pE = -np.log10((mol_e_self + mol_e_other) / mix_vol.to("L").magnitude)
|
|
2591
2592
|
|
|
2592
2593
|
# create a new solution
|
|
2593
2594
|
return Solution(
|
|
@@ -2599,10 +2600,10 @@ class Solution(MSONable):
|
|
|
2599
2600
|
pE=mix_pE,
|
|
2600
2601
|
)
|
|
2601
2602
|
|
|
2602
|
-
def __sub__(self, other: Solution):
|
|
2603
|
+
def __sub__(self, other: Solution) -> None:
|
|
2603
2604
|
raise NotImplementedError("Subtraction of solutions is not implemented.")
|
|
2604
2605
|
|
|
2605
|
-
def __mul__(self, factor: float):
|
|
2606
|
+
def __mul__(self, factor: float) -> None:
|
|
2606
2607
|
"""
|
|
2607
2608
|
Solution multiplication: scale all components by a factor. For example, Solution * 2 will double the moles of
|
|
2608
2609
|
every component (including solvent). No other properties will change.
|
|
@@ -2610,7 +2611,7 @@ class Solution(MSONable):
|
|
|
2610
2611
|
self.volume *= factor
|
|
2611
2612
|
return self
|
|
2612
2613
|
|
|
2613
|
-
def __truediv__(self, factor: float):
|
|
2614
|
+
def __truediv__(self, factor: float) -> None:
|
|
2614
2615
|
"""
|
|
2615
2616
|
Solution division: scale all components by a factor. For example, Solution / 2 will remove half of the moles
|
|
2616
2617
|
of every compoonents (including solvent). No other properties will change.
|
|
@@ -2656,7 +2657,7 @@ class Solution(MSONable):
|
|
|
2656
2657
|
|
|
2657
2658
|
print(f"{i}:\t {amt:0.{places}f}")
|
|
2658
2659
|
|
|
2659
|
-
def __str__(self):
|
|
2660
|
+
def __str__(self) -> str:
|
|
2660
2661
|
# set output of the print() statement for the solution
|
|
2661
2662
|
l1 = f"Volume: {self.volume:.3f~}"
|
|
2662
2663
|
l2 = f"Temperature: {self.temperature:.3f~}"
|
pyEQL/utils.py
CHANGED
|
@@ -9,6 +9,7 @@ pyEQL utilities
|
|
|
9
9
|
import logging
|
|
10
10
|
from collections import UserDict
|
|
11
11
|
from functools import lru_cache
|
|
12
|
+
from typing import Any
|
|
12
13
|
|
|
13
14
|
from iapws import IAPWS95, IAPWS97
|
|
14
15
|
from pymatgen.core.ion import Ion
|
|
@@ -59,7 +60,39 @@ def standardize_formula(formula: str):
|
|
|
59
60
|
be enclosed in square brackets to remove any ambiguity in the meaning of the formula. For example, 'Na+',
|
|
60
61
|
'Na+1', and 'Na[+]' will all standardize to "Na[+1]"
|
|
61
62
|
"""
|
|
62
|
-
|
|
63
|
+
sform = Ion.from_formula(formula).reduced_formula
|
|
64
|
+
|
|
65
|
+
# TODO - manual formula adjustments. May be implemented upstream in pymatgen in the future
|
|
66
|
+
# thanks to @xiaoxiaozhu123 for pointing out these issues in
|
|
67
|
+
# https://github.com/KingsburyLab/pyEQL/issues/136
|
|
68
|
+
|
|
69
|
+
# ammonia
|
|
70
|
+
if sform == "H4N[+1]":
|
|
71
|
+
sform = "NH4[+1]"
|
|
72
|
+
elif sform == "H3N(aq)":
|
|
73
|
+
sform = "NH3(aq)"
|
|
74
|
+
# phosphoric acid system
|
|
75
|
+
elif sform == "PH3O4(aq)":
|
|
76
|
+
sform = "H3PO4(aq)"
|
|
77
|
+
elif sform == "PHO4[-2]":
|
|
78
|
+
sform = "HPO4[-2]"
|
|
79
|
+
elif sform == "P(HO2)2[-1]":
|
|
80
|
+
sform = "H2PO4[-1]"
|
|
81
|
+
# thiocyanate
|
|
82
|
+
elif sform == "CSN[-1]":
|
|
83
|
+
sform = "SCN[-1]"
|
|
84
|
+
# triiodide
|
|
85
|
+
elif sform == "I[-0.33333333]":
|
|
86
|
+
sform = "I3[-1]"
|
|
87
|
+
# formate
|
|
88
|
+
elif sform == "HCOO[-1]":
|
|
89
|
+
sform = "HCO2[-1]"
|
|
90
|
+
# oxalate
|
|
91
|
+
elif sform == "CO2[-1]":
|
|
92
|
+
sform = "C2O4[-2]"
|
|
93
|
+
|
|
94
|
+
# TODO - consider adding recognition of special formulas like MeOH for methanol or Cit for citrate
|
|
95
|
+
return sform
|
|
63
96
|
|
|
64
97
|
|
|
65
98
|
def format_solutes_dict(solute_dict: dict, units: str):
|
|
@@ -115,18 +148,18 @@ class FormulaDict(UserDict):
|
|
|
115
148
|
formula notation (e.g., "Na+", "Na+1", "Na[+]" all have the same effect)
|
|
116
149
|
"""
|
|
117
150
|
|
|
118
|
-
def __getitem__(self, key):
|
|
151
|
+
def __getitem__(self, key) -> Any:
|
|
119
152
|
return super().__getitem__(standardize_formula(key))
|
|
120
153
|
|
|
121
|
-
def __setitem__(self, key, value):
|
|
154
|
+
def __setitem__(self, key, value) -> None:
|
|
122
155
|
super().__setitem__(standardize_formula(key), value)
|
|
123
156
|
# sort contents anytime an item is set
|
|
124
157
|
self.data = dict(sorted(self.items(), key=lambda x: x[1], reverse=True))
|
|
125
158
|
|
|
126
159
|
# Necessary to define this so that .get() works properly in python 3.12+
|
|
127
160
|
# see https://github.com/python/cpython/issues/105524
|
|
128
|
-
def __contains__(self, key):
|
|
161
|
+
def __contains__(self, key) -> bool:
|
|
129
162
|
return standardize_formula(key) in self.data
|
|
130
163
|
|
|
131
|
-
def __delitem__(self, key):
|
|
164
|
+
def __delitem__(self, key) -> None:
|
|
132
165
|
super().__delitem__(standardize_formula(key))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pyEQL
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: Python tools for solution chemistry
|
|
5
5
|
Home-page: https://github.com/KingsburyLab/pyEQL
|
|
6
6
|
Author: Ryan Kingsbury
|
|
@@ -19,12 +19,12 @@ License-File: LICENSE.txt
|
|
|
19
19
|
License-File: COPYING
|
|
20
20
|
License-File: AUTHORS.md
|
|
21
21
|
Requires-Dist: pint >=0.19
|
|
22
|
-
Requires-Dist: numpy
|
|
22
|
+
Requires-Dist: numpy <2
|
|
23
23
|
Requires-Dist: scipy
|
|
24
|
-
Requires-Dist: pymatgen
|
|
24
|
+
Requires-Dist: pymatgen ==2024.5.1
|
|
25
25
|
Requires-Dist: iapws
|
|
26
26
|
Requires-Dist: monty
|
|
27
|
-
Requires-Dist: maggma >=0.
|
|
27
|
+
Requires-Dist: maggma >=0.67.0
|
|
28
28
|
Requires-Dist: phreeqpython
|
|
29
29
|
Provides-Extra: docs
|
|
30
30
|
Requires-Dist: sphinx >=3.2.1 ; extra == 'docs'
|
|
@@ -37,6 +37,7 @@ Requires-Dist: setuptools ; extra == 'testing'
|
|
|
37
37
|
Requires-Dist: pre-commit ; extra == 'testing'
|
|
38
38
|
Requires-Dist: pytest ; extra == 'testing'
|
|
39
39
|
Requires-Dist: pytest-cov ; extra == 'testing'
|
|
40
|
+
Requires-Dist: pytest-xdist ; extra == 'testing'
|
|
40
41
|
Requires-Dist: black ; extra == 'testing'
|
|
41
42
|
Requires-Dist: mypy ; extra == 'testing'
|
|
42
43
|
Requires-Dist: ruff ; extra == 'testing'
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
pyEQL/__init__.py,sha256=JErflmaJVP373dm3-YGHUomFu05dgX0iXWS-Z5AgSTU,2118
|
|
2
|
+
pyEQL/activity_correction.py,sha256=eOixjgTd5hTrTRD5s6aPCCG12lAIH7-lRN0Z1qHu678,37151
|
|
3
|
+
pyEQL/engines.py,sha256=b9ay7FYqmnINmhSMyNpVmENAtZVW-gyBfXUPf1PEUoY,34946
|
|
4
|
+
pyEQL/equilibrium.py,sha256=YCtoAJSgn1WC9NJnc3H4FTJdKQvogsvCuj7HqlKMtww,8307
|
|
5
|
+
pyEQL/functions.py,sha256=nc-Hc61MmW-ELBR1PByJvQnELxM7PZexMHbU_O5-Bnw,10584
|
|
6
|
+
pyEQL/pint_custom_units.txt,sha256=XHmcMlwVvqF9nEW7_e9Xgyq-xWEr-cDYqieas11T3eY,2882
|
|
7
|
+
pyEQL/salt_ion_match.py,sha256=0nCZXmeo67VqcyYWQpPx-81hjSvnsg8HFB3fIyfjW_k,4070
|
|
8
|
+
pyEQL/solute.py,sha256=no00Rc3tRfHmyht4wm2UXA1KZhKC45tWMO5QEkZY6yg,5140
|
|
9
|
+
pyEQL/solution.py,sha256=XwtQmll3gPwdnnkXpQsnO7QebwTjplmlfhbEIU8rHDo,115940
|
|
10
|
+
pyEQL/utils.py,sha256=unsY7zrrhl_1mOmt9_kumSKmLE5N5Hvh4bOErfS3nws,5168
|
|
11
|
+
pyEQL/database/geothermal.dat,sha256=kksnfcBtWdOTpNn4CLXU1Mz16cwas2WuVKpuMU8CaVI,234230
|
|
12
|
+
pyEQL/database/llnl.dat,sha256=jN-a0kfUFbQlYMn2shTVRg1JX_ZhLa-tJ0lLw2YSpLU,751462
|
|
13
|
+
pyEQL/database/phreeqc_license.txt,sha256=8W1r8VxC2kVptIMSU9sDFNASYqN7MdwKEtIWWfjTQuM,2906
|
|
14
|
+
pyEQL/database/pyeql_db.json,sha256=TQKKofds7QBNd-Hw5QQuPwP6rQ8YWh_hHlRAtoQX0m8,1080793
|
|
15
|
+
pyEQL/presets/Ringers lactate.yaml,sha256=vtSnuvgALHR27XEjpDzC0xyw5-E6b2FSsF1EUEBiWpw,413
|
|
16
|
+
pyEQL/presets/normal saline.yaml,sha256=i2znhnIeXfNx1iMFFSif7crMRCFRP6xN1m7Wp7USduM,318
|
|
17
|
+
pyEQL/presets/rainwater.yaml,sha256=S0WHZNDfCJyjSSFxNFdkypjn2s3P0jJGCiYIxvi1ibA,337
|
|
18
|
+
pyEQL/presets/seawater.yaml,sha256=oryc1CkhRz20RpWE6uiGiT93HoZnqlB0s-0PmBWr3-U,843
|
|
19
|
+
pyEQL/presets/urine.yaml,sha256=0Njtc-H1fFRo7UhquHdiSTT4z-8VZJ1utDCk02qk28M,679
|
|
20
|
+
pyEQL/presets/wastewater.yaml,sha256=jTTFBpmKxczaEtkCZb0xUULIPZt7wfC8eAJ6rthGnmw,502
|
|
21
|
+
pyEQL-1.0.2.dist-info/AUTHORS.md,sha256=K9ZLhKFwZ2zLlFXwN62VuUYCpr5T6n4mOUCUHlytTUs,415
|
|
22
|
+
pyEQL-1.0.2.dist-info/COPYING,sha256=Ww2oUywfFTn242v9ksCgQdIVSpcMXJiKKePn0GFm25E,7649
|
|
23
|
+
pyEQL-1.0.2.dist-info/LICENSE.txt,sha256=2Zf1F7RzbpeposgIxUydpurqNCMoMgDi2gAB65_GjwQ,969
|
|
24
|
+
pyEQL-1.0.2.dist-info/METADATA,sha256=YwIdTX8P0sFSCsTf65iFdxGZtQR_W_SdgfFEZy7RM0M,5889
|
|
25
|
+
pyEQL-1.0.2.dist-info/WHEEL,sha256=0XQbNV6JE5ziJsWjIU8TRRv0N6SohNonLWgP86g5fiI,109
|
|
26
|
+
pyEQL-1.0.2.dist-info/top_level.txt,sha256=QMOaZjCAm_lS4Njsjh4L0B5aWnJFGQMYKhuH88CG1co,6
|
|
27
|
+
pyEQL-1.0.2.dist-info/RECORD,,
|
pyEQL-1.0.0.dist-info/RECORD
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
pyEQL/__init__.py,sha256=IebzxPm-7Kz9cG2bl4anfo8NxOOKZj_ggkxBYyySvWE,2101
|
|
2
|
-
pyEQL/activity_correction.py,sha256=mtpHdrjyrqkwWGUKh-91lH10HLOTTw3dQUqA10hMgms,36999
|
|
3
|
-
pyEQL/engines.py,sha256=xpKlnAhdOXX25fM9EUzHMmYkJgaPX1FKqJ0ldMvFMKY,33539
|
|
4
|
-
pyEQL/equilibrium.py,sha256=Y_jFqsPW-mcfqC524zV4dvMzWr4uwP7ZjGv8Kgkx5G0,8304
|
|
5
|
-
pyEQL/functions.py,sha256=qsFXOs1BTQ6TB1DH_oD9WhX78DyaYHOjKKVZkaPW6Ew,10582
|
|
6
|
-
pyEQL/pint_custom_units.txt,sha256=XHmcMlwVvqF9nEW7_e9Xgyq-xWEr-cDYqieas11T3eY,2882
|
|
7
|
-
pyEQL/salt_ion_match.py,sha256=D4HJdV7iVsBEW5bW9tWpvc6pK-jrexTlvJV80YabHo4,4062
|
|
8
|
-
pyEQL/solute.py,sha256=R4XjPiAZP2787ImIo072CWD1vjIYjGPt6zvxCrVO1pA,5133
|
|
9
|
-
pyEQL/solution.py,sha256=nMbR1qprVag5UkIJFzLwkzDHJfpLyIdTgOzFzZdyHd8,115753
|
|
10
|
-
pyEQL/utils.py,sha256=2VLl_sqcKcqBrVdP0ogtIYsOlc-wl4repi43Jqr-8yg,4156
|
|
11
|
-
pyEQL/database/geothermal.dat,sha256=kksnfcBtWdOTpNn4CLXU1Mz16cwas2WuVKpuMU8CaVI,234230
|
|
12
|
-
pyEQL/database/llnl.dat,sha256=jN-a0kfUFbQlYMn2shTVRg1JX_ZhLa-tJ0lLw2YSpLU,751462
|
|
13
|
-
pyEQL/database/phreeqc_license.txt,sha256=8W1r8VxC2kVptIMSU9sDFNASYqN7MdwKEtIWWfjTQuM,2906
|
|
14
|
-
pyEQL/database/pyeql_db.json,sha256=TQKKofds7QBNd-Hw5QQuPwP6rQ8YWh_hHlRAtoQX0m8,1080793
|
|
15
|
-
pyEQL/presets/Ringers lactate.yaml,sha256=vtSnuvgALHR27XEjpDzC0xyw5-E6b2FSsF1EUEBiWpw,413
|
|
16
|
-
pyEQL/presets/normal saline.yaml,sha256=i2znhnIeXfNx1iMFFSif7crMRCFRP6xN1m7Wp7USduM,318
|
|
17
|
-
pyEQL/presets/rainwater.yaml,sha256=S0WHZNDfCJyjSSFxNFdkypjn2s3P0jJGCiYIxvi1ibA,337
|
|
18
|
-
pyEQL/presets/seawater.yaml,sha256=oryc1CkhRz20RpWE6uiGiT93HoZnqlB0s-0PmBWr3-U,843
|
|
19
|
-
pyEQL/presets/urine.yaml,sha256=0Njtc-H1fFRo7UhquHdiSTT4z-8VZJ1utDCk02qk28M,679
|
|
20
|
-
pyEQL/presets/wastewater.yaml,sha256=jTTFBpmKxczaEtkCZb0xUULIPZt7wfC8eAJ6rthGnmw,502
|
|
21
|
-
pyEQL-1.0.0.dist-info/AUTHORS.md,sha256=K9ZLhKFwZ2zLlFXwN62VuUYCpr5T6n4mOUCUHlytTUs,415
|
|
22
|
-
pyEQL-1.0.0.dist-info/COPYING,sha256=Ww2oUywfFTn242v9ksCgQdIVSpcMXJiKKePn0GFm25E,7649
|
|
23
|
-
pyEQL-1.0.0.dist-info/LICENSE.txt,sha256=2Zf1F7RzbpeposgIxUydpurqNCMoMgDi2gAB65_GjwQ,969
|
|
24
|
-
pyEQL-1.0.0.dist-info/METADATA,sha256=5y38qhav2GITqPJ1yYXjBuMu_DZYKBvAkVpoodWcP4A,5839
|
|
25
|
-
pyEQL-1.0.0.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
|
|
26
|
-
pyEQL-1.0.0.dist-info/top_level.txt,sha256=QMOaZjCAm_lS4Njsjh4L0B5aWnJFGQMYKhuH88CG1co,6
|
|
27
|
-
pyEQL-1.0.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|