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/solution.py CHANGED
@@ -8,7 +8,7 @@ pyEQL Solution Class.
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
- import math
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 attached to this class
129
- # self.logger = logging.getLogger(type(self).__name__)
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.info(f"Created maggma JSONStore from .json file {database}")
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.info(f"Connected to property database {self.database!s}")
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
- logger.warning(
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(f"Solution is not electroneutral (C.B. = {cb} eq/L). Adding {balance_charge} to compensate.")
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.info("Viscosity coefficients for %s not found. Viscosity will be approximate." % salt.formula)
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 = math.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)
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 * math.log(self.get_water_activity())
979
+ -1 * ureg.R * self.temperature / partial_molar_volume_water * np.log(self.get_water_activity())
957
980
  )
958
- logger.info(
959
- f"Computed osmotic pressure of solution as {osmotic_pressure} Pa at T= {self.temperature} degrees C"
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.warning(
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
- warnings.warn(f"No oxidation state found for element {el}")
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
- warnings.warn(f"Guessing oxi states failed for {s}")
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.warning(
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.warning(
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
- logger.error("Negative amount specified for solute %s. Concentration not changed." % solute)
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
- elif ureg.Quantity(amount).dimensionality in (
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.error("All solvent has been depleted from the solution")
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.warning("Calculation unsuccessful. Returning unit activity coefficient.")
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.info(f"Calculated {scale} scale activity of solute {solute} as {activity}")
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
- / math.log(self.get_amount(self.solvent, "fraction"))
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
- - math.log(self.get_amount(self.solvent, "fraction"))
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.info("Calculated water activity using osmotic coefficient")
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
- * math.log(self.get_activity(item))
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
- * math.log(self.get_amount(item, "fraction"))
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 %s not corrected for temperature" % solute)
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.warning(f"Property {name} for solute {solute} not found in database. Returning None.")
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.info(f"Computed molar conductivity as {molar_cond} from D = {D!s} at T={self.temperature}")
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.info(
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.info(
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.info(f"Computed ionic mobility as {mobility} from D = {D!s} at T={self.temperature}")
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("Adding two solutions of different pressure. Pressures will be averaged (weighted by volume)")
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 = -math.log10(float(mix_species["H+"].split(" ")[0]) / mix_vol.to("L").magnitude)
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 = -math.log10((mol_e_self + mol_e_other) / mix_vol.to("L").magnitude)
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)