pyEQL 1.2.0__py3-none-any.whl → 1.3.0__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 +9 -9
- pyEQL/database/pyeql_db.json +20839 -20936
- pyEQL/engines.py +61 -66
- pyEQL/equilibrium.py +2 -2
- pyEQL/functions.py +5 -5
- pyEQL/py.typed +0 -0
- pyEQL/salt_ion_match.py +2 -2
- pyEQL/solution.py +199 -293
- pyEQL/utils.py +4 -0
- {pyEQL-1.2.0.dist-info → pyeql-1.3.0.dist-info}/METADATA +26 -26
- pyeql-1.3.0.dist-info/RECORD +26 -0
- {pyEQL-1.2.0.dist-info → pyeql-1.3.0.dist-info}/WHEEL +1 -1
- {pyEQL-1.2.0.dist-info → pyeql-1.3.0.dist-info/licenses}/AUTHORS.md +5 -0
- pyEQL-1.2.0.dist-info/COPYING +0 -165
- pyEQL-1.2.0.dist-info/RECORD +0 -26
- {pyEQL-1.2.0.dist-info → pyeql-1.3.0.dist-info/licenses}/LICENSE.txt +0 -0
- {pyEQL-1.2.0.dist-info → pyeql-1.3.0.dist-info}/top_level.txt +0 -0
pyEQL/solution.py
CHANGED
|
@@ -59,7 +59,7 @@ class Solution(MSONable):
|
|
|
59
59
|
default_diffusion_coeff: float = 1.6106e-9,
|
|
60
60
|
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = "ERROR",
|
|
61
61
|
) -> None:
|
|
62
|
-
"""
|
|
62
|
+
r"""
|
|
63
63
|
Instantiate a Solution from a composition.
|
|
64
64
|
|
|
65
65
|
Args:
|
|
@@ -88,26 +88,59 @@ class Solution(MSONable):
|
|
|
88
88
|
Negative log of H+ activity. If omitted, the solution will be
|
|
89
89
|
initialized to pH 7 (neutral) with appropriate quantities of
|
|
90
90
|
H+ and OH- ions
|
|
91
|
-
pE: the
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
pE: the :math:`pe` value of the solution. :math:`pe` measures the relative abundance of electrons
|
|
92
|
+
analogous to how pH measures the relative abundance of protons. Specifically, :math:`pe` is defined in
|
|
93
|
+
terms of the activity of electrons :math:`[e^{-}]`:
|
|
94
|
+
|
|
95
|
+
.. math:: pe = - \log [e^{-}]
|
|
96
|
+
|
|
97
|
+
The relationship between the redox potential :math:`Eh` and :math:`pe` can be illustrated by considering
|
|
98
|
+
the general redox reaction,
|
|
99
|
+
|
|
100
|
+
.. math::
|
|
101
|
+
|
|
102
|
+
\begin{gather*}
|
|
103
|
+
\text{A}^x \pm ne^{-} \longrightarrow \text{A}^{x \mp n} \quad\quad
|
|
104
|
+
K = \frac{[\text{A}^{x \mp n}]}{[\text{A}^x][e^{-}]^{\pm n}}
|
|
105
|
+
\end{gather*}
|
|
106
|
+
|
|
107
|
+
Writing :math:`pe` in terms of the equilibrium constant :math:`K` and the activities,
|
|
108
|
+
:math:`[\text{A}^{x}]` and :math:`[\text{A}^{x \mp n}]`, we have:
|
|
109
|
+
|
|
110
|
+
.. math::
|
|
111
|
+
|
|
112
|
+
\begin{gather*}
|
|
113
|
+
pe = -\log[e^{-}] = \mp \frac{1}{n} \log\left(\frac{1}{K} \frac{[\text{A}^{x \mp n}]}{[\text{A}^x]}\right)
|
|
114
|
+
= \mp \frac{\Delta G}{nRT \ln 10} = \frac{FEh}{RT \ln 10}
|
|
115
|
+
\end{gather*}
|
|
116
|
+
|
|
117
|
+
Thus, the redox potential :math:`Eh` is then related to :math:`pe` via:
|
|
118
|
+
|
|
119
|
+
.. math:: Eh = 2.303 \frac{RT}{F}pe
|
|
120
|
+
|
|
121
|
+
where :math:`F` is Faraday's constant. Note that lower values of ``pE`` (and thus :math:`Eh`)
|
|
122
|
+
correspond to more reducing environments, while higher values = more oxidizing. At pH 7, water is stable
|
|
123
|
+
between approximately -7 to +14. The default value corresponds to a :math:`pe` value typical of natural
|
|
94
124
|
waters in equilibrium with the atmosphere.
|
|
95
125
|
balance_charge: The strategy for balancing charge during init and equilibrium calculations. Valid options
|
|
96
126
|
are
|
|
127
|
+
|
|
97
128
|
- 'pH', which will adjust the solution pH to balance charge,
|
|
98
129
|
- 'auto' which will use the majority cation or anion (i.e., that with the largest concentration)
|
|
99
|
-
|
|
130
|
+
as needed,
|
|
100
131
|
- 'pE' (not currently implemented) which will adjust the redox equilibrium to balance charge, or
|
|
101
|
-
|
|
102
|
-
|
|
132
|
+
the name of a dissolved species e.g. 'Ca+2' or 'Cl-' that will be added/subtracted to balance
|
|
133
|
+
charge.
|
|
103
134
|
- None (default), in which case no charge balancing will be performed either on init or when
|
|
104
|
-
|
|
135
|
+
equilibrate() is called. Note that in this case, equilibrate() can distort the charge balance!
|
|
136
|
+
|
|
105
137
|
solvent: Formula of the solvent. Solvents other than water are not supported at this time.
|
|
106
138
|
engine: Electrolyte modeling engine to use. See documentation for details on the available engines.
|
|
107
139
|
database: path to a .json file (str or Path) or maggma Store instance that
|
|
108
140
|
contains serialized SoluteDocs. `None` (default) will use the built-in pyEQL database.
|
|
109
141
|
log_level: Log messages of this or higher severity will be printed to stdout. Defaults to 'ERROR', meaning
|
|
110
|
-
that ERROR and CRITICAL messages will be shown, while WARNING, INFO, and DEBUG messages are not. If set
|
|
142
|
+
that ERROR and CRITICAL messages will be shown, while WARNING, INFO, and DEBUG messages are not. If set
|
|
143
|
+
to None, nothing will be printed.
|
|
111
144
|
default_diffusion_coeff: Diffusion coefficient value in m^2/s to use in
|
|
112
145
|
calculations when there is no diffusion coefficient for a species in the database. This affects several
|
|
113
146
|
important property calculations including conductivity and transport number, which are related to the
|
|
@@ -140,7 +173,7 @@ class Solution(MSONable):
|
|
|
140
173
|
self.logger.handlers.clear()
|
|
141
174
|
# use rich for pretty log formatting, if installed
|
|
142
175
|
try:
|
|
143
|
-
from rich.logging import RichHandler
|
|
176
|
+
from rich.logging import RichHandler # noqa: PLC0415
|
|
144
177
|
|
|
145
178
|
sh = RichHandler(rich_tracebacks=True)
|
|
146
179
|
except ImportError:
|
|
@@ -194,7 +227,7 @@ class Solution(MSONable):
|
|
|
194
227
|
if database is None:
|
|
195
228
|
# load the default database, which is a JSONStore
|
|
196
229
|
db_store = IonDB
|
|
197
|
-
elif isinstance(database,
|
|
230
|
+
elif isinstance(database, str | Path):
|
|
198
231
|
db_store = JSONStore(str(database), key="formula")
|
|
199
232
|
self.logger.debug(f"Created maggma JSONStore from .json file {database}")
|
|
200
233
|
else:
|
|
@@ -228,15 +261,6 @@ class Solution(MSONable):
|
|
|
228
261
|
self.solvent = standardize_formula(solvent[0])
|
|
229
262
|
"""Formula of the component that is set as the solvent (currently only H2O(aq) is supported)."""
|
|
230
263
|
|
|
231
|
-
# TODO - do I need the ability to specify the solvent mass?
|
|
232
|
-
# # raise an error if the solvent volume has also been given
|
|
233
|
-
# if volume_set is True:
|
|
234
|
-
# self.logger.error(
|
|
235
|
-
# "Solvent volume and mass cannot both be specified. Calculating volume based on solvent mass."
|
|
236
|
-
# )
|
|
237
|
-
# # add the solvent and the mass
|
|
238
|
-
# self.add_solvent(self.solvent, kwargs["solvent"][1])
|
|
239
|
-
|
|
240
264
|
# calculate the moles of solvent (water) on the density and the solution volume
|
|
241
265
|
moles = self.volume.magnitude / 55.55 # molarity of pure water
|
|
242
266
|
self.components["H2O"] = moles
|
|
@@ -249,9 +273,20 @@ class Solution(MSONable):
|
|
|
249
273
|
self._solutes = solutes
|
|
250
274
|
if self._solutes is None:
|
|
251
275
|
self._solutes = {}
|
|
276
|
+
|
|
252
277
|
if isinstance(self._solutes, dict):
|
|
253
278
|
for k, v in self._solutes.items():
|
|
254
279
|
self.add_solute(k, v)
|
|
280
|
+
# if user has specified H+ in solutes, check consistency with pH kwarg
|
|
281
|
+
if standardize_formula(k) == "H[+1]":
|
|
282
|
+
# if user has not specified pH (default value), override the pH argument
|
|
283
|
+
if self._pH == 7:
|
|
284
|
+
self.logger.warning(f"H[+1] = {v} found in solutes. Overriding default pH with this value.")
|
|
285
|
+
# if user specifies non-default pH that does not match the supplied H+, raise an error
|
|
286
|
+
elif not np.isclose(self.pH, self._pH, atol=1e-4):
|
|
287
|
+
raise ValueError(
|
|
288
|
+
"Cannot specify both a non-default pH and H+ at the same time. Please provide only one."
|
|
289
|
+
)
|
|
255
290
|
elif isinstance(self._solutes, list):
|
|
256
291
|
msg = (
|
|
257
292
|
'List input of solutes (e.g., [["Na+", "0.5 mol/L]]) is deprecated! Use dictionary formatted input '
|
|
@@ -347,13 +382,6 @@ class Solution(MSONable):
|
|
|
347
382
|
>>> mysol = Solution([['Na+','2 mol/L'],['Cl-','0.01 mol/L']],volume='500 mL')
|
|
348
383
|
>>> print(mysol.volume)
|
|
349
384
|
0.5000883925072983 l
|
|
350
|
-
>>> mysol.list_concentrations()
|
|
351
|
-
{'H2O': '55.508435061791985 mol/kg', 'Cl-': '0.00992937605907076 mol/kg', 'Na+': '2.0059345573880325 mol/kg'}
|
|
352
|
-
>>> mysol.volume = '200 mL')
|
|
353
|
-
>>> print(mysol.volume)
|
|
354
|
-
0.2 l
|
|
355
|
-
>>> mysol.list_concentrations()
|
|
356
|
-
{'H2O': '55.50843506179199 mol/kg', 'Cl-': '0.00992937605907076 mol/kg', 'Na+': '2.0059345573880325 mol/kg'}
|
|
357
385
|
|
|
358
386
|
"""
|
|
359
387
|
# figure out the factor to multiply the old concentrations by
|
|
@@ -415,11 +443,11 @@ class Solution(MSONable):
|
|
|
415
443
|
self.volume_update_required = True
|
|
416
444
|
|
|
417
445
|
@property
|
|
418
|
-
def pH(self) -> float
|
|
446
|
+
def pH(self) -> float:
|
|
419
447
|
"""Return the pH of the solution."""
|
|
420
448
|
return self.p("H+", activity=False)
|
|
421
449
|
|
|
422
|
-
def p(self, solute: str, activity=True) -> float
|
|
450
|
+
def p(self, solute: str, activity=True) -> float:
|
|
423
451
|
"""
|
|
424
452
|
Return the negative log of the activity of solute.
|
|
425
453
|
|
|
@@ -435,19 +463,18 @@ class Solution(MSONable):
|
|
|
435
463
|
Returns:
|
|
436
464
|
Quantity
|
|
437
465
|
The negative log10 of the activity (or molar concentration if
|
|
438
|
-
activity = False) of the solute.
|
|
466
|
+
activity = False) of the solute. If the solute has zero concentration
|
|
467
|
+
then np.nan (not a number) is returned.
|
|
439
468
|
"""
|
|
440
469
|
try:
|
|
441
|
-
# TODO - for some reason this specific method requires the use of math.log10 rather than np.log10.
|
|
442
|
-
# Using np.exp raises ZeroDivisionError
|
|
443
|
-
import math
|
|
444
|
-
|
|
445
470
|
if activity is True:
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
471
|
+
amt = self.get_activity(solute).magnitude
|
|
472
|
+
else:
|
|
473
|
+
amt = self.get_amount(solute, "mol/L").magnitude
|
|
474
|
+
return float(-1 * np.log10(amt))
|
|
475
|
+
# if the solute has zero or negative concentration, np.log10 raises a RuntimeWarning
|
|
476
|
+
except RuntimeWarning:
|
|
477
|
+
return np.nan
|
|
451
478
|
|
|
452
479
|
@property
|
|
453
480
|
def density(self) -> Quantity:
|
|
@@ -686,7 +713,7 @@ class Solution(MSONable):
|
|
|
686
713
|
|
|
687
714
|
References:
|
|
688
715
|
.. [aq] https://www.aqion.de/site/electrical-conductivity
|
|
689
|
-
.. [hc]
|
|
716
|
+
.. [hc] https://www.hydrochemistry.eu/exmpls/sc.html
|
|
690
717
|
|
|
691
718
|
See Also:
|
|
692
719
|
:py:attr:`ionic_strength`
|
|
@@ -885,7 +912,7 @@ class Solution(MSONable):
|
|
|
885
912
|
Returns The Debye length, in nanometers.
|
|
886
913
|
|
|
887
914
|
References:
|
|
888
|
-
.. [wk3] https://en.wikipedia.org/wiki/Debye_length#
|
|
915
|
+
.. [wk3] https://en.wikipedia.org/wiki/Debye_length#In_an_electrolyte_solution
|
|
889
916
|
|
|
890
917
|
See Also:
|
|
891
918
|
:attr:`ionic_strength`
|
|
@@ -968,7 +995,7 @@ class Solution(MSONable):
|
|
|
968
995
|
.. [sata] Sata, Toshikatsu. Ion Exchange Membranes: Preparation, Characterization, and Modification.
|
|
969
996
|
Royal Society of Chemistry, 2004, p. 10.
|
|
970
997
|
|
|
971
|
-
.. [wk]
|
|
998
|
+
.. [wk] https://en.wikipedia.org/wiki/Osmotic_pressure#Derivation_of_the_van_'t_Hoff_formula
|
|
972
999
|
|
|
973
1000
|
Examples:
|
|
974
1001
|
>>> s1=pyEQL.Solution()
|
|
@@ -1262,15 +1289,6 @@ class Solution(MSONable):
|
|
|
1262
1289
|
# set the volume recalculation flag
|
|
1263
1290
|
self.volume_update_required = True
|
|
1264
1291
|
|
|
1265
|
-
# TODO - deprecate this method. Solvent should be added to the dict like anything else
|
|
1266
|
-
# and solvent_name will track which component it is.
|
|
1267
|
-
def add_solvent(self, formula: str, amount: str):
|
|
1268
|
-
"""Same as add_solute but omits the need to pass solvent mass to pint."""
|
|
1269
|
-
quantity = ureg.Quantity(amount)
|
|
1270
|
-
mw = self.get_property(formula, "molecular_weight")
|
|
1271
|
-
target_mol = quantity.to("moles", "chem", mw=mw, volume=self.volume, solvent_mass=self.solvent_mass)
|
|
1272
|
-
self.components[formula] = target_mol.to("moles").magnitude
|
|
1273
|
-
|
|
1274
1292
|
def add_amount(self, solute: str, amount: str):
|
|
1275
1293
|
"""
|
|
1276
1294
|
Add the amount of 'solute' to the parent solution.
|
|
@@ -1473,66 +1491,81 @@ class Solution(MSONable):
|
|
|
1473
1491
|
>>> s2.get_salt().z_cation
|
|
1474
1492
|
2
|
|
1475
1493
|
"""
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1494
|
+
try:
|
|
1495
|
+
salt: Salt = next(d["salt"] for d in self.get_salt_dict().values())
|
|
1496
|
+
return salt
|
|
1497
|
+
except StopIteration:
|
|
1498
|
+
return None
|
|
1479
1499
|
|
|
1480
1500
|
# TODO - modify? deprecate? make a salts property?
|
|
1481
|
-
def get_salt_dict(self, cutoff: float =
|
|
1501
|
+
def get_salt_dict(self, cutoff: float = 1e-6, use_totals: bool = True) -> dict[str, dict[str, float | Salt]]:
|
|
1482
1502
|
"""
|
|
1483
|
-
Returns a dict
|
|
1484
|
-
keyed by formula and the values are the total moles present in the solution, e.g., {"NaCl(aq)": 1}. If the
|
|
1485
|
-
Solution is pure water, the returned dict contains only 'HOH'.
|
|
1486
|
-
|
|
1487
|
-
Args:
|
|
1488
|
-
cutoff: Lowest salt concentration to consider. Analysis will stop once the concentrations of Salts being
|
|
1489
|
-
analyzed goes below this value. Useful for excluding analysis of trace anions.
|
|
1490
|
-
use_totals: Whether to base the analysis on total element concentrations or individual species
|
|
1491
|
-
concentrations.
|
|
1503
|
+
Returns a dict that represents the salts of the Solution by pairing anions and cations.
|
|
1492
1504
|
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
Many empirical equations for solution properties such as activity coefficient,
|
|
1498
|
-
partial molar volume, or viscosity are based on the concentration of
|
|
1499
|
-
single salts (e.g., NaCl). When multiple ions are present (e.g., a solution
|
|
1500
|
-
containing Na+, Cl-, and Mg+2), it is generally not possible to directly model
|
|
1501
|
-
these quantities.
|
|
1505
|
+
The ``get_salt_dict()`` method examines the ionic composition of a solution and approximates it as a set of
|
|
1506
|
+
salts instead of individual ions. The method returns a dictionary of Salt objects where the keys are the salt
|
|
1507
|
+
formulas (e.g., 'NaCl'). The Salt object contains information about the stoichiometry of the salt to
|
|
1508
|
+
enable its effective concentration to be calculated (e.g., 1 M MgCl2 yields 1 M Mg+2 and 2 M Cl-).
|
|
1502
1509
|
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1510
|
+
Args:
|
|
1511
|
+
cutoff: Lowest molal concentration to consider. No salts below this value will be included in the output.
|
|
1512
|
+
Useful for excluding analysis of trace anions. Defaults to 1e-6 (1 part per million).
|
|
1513
|
+
use_totals: Whether or not to base the analysis on the concentration of the predominant species of each
|
|
1514
|
+
element. Note that species in which a given element assumes a different oxidation state are always
|
|
1515
|
+
treated separately.
|
|
1509
1516
|
|
|
1510
1517
|
Returns:
|
|
1511
1518
|
dict
|
|
1512
|
-
A dictionary of
|
|
1513
|
-
|
|
1514
|
-
See Also:
|
|
1515
|
-
:py:attr:`osmotic_pressure`
|
|
1516
|
-
:py:attr:`viscosity_kinematic`
|
|
1517
|
-
:py:meth:`get_activity`
|
|
1518
|
-
:py:meth:`get_activity_coefficient`
|
|
1519
|
-
:py:meth:`get_water_activity`
|
|
1520
|
-
:py:meth:`get_osmotic_coefficient`
|
|
1521
|
-
"""
|
|
1522
|
-
"""
|
|
1523
|
-
Returns a dict of salts that approximates the composition of the Solution. Like `components`, the dict is
|
|
1524
|
-
keyed by formula and the values are the total moles of salt present in the solution, e.g., {"NaCl(aq)": 1}
|
|
1519
|
+
A dictionary of representing salts in the solution, keyed by the salt formula.
|
|
1525
1520
|
|
|
1526
1521
|
Notes:
|
|
1527
|
-
|
|
1528
|
-
|
|
1522
|
+
The dict maps salt formulas to dictionaries containing their amounts and composition. The amount is stored
|
|
1523
|
+
in moles under the key "mol", and a :class:`pyEQL.salt_ion_match.Salt` object stored under the "salt" key
|
|
1524
|
+
represents the composition. Salts are identified by pairing the predominant cations and anions in the
|
|
1525
|
+
solution, in descending order of their respective equivalent amounts.
|
|
1526
|
+
|
|
1527
|
+
Many empirical equations for solution properties such as activity coefficient, partial molar volume, or
|
|
1528
|
+
viscosity are based on the concentration of single salts (e.g., NaCl). When multiple ions are present
|
|
1529
|
+
(e.g., a solution containing Na+, Cl-, and Mg+2), it is generally not possible to directly model
|
|
1530
|
+
these quantities.
|
|
1531
|
+
|
|
1532
|
+
Examples:
|
|
1533
|
+
>>> from pyEQL import Solution
|
|
1534
|
+
>>> from pyEQL.salt_ion_match import Salt
|
|
1535
|
+
>>> s1 = Solution(
|
|
1536
|
+
... solutes={
|
|
1537
|
+
... 'Na[+1]': '1 mol/L',
|
|
1538
|
+
... 'Cl[-1]': '1 mol/L',
|
|
1539
|
+
... 'Ca[+2]': '0.01 mol/kg',
|
|
1540
|
+
... 'HCO3[-1]': '0.007 mol/kg',
|
|
1541
|
+
... 'CO3[-2]': '0.001 mol/kg',
|
|
1542
|
+
... 'ClO[-1]': '0.001 mol/kg',
|
|
1543
|
+
... }
|
|
1544
|
+
... )
|
|
1545
|
+
>>> salt_dict = s1.get_salt_dict()
|
|
1546
|
+
>>> list(salt_dict) # Only returns salts with concentrations > 1e-3 m
|
|
1547
|
+
['NaCl', 'Ca(HCO3)2']
|
|
1548
|
+
>>> salt_dict['NaCl']['salt']
|
|
1549
|
+
<pyEQL.salt_ion_match.Salt object at ...>
|
|
1550
|
+
>>> salt_dict['NaCl']['mol']
|
|
1551
|
+
1.0
|
|
1552
|
+
>>> salt_dict = s1.get_salt_dict(cutoff=1e-4)
|
|
1553
|
+
>>> list(salt_dict) # Returns 'Ca(ClO)2' because of reduced cutoff and Cl has different oxidation state
|
|
1554
|
+
['NaCl', 'Ca(HCO3)2', 'Ca(ClO)2']
|
|
1555
|
+
>>> salt_dict = s1.get_salt_dict(cutoff=1e-4, use_totals=False)
|
|
1556
|
+
>>> list(salt_dict) # Returns salts with minor (same oxidation state) species since use_totals=False
|
|
1557
|
+
['NaCl', 'Ca(HCO3)2', 'CaCO3', 'Ca(ClO)2']
|
|
1529
1558
|
|
|
1530
1559
|
See Also:
|
|
1531
1560
|
:attr:`components`
|
|
1532
1561
|
:attr:`cations`
|
|
1533
1562
|
:attr:`anions`
|
|
1563
|
+
:class:`pyEQL.salt_ion_match.Salt`
|
|
1564
|
+
:py:meth:`get_activity_coefficient`
|
|
1565
|
+
:py:meth:`get_water_activity`
|
|
1566
|
+
:py:meth:`get_osmotic_coefficient`
|
|
1534
1567
|
"""
|
|
1535
|
-
salt_dict: dict[str, float] = {}
|
|
1568
|
+
salt_dict: dict[str, dict[str, float | Salt]] = {}
|
|
1536
1569
|
|
|
1537
1570
|
if use_totals:
|
|
1538
1571
|
# # use only the predominant species for each element
|
|
@@ -1558,7 +1591,7 @@ class Solution(MSONable):
|
|
|
1558
1591
|
# calculate the charge-weighted (equivalent) concentration of each ion
|
|
1559
1592
|
cation_equiv = {k: self.get_property(k, "charge") * components[k] for k in cations}
|
|
1560
1593
|
anion_equiv = {
|
|
1561
|
-
k:
|
|
1594
|
+
k: self.get_property(k, "charge") * components[k] * -1 for k in anions
|
|
1562
1595
|
} # make sure amounts are positive
|
|
1563
1596
|
|
|
1564
1597
|
# sort in descending order of equivalent concentration
|
|
@@ -1568,78 +1601,36 @@ class Solution(MSONable):
|
|
|
1568
1601
|
len_cat = len(cation_equiv)
|
|
1569
1602
|
len_an = len(anion_equiv)
|
|
1570
1603
|
|
|
1571
|
-
# Only ions are H+ and OH-; return a Salt represnting water (with no amount)
|
|
1572
|
-
if len_cat <= 1 and len_an <= 1 and self.solvent == "H2O(aq)":
|
|
1573
|
-
x = Salt("H[+1]", "OH[-1]")
|
|
1574
|
-
salt_dict.update({x.formula: x.as_dict()})
|
|
1575
|
-
salt_dict[x.formula]["mol"] = self.get_amount("H2O", "mol")
|
|
1576
|
-
return salt_dict
|
|
1577
|
-
|
|
1578
1604
|
# start with the first cation and anion
|
|
1579
1605
|
index_cat = 0
|
|
1580
1606
|
index_an = 0
|
|
1581
1607
|
|
|
1582
|
-
# list(dict) returns a list of [
|
|
1583
|
-
cation_list =
|
|
1584
|
-
anion_list =
|
|
1585
|
-
|
|
1586
|
-
#
|
|
1587
|
-
|
|
1588
|
-
|
|
1608
|
+
# list(dict) returns a list of [[key, value],]
|
|
1609
|
+
cation_list = [[k, v] for k, v in cation_equiv.items()]
|
|
1610
|
+
anion_list = [[k, v] for k, v in anion_equiv.items()]
|
|
1611
|
+
solvent_mass = self.solvent_mass.to("kg").m
|
|
1612
|
+
# tolerance for detecting edge cases where equilibrate() slightly changes the
|
|
1613
|
+
# total amount of a solute
|
|
1614
|
+
_atol = 1e-16
|
|
1589
1615
|
|
|
1590
1616
|
while index_cat < len_cat and index_an < len_an:
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
except IndexError:
|
|
1607
|
-
continue
|
|
1608
|
-
# if the anion concentration is greater, there will be leftover anions
|
|
1609
|
-
if c1 < a1:
|
|
1610
|
-
# create the salt
|
|
1611
|
-
x = Salt(cation_list[index_cat][0], anion_list[index_an][0])
|
|
1612
|
-
# there will be leftover anion, so use the cation amount
|
|
1613
|
-
salt_dict.update({x.formula: x.as_dict()})
|
|
1614
|
-
salt_dict[x.formula]["mol"] = c1 / x.z_cation * x.nu_cation
|
|
1615
|
-
# calculate the leftover cation amount
|
|
1616
|
-
a1 = a1 - c1
|
|
1617
|
-
# move to the next cation
|
|
1618
|
-
index_cat += 1
|
|
1619
|
-
try:
|
|
1620
|
-
a1 = cation_list[index_cat][-1]
|
|
1621
|
-
if a1 < cutoff:
|
|
1622
|
-
continue
|
|
1623
|
-
except IndexError:
|
|
1624
|
-
continue
|
|
1625
|
-
if np.isclose(c1, a1):
|
|
1626
|
-
# create the salt
|
|
1627
|
-
x = Salt(cation_list[index_cat][0], anion_list[index_an][0])
|
|
1628
|
-
# there will be nothing leftover, so it doesn't matter which ion you use
|
|
1629
|
-
salt_dict.update({x.formula: x.as_dict()})
|
|
1630
|
-
salt_dict[x.formula]["mol"] = c1 / x.z_cation * x.nu_cation
|
|
1631
|
-
# move to the next cation and anion
|
|
1632
|
-
index_an += 1
|
|
1633
|
-
index_cat += 1
|
|
1634
|
-
try:
|
|
1635
|
-
c1 = cation_list[index_cat][-1]
|
|
1636
|
-
a1 = anion_list[index_an][-1]
|
|
1637
|
-
if (c1 < cutoff) or (a1 < cutoff):
|
|
1638
|
-
continue
|
|
1639
|
-
except IndexError:
|
|
1640
|
-
continue
|
|
1617
|
+
c1 = cation_list[index_cat][-1]
|
|
1618
|
+
a1 = anion_list[index_an][-1]
|
|
1619
|
+
salt = Salt(cation_list[index_cat][0], anion_list[index_an][0])
|
|
1620
|
+
|
|
1621
|
+
# Use the smaller of the two amounts
|
|
1622
|
+
equivs_consumed = min(c1, a1)
|
|
1623
|
+
cation_list[index_cat][-1] -= equivs_consumed
|
|
1624
|
+
anion_list[index_an][-1] -= equivs_consumed
|
|
1625
|
+
index_an += 1 if a1 == equivs_consumed else 0
|
|
1626
|
+
index_cat += 1 if c1 == equivs_consumed else 0
|
|
1627
|
+
mol = equivs_consumed / (salt.z_cation * salt.nu_cation)
|
|
1628
|
+
|
|
1629
|
+
# filter out water and zero, effectively zero, and sub-cutoff salt amounts
|
|
1630
|
+
if salt.formula != "HOH" and (mol / solvent_mass + _atol) >= cutoff:
|
|
1631
|
+
salt_dict[salt.formula] = {"salt": salt, "mol": mol}
|
|
1641
1632
|
|
|
1642
|
-
return salt_dict
|
|
1633
|
+
return dict(sorted(salt_dict.items(), key=lambda x: x[1]["mol"], reverse=True))
|
|
1643
1634
|
|
|
1644
1635
|
def equilibrate(self, **kwargs) -> None:
|
|
1645
1636
|
"""
|
|
@@ -1864,9 +1855,8 @@ class Solution(MSONable):
|
|
|
1864
1855
|
anion, and water).
|
|
1865
1856
|
|
|
1866
1857
|
References:
|
|
1867
|
-
.. [koga] Koga, Yoshikata, 2007. *Solution Thermodynamics and its Application to Aqueous Solutions
|
|
1868
|
-
|
|
1869
|
-
|
|
1858
|
+
.. [koga] Koga, Yoshikata, 2007. *Solution Thermodynamics and its Application to Aqueous Solutions:*
|
|
1859
|
+
*A differential approach.* Elsevier, 2007, pp. 23-37.
|
|
1870
1860
|
"""
|
|
1871
1861
|
E = ureg.Quantity(0, "J")
|
|
1872
1862
|
|
|
@@ -2100,7 +2090,7 @@ class Solution(MSONable):
|
|
|
2100
2090
|
|
|
2101
2091
|
2. https://www.hydrochemistry.eu/exmpls/sc.html
|
|
2102
2092
|
|
|
2103
|
-
3. Appelo, C.A.J. Solute transport solved with the Nernst-Planck equation for concrete pores with
|
|
2093
|
+
3. Appelo, C.A.J. Solute transport solved with the Nernst-Planck equation for concrete pores with 'free'
|
|
2104
2094
|
water and a double layer. Cement and Concrete Research 101, 2017.
|
|
2105
2095
|
https://dx.doi.org/10.1016/j.cemconres.2017.08.030
|
|
2106
2096
|
|
|
@@ -2145,7 +2135,7 @@ class Solution(MSONable):
|
|
|
2145
2135
|
|
|
2146
2136
|
.. math::
|
|
2147
2137
|
|
|
2148
|
-
D_{\gamma} = D^0 \exp(\frac{-a1 A |z_i| \sqrt{I}}{1+\kappa a}
|
|
2138
|
+
D_{\gamma} = D^0 \exp(\frac{-a1 A |z_i| \sqrt{I}}{1+\kappa a})
|
|
2149
2139
|
|
|
2150
2140
|
.. math::
|
|
2151
2141
|
|
|
@@ -2157,7 +2147,7 @@ class Solution(MSONable):
|
|
|
2157
2147
|
|
|
2158
2148
|
References:
|
|
2159
2149
|
1. https://www.hydrochemistry.eu/exmpls/sc.html
|
|
2160
|
-
2. Appelo, C.A.J. Solute transport solved with the Nernst-Planck equation for concrete pores with
|
|
2150
|
+
2. Appelo, C.A.J. Solute transport solved with the Nernst-Planck equation for concrete pores with 'free'
|
|
2161
2151
|
water and a double layer. Cement and Concrete Research 101, 2017.
|
|
2162
2152
|
https://dx.doi.org/10.1016/j.cemconres.2017.08.030
|
|
2163
2153
|
3. CRC Handbook of Chemistry and Physics
|
|
@@ -2205,8 +2195,9 @@ class Solution(MSONable):
|
|
|
2205
2195
|
a1 = 1.6
|
|
2206
2196
|
a2 = 4.73
|
|
2207
2197
|
|
|
2208
|
-
# use the PHREEQC model from Ref 2 to correct for temperature
|
|
2209
|
-
|
|
2198
|
+
# use the PHREEQC model from Ref 2 to correct for temperature if more than 1 degree different from T_ref
|
|
2199
|
+
if abs(T_sol - T_ref) > 1:
|
|
2200
|
+
D *= np.exp(d / T_sol - d / T_ref) * mu_ref / mu
|
|
2210
2201
|
|
|
2211
2202
|
if activity_correction:
|
|
2212
2203
|
A = _debye_parameter_activity(str(self.temperature)).to("kg**0.5/mol**0.5").magnitude / 2.303
|
|
@@ -2215,14 +2206,12 @@ class Solution(MSONable):
|
|
|
2215
2206
|
IS = self.ionic_strength.magnitude
|
|
2216
2207
|
kappaa = B * IS**0.5 * a2 / (1 + IS**0.75)
|
|
2217
2208
|
# correct for ionic strength
|
|
2218
|
-
|
|
2209
|
+
D *= np.exp(-a1 * A * abs(z) * IS**0.5 / (1 + kappaa))
|
|
2219
2210
|
# else:
|
|
2220
2211
|
# # per CRC handbook, D increases by 2-3% per degree above 25 C
|
|
2221
2212
|
# return D * (1 + 0.025 * (T_sol - T_ref))
|
|
2222
|
-
else:
|
|
2223
|
-
D_final = D
|
|
2224
2213
|
|
|
2225
|
-
return
|
|
2214
|
+
return D
|
|
2226
2215
|
|
|
2227
2216
|
def _get_mobility(self, solute: str) -> Quantity:
|
|
2228
2217
|
r"""
|
|
@@ -2314,17 +2303,17 @@ class Solution(MSONable):
|
|
|
2314
2303
|
]
|
|
2315
2304
|
)
|
|
2316
2305
|
self.set_amount("H+", f"{new_hplus} mol/L")
|
|
2317
|
-
self.set_amount("OH-", f"{K_W/new_hplus} mol/L")
|
|
2306
|
+
self.set_amount("OH-", f"{K_W / new_hplus} mol/L")
|
|
2318
2307
|
return
|
|
2319
2308
|
|
|
2320
2309
|
z = self.get_property(self._cb_species, "charge")
|
|
2321
2310
|
try:
|
|
2322
|
-
self.add_amount(self._cb_species, f"{-1*cb/z} mol")
|
|
2311
|
+
self.add_amount(self._cb_species, f"{-1 * cb / z} mol")
|
|
2323
2312
|
return
|
|
2324
2313
|
except ValueError:
|
|
2325
2314
|
# if the concentration is negative, it must mean there is not enough present.
|
|
2326
2315
|
# remove everything that's present and log an error.
|
|
2327
|
-
self.components[self._cb_species] = 0
|
|
2316
|
+
self.components[self._cb_species] = 0.0
|
|
2328
2317
|
self.logger.error(
|
|
2329
2318
|
f"There is not enough {self._cb_species} present to balance the charge. Try a different species."
|
|
2330
2319
|
)
|
|
@@ -2389,7 +2378,7 @@ class Solution(MSONable):
|
|
|
2389
2378
|
def from_preset(
|
|
2390
2379
|
cls, preset: Literal["seawater", "rainwater", "wastewater", "urine", "normal saline", "Ringers lactate"]
|
|
2391
2380
|
) -> Solution:
|
|
2392
|
-
"""Instantiate a solution from a preset composition.
|
|
2381
|
+
r"""Instantiate a solution from a preset composition.
|
|
2393
2382
|
|
|
2394
2383
|
Args:
|
|
2395
2384
|
preset (str): String representing the desired solution.
|
|
@@ -2406,22 +2395,22 @@ class Solution(MSONable):
|
|
|
2406
2395
|
The following sections explain the different solution options:
|
|
2407
2396
|
|
|
2408
2397
|
- 'rainwater' - pure water in equilibrium with atmospheric CO2 at pH 6
|
|
2409
|
-
- 'seawater' or 'SW'- Standard Seawater. See Table 4 of the Reference for Composition [
|
|
2410
|
-
- 'wastewater' or 'WW' - medium strength domestic wastewater. See Table 3-18 of [
|
|
2411
|
-
- 'urine' - typical human urine. See Table 3-15 of [
|
|
2412
|
-
- 'normal saline' or 'NS' - normal saline solution used in medicine [
|
|
2413
|
-
- 'Ringers lacatate' or 'RL' - Ringer's lactate solution used in medicine [
|
|
2398
|
+
- 'seawater' or 'SW'- Standard Seawater. See Table 4 of the Reference for Composition [mf08]_
|
|
2399
|
+
- 'wastewater' or 'WW' - medium strength domestic wastewater. See Table 3-18 of [me13]_
|
|
2400
|
+
- 'urine' - typical human urine. See Table 3-15 of [me13]_
|
|
2401
|
+
- 'normal saline' or 'NS' - normal saline solution used in medicine [saline]_
|
|
2402
|
+
- 'Ringers lacatate' or 'RL' - Ringer's lactate solution used in medicine [lactate]_
|
|
2414
2403
|
|
|
2415
2404
|
References:
|
|
2416
|
-
.. [
|
|
2417
|
-
|
|
2405
|
+
.. [mf08] Millero, Frank J. "The composition of Standard Seawater and the definition of
|
|
2406
|
+
the Reference-Composition Salinity Scale." *Deep-sea Research. Part I* 55(1), 2008, 50-72.
|
|
2418
2407
|
|
|
2419
|
-
.. [
|
|
2420
|
-
|
|
2408
|
+
.. [me13] Metcalf & Eddy, Inc. et al. *Wastewater Engineering: Treatment and Resource Recovery*, 5th Ed.
|
|
2409
|
+
McGraw-Hill, 2013.
|
|
2421
2410
|
|
|
2422
|
-
.. [
|
|
2411
|
+
.. [saline] https://en.wikipedia.org/w/index.php?title=Saline_(medicine)&oldid=1298292693
|
|
2423
2412
|
|
|
2424
|
-
.. [
|
|
2413
|
+
.. [lactate] https://en.wikipedia.org/wiki/Ringer%27s_lactate_solution
|
|
2425
2414
|
"""
|
|
2426
2415
|
# preset_dir = files("pyEQL") / "presets"
|
|
2427
2416
|
# Path to the YAML and JSON files corresponding to the preset
|
|
@@ -2550,15 +2539,9 @@ class Solution(MSONable):
|
|
|
2550
2539
|
|
|
2551
2540
|
# retrieve the amount of each component in the parent solution and
|
|
2552
2541
|
# store in a list.
|
|
2553
|
-
|
|
2554
|
-
for sol, amt in self.components.items():
|
|
2555
|
-
|
|
2556
|
-
for sol2, amt2 in other.components.items():
|
|
2557
|
-
if mix_species.get(sol2):
|
|
2558
|
-
orig_amt = float(mix_species[sol2].split(" ")[0])
|
|
2559
|
-
mix_species[sol2] = f"{orig_amt+amt2} mol"
|
|
2560
|
-
else:
|
|
2561
|
-
mix_species.update({sol2: f"{amt2} mol"})
|
|
2542
|
+
mix_amounts = FormulaDict({})
|
|
2543
|
+
for sol, amt in [*self.components.items(), *other.components.items()]:
|
|
2544
|
+
mix_amounts[sol] = amt + mix_amounts.get(sol, 0.0)
|
|
2562
2545
|
|
|
2563
2546
|
# TODO - call equilibrate() here once the method is functional to get new pH and pE, instead of the below
|
|
2564
2547
|
warnings.warn(
|
|
@@ -2566,21 +2549,27 @@ class Solution(MSONable):
|
|
|
2566
2549
|
"this property is planned for a future release."
|
|
2567
2550
|
)
|
|
2568
2551
|
# calculate the new pH and pE (before reactions) by mixing
|
|
2569
|
-
|
|
2552
|
+
# for pH, we make sure to conserve the mass of H+ and OH-. By not passing
|
|
2553
|
+
# a kwarg for pH (i.e., by using the default value), the H+ concentration
|
|
2554
|
+
# will override and determine the pH value of the mixed solution.
|
|
2570
2555
|
|
|
2571
2556
|
# pE = -log[e-], so calculate the moles of e- in each solution and mix them
|
|
2572
2557
|
mol_e_self = 10 ** (-1 * self.pE) * self.volume.to("L").magnitude
|
|
2573
2558
|
mol_e_other = 10 ** (-1 * other.pE) * other.volume.to("L").magnitude
|
|
2574
2559
|
mix_pE = -np.log10((mol_e_self + mol_e_other) / mix_vol.to("L").magnitude)
|
|
2560
|
+
solutes = {sol: f"{amount} mol" for sol, amount in mix_amounts.items()}
|
|
2575
2561
|
|
|
2576
2562
|
# create a new solution
|
|
2577
2563
|
return Solution(
|
|
2578
|
-
|
|
2564
|
+
solutes=solutes,
|
|
2579
2565
|
volume=str(mix_vol),
|
|
2580
2566
|
pressure=str(mix_pressure),
|
|
2581
2567
|
temperature=str(mix_temperature.to("K")),
|
|
2582
|
-
pH=
|
|
2568
|
+
# pH=7, # leave at default value so that H+ concentration determines pH
|
|
2583
2569
|
pE=mix_pE,
|
|
2570
|
+
engine=self._engine,
|
|
2571
|
+
solvent=self.solvent,
|
|
2572
|
+
database=self.database,
|
|
2584
2573
|
)
|
|
2585
2574
|
|
|
2586
2575
|
def __sub__(self, other: Solution) -> None:
|
|
@@ -2620,7 +2609,7 @@ class Solution(MSONable):
|
|
|
2620
2609
|
places: The number of decimal places to round the solute amounts.
|
|
2621
2610
|
"""
|
|
2622
2611
|
print(self)
|
|
2623
|
-
str1 = "Activities" if units == "activity" else "
|
|
2612
|
+
str1 = "Activities" if units == "activity" else "Concentrations"
|
|
2624
2613
|
str2 = f" ({units})" if units != "activity" else ""
|
|
2625
2614
|
header = f"\nComponent {str1}{str2}:"
|
|
2626
2615
|
print(header)
|
|
@@ -2638,7 +2627,7 @@ class Solution(MSONable):
|
|
|
2638
2627
|
|
|
2639
2628
|
amt = self.get_activity(i).magnitude if units == "activity" else self.get_amount(i, units).magnitude
|
|
2640
2629
|
|
|
2641
|
-
print(f"{i}
|
|
2630
|
+
print(f"{i:<12} {amt:0.{places}f}")
|
|
2642
2631
|
|
|
2643
2632
|
def __str__(self) -> str:
|
|
2644
2633
|
# set output of the print() statement for the solution
|
|
@@ -2648,100 +2637,17 @@ class Solution(MSONable):
|
|
|
2648
2637
|
l4 = f"pH: {self.pH:.1f}"
|
|
2649
2638
|
l5 = f"pE: {self.pE:.1f}"
|
|
2650
2639
|
l6 = f"Solvent: {self.solvent}"
|
|
2651
|
-
l7 = f"Components: {self.
|
|
2640
|
+
l7 = f"Components: {self.components.keys():}"
|
|
2652
2641
|
return f"{l1}\n{l2}\n{l3}\n{l4}\n{l5}\n{l6}\n{l7}"
|
|
2653
2642
|
|
|
2654
2643
|
"""
|
|
2655
2644
|
Legacy methods to be deprecated in a future release.
|
|
2656
2645
|
"""
|
|
2657
2646
|
|
|
2658
|
-
@deprecated(
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
@deprecated(
|
|
2666
|
-
message="list_solutes() is deprecated and will be removed in the next release! Use Solution.components.keys() instead.)"
|
|
2667
|
-
)
|
|
2668
|
-
def list_solutes(self): # pragma: no cover
|
|
2669
|
-
"""List all the solutes in the solution."""
|
|
2670
|
-
return list(self.components.keys())
|
|
2671
|
-
|
|
2672
|
-
@deprecated(
|
|
2673
|
-
message="list_concentrations() is deprecated and will be removed in the next release! Use Solution.print() instead.)"
|
|
2674
|
-
)
|
|
2675
|
-
def list_concentrations(self, unit="mol/kg", decimals=4, type="all"): # pragma: no cover
|
|
2676
|
-
"""
|
|
2677
|
-
List the concentration of each species in a solution.
|
|
2678
|
-
|
|
2679
|
-
Parameters
|
|
2680
|
-
----------
|
|
2681
|
-
unit: str
|
|
2682
|
-
String representing the desired concentration ureg.
|
|
2683
|
-
decimals: int
|
|
2684
|
-
The number of decimal places to display. Defaults to 4.
|
|
2685
|
-
type : str
|
|
2686
|
-
The type of component to be sorted. Defaults to 'all' for all
|
|
2687
|
-
solutes. Other valid arguments are 'cations' and 'anions' which
|
|
2688
|
-
return lists of cations and anions, respectively.
|
|
2689
|
-
|
|
2690
|
-
Returns:
|
|
2691
|
-
-------
|
|
2692
|
-
dict
|
|
2693
|
-
Dictionary containing a list of the species in solution paired with their amount in the specified units
|
|
2694
|
-
:meta private:
|
|
2695
|
-
"""
|
|
2696
|
-
result_list = []
|
|
2697
|
-
# populate a list with component names
|
|
2698
|
-
|
|
2699
|
-
if type == "all":
|
|
2700
|
-
print("Component Concentrations:\n")
|
|
2701
|
-
print("========================\n")
|
|
2702
|
-
for item in self.components:
|
|
2703
|
-
amount = self.get_amount(item, unit)
|
|
2704
|
-
result_list.append([item, amount])
|
|
2705
|
-
print(item + ":" + "\t {0:0.{decimals}f~}".format(amount, decimals=decimals))
|
|
2706
|
-
elif type == "cations":
|
|
2707
|
-
print("Cation Concentrations:\n")
|
|
2708
|
-
print("========================\n")
|
|
2709
|
-
for item in self.components:
|
|
2710
|
-
if self.components[item].charge > 0:
|
|
2711
|
-
amount = self.get_amount(item, unit)
|
|
2712
|
-
result_list.append([item, amount])
|
|
2713
|
-
print(item + ":" + "\t {0:0.{decimals}f~}".format(amount, decimals=decimals))
|
|
2714
|
-
elif type == "anions":
|
|
2715
|
-
print("Anion Concentrations:\n")
|
|
2716
|
-
print("========================\n")
|
|
2717
|
-
for item in self.components:
|
|
2718
|
-
if self.components[item].charge < 0:
|
|
2719
|
-
amount = self.get_amount(item, unit)
|
|
2720
|
-
result_list.append([item, amount])
|
|
2721
|
-
print(item + ":" + "\t {0:0.{decimals}f~}".format(amount, decimals=decimals))
|
|
2722
|
-
|
|
2723
|
-
return result_list
|
|
2724
|
-
|
|
2725
|
-
@deprecated(
|
|
2726
|
-
message="list_activities() is deprecated and will be removed in the next release! Use Solution.print() instead.)"
|
|
2727
|
-
)
|
|
2728
|
-
def list_activities(self, decimals=4): # pragma: no cover
|
|
2729
|
-
"""
|
|
2730
|
-
List the activity of each species in a solution.
|
|
2731
|
-
|
|
2732
|
-
Parameters
|
|
2733
|
-
----------
|
|
2734
|
-
decimals: int
|
|
2735
|
-
The number of decimal places to display. Defaults to 4.
|
|
2736
|
-
|
|
2737
|
-
Returns:
|
|
2738
|
-
-------
|
|
2739
|
-
dict
|
|
2740
|
-
Dictionary containing a list of the species in solution paired with their activity
|
|
2741
|
-
|
|
2742
|
-
:meta private:
|
|
2743
|
-
"""
|
|
2744
|
-
print("Component Activities:\n")
|
|
2745
|
-
print("=====================\n")
|
|
2746
|
-
for i in self.components:
|
|
2747
|
-
print(i + ":" + "\t {0.magnitude:0.{decimals}f}".format(self.get_activity(i), decimals=decimals))
|
|
2647
|
+
@deprecated(message="add_solute() is deprecated. Use add_amount() instead.")
|
|
2648
|
+
def add_solvent(self, formula: str, amount: str): # pragma: no cover
|
|
2649
|
+
"""Same as add_solute but omits the need to pass solvent mass to pint."""
|
|
2650
|
+
quantity = ureg.Quantity(amount)
|
|
2651
|
+
mw = self.get_property(formula, "molecular_weight")
|
|
2652
|
+
target_mol = quantity.to("moles", "chem", mw=mw, volume=self.volume, solvent_mass=self.solvent_mass)
|
|
2653
|
+
self.components[formula] = target_mol.to("moles").magnitude
|