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/solution.py
CHANGED
|
@@ -8,7 +8,7 @@ pyEQL Solution Class.
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
-
import
|
|
11
|
+
import logging
|
|
12
12
|
import os
|
|
13
13
|
import warnings
|
|
14
14
|
from functools import lru_cache
|
|
@@ -28,9 +28,6 @@ from pymatgen.core.ion import Ion
|
|
|
28
28
|
from pyEQL import IonDB, ureg
|
|
29
29
|
from pyEQL.activity_correction import _debye_parameter_activity, _debye_parameter_B
|
|
30
30
|
from pyEQL.engines import EOS, IdealEOS, NativeEOS, PhreeqcEOS
|
|
31
|
-
|
|
32
|
-
# logging system
|
|
33
|
-
from pyEQL.logging_system import logger
|
|
34
31
|
from pyEQL.salt_ion_match import Salt
|
|
35
32
|
from pyEQL.solute import Solute
|
|
36
33
|
from pyEQL.utils import FormulaDict, create_water_substance, interpret_units, standardize_formula
|
|
@@ -59,6 +56,7 @@ class Solution(MSONable):
|
|
|
59
56
|
engine: EOS | Literal["native", "ideal", "phreeqc"] = "native",
|
|
60
57
|
database: str | Path | Store | None = None,
|
|
61
58
|
default_diffusion_coeff: float = 1.6106e-9,
|
|
59
|
+
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = "ERROR",
|
|
62
60
|
):
|
|
63
61
|
"""
|
|
64
62
|
Instantiate a Solution from a composition.
|
|
@@ -103,6 +101,8 @@ class Solution(MSONable):
|
|
|
103
101
|
engine: Electrolyte modeling engine to use. See documentation for details on the available engines.
|
|
104
102
|
database: path to a .json file (str or Path) or maggma Store instance that
|
|
105
103
|
contains serialized SoluteDocs. `None` (default) will use the built-in pyEQL database.
|
|
104
|
+
log_level: Log messages of this or higher severity will be printed to stdout. Defaults to 'ERROR', meaning
|
|
105
|
+
that ERROR and CRITICAL messages will be shown, while WARNING, INFO, and DEBUG messages are not. If set to None, nothing will be printed.
|
|
106
106
|
default_diffusion_coeff: Diffusion coefficient value in m^2/s to use in
|
|
107
107
|
calculations when there is no diffusion coefficient for a species in the database. This affects several
|
|
108
108
|
important property calculations including conductivity and transport number, which are related to the
|
|
@@ -125,8 +125,25 @@ class Solution(MSONable):
|
|
|
125
125
|
Temperature: 293.150 K
|
|
126
126
|
Components: ['H2O(aq)', 'H[+1]', 'OH[-1]', 'Na[+1]', 'Cl[-1]']
|
|
127
127
|
"""
|
|
128
|
-
# create a logger
|
|
129
|
-
|
|
128
|
+
# create a logger and attach it to this class
|
|
129
|
+
self.log_level = log_level.upper()
|
|
130
|
+
self.logger = logging.getLogger("pyEQL")
|
|
131
|
+
if self.log_level is not None:
|
|
132
|
+
# set the level of the module logger
|
|
133
|
+
self.logger.setLevel(self.log_level)
|
|
134
|
+
# clear handlers and add a StreamHandler
|
|
135
|
+
self.logger.handlers.clear()
|
|
136
|
+
# use rich for pretty log formatting, if installed
|
|
137
|
+
try:
|
|
138
|
+
from rich.logging import RichHandler
|
|
139
|
+
|
|
140
|
+
sh = RichHandler(rich_tracebacks=True)
|
|
141
|
+
except ImportError:
|
|
142
|
+
sh = logging.StreamHandler()
|
|
143
|
+
# the formatter determines what our logs will look like
|
|
144
|
+
formatter = logging.Formatter("[%(asctime)s] [%(levelname)8s] --- %(message)s (%(filename)s:%(lineno)d)")
|
|
145
|
+
sh.setFormatter(formatter)
|
|
146
|
+
self.logger.addHandler(sh)
|
|
130
147
|
|
|
131
148
|
# per-instance cache of get_property and other calls that do not depend
|
|
132
149
|
# on composition
|
|
@@ -174,13 +191,13 @@ class Solution(MSONable):
|
|
|
174
191
|
db_store = IonDB
|
|
175
192
|
elif isinstance(database, (str, Path)):
|
|
176
193
|
db_store = JSONStore(str(database), key="formula")
|
|
177
|
-
logger.
|
|
194
|
+
self.logger.debug(f"Created maggma JSONStore from .json file {database}")
|
|
178
195
|
else:
|
|
179
196
|
db_store = database
|
|
180
197
|
self.database = db_store
|
|
181
198
|
"""`Store` instance containing the solute property database."""
|
|
182
199
|
self.database.connect()
|
|
183
|
-
logger.
|
|
200
|
+
self.logger.debug(f"Connected to property database {self.database!s}")
|
|
184
201
|
|
|
185
202
|
# set the equation of state engine
|
|
186
203
|
self._engine = engine
|
|
@@ -209,7 +226,7 @@ class Solution(MSONable):
|
|
|
209
226
|
# TODO - do I need the ability to specify the solvent mass?
|
|
210
227
|
# # raise an error if the solvent volume has also been given
|
|
211
228
|
# if volume_set is True:
|
|
212
|
-
# logger.error(
|
|
229
|
+
# self.logger.error(
|
|
213
230
|
# "Solvent volume and mass cannot both be specified. Calculating volume based on solvent mass."
|
|
214
231
|
# )
|
|
215
232
|
# # add the solvent and the mass
|
|
@@ -231,10 +248,12 @@ class Solution(MSONable):
|
|
|
231
248
|
for k, v in self._solutes.items():
|
|
232
249
|
self.add_solute(k, v)
|
|
233
250
|
elif isinstance(self._solutes, list):
|
|
234
|
-
|
|
251
|
+
msg = (
|
|
235
252
|
'List input of solutes (e.g., [["Na+", "0.5 mol/L]]) is deprecated! Use dictionary formatted input '
|
|
236
253
|
'(e.g., {"Na+":"0.5 mol/L"} instead.)'
|
|
237
254
|
)
|
|
255
|
+
self.logger.warning(msg)
|
|
256
|
+
warnings.warn(msg, DeprecationWarning)
|
|
238
257
|
for item in self._solutes:
|
|
239
258
|
self.add_solute(*item)
|
|
240
259
|
|
|
@@ -242,7 +261,9 @@ class Solution(MSONable):
|
|
|
242
261
|
cb = self.charge_balance
|
|
243
262
|
if not np.isclose(cb, 0, atol=1e-8) and self.balance_charge is not None:
|
|
244
263
|
balanced = False
|
|
245
|
-
logger.info(
|
|
264
|
+
self.logger.info(
|
|
265
|
+
f"Solution is not electroneutral (C.B. = {cb} eq/L). Adding {balance_charge} to compensate."
|
|
266
|
+
)
|
|
246
267
|
if self.balance_charge == "pH":
|
|
247
268
|
self.components["H+"] += (
|
|
248
269
|
-1 * cb * self.volume.to("L").magnitude
|
|
@@ -411,6 +432,10 @@ class Solution(MSONable):
|
|
|
411
432
|
activity = False) of the solute.
|
|
412
433
|
"""
|
|
413
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
|
+
|
|
414
439
|
if activity is True:
|
|
415
440
|
return -1 * math.log10(self.get_activity(solute))
|
|
416
441
|
return -1 * math.log10(self.get_amount(solute, "mol/L").magnitude)
|
|
@@ -469,7 +494,7 @@ class Solution(MSONable):
|
|
|
469
494
|
if coefficient is not None:
|
|
470
495
|
denominator += coefficient * fraction
|
|
471
496
|
# except TypeError:
|
|
472
|
-
# logger.warning("No dielectric parameters found for species %s." % item)
|
|
497
|
+
# self.logger.warning("No dielectric parameters found for species %s." % item)
|
|
473
498
|
# continue
|
|
474
499
|
|
|
475
500
|
return ureg.Quantity(di_water / denominator, "dimensionless")
|
|
@@ -538,7 +563,7 @@ class Solution(MSONable):
|
|
|
538
563
|
# model should be integrated here as a fallback, in case salt parameters for the
|
|
539
564
|
# other model are not available.
|
|
540
565
|
# if self.ionic_strength.magnitude > 0.2:
|
|
541
|
-
# logger.warning('Viscosity calculation has limited accuracy above 0.2m')
|
|
566
|
+
# self.logger.warning('Viscosity calculation has limited accuracy above 0.2m')
|
|
542
567
|
|
|
543
568
|
# viscosity_rel = 1
|
|
544
569
|
# for item in self.components:
|
|
@@ -607,7 +632,7 @@ class Solution(MSONable):
|
|
|
607
632
|
else:
|
|
608
633
|
# TODO - fall back to the Jones-Dole model! There are currently no eyring parameters in the database!
|
|
609
634
|
# proceed with the coefficients equal to zero and log a warning
|
|
610
|
-
logger.
|
|
635
|
+
self.logger.warning(f"Viscosity coefficients for {salt.formula} not found. Viscosity will be approximate.")
|
|
611
636
|
G_123 = G_23 = 0
|
|
612
637
|
|
|
613
638
|
# get the kinematic viscosity of water, returned by IAPWS in m2/s
|
|
@@ -625,7 +650,7 @@ class Solution(MSONable):
|
|
|
625
650
|
x_cat = self.get_amount(salt.cation, "fraction").magnitude
|
|
626
651
|
|
|
627
652
|
# calculate the kinematic viscosity
|
|
628
|
-
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)
|
|
629
654
|
|
|
630
655
|
return ureg.Quantity(np.exp(nu), "m**2 / s")
|
|
631
656
|
|
|
@@ -908,9 +933,7 @@ class Solution(MSONable):
|
|
|
908
933
|
:attr:`dielectric_constant`
|
|
909
934
|
|
|
910
935
|
"""
|
|
911
|
-
bjerrum_length = ureg.e**2 / (
|
|
912
|
-
4 * math.pi * self.dielectric_constant * ureg.epsilon_0 * ureg.k * self.temperature
|
|
913
|
-
)
|
|
936
|
+
bjerrum_length = ureg.e**2 / (4 * np.pi * self.dielectric_constant * ureg.epsilon_0 * ureg.k * self.temperature)
|
|
914
937
|
return bjerrum_length.to("nm")
|
|
915
938
|
|
|
916
939
|
@property
|
|
@@ -953,10 +976,10 @@ class Solution(MSONable):
|
|
|
953
976
|
partial_molar_volume_water = self.get_property(self.solvent, "size.molar_volume")
|
|
954
977
|
|
|
955
978
|
osmotic_pressure = (
|
|
956
|
-
-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())
|
|
957
980
|
)
|
|
958
|
-
logger.
|
|
959
|
-
f"
|
|
981
|
+
self.logger.debug(
|
|
982
|
+
f"Calculated osmotic pressure of solution as {osmotic_pressure} Pa at T= {self.temperature} degrees C"
|
|
960
983
|
)
|
|
961
984
|
return osmotic_pressure.to("Pa")
|
|
962
985
|
|
|
@@ -1018,7 +1041,7 @@ class Solution(MSONable):
|
|
|
1018
1041
|
try:
|
|
1019
1042
|
return ureg.Quantity(0, _units)
|
|
1020
1043
|
except DimensionalityError:
|
|
1021
|
-
logger.
|
|
1044
|
+
self.logger.error(
|
|
1022
1045
|
f"Unsupported unit {units} specified for zero-concentration solute {solute}. Returned 0."
|
|
1023
1046
|
)
|
|
1024
1047
|
return ureg.Quantity(0, "dimensionless")
|
|
@@ -1073,7 +1096,7 @@ class Solution(MSONable):
|
|
|
1073
1096
|
oxi_states = self.get_property(s, "oxi_state_guesses")
|
|
1074
1097
|
oxi_state = oxi_states.get(el, UNKNOWN_OXI_STATE)
|
|
1075
1098
|
except (TypeError, IndexError):
|
|
1076
|
-
|
|
1099
|
+
self.logger.error(f"No oxidation state found for element {el}. Assigning '{UNKNOWN_OXI_STATE}'")
|
|
1077
1100
|
oxi_state = UNKNOWN_OXI_STATE
|
|
1078
1101
|
key = f"{el}({oxi_state})"
|
|
1079
1102
|
if d.get(key):
|
|
@@ -1103,7 +1126,7 @@ class Solution(MSONable):
|
|
|
1103
1126
|
oxi_states = self.get_property(s, "oxi_state_guesses")
|
|
1104
1127
|
oxi_state = oxi_states.get(el, UNKNOWN_OXI_STATE)
|
|
1105
1128
|
except (TypeError, IndexError):
|
|
1106
|
-
|
|
1129
|
+
self.logger.error(f"No oxidation state found for element {el}. Assigning '{UNKNOWN_OXI_STATE}'")
|
|
1107
1130
|
oxi_state = UNKNOWN_OXI_STATE
|
|
1108
1131
|
key = f"{el}({oxi_state})"
|
|
1109
1132
|
if d.get(key):
|
|
@@ -1213,9 +1236,7 @@ class Solution(MSONable):
|
|
|
1213
1236
|
# mw = ureg.Quantity(self.get_property(self.solvent_name, "molecular_weight"))
|
|
1214
1237
|
mw = self.get_property(self.solvent, "molecular_weight")
|
|
1215
1238
|
if mw is None:
|
|
1216
|
-
raise ValueError(
|
|
1217
|
-
f"Molecular weight for solvent {self.solvent} not found in database. This is required to proceed."
|
|
1218
|
-
)
|
|
1239
|
+
raise ValueError(f"Molecular weight for solvent {self.solvent} not found in database. Cannot proceed.")
|
|
1219
1240
|
target_mol = target_mass.to("g") / mw.to("g/mol")
|
|
1220
1241
|
self.components[self.solvent] = target_mol.magnitude
|
|
1221
1242
|
|
|
@@ -1229,7 +1250,7 @@ class Solution(MSONable):
|
|
|
1229
1250
|
# update the volume to account for the space occupied by all the solutes
|
|
1230
1251
|
# make sure that there is still solvent present in the first place
|
|
1231
1252
|
if self.solvent_mass <= ureg.Quantity(0, "kg"):
|
|
1232
|
-
logger.error("All solvent has been depleted from the solution")
|
|
1253
|
+
self.logger.error("All solvent has been depleted from the solution")
|
|
1233
1254
|
return
|
|
1234
1255
|
# set the volume recalculation flag
|
|
1235
1256
|
self.volume_update_required = True
|
|
@@ -1287,7 +1308,7 @@ class Solution(MSONable):
|
|
|
1287
1308
|
# set the amount to zero and log a warning if the desired amount
|
|
1288
1309
|
# change would result in a negative concentration
|
|
1289
1310
|
if self.get_amount(solute, "mol").magnitude < 0:
|
|
1290
|
-
logger.
|
|
1311
|
+
self.logger.error(
|
|
1291
1312
|
"Attempted to set a negative concentration for solute %s. Concentration set to 0" % solute
|
|
1292
1313
|
)
|
|
1293
1314
|
self.set_amount(solute, "0 mol")
|
|
@@ -1323,7 +1344,7 @@ class Solution(MSONable):
|
|
|
1323
1344
|
# set the amount to zero and log a warning if the desired amount
|
|
1324
1345
|
# change would result in a negative concentration
|
|
1325
1346
|
if self.get_amount(solute, "mol").magnitude < 0:
|
|
1326
|
-
logger.
|
|
1347
|
+
self.logger.error(
|
|
1327
1348
|
"Attempted to set a negative concentration for solute %s. Concentration set to 0" % solute
|
|
1328
1349
|
)
|
|
1329
1350
|
self.set_amount(solute, "0 mol")
|
|
@@ -1331,7 +1352,7 @@ class Solution(MSONable):
|
|
|
1331
1352
|
# update the volume to account for the space occupied by all the solutes
|
|
1332
1353
|
# make sure that there is still solvent present in the first place
|
|
1333
1354
|
if self.solvent_mass <= ureg.Quantity(0, "kg"):
|
|
1334
|
-
logger.error("All solvent has been depleted from the solution")
|
|
1355
|
+
self.logger.error("All solvent has been depleted from the solution")
|
|
1335
1356
|
return
|
|
1336
1357
|
|
|
1337
1358
|
# set the volume recalculation flag
|
|
@@ -1363,12 +1384,12 @@ class Solution(MSONable):
|
|
|
1363
1384
|
"""
|
|
1364
1385
|
# raise an error if a negative amount is specified
|
|
1365
1386
|
if ureg.Quantity(amount).magnitude < 0:
|
|
1366
|
-
|
|
1387
|
+
raise ValueError(f"Negative amount specified for solute {solute}. Concentration not changed.")
|
|
1367
1388
|
|
|
1368
1389
|
# if units are given on a per-volume basis,
|
|
1369
1390
|
# iteratively solve for the amount of solute that will preserve the
|
|
1370
1391
|
# original volume and result in the desired concentration
|
|
1371
|
-
|
|
1392
|
+
if ureg.Quantity(amount).dimensionality in (
|
|
1372
1393
|
"[substance]/[length]**3",
|
|
1373
1394
|
"[mass]/[length]**3",
|
|
1374
1395
|
):
|
|
@@ -1417,7 +1438,7 @@ class Solution(MSONable):
|
|
|
1417
1438
|
# update the volume to account for the space occupied by all the solutes
|
|
1418
1439
|
# make sure that there is still solvent present in the first place
|
|
1419
1440
|
if self.solvent_mass <= ureg.Quantity(0, "kg"):
|
|
1420
|
-
logger.
|
|
1441
|
+
self.logger.critical("All solvent has been depleted from the solution")
|
|
1421
1442
|
return
|
|
1422
1443
|
|
|
1423
1444
|
self._update_volume()
|
|
@@ -1590,7 +1611,7 @@ class Solution(MSONable):
|
|
|
1590
1611
|
|
|
1591
1612
|
# warn if something other than water is the predominant component
|
|
1592
1613
|
if next(iter(components)) != "H2O(aq)":
|
|
1593
|
-
logger.warning("H2O(aq) is not the most prominent component in this Solution!")
|
|
1614
|
+
self.logger.warning("H2O(aq) is not the most prominent component in this Solution!")
|
|
1594
1615
|
|
|
1595
1616
|
# equivalents (charge-weighted moles) of cations and anions
|
|
1596
1617
|
cations = set(self.cations.keys()).intersection(components.keys())
|
|
@@ -1723,7 +1744,7 @@ class Solution(MSONable):
|
|
|
1723
1744
|
# get the molal-scale activity coefficient from the EOS engine
|
|
1724
1745
|
molal = self.engine.get_activity_coefficient(solution=self, solute=solute)
|
|
1725
1746
|
except (ValueError, ZeroDivisionError):
|
|
1726
|
-
logger.
|
|
1747
|
+
self.logger.error("Calculation unsuccessful. Returning unit activity coefficient.", exc_info=True)
|
|
1727
1748
|
return ureg.Quantity(1, "dimensionless")
|
|
1728
1749
|
|
|
1729
1750
|
# if necessary, convert the activity coefficient to another scale, and return the result
|
|
@@ -1793,7 +1814,7 @@ class Solution(MSONable):
|
|
|
1793
1814
|
raise ValueError("Invalid scale argument. Pass 'molal', 'molar', or 'rational'.")
|
|
1794
1815
|
|
|
1795
1816
|
activity = (self.get_activity_coefficient(solute, scale=scale) * self.get_amount(solute, units)).magnitude
|
|
1796
|
-
logger.
|
|
1817
|
+
self.logger.debug(f"Calculated {scale} scale activity of solute {solute} as {activity}")
|
|
1797
1818
|
|
|
1798
1819
|
return ureg.Quantity(activity, "dimensionless")
|
|
1799
1820
|
|
|
@@ -1815,12 +1836,12 @@ class Solution(MSONable):
|
|
|
1815
1836
|
* ureg.Quantity(0.018015, "kg/mol")
|
|
1816
1837
|
* self.get_total_moles_solute()
|
|
1817
1838
|
/ self.solvent_mass
|
|
1818
|
-
/
|
|
1839
|
+
/ np.log(self.get_amount(self.solvent, "fraction"))
|
|
1819
1840
|
)
|
|
1820
1841
|
if scale == "fugacity":
|
|
1821
1842
|
return np.exp(
|
|
1822
1843
|
-molal_phi * ureg.Quantity(0.018015, "kg/mol") * self.get_total_moles_solute() / self.solvent_mass
|
|
1823
|
-
-
|
|
1844
|
+
- np.log(self.get_amount(self.solvent, "fraction"))
|
|
1824
1845
|
) * ureg.Quantity(1, "dimensionless")
|
|
1825
1846
|
|
|
1826
1847
|
raise ValueError("Invalid scale argument. Pass 'molal', 'rational', or 'fugacity'.")
|
|
@@ -1861,13 +1882,13 @@ class Solution(MSONable):
|
|
|
1861
1882
|
osmotic_coefficient = self.get_osmotic_coefficient()
|
|
1862
1883
|
|
|
1863
1884
|
if osmotic_coefficient == 1:
|
|
1864
|
-
logger.warning("Pitzer parameters not found. Water activity set equal to mole fraction")
|
|
1885
|
+
self.logger.warning("Pitzer parameters not found. Water activity set equal to mole fraction")
|
|
1865
1886
|
return self.get_amount("H2O", "fraction")
|
|
1866
1887
|
|
|
1867
1888
|
concentration_sum = np.sum([mol for item, mol in self.components.items() if item != "H2O(aq)"])
|
|
1868
1889
|
concentration_sum /= self.solvent_mass.to("kg").magnitude # converts to mol/kg
|
|
1869
1890
|
|
|
1870
|
-
logger.
|
|
1891
|
+
self.logger.debug("Calculated water activity using osmotic coefficient")
|
|
1871
1892
|
|
|
1872
1893
|
return ureg.Quantity(np.exp(-osmotic_coefficient * 0.018015 * concentration_sum), "dimensionless")
|
|
1873
1894
|
|
|
@@ -1919,14 +1940,14 @@ class Solution(MSONable):
|
|
|
1919
1940
|
ureg.R
|
|
1920
1941
|
* self.temperature.to("K")
|
|
1921
1942
|
* self.get_amount(item, "mol")
|
|
1922
|
-
*
|
|
1943
|
+
* np.log(self.get_activity(item))
|
|
1923
1944
|
)
|
|
1924
1945
|
else:
|
|
1925
1946
|
E += (
|
|
1926
1947
|
ureg.R
|
|
1927
1948
|
* self.temperature.to("K")
|
|
1928
1949
|
* self.get_amount(item, "mol")
|
|
1929
|
-
*
|
|
1950
|
+
* np.log(self.get_amount(item, "fraction"))
|
|
1930
1951
|
)
|
|
1931
1952
|
# If we have a solute with zero concentration, we will get a ValueError
|
|
1932
1953
|
except ValueError:
|
|
@@ -1964,7 +1985,7 @@ class Solution(MSONable):
|
|
|
1964
1985
|
# formulas should always be unique in the database. len==0 indicates no
|
|
1965
1986
|
# data. len>1 indicates duplicate data.
|
|
1966
1987
|
if len(data) > 1:
|
|
1967
|
-
logger.warning(f"Duplicate database entries for solute {solute} found!")
|
|
1988
|
+
self.logger.warning(f"Duplicate database entries for solute {solute} found!")
|
|
1968
1989
|
if len(data) == 0:
|
|
1969
1990
|
# TODO - add molar volume of water to database?
|
|
1970
1991
|
if name == "size.molar_volume" and rform == "H2O(aq)":
|
|
@@ -1995,7 +2016,7 @@ class Solution(MSONable):
|
|
|
1995
2016
|
if data is not None:
|
|
1996
2017
|
base_value = ureg.Quantity(doc["size"]["molar_volume"].get("value"))
|
|
1997
2018
|
if self.temperature != base_temperature:
|
|
1998
|
-
logger.warning("Partial molar volume for species
|
|
2019
|
+
self.logger.warning(f"Partial molar volume for species {solute} not corrected for temperature")
|
|
1999
2020
|
return base_value
|
|
2000
2021
|
return data
|
|
2001
2022
|
|
|
@@ -2030,12 +2051,12 @@ class Solution(MSONable):
|
|
|
2030
2051
|
return doc.get(name)
|
|
2031
2052
|
|
|
2032
2053
|
val = doc[name].get("value")
|
|
2033
|
-
# logger.warning("%s has not been corrected for solution conditions" % name)
|
|
2054
|
+
# self.logger.warning("%s has not been corrected for solution conditions" % name)
|
|
2034
2055
|
if val is not None:
|
|
2035
2056
|
return ureg.Quantity(val)
|
|
2036
2057
|
|
|
2037
2058
|
except KeyError:
|
|
2038
|
-
logger.
|
|
2059
|
+
self.logger.error(f"Property {name} for solute {solute} not found in database. Returning None.")
|
|
2039
2060
|
return None
|
|
2040
2061
|
|
|
2041
2062
|
if name == "model_parameters.molar_volume_pitzer":
|
|
@@ -2056,7 +2077,7 @@ class Solution(MSONable):
|
|
|
2056
2077
|
return doc.get(name)
|
|
2057
2078
|
|
|
2058
2079
|
val = doc[name].get("value")
|
|
2059
|
-
# logger.warning("%s has not been corrected for solution conditions" % name)
|
|
2080
|
+
# self.logger.warning("%s has not been corrected for solution conditions" % name)
|
|
2060
2081
|
if val is not None:
|
|
2061
2082
|
return ureg.Quantity(val)
|
|
2062
2083
|
return None
|
|
@@ -2159,7 +2180,7 @@ class Solution(MSONable):
|
|
|
2159
2180
|
else:
|
|
2160
2181
|
molar_cond = ureg.Quantity(0, "mS / cm / (mol/L)")
|
|
2161
2182
|
|
|
2162
|
-
logger.
|
|
2183
|
+
self.logger.debug(f"Calculated molar conductivity as {molar_cond} from D = {D!s} at T={self.temperature}")
|
|
2163
2184
|
|
|
2164
2185
|
return molar_cond.to("mS / cm / (mol/L)")
|
|
2165
2186
|
|
|
@@ -2211,7 +2232,7 @@ class Solution(MSONable):
|
|
|
2211
2232
|
D = self.get_property(solute, "transport.diffusion_coefficient")
|
|
2212
2233
|
rform = standardize_formula(solute)
|
|
2213
2234
|
if D is None or D.magnitude == 0:
|
|
2214
|
-
logger.
|
|
2235
|
+
self.logger.warning(
|
|
2215
2236
|
f"Diffusion coefficient not found for species {rform}. Using default value of "
|
|
2216
2237
|
f"{self.default_diffusion_coeff} m**2/s."
|
|
2217
2238
|
)
|
|
@@ -2238,7 +2259,7 @@ class Solution(MSONable):
|
|
|
2238
2259
|
except TypeError:
|
|
2239
2260
|
# this means the database doesn't contain a d value.
|
|
2240
2261
|
# according to Ref 2, the following are recommended default parameters
|
|
2241
|
-
logger.
|
|
2262
|
+
self.logger.warning(
|
|
2242
2263
|
f"Temperature and ionic strength correction parameters for solute {rform} diffusion "
|
|
2243
2264
|
"coefficient not in database. Using recommended default values of a1=1.6, a2=4.73, and d=0."
|
|
2244
2265
|
)
|
|
@@ -2289,7 +2310,7 @@ class Solution(MSONable):
|
|
|
2289
2310
|
|
|
2290
2311
|
mobility = ureg.N_A * ureg.e * abs(self.get_property(solute, "charge")) * D / (ureg.R * self.temperature)
|
|
2291
2312
|
|
|
2292
|
-
logger.
|
|
2313
|
+
self.logger.debug(f"Calculated ionic mobility as {mobility} from D = {D!s} at T={self.temperature}")
|
|
2293
2314
|
|
|
2294
2315
|
return mobility.to("m**2/V/s")
|
|
2295
2316
|
|
|
@@ -2355,6 +2376,7 @@ class Solution(MSONable):
|
|
|
2355
2376
|
d["solutes"] = {k: f"{v} mol" for k, v in self.components.items()}
|
|
2356
2377
|
# replace the engine with the associated str
|
|
2357
2378
|
d["engine"] = self._engine
|
|
2379
|
+
# d["logger"] = self.logger.__dict__
|
|
2358
2380
|
return d
|
|
2359
2381
|
|
|
2360
2382
|
@classmethod
|
|
@@ -2380,6 +2402,115 @@ class Solution(MSONable):
|
|
|
2380
2402
|
new_sol.volume_update_required = False
|
|
2381
2403
|
return new_sol
|
|
2382
2404
|
|
|
2405
|
+
@classmethod
|
|
2406
|
+
def from_preset(
|
|
2407
|
+
cls, preset: Literal["seawater", "rainwater", "wastewater", "urine", "normal saline", "Ringers lactate"]
|
|
2408
|
+
) -> Solution:
|
|
2409
|
+
"""Instantiate a solution from a preset composition.
|
|
2410
|
+
|
|
2411
|
+
Args:
|
|
2412
|
+
preset (str): String representing the desired solution.
|
|
2413
|
+
Valid entries are 'seawater', 'rainwater', 'wastewater',
|
|
2414
|
+
'urine', 'normal saline' and 'Ringers lactate'.
|
|
2415
|
+
|
|
2416
|
+
Returns:
|
|
2417
|
+
A pyEQL Solution object.
|
|
2418
|
+
|
|
2419
|
+
Raises:
|
|
2420
|
+
FileNotFoundError: If the given preset file doesn't exist on the file system.
|
|
2421
|
+
|
|
2422
|
+
Notes:
|
|
2423
|
+
The following sections explain the different solution options:
|
|
2424
|
+
|
|
2425
|
+
- 'rainwater' - pure water in equilibrium with atmospheric CO2 at pH 6
|
|
2426
|
+
- 'seawater' or 'SW'- Standard Seawater. See Table 4 of the Reference for Composition [1]_
|
|
2427
|
+
- 'wastewater' or 'WW' - medium strength domestic wastewater. See Table 3-18 of [2]_
|
|
2428
|
+
- 'urine' - typical human urine. See Table 3-15 of [2]_
|
|
2429
|
+
- 'normal saline' or 'NS' - normal saline solution used in medicine [3]_
|
|
2430
|
+
- 'Ringers lacatate' or 'RL' - Ringer's lactate solution used in medicine [4]_
|
|
2431
|
+
|
|
2432
|
+
References:
|
|
2433
|
+
.. [1] Millero, Frank J. "The composition of Standard Seawater and the definition of
|
|
2434
|
+
the Reference-Composition Salinity Scale." *Deep-sea Research. Part I* 55(1), 2008, 50-72.
|
|
2435
|
+
|
|
2436
|
+
.. [2] Metcalf & Eddy, Inc. et al. *Wastewater Engineering: Treatment and Resource Recovery*, 5th Ed.
|
|
2437
|
+
McGraw-Hill, 2013.
|
|
2438
|
+
|
|
2439
|
+
.. [3] https://en.wikipedia.org/wiki/Saline_(medicine)
|
|
2440
|
+
|
|
2441
|
+
.. [4] https://en.wikipedia.org/wiki/Ringer%27s_lactate_solution
|
|
2442
|
+
"""
|
|
2443
|
+
# preset_dir = files("pyEQL") / "presets"
|
|
2444
|
+
# Path to the YAML and JSON files corresponding to the preset
|
|
2445
|
+
yaml_path = files("pyEQL") / "presets" / f"{preset}.yaml"
|
|
2446
|
+
json_path = files("pyEQL") / "presets" / f"{preset}.json"
|
|
2447
|
+
|
|
2448
|
+
# Check if the file exists
|
|
2449
|
+
if yaml_path.exists():
|
|
2450
|
+
preset_path = yaml_path
|
|
2451
|
+
elif json_path.exists():
|
|
2452
|
+
preset_path = json_path
|
|
2453
|
+
else:
|
|
2454
|
+
raise FileNotFoundError(f"Invalid preset! File '{yaml_path}' or '{json_path} not found!")
|
|
2455
|
+
|
|
2456
|
+
# Create and return a Solution object
|
|
2457
|
+
return cls().from_file(preset_path)
|
|
2458
|
+
|
|
2459
|
+
def to_file(self, filename: str | Path) -> None:
|
|
2460
|
+
"""Saving to a .yaml or .json file.
|
|
2461
|
+
|
|
2462
|
+
Args:
|
|
2463
|
+
filename (str | Path): The path to the file to save Solution.
|
|
2464
|
+
Valid extensions are .json or .yaml.
|
|
2465
|
+
"""
|
|
2466
|
+
str_filename = str(filename)
|
|
2467
|
+
if not ("yaml" in str_filename.lower() or "json" in str_filename.lower()):
|
|
2468
|
+
self.logger.error("Invalid file extension entered - %s" % str_filename)
|
|
2469
|
+
raise ValueError("File extension must be .json or .yaml")
|
|
2470
|
+
if "yaml" in str_filename.lower():
|
|
2471
|
+
solution_dict = self.as_dict()
|
|
2472
|
+
solution_dict.pop("database")
|
|
2473
|
+
dumpfn(solution_dict, filename)
|
|
2474
|
+
else:
|
|
2475
|
+
dumpfn(self, filename)
|
|
2476
|
+
|
|
2477
|
+
@classmethod
|
|
2478
|
+
def from_file(self, filename: str | Path) -> Solution:
|
|
2479
|
+
"""Loading from a .yaml or .json file.
|
|
2480
|
+
|
|
2481
|
+
Args:
|
|
2482
|
+
filename (str | Path): Path to the .json or .yaml file (including extension) to load the Solution from.
|
|
2483
|
+
Valid extensions are .json or .yaml.
|
|
2484
|
+
|
|
2485
|
+
Returns:
|
|
2486
|
+
A pyEQL Solution object.
|
|
2487
|
+
|
|
2488
|
+
Raises:
|
|
2489
|
+
FileNotFoundError: If the given filename doesn't exist on the file system.
|
|
2490
|
+
"""
|
|
2491
|
+
if not os.path.exists(filename):
|
|
2492
|
+
raise FileNotFoundError(f"File '{filename}' not found!")
|
|
2493
|
+
str_filename = str(filename)
|
|
2494
|
+
if "yaml" in str_filename.lower():
|
|
2495
|
+
true_keys = [
|
|
2496
|
+
"solutes",
|
|
2497
|
+
"volume",
|
|
2498
|
+
"temperature",
|
|
2499
|
+
"pressure",
|
|
2500
|
+
"pH",
|
|
2501
|
+
"pE",
|
|
2502
|
+
"balance_charge",
|
|
2503
|
+
"solvent",
|
|
2504
|
+
"engine",
|
|
2505
|
+
# "database",
|
|
2506
|
+
]
|
|
2507
|
+
solution_dict = loadfn(filename)
|
|
2508
|
+
keys_to_delete = [key for key in solution_dict if key not in true_keys]
|
|
2509
|
+
for key in keys_to_delete:
|
|
2510
|
+
solution_dict.pop(key)
|
|
2511
|
+
return Solution(**solution_dict)
|
|
2512
|
+
return loadfn(filename)
|
|
2513
|
+
|
|
2383
2514
|
# arithmetic operations
|
|
2384
2515
|
def __add__(self, other: Solution):
|
|
2385
2516
|
"""
|
|
@@ -2420,12 +2551,14 @@ class Solution(MSONable):
|
|
|
2420
2551
|
|
|
2421
2552
|
# check to see if the solutions have the same temperature and pressure
|
|
2422
2553
|
if p1 != p2:
|
|
2423
|
-
logger.info(
|
|
2554
|
+
self.logger.info(
|
|
2555
|
+
"Adding two solutions of different pressure. Pressures will be averaged (weighted by volume)"
|
|
2556
|
+
)
|
|
2424
2557
|
|
|
2425
2558
|
mix_pressure = (p1 * v1 + p2 * v2) / (mix_vol)
|
|
2426
2559
|
|
|
2427
2560
|
if t1 != t2:
|
|
2428
|
-
logger.info(
|
|
2561
|
+
self.logger.info(
|
|
2429
2562
|
"Adding two solutions of different temperature. Temperatures will be averaged (weighted by volume)"
|
|
2430
2563
|
)
|
|
2431
2564
|
|
|
@@ -2450,12 +2583,12 @@ class Solution(MSONable):
|
|
|
2450
2583
|
"this property is planned for a future release."
|
|
2451
2584
|
)
|
|
2452
2585
|
# calculate the new pH and pE (before reactions) by mixing
|
|
2453
|
-
mix_pH = -
|
|
2586
|
+
mix_pH = -np.log10(float(mix_species["H+"].split(" ")[0]) / mix_vol.to("L").magnitude)
|
|
2454
2587
|
|
|
2455
2588
|
# pE = -log[e-], so calculate the moles of e- in each solution and mix them
|
|
2456
2589
|
mol_e_self = 10 ** (-1 * self.pE) * self.volume.to("L").magnitude
|
|
2457
2590
|
mol_e_other = 10 ** (-1 * other.pE) * other.volume.to("L").magnitude
|
|
2458
|
-
mix_pE = -
|
|
2591
|
+
mix_pE = -np.log10((mol_e_self + mol_e_other) / mix_vol.to("L").magnitude)
|
|
2459
2592
|
|
|
2460
2593
|
# create a new solution
|
|
2461
2594
|
return Solution(
|
|
@@ -2629,710 +2762,3 @@ class Solution(MSONable):
|
|
|
2629
2762
|
print("=====================\n")
|
|
2630
2763
|
for i in self.components:
|
|
2631
2764
|
print(i + ":" + "\t {0.magnitude:0.{decimals}f}".format(self.get_activity(i), decimals=decimals))
|
|
2632
|
-
|
|
2633
|
-
@deprecated(
|
|
2634
|
-
message="get_solute() is deprecated and will be removed in the next release! Access solutes via the Solution.components attribute and their properties via Solution.get_property(solute, ...)"
|
|
2635
|
-
)
|
|
2636
|
-
def get_solute(self, i): # pragma: no cover
|
|
2637
|
-
"""Return the specified solute object.
|
|
2638
|
-
|
|
2639
|
-
:meta private:
|
|
2640
|
-
"""
|
|
2641
|
-
return self.components[i]
|
|
2642
|
-
|
|
2643
|
-
@deprecated(
|
|
2644
|
-
message="get_solvent is deprecated and will be removed in the next release! Use Solution.solvent instead."
|
|
2645
|
-
)
|
|
2646
|
-
def get_solvent(self): # pragma: no cover
|
|
2647
|
-
"""Return the solvent object.
|
|
2648
|
-
|
|
2649
|
-
:meta private:
|
|
2650
|
-
"""
|
|
2651
|
-
return self.components[self.solvent]
|
|
2652
|
-
|
|
2653
|
-
@deprecated(
|
|
2654
|
-
message="get_temperature() will be removed in the next release. Access the temperature directly via the property Solution.temperature"
|
|
2655
|
-
)
|
|
2656
|
-
def get_temperature(self): # pragma: no cover
|
|
2657
|
-
"""
|
|
2658
|
-
Return the temperature of the solution.
|
|
2659
|
-
|
|
2660
|
-
Parameters
|
|
2661
|
-
----------
|
|
2662
|
-
None
|
|
2663
|
-
|
|
2664
|
-
Returns:
|
|
2665
|
-
-------
|
|
2666
|
-
Quantity: The temperature of the solution, in Kelvin.
|
|
2667
|
-
|
|
2668
|
-
:meta private:
|
|
2669
|
-
"""
|
|
2670
|
-
return self.temperature
|
|
2671
|
-
|
|
2672
|
-
@deprecated(
|
|
2673
|
-
message="set_temperature() will be removed in the next release. Set the temperature directly via the property Solution.temperature"
|
|
2674
|
-
)
|
|
2675
|
-
def set_temperature(self, temperature): # pragma: no cover
|
|
2676
|
-
"""
|
|
2677
|
-
Set the solution temperature.
|
|
2678
|
-
|
|
2679
|
-
Parameters
|
|
2680
|
-
----------
|
|
2681
|
-
temperature : str
|
|
2682
|
-
String representing the temperature, e.g. '25 degC'
|
|
2683
|
-
|
|
2684
|
-
:meta private:
|
|
2685
|
-
"""
|
|
2686
|
-
self.temperature = ureg.Quantity(temperature)
|
|
2687
|
-
|
|
2688
|
-
# recalculate the volume
|
|
2689
|
-
self._update_volume()
|
|
2690
|
-
|
|
2691
|
-
@deprecated(
|
|
2692
|
-
message="get_pressure() will be removed in the next release. Access the pressure directly via the property Solution.pressure"
|
|
2693
|
-
)
|
|
2694
|
-
def get_pressure(self): # pragma: no cover
|
|
2695
|
-
"""
|
|
2696
|
-
Return the hydrostatic pressure of the solution.
|
|
2697
|
-
|
|
2698
|
-
Returns:
|
|
2699
|
-
-------
|
|
2700
|
-
Quantity: The hydrostatic pressure of the solution, in atm.
|
|
2701
|
-
|
|
2702
|
-
:meta private:
|
|
2703
|
-
"""
|
|
2704
|
-
return self.pressure
|
|
2705
|
-
|
|
2706
|
-
@deprecated(
|
|
2707
|
-
message="set_pressure() will be removed in the next release. Set the pressure directly via Solution.pressure"
|
|
2708
|
-
)
|
|
2709
|
-
def set_pressure(self, pressure): # pragma: no cover
|
|
2710
|
-
"""
|
|
2711
|
-
Set the hydrostatic pressure of the solution.
|
|
2712
|
-
|
|
2713
|
-
Parameters
|
|
2714
|
-
----------
|
|
2715
|
-
pressure : str
|
|
2716
|
-
String representing the temperature, e.g. '25 degC'
|
|
2717
|
-
|
|
2718
|
-
:meta private:
|
|
2719
|
-
"""
|
|
2720
|
-
self._pressure = ureg.Quantity(pressure)
|
|
2721
|
-
|
|
2722
|
-
@deprecated(
|
|
2723
|
-
message="get_volume() will be removed in the next release. Access the volume directly via Solution.volume"
|
|
2724
|
-
)
|
|
2725
|
-
def get_volume(self): # pragma: no cover
|
|
2726
|
-
""" """
|
|
2727
|
-
return self.volume
|
|
2728
|
-
|
|
2729
|
-
@deprecated(
|
|
2730
|
-
message="set_pressure() will be removed in the next release. Set the pressure directly via Solution.pressure"
|
|
2731
|
-
)
|
|
2732
|
-
def set_volume(self, volume: str): # pragma: no cover
|
|
2733
|
-
""" """
|
|
2734
|
-
self.volume = volume # type: ignore
|
|
2735
|
-
|
|
2736
|
-
@deprecated(message="get_mass() will be removed in the next release. Use the Solution.mass property instead.")
|
|
2737
|
-
def get_mass(self): # pragma: no cover
|
|
2738
|
-
"""
|
|
2739
|
-
Return the total mass of the solution.
|
|
2740
|
-
|
|
2741
|
-
The mass is calculated each time this method is called.
|
|
2742
|
-
Parameters
|
|
2743
|
-
----------
|
|
2744
|
-
None
|
|
2745
|
-
|
|
2746
|
-
Returns:
|
|
2747
|
-
-------
|
|
2748
|
-
Quantity: the mass of the solution, in kg
|
|
2749
|
-
|
|
2750
|
-
:meta private:
|
|
2751
|
-
|
|
2752
|
-
"""
|
|
2753
|
-
return self.mass
|
|
2754
|
-
|
|
2755
|
-
@deprecated(message="get_density() will be removed in the next release. Use the Solution.density property instead.")
|
|
2756
|
-
def get_density(self): # pragma: no cover
|
|
2757
|
-
"""
|
|
2758
|
-
Return the density of the solution.
|
|
2759
|
-
|
|
2760
|
-
Density is calculated from the mass and volume each time this method is called.
|
|
2761
|
-
|
|
2762
|
-
Returns:
|
|
2763
|
-
-------
|
|
2764
|
-
Quantity: The density of the solution.
|
|
2765
|
-
|
|
2766
|
-
:meta private:
|
|
2767
|
-
"""
|
|
2768
|
-
return self.density
|
|
2769
|
-
|
|
2770
|
-
@deprecated(message="get_viscosity_relative() will be removed in the next release.")
|
|
2771
|
-
def get_viscosity_relative(self): # pragma: no cover
|
|
2772
|
-
r"""
|
|
2773
|
-
Return the viscosity of the solution relative to that of water.
|
|
2774
|
-
|
|
2775
|
-
This is calculated using a simplified form of the Jones-Dole equation:
|
|
2776
|
-
|
|
2777
|
-
.. math:: \eta_{rel} = 1 + \sum_i B_i m_i
|
|
2778
|
-
|
|
2779
|
-
Where :math:`m` is the molal concentration and :math:`B` is an empirical parameter.
|
|
2780
|
-
|
|
2781
|
-
See
|
|
2782
|
-
<http://www.nrcresearchpress.com/doi/pdf/10.1139/v77-148>
|
|
2783
|
-
|
|
2784
|
-
:meta private:
|
|
2785
|
-
|
|
2786
|
-
"""
|
|
2787
|
-
# if self.ionic_strength.magnitude > 0.2:
|
|
2788
|
-
# logger.warning('Viscosity calculation has limited accuracy above 0.2m')
|
|
2789
|
-
|
|
2790
|
-
# viscosity_rel = 1
|
|
2791
|
-
# for item in self.components:
|
|
2792
|
-
# # ignore water
|
|
2793
|
-
# if item != 'H2O':
|
|
2794
|
-
# # skip over solutes that don't have parameters
|
|
2795
|
-
# try:
|
|
2796
|
-
# conc = self.get_amount(item,'mol/kg').magnitude
|
|
2797
|
-
# coefficients= self.get_property(item, 'jones_dole_viscosity')
|
|
2798
|
-
# viscosity_rel += coefficients[0] * conc ** 0.5 + coefficients[1] * conc + \
|
|
2799
|
-
# coefficients[2] * conc ** 2
|
|
2800
|
-
# except TypeError:
|
|
2801
|
-
# continue
|
|
2802
|
-
return ureg.Quantity(self.viscosity_dynamic / self.water_substance.mu, "Pa*s")
|
|
2803
|
-
|
|
2804
|
-
@deprecated(
|
|
2805
|
-
message="get_viscosity_dynamic() will be removed in the next release. Access directly via the property Solution.viscosity_dynamic."
|
|
2806
|
-
)
|
|
2807
|
-
def get_viscosity_dynamic(self): # pragma: no cover
|
|
2808
|
-
"""
|
|
2809
|
-
Return the dynamic (absolute) viscosity of the solution.
|
|
2810
|
-
|
|
2811
|
-
Calculated from the kinematic viscosity
|
|
2812
|
-
|
|
2813
|
-
See Also:
|
|
2814
|
-
--------
|
|
2815
|
-
get_viscosity_kinematic
|
|
2816
|
-
|
|
2817
|
-
:meta private:
|
|
2818
|
-
"""
|
|
2819
|
-
return self.viscosity_dynamic
|
|
2820
|
-
|
|
2821
|
-
@deprecated(
|
|
2822
|
-
message="get_viscosity_kinematic() will be removed in the next release. Access directly via the property Solution.viscosity_kinematic."
|
|
2823
|
-
)
|
|
2824
|
-
def get_viscosity_kinematic(self): # pragma: no cover
|
|
2825
|
-
"""
|
|
2826
|
-
Return the kinematic viscosity of the solution.
|
|
2827
|
-
|
|
2828
|
-
Notes:
|
|
2829
|
-
-----
|
|
2830
|
-
The calculation is based on a model derived from the Eyring equation
|
|
2831
|
-
and presented by Vásquez-Castillo et al.
|
|
2832
|
-
|
|
2833
|
-
.. math::
|
|
2834
|
-
|
|
2835
|
-
\\ln \nu = \\ln {\nu_w MW_w \\over \\sum_i x_i MW_i } +
|
|
2836
|
-
15 x_+^2 + x_+^3 \\delta G^*_{123} + 3 x_+ \\delta G^*_{23} (1-0.05x_+)
|
|
2837
|
-
|
|
2838
|
-
Where:
|
|
2839
|
-
|
|
2840
|
-
.. math:: \\delta G^*_{123} = a_o + a_1 (T)^{0.75}
|
|
2841
|
-
.. math:: \\delta G^*_{23} = b_o + b_1 (T)^{0.5}
|
|
2842
|
-
|
|
2843
|
-
In which :math:`\nu` is the kinematic viscosity, MW is the molecular weight,
|
|
2844
|
-
`x_+` is the mole fraction of cations, and T is the temperature in degrees C.
|
|
2845
|
-
|
|
2846
|
-
The a and b fitting parameters for a variety of common salts are included in the
|
|
2847
|
-
database.
|
|
2848
|
-
|
|
2849
|
-
References:
|
|
2850
|
-
----------
|
|
2851
|
-
Vásquez-Castillo, G.; Iglesias-Silva, G. a.; Hall, K. R. An extension
|
|
2852
|
-
of the McAllister model to correlate kinematic viscosity of electrolyte solutions.
|
|
2853
|
-
Fluid Phase Equilib. 2013, 358, 44-49.
|
|
2854
|
-
|
|
2855
|
-
See Also:
|
|
2856
|
-
--------
|
|
2857
|
-
viscosity_dynamic
|
|
2858
|
-
|
|
2859
|
-
:meta private:
|
|
2860
|
-
|
|
2861
|
-
"""
|
|
2862
|
-
return self.viscosity_kinematic
|
|
2863
|
-
|
|
2864
|
-
@deprecated(
|
|
2865
|
-
message="get_conductivity() will be removed in the next release. Access directly via the property Solution.conductivity."
|
|
2866
|
-
)
|
|
2867
|
-
def get_conductivity(self): # pragma: no cover
|
|
2868
|
-
"""
|
|
2869
|
-
Compute the electrical conductivity of the solution.
|
|
2870
|
-
|
|
2871
|
-
Parameters
|
|
2872
|
-
----------
|
|
2873
|
-
None
|
|
2874
|
-
|
|
2875
|
-
Returns:
|
|
2876
|
-
-------
|
|
2877
|
-
Quantity
|
|
2878
|
-
The electrical conductivity of the solution in Siemens / meter.
|
|
2879
|
-
|
|
2880
|
-
Notes:
|
|
2881
|
-
-----
|
|
2882
|
-
Conductivity is calculated by summing the molar conductivities of the respective
|
|
2883
|
-
solutes, but they are activity-corrected and adjusted using an empricial exponent.
|
|
2884
|
-
This approach is used in PHREEQC and Aqion models [#]_ [#]_
|
|
2885
|
-
|
|
2886
|
-
.. math::
|
|
2887
|
-
|
|
2888
|
-
EC = {F^2 \\over R T} \\sum_i D_i z_i ^ 2 \\gamma_i ^ {\alpha} m_i
|
|
2889
|
-
|
|
2890
|
-
Where:
|
|
2891
|
-
|
|
2892
|
-
.. math::
|
|
2893
|
-
|
|
2894
|
-
\alpha = \begin{cases} {0.6 \\over \\sqrt{|z_i|}} & {I < 0.36|z_i|} \\ {\\sqrt{I} \\over |z_i|} & otherwise \\end{cases}
|
|
2895
|
-
|
|
2896
|
-
Note: PHREEQC uses the molal rather than molar concentration according to
|
|
2897
|
-
http://wwwbrr.cr.usgs.gov/projects/GWC_coupled/phreeqc/phreeqc3-html/phreeqc3-43.htm
|
|
2898
|
-
|
|
2899
|
-
References:
|
|
2900
|
-
----------
|
|
2901
|
-
.. [#] https://www.aqion.de/site/electrical-conductivity
|
|
2902
|
-
.. [#] http://www.hydrochemistry.eu/exmpls/sc.html
|
|
2903
|
-
|
|
2904
|
-
See Also:
|
|
2905
|
-
--------
|
|
2906
|
-
ionic_strength
|
|
2907
|
-
get_molar_conductivity()
|
|
2908
|
-
get_activity_coefficient()
|
|
2909
|
-
|
|
2910
|
-
:meta private:
|
|
2911
|
-
|
|
2912
|
-
"""
|
|
2913
|
-
return self.conductivity
|
|
2914
|
-
|
|
2915
|
-
@deprecated(
|
|
2916
|
-
replacement=get_amount,
|
|
2917
|
-
message="get_mole_fraction() will be removed in the next release. Use get_amount() with units='fraction' instead.",
|
|
2918
|
-
)
|
|
2919
|
-
def get_mole_fraction(self, solute): # pragma: no cover
|
|
2920
|
-
"""
|
|
2921
|
-
Return the mole fraction of 'solute' in the solution.
|
|
2922
|
-
|
|
2923
|
-
Notes:
|
|
2924
|
-
-----
|
|
2925
|
-
This function is DEPRECATED.
|
|
2926
|
-
Use get_amount() instead and specify 'fraction' as the unit type.
|
|
2927
|
-
|
|
2928
|
-
:meta private:
|
|
2929
|
-
"""
|
|
2930
|
-
|
|
2931
|
-
@deprecated(
|
|
2932
|
-
message="get_ionic_strength() will be removed in the next release. Access directly via the property Solution.ionic_strength"
|
|
2933
|
-
)
|
|
2934
|
-
def get_ionic_strength(self): # pragma: no cover
|
|
2935
|
-
r"""
|
|
2936
|
-
Return the ionic strength of the solution.
|
|
2937
|
-
|
|
2938
|
-
Return the ionic strength of the solution, calculated as 1/2 * sum ( molality * charge ^2) over all the ions.
|
|
2939
|
-
Molal (mol/kg) scale concentrations are used for compatibility with the activity correction formulas.
|
|
2940
|
-
|
|
2941
|
-
Returns:
|
|
2942
|
-
-------
|
|
2943
|
-
Quantity:
|
|
2944
|
-
The ionic strength of the parent solution, mol/kg.
|
|
2945
|
-
|
|
2946
|
-
See Also:
|
|
2947
|
-
--------
|
|
2948
|
-
get_activity
|
|
2949
|
-
get_water_activity
|
|
2950
|
-
|
|
2951
|
-
Notes:
|
|
2952
|
-
-----
|
|
2953
|
-
The ionic strength is calculated according to:
|
|
2954
|
-
|
|
2955
|
-
.. math:: I = \sum_i m_i z_i^2
|
|
2956
|
-
|
|
2957
|
-
Where :math:`m_i` is the molal concentration and :math:`z_i` is the charge on species i.
|
|
2958
|
-
|
|
2959
|
-
Examples:
|
|
2960
|
-
--------
|
|
2961
|
-
>>> s1 = pyEQL.Solution([['Na+','0.2 mol/kg'],['Cl-','0.2 mol/kg']])
|
|
2962
|
-
>>> s1.ionic_strength
|
|
2963
|
-
<Quantity(0.20000010029672785, 'mole / kilogram')>
|
|
2964
|
-
|
|
2965
|
-
>>> s1 = pyEQL.Solution([['Mg+2','0.3 mol/kg'],['Na+','0.1 mol/kg'],['Cl-','0.7 mol/kg']],temperature='30 degC')
|
|
2966
|
-
>>> s1.ionic_strength
|
|
2967
|
-
<Quantity(1.0000001004383303, 'mole / kilogram')>
|
|
2968
|
-
|
|
2969
|
-
:meta private:
|
|
2970
|
-
"""
|
|
2971
|
-
return self.ionic_strength
|
|
2972
|
-
|
|
2973
|
-
@deprecated(
|
|
2974
|
-
message="get_charge_balance() will be removed in the next release. Access directly via the property Solution.charge_balance"
|
|
2975
|
-
)
|
|
2976
|
-
def get_charge_balance(self): # pragma: no cover
|
|
2977
|
-
r"""
|
|
2978
|
-
Return the charge balance of the solution.
|
|
2979
|
-
|
|
2980
|
-
Return the charge balance of the solution. The charge balance represents the net electric charge
|
|
2981
|
-
on the solution and SHOULD equal zero at all times, but due to numerical errors will usually
|
|
2982
|
-
have a small nonzero value.
|
|
2983
|
-
|
|
2984
|
-
Returns:
|
|
2985
|
-
-------
|
|
2986
|
-
float :
|
|
2987
|
-
The charge balance of the solution, in equivalents.
|
|
2988
|
-
|
|
2989
|
-
Notes:
|
|
2990
|
-
-----
|
|
2991
|
-
The charge balance is calculated according to:
|
|
2992
|
-
|
|
2993
|
-
.. math:: CB = F \sum_i n_i z_i
|
|
2994
|
-
|
|
2995
|
-
Where :math:`n_i` is the number of moles, :math:`z_i` is the charge on species i, and :math:`F` is the Faraday constant.
|
|
2996
|
-
|
|
2997
|
-
:meta private:
|
|
2998
|
-
|
|
2999
|
-
"""
|
|
3000
|
-
return self.charge_balance
|
|
3001
|
-
|
|
3002
|
-
@deprecated(
|
|
3003
|
-
message="get_alkalinity() will be removed in the next release. Access directly via the property Solution.alkalinity"
|
|
3004
|
-
)
|
|
3005
|
-
def get_alkalinity(self): # pragma: no cover
|
|
3006
|
-
r"""
|
|
3007
|
-
Return the alkalinity or acid neutralizing capacity of a solution.
|
|
3008
|
-
|
|
3009
|
-
Returns:
|
|
3010
|
-
-------
|
|
3011
|
-
Quantity:
|
|
3012
|
-
The alkalinity of the solution in mg/L as CaCO3
|
|
3013
|
-
|
|
3014
|
-
Notes:
|
|
3015
|
-
-----
|
|
3016
|
-
The alkalinity is calculated according to:
|
|
3017
|
-
|
|
3018
|
-
.. math:: Alk = F \sum_i z_i C_B - \sum_i z_i C_A
|
|
3019
|
-
|
|
3020
|
-
Where :math:`C_B` and :math:`C_A` are conservative cations and anions, respectively
|
|
3021
|
-
(i.e. ions that do not participate in acid-base reactions), and :math:`z_i` is their charge.
|
|
3022
|
-
In this method, the set of conservative cations is all Group I and Group II cations, and the conservative anions are all the anions of strong acids.
|
|
3023
|
-
|
|
3024
|
-
References:
|
|
3025
|
-
----------
|
|
3026
|
-
Stumm, Werner and Morgan, James J. Aquatic Chemistry, 3rd ed,
|
|
3027
|
-
pp 165. Wiley Interscience, 1996.
|
|
3028
|
-
|
|
3029
|
-
:meta private:
|
|
3030
|
-
|
|
3031
|
-
"""
|
|
3032
|
-
return self.alkalinity
|
|
3033
|
-
|
|
3034
|
-
@deprecated(
|
|
3035
|
-
message="get_hardness() will be removed in the next release. Access directly via the property Solution.hardness"
|
|
3036
|
-
)
|
|
3037
|
-
def get_hardness(self): # pragma: no cover
|
|
3038
|
-
"""
|
|
3039
|
-
Return the hardness of a solution.
|
|
3040
|
-
|
|
3041
|
-
Hardness is defined as the sum of the equivalent concentrations
|
|
3042
|
-
of multivalent cations as calcium carbonate.
|
|
3043
|
-
|
|
3044
|
-
NOTE: at present pyEQL cannot distinguish between mg/L as CaCO3
|
|
3045
|
-
and mg/L units. Use with caution.
|
|
3046
|
-
|
|
3047
|
-
Parameters
|
|
3048
|
-
----------
|
|
3049
|
-
None
|
|
3050
|
-
|
|
3051
|
-
Returns:
|
|
3052
|
-
-------
|
|
3053
|
-
Quantity
|
|
3054
|
-
The hardness of the solution in mg/L as CaCO3
|
|
3055
|
-
|
|
3056
|
-
:meta private:
|
|
3057
|
-
|
|
3058
|
-
"""
|
|
3059
|
-
return self.hardness
|
|
3060
|
-
|
|
3061
|
-
@deprecated(
|
|
3062
|
-
message="get_debye_length() will be removed in the next release. Access directly via the property Solution.debye_length"
|
|
3063
|
-
)
|
|
3064
|
-
def get_debye_length(self): # pragma: no cover
|
|
3065
|
-
r"""
|
|
3066
|
-
Return the Debye length of a solution.
|
|
3067
|
-
|
|
3068
|
-
Debye length is calculated as
|
|
3069
|
-
|
|
3070
|
-
.. math::
|
|
3071
|
-
|
|
3072
|
-
\kappa^{-1} = \sqrt({\epsilon_r \epsilon_o k_B T \over (2 N_A e^2 I)})
|
|
3073
|
-
|
|
3074
|
-
where :math:`I` is the ionic strength, :math:`epsilon_r` and :math:`epsilon_r`
|
|
3075
|
-
are the relative permittivity and vacuum permittivity, :math:`k_B` is the
|
|
3076
|
-
Boltzmann constant, and :math:`T` is the temperature, :math:`e` is the
|
|
3077
|
-
elementary charge, and :math:`N_A` is Avogadro's number.
|
|
3078
|
-
|
|
3079
|
-
Parameters
|
|
3080
|
-
----------
|
|
3081
|
-
None
|
|
3082
|
-
|
|
3083
|
-
Returns:
|
|
3084
|
-
-------
|
|
3085
|
-
Quantity
|
|
3086
|
-
The Debye length, in nanometers.
|
|
3087
|
-
|
|
3088
|
-
References:
|
|
3089
|
-
----------
|
|
3090
|
-
https://en.wikipedia.org/wiki/Debye_length#Debye_length_in_an_electrolyte
|
|
3091
|
-
|
|
3092
|
-
See Also:
|
|
3093
|
-
--------
|
|
3094
|
-
ionic_strength
|
|
3095
|
-
get_dielectric_constant()
|
|
3096
|
-
|
|
3097
|
-
:meta private:
|
|
3098
|
-
|
|
3099
|
-
"""
|
|
3100
|
-
return self.debye_length
|
|
3101
|
-
|
|
3102
|
-
@deprecated(
|
|
3103
|
-
message="get_bjerrum_length() will be removed in the next release. Access directly via the property Solution.bjerrum_length"
|
|
3104
|
-
)
|
|
3105
|
-
def get_bjerrum_length(self): # pragma: no cover
|
|
3106
|
-
r"""
|
|
3107
|
-
Return the Bjerrum length of a solution.
|
|
3108
|
-
|
|
3109
|
-
Bjerrum length represents the distance at which electrostatic
|
|
3110
|
-
interactions between particles become comparable in magnitude
|
|
3111
|
-
to the thermal energy.:math:`\lambda_B` is calculated as
|
|
3112
|
-
|
|
3113
|
-
.. math::
|
|
3114
|
-
|
|
3115
|
-
\lambda_B = {e^2 \over (4 \pi \epsilon_r \epsilon_o k_B T)}
|
|
3116
|
-
|
|
3117
|
-
where :math:`e` is the fundamental charge, :math:`epsilon_r` and :math:`epsilon_r`
|
|
3118
|
-
are the relative permittivity and vacuum permittivity, :math:`k_B` is the
|
|
3119
|
-
Boltzmann constant, and :math:`T` is the temperature.
|
|
3120
|
-
|
|
3121
|
-
Parameters
|
|
3122
|
-
----------
|
|
3123
|
-
None
|
|
3124
|
-
|
|
3125
|
-
Returns:
|
|
3126
|
-
-------
|
|
3127
|
-
Quantity
|
|
3128
|
-
The Bjerrum length, in nanometers.
|
|
3129
|
-
|
|
3130
|
-
References:
|
|
3131
|
-
----------
|
|
3132
|
-
https://en.wikipedia.org/wiki/Bjerrum_length
|
|
3133
|
-
|
|
3134
|
-
Examples:
|
|
3135
|
-
--------
|
|
3136
|
-
>>> s1 = pyEQL.Solution()
|
|
3137
|
-
>>> s1.get_bjerrum_length()
|
|
3138
|
-
<Quantity(0.7152793009386953, 'nanometer')>
|
|
3139
|
-
|
|
3140
|
-
See Also:
|
|
3141
|
-
--------
|
|
3142
|
-
get_dielectric_constant()
|
|
3143
|
-
|
|
3144
|
-
:meta private:
|
|
3145
|
-
|
|
3146
|
-
"""
|
|
3147
|
-
return self.bjerrum_length
|
|
3148
|
-
|
|
3149
|
-
@deprecated(
|
|
3150
|
-
message="get_dielectric_constant() will be removed in the next release. Access directly via the property Solution.dielectric_constant"
|
|
3151
|
-
)
|
|
3152
|
-
def get_dielectric_constant(self): # pragma: no cover
|
|
3153
|
-
"""
|
|
3154
|
-
Returns the dielectric constant of the solution.
|
|
3155
|
-
|
|
3156
|
-
Parameters
|
|
3157
|
-
----------
|
|
3158
|
-
None
|
|
3159
|
-
|
|
3160
|
-
Returns:
|
|
3161
|
-
-------
|
|
3162
|
-
Quantity: the dielectric constant of the solution, dimensionless.
|
|
3163
|
-
|
|
3164
|
-
Notes:
|
|
3165
|
-
-----
|
|
3166
|
-
Implements the following equation as given by [zub]_
|
|
3167
|
-
|
|
3168
|
-
.. math:: \\epsilon = \\epsilon_solvent \\over 1 + \\sum_i \alpha_i x_i
|
|
3169
|
-
|
|
3170
|
-
where :math:`\alpha_i` is a coefficient specific to the solvent and ion, and :math:`x_i`
|
|
3171
|
-
is the mole fraction of the ion in solution.
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
References:
|
|
3175
|
-
----------
|
|
3176
|
-
.. [zub] A. Zuber, L. Cardozo-Filho, V.F. Cabral, R.F. Checoni, M. Castier,
|
|
3177
|
-
An empirical equation for the dielectric constant in aqueous and nonaqueous
|
|
3178
|
-
electrolyte mixtures, Fluid Phase Equilib. 376 (2014) 116-123.
|
|
3179
|
-
doi:10.1016/j.fluid.2014.05.037.
|
|
3180
|
-
|
|
3181
|
-
:meta private:
|
|
3182
|
-
"""
|
|
3183
|
-
return self.dielectric_constant
|
|
3184
|
-
|
|
3185
|
-
@deprecated(
|
|
3186
|
-
message="get_solvent_mass() will be removed in the next release. Access directly via the property Solution.solvent_mass"
|
|
3187
|
-
)
|
|
3188
|
-
def get_solvent_mass(self): # pragma: no cover
|
|
3189
|
-
"""
|
|
3190
|
-
Return the mass of the solvent.
|
|
3191
|
-
|
|
3192
|
-
This method is used whenever mol/kg (or similar) concentrations
|
|
3193
|
-
are requested by get_amount()
|
|
3194
|
-
|
|
3195
|
-
Parameters
|
|
3196
|
-
----------
|
|
3197
|
-
None
|
|
3198
|
-
|
|
3199
|
-
Returns:
|
|
3200
|
-
-------
|
|
3201
|
-
Quantity: the mass of the solvent, in kg
|
|
3202
|
-
|
|
3203
|
-
See Also:
|
|
3204
|
-
--------
|
|
3205
|
-
:py:meth:`get_amount()`
|
|
3206
|
-
|
|
3207
|
-
:meta private:
|
|
3208
|
-
"""
|
|
3209
|
-
return self.solvent_mass
|
|
3210
|
-
|
|
3211
|
-
@deprecated(
|
|
3212
|
-
message="osmotic_pressure will be removed in the next release. Access directly via the property Solution.osmotic_pressure"
|
|
3213
|
-
)
|
|
3214
|
-
def get_osmotic_pressure(self): # pragma: no cover
|
|
3215
|
-
""":meta private:"""
|
|
3216
|
-
return self.osmotic_pressure
|
|
3217
|
-
|
|
3218
|
-
@deprecated(message="get_salt_list() will be removed in the next release. Use get_salt_dict() instead.")
|
|
3219
|
-
def get_salt_list(self): # pragma: no cover
|
|
3220
|
-
"""
|
|
3221
|
-
See get_salt_dict().
|
|
3222
|
-
|
|
3223
|
-
See Also:
|
|
3224
|
-
--------
|
|
3225
|
-
:py:meth:`get_salt_dict`
|
|
3226
|
-
"""
|
|
3227
|
-
return self.get_salt_dict()
|
|
3228
|
-
|
|
3229
|
-
@classmethod
|
|
3230
|
-
def from_preset(
|
|
3231
|
-
cls, preset: Literal["seawater", "rainwater", "wastewater", "urine", "normal saline", "Ringers lactate"]
|
|
3232
|
-
) -> Solution:
|
|
3233
|
-
"""Instantiate a solution from a preset composition.
|
|
3234
|
-
|
|
3235
|
-
Args:
|
|
3236
|
-
preset (str): String representing the desired solution.
|
|
3237
|
-
Valid entries are 'seawater', 'rainwater', 'wastewater',
|
|
3238
|
-
'urine', 'normal saline' and 'Ringers lactate'.
|
|
3239
|
-
|
|
3240
|
-
Returns:
|
|
3241
|
-
A pyEQL Solution object.
|
|
3242
|
-
|
|
3243
|
-
Raises:
|
|
3244
|
-
FileNotFoundError: If the given preset file doesn't exist on the file system.
|
|
3245
|
-
|
|
3246
|
-
Notes:
|
|
3247
|
-
The following sections explain the different solution options:
|
|
3248
|
-
|
|
3249
|
-
- 'rainwater' - pure water in equilibrium with atmospheric CO2 at pH 6
|
|
3250
|
-
- 'seawater' or 'SW'- Standard Seawater. See Table 4 of the Reference for Composition [1]_
|
|
3251
|
-
- 'wastewater' or 'WW' - medium strength domestic wastewater. See Table 3-18 of [2]_
|
|
3252
|
-
- 'urine' - typical human urine. See Table 3-15 of [2]_
|
|
3253
|
-
- 'normal saline' or 'NS' - normal saline solution used in medicine [3]_
|
|
3254
|
-
- 'Ringers lacatate' or 'RL' - Ringer's lactate solution used in medicine [4]_
|
|
3255
|
-
|
|
3256
|
-
References:
|
|
3257
|
-
.. [1] Millero, Frank J. "The composition of Standard Seawater and the definition of
|
|
3258
|
-
the Reference-Composition Salinity Scale." *Deep-sea Research. Part I* 55(1), 2008, 50-72.
|
|
3259
|
-
|
|
3260
|
-
.. [2] Metcalf & Eddy, Inc. et al. *Wastewater Engineering: Treatment and Resource Recovery*, 5th Ed.
|
|
3261
|
-
McGraw-Hill, 2013.
|
|
3262
|
-
|
|
3263
|
-
.. [3] https://en.wikipedia.org/wiki/Saline_(medicine)
|
|
3264
|
-
|
|
3265
|
-
.. [4] https://en.wikipedia.org/wiki/Ringer%27s_lactate_solution
|
|
3266
|
-
"""
|
|
3267
|
-
# preset_dir = files("pyEQL") / "presets"
|
|
3268
|
-
# Path to the YAML and JSON files corresponding to the preset
|
|
3269
|
-
yaml_path = files("pyEQL") / "presets" / f"{preset}.yaml"
|
|
3270
|
-
json_path = files("pyEQL") / "presets" / f"{preset}.json"
|
|
3271
|
-
|
|
3272
|
-
# Check if the file exists
|
|
3273
|
-
if yaml_path.exists():
|
|
3274
|
-
preset_path = yaml_path
|
|
3275
|
-
elif json_path.exists():
|
|
3276
|
-
preset_path = json_path
|
|
3277
|
-
else:
|
|
3278
|
-
logger.error("Invalid solution entered - %s" % preset)
|
|
3279
|
-
raise FileNotFoundError(f"Files '{yaml_path}' and '{json_path} not found!")
|
|
3280
|
-
|
|
3281
|
-
# Create and return a Solution object
|
|
3282
|
-
return cls().from_file(preset_path)
|
|
3283
|
-
|
|
3284
|
-
def to_file(self, filename: str | Path) -> None:
|
|
3285
|
-
"""Saving to a .yaml or .json file.
|
|
3286
|
-
|
|
3287
|
-
Args:
|
|
3288
|
-
filename (str | Path): The path to the file to save Solution.
|
|
3289
|
-
Valid extensions are .json or .yaml.
|
|
3290
|
-
"""
|
|
3291
|
-
str_filename = str(filename)
|
|
3292
|
-
if not ("yaml" in str_filename.lower() or "json" in str_filename.lower()):
|
|
3293
|
-
logger.error("Invalid file extension entered - %s" % str_filename)
|
|
3294
|
-
raise ValueError("File extension must be .json or .yaml")
|
|
3295
|
-
if "yaml" in str_filename.lower():
|
|
3296
|
-
solution_dict = self.as_dict()
|
|
3297
|
-
solution_dict.pop("database")
|
|
3298
|
-
dumpfn(solution_dict, filename)
|
|
3299
|
-
else:
|
|
3300
|
-
dumpfn(self, filename)
|
|
3301
|
-
|
|
3302
|
-
@classmethod
|
|
3303
|
-
def from_file(self, filename: str | Path) -> Solution:
|
|
3304
|
-
"""Loading from a .yaml or .json file.
|
|
3305
|
-
|
|
3306
|
-
Args:
|
|
3307
|
-
filename (str | Path): Path to the .json or .yaml file (including extension) to load the Solution from.
|
|
3308
|
-
Valid extensions are .json or .yaml.
|
|
3309
|
-
|
|
3310
|
-
Returns:
|
|
3311
|
-
A pyEQL Solution object.
|
|
3312
|
-
|
|
3313
|
-
Raises:
|
|
3314
|
-
FileNotFoundError: If the given filename doesn't exist on the file system.
|
|
3315
|
-
"""
|
|
3316
|
-
if not os.path.exists(filename):
|
|
3317
|
-
logger.error("Invalid path to file entered - %s" % filename)
|
|
3318
|
-
raise FileNotFoundError(f"File '{filename}' not found!")
|
|
3319
|
-
str_filename = str(filename)
|
|
3320
|
-
if "yaml" in str_filename.lower():
|
|
3321
|
-
true_keys = [
|
|
3322
|
-
"solutes",
|
|
3323
|
-
"volume",
|
|
3324
|
-
"temperature",
|
|
3325
|
-
"pressure",
|
|
3326
|
-
"pH",
|
|
3327
|
-
"pE",
|
|
3328
|
-
"balance_charge",
|
|
3329
|
-
"solvent",
|
|
3330
|
-
"engine",
|
|
3331
|
-
# "database",
|
|
3332
|
-
]
|
|
3333
|
-
solution_dict = loadfn(filename)
|
|
3334
|
-
keys_to_delete = [key for key in solution_dict if key not in true_keys]
|
|
3335
|
-
for key in keys_to_delete:
|
|
3336
|
-
solution_dict.pop(key)
|
|
3337
|
-
return Solution(**solution_dict)
|
|
3338
|
-
return loadfn(filename)
|