pyEQL 1.1.0__py3-none-any.whl → 1.1.2__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 CHANGED
@@ -37,11 +37,8 @@ ureg = UnitRegistry(cache_folder=":auto:")
37
37
  # convert "offset units" so that, e.g. Quantity('25 degC') works without error
38
38
  # see https://pint.readthedocs.io/en/0.22/user/nonmult.html?highlight=offset#temperature-conversion
39
39
  ureg.autoconvert_offset_to_baseunit = True
40
- # append custom unit definitions and contexts
41
- fname = files("pyEQL") / "pint_custom_units.txt"
42
- ureg.load_definitions(fname)
43
40
  # activate the "chemistry" context globally
44
- ureg.enable_contexts("chem")
41
+ ureg.enable_contexts("chemistry")
45
42
  # set the default string formatting for pint quantities
46
43
  ureg.default_format = "P~"
47
44
 
@@ -6698,7 +6698,7 @@
6698
6698
  "n_elements": 1,
6699
6699
  "size": {
6700
6700
  "radius_ionic": {
6701
- "value": "0.755",
6701
+ "value": "0.755",
6702
6702
  "reference": "pymatgen",
6703
6703
  "data_type": "experimental"
6704
6704
  },
@@ -24663,7 +24663,7 @@
24663
24663
  "n_elements": 1,
24664
24664
  "size": {
24665
24665
  "radius_ionic": {
24666
- "value": "0.83",
24666
+ "value": "0.83",
24667
24667
  "reference": "pymatgen",
24668
24668
  "data_type": "experimental"
24669
24669
  },
pyEQL/engines.py CHANGED
@@ -11,7 +11,7 @@ import os
11
11
  import warnings
12
12
  from abc import ABC, abstractmethod
13
13
  from pathlib import Path
14
- from typing import Literal
14
+ from typing import TYPE_CHECKING, Literal
15
15
 
16
16
  from phreeqpython import PhreeqPython
17
17
 
@@ -26,6 +26,9 @@ SPECIAL_ELEMENTS = ["S", "C", "N", "Cu", "Fe", "Mn"]
26
26
 
27
27
  logger = logging.getLogger(__name__)
28
28
 
29
+ if TYPE_CHECKING:
30
+ from pyEQL import Solution
31
+
29
32
 
30
33
  class EOS(ABC):
31
34
  """
@@ -38,7 +41,7 @@ class EOS(ABC):
38
41
  """
39
42
 
40
43
  @abstractmethod
41
- def get_activity_coefficient(self, solution, solute):
44
+ def get_activity_coefficient(self, solution: "Solution", solute: str) -> ureg.Quantity:
42
45
  """
43
46
  Return the *molal scale* activity coefficient of solute, given a Solution
44
47
  object.
@@ -55,7 +58,7 @@ class EOS(ABC):
55
58
  """
56
59
 
57
60
  @abstractmethod
58
- def get_osmotic_coefficient(self, solution):
61
+ def get_osmotic_coefficient(self, solution: "Solution") -> ureg.Quantity:
59
62
  """
60
63
  Return the *molal scale* osmotic coefficient of a Solution.
61
64
 
@@ -70,7 +73,7 @@ class EOS(ABC):
70
73
  """
71
74
 
72
75
  @abstractmethod
73
- def get_solute_volume(self):
76
+ def get_solute_volume(self, solution: "Solution") -> ureg.Quantity:
74
77
  """
75
78
  Return the volume of only the solutes.
76
79
 
@@ -85,7 +88,7 @@ class EOS(ABC):
85
88
  """
86
89
 
87
90
  @abstractmethod
88
- def equilibrate(self, solution):
91
+ def equilibrate(self, solution: "Solution") -> None:
89
92
  """
90
93
  Adjust the speciation and pH of a Solution object to achieve chemical equilibrium.
91
94
 
@@ -105,25 +108,25 @@ class EOS(ABC):
105
108
  class IdealEOS(EOS):
106
109
  """Ideal solution equation of state engine."""
107
110
 
108
- def get_activity_coefficient(self, solution, solute):
111
+ def get_activity_coefficient(self, solution: "Solution", solute: str) -> ureg.Quantity:
109
112
  """
110
113
  Return the *molal scale* activity coefficient of solute, given a Solution
111
114
  object.
112
115
  """
113
116
  return ureg.Quantity(1, "dimensionless")
114
117
 
115
- def get_osmotic_coefficient(self, solution):
118
+ def get_osmotic_coefficient(self, solution: "Solution") -> ureg.Quantity:
116
119
  """
117
120
  Return the *molal scale* osmotic coefficient of solute, given a Solution
118
121
  object.
119
122
  """
120
123
  return ureg.Quantity(1, "dimensionless")
121
124
 
122
- def get_solute_volume(self, solution):
125
+ def get_solute_volume(self, solution: "Solution") -> ureg.Quantity:
123
126
  """Return the volume of the solutes."""
124
127
  return ureg.Quantity(0, "L")
125
128
 
126
- def equilibrate(self, solution):
129
+ def equilibrate(self, solution: "Solution") -> None:
127
130
  """Adjust the speciation of a Solution object to achieve chemical equilibrium."""
128
131
  warnings.warn("equilibrate() has no effect in IdealEOS!")
129
132
  return
@@ -138,7 +141,9 @@ class NativeEOS(EOS):
138
141
 
139
142
  def __init__(
140
143
  self,
141
- phreeqc_db: Literal["vitens.dat", "wateq4f_PWN.dat", "pitzer.dat", "llnl.dat", "geothermal.dat"] = "llnl.dat",
144
+ phreeqc_db: Literal[
145
+ "phreeqc.dat", "vitens.dat", "wateq4f_PWN.dat", "pitzer.dat", "llnl.dat", "geothermal.dat"
146
+ ] = "llnl.dat",
142
147
  ) -> None:
143
148
  """
144
149
  Args:
@@ -172,7 +177,7 @@ class NativeEOS(EOS):
172
177
  # store the solution composition to see whether we need to re-instantiate the solution
173
178
  self._stored_comp = None
174
179
 
175
- def _setup_ppsol(self, solution):
180
+ def _setup_ppsol(self, solution: "Solution") -> None:
176
181
  """Helper method to set up a PhreeqPython solution for subsequent analysis."""
177
182
  self._stored_comp = solution.components.copy()
178
183
  solv_mass = solution.solvent_mass.to("kg").magnitude
@@ -224,10 +229,7 @@ class NativeEOS(EOS):
224
229
  d[key] = str(mol / solv_mass)
225
230
 
226
231
  # tell PHREEQC which species to use for charge balance
227
- if (
228
- solution.balance_charge is not None
229
- and solution.balance_charge in solution.get_components_by_element()[el]
230
- ):
232
+ if solution.balance_charge is not None and solution._cb_species in solution.get_components_by_element()[el]:
231
233
  d[key] += " charge"
232
234
 
233
235
  # create the PHREEQC solution object
@@ -244,13 +246,13 @@ class NativeEOS(EOS):
244
246
 
245
247
  self.ppsol = ppsol
246
248
 
247
- def _destroy_ppsol(self):
248
- """Remove the PhreeqPython solution from memory"""
249
+ def _destroy_ppsol(self) -> None:
250
+ """Remove the PhreeqPython solution from memory."""
249
251
  if self.ppsol is not None:
250
252
  self.ppsol.forget()
251
253
  self.ppsol = None
252
254
 
253
- def get_activity_coefficient(self, solution, solute):
255
+ def get_activity_coefficient(self, solution: "Solution", solute: str):
254
256
  r"""
255
257
  Whenever the appropriate parameters are available, the Pitzer model [may]_ is used.
256
258
  If no Pitzer parameters are available, then the appropriate equations are selected
@@ -329,7 +331,7 @@ class NativeEOS(EOS):
329
331
 
330
332
  # show an error if no salt can be found that contains the solute
331
333
  if salt is None:
332
- logger.error("No salts found that contain solute %s. Returning unit activity coefficient." % solute)
334
+ logger.error(f"No salts found that contain solute {solute}. Returning unit activity coefficient.")
333
335
  return ureg.Quantity(1, "dimensionless")
334
336
 
335
337
  # use the Pitzer model for higher ionic strength, if the parameters are available
@@ -344,14 +346,14 @@ class NativeEOS(EOS):
344
346
  # alpha1 and alpha2 based on charge
345
347
  if salt.nu_cation >= 2 and salt.nu_anion <= -2:
346
348
  if salt.nu_cation >= 3 or salt.nu_anion <= -3:
347
- alpha1 = 2
348
- alpha2 = 50
349
+ alpha1 = 2.0
350
+ alpha2 = 50.0
349
351
  else:
350
352
  alpha1 = 1.4
351
353
  alpha2 = 12
352
354
  else:
353
- alpha1 = 2
354
- alpha2 = 0
355
+ alpha1 = 2.0
356
+ alpha2 = 0.0
355
357
 
356
358
  # determine the average molality of the salt
357
359
  # this is necessary for solutions inside e.g. an ion exchange
@@ -427,7 +429,7 @@ class NativeEOS(EOS):
427
429
 
428
430
  return molal
429
431
 
430
- def get_osmotic_coefficient(self, solution):
432
+ def get_osmotic_coefficient(self, solution: "Solution") -> ureg.Quantity:
431
433
  r"""
432
434
  Return the *molal scale* osmotic coefficient of solute, given a Solution
433
435
  object.
@@ -574,7 +576,7 @@ class NativeEOS(EOS):
574
576
  # this means the solution is empty
575
577
  return 1
576
578
 
577
- def get_solute_volume(self, solution):
579
+ def get_solute_volume(self, solution: "Solution") -> ureg.Quantity:
578
580
  """Return the volume of the solutes."""
579
581
  # identify the predominant salt in the solution
580
582
  salt = solution.get_salt()
@@ -596,14 +598,14 @@ class NativeEOS(EOS):
596
598
  # alpha1 and alpha2 based on charge
597
599
  if salt.nu_cation >= 2 and salt.nu_anion >= 2:
598
600
  if salt.nu_cation >= 3 or salt.nu_anion >= 3:
599
- alpha1 = 2
600
- alpha2 = 50
601
+ alpha1 = 2.0
602
+ alpha2 = 50.0
601
603
  else:
602
604
  alpha1 = 1.4
603
605
  alpha2 = 12
604
606
  else:
605
- alpha1 = 2
606
- alpha2 = 0
607
+ alpha1 = 2.0
608
+ alpha2 = 0.0
607
609
 
608
610
  apparent_vol = ac.get_apparent_volume_pitzer(
609
611
  solution.ionic_strength,
@@ -633,7 +635,7 @@ class NativeEOS(EOS):
633
635
 
634
636
  pitzer_calc = True
635
637
 
636
- logger.debug("Updated solution volume using Pitzer model for solute %s" % salt.formula)
638
+ logger.debug(f"Updated solution volume using Pitzer model for solute {salt.formula}")
637
639
 
638
640
  # add the partial molar volume of any other solutes, except for water
639
641
  # or the parent salt, which is already accounted for by the Pitzer parameters
@@ -649,7 +651,7 @@ class NativeEOS(EOS):
649
651
  part_vol = solution.get_property(solute, "size.molar_volume")
650
652
  if part_vol is not None:
651
653
  solute_vol += part_vol * ureg.Quantity(mol, "mol")
652
- logger.debug("Updated solution volume using direct partial molar volume for solute %s" % solute)
654
+ logger.debug(f"Updated solution volume using direct partial molar volume for solute {solute}")
653
655
 
654
656
  else:
655
657
  logger.warning(
@@ -658,7 +660,7 @@ class NativeEOS(EOS):
658
660
 
659
661
  return solute_vol.to("L")
660
662
 
661
- def equilibrate(self, solution):
663
+ def equilibrate(self, solution: "Solution") -> None:
662
664
  """Adjust the speciation of a Solution object to achieve chemical equilibrium."""
663
665
  if self.ppsol is not None:
664
666
  self.ppsol.forget()
@@ -693,18 +695,12 @@ class NativeEOS(EOS):
693
695
  if charge_adjust != 0:
694
696
  logger.warning(
695
697
  "After equilibration, the charge balance of the solution was not electroneutral."
696
- f" {charge_adjust} eq of charge were added via {solution.balance_charge}"
698
+ f" {charge_adjust} eq of charge were added via {solution._cb_species}"
697
699
  )
698
700
 
699
- if solution.balance_charge is None:
700
- pass
701
- elif solution.balance_charge == "pH":
702
- solution.components["H+"] += charge_adjust.magnitude
703
- elif solution.balance_charge == "pE":
704
- raise NotImplementedError
705
- else:
706
- z = solution.get_property(solution.balance_charge, "charge")
707
- solution.add_amount(solution.balance_charge, f"{charge_adjust/z} mol")
701
+ if solution.balance_charge is not None:
702
+ z = solution.get_property(solution._cb_species, "charge")
703
+ solution.add_amount(solution._cb_species, f"{charge_adjust/z} mol")
708
704
 
709
705
  # rescale the solvent mass to ensure the total mass of solution does not change
710
706
  # this is important because PHREEQC and the pyEQL database may use slightly different molecular
@@ -734,7 +730,7 @@ class PhreeqcEOS(NativeEOS):
734
730
  def __init__(
735
731
  self,
736
732
  phreeqc_db: Literal[
737
- "vitens.dat", "wateq4f_PWN.dat", "pitzer.dat", "llnl.dat", "geothermal.dat"
733
+ "phreeqc.dat", "vitens.dat", "wateq4f_PWN.dat", "pitzer.dat", "llnl.dat", "geothermal.dat"
738
734
  ] = "phreeqc.dat",
739
735
  ) -> None:
740
736
  """
@@ -751,12 +747,12 @@ class PhreeqcEOS(NativeEOS):
751
747
  """
752
748
  super().__init__(phreeqc_db=phreeqc_db)
753
749
 
754
- def get_activity_coefficient(self, solution, solute):
750
+ def get_activity_coefficient(self, solution: "Solution", solute: str) -> ureg.Quantity:
755
751
  """
756
752
  Return the *molal scale* activity coefficient of solute, given a Solution
757
753
  object.
758
754
  """
759
- if self.ppsol is None or solution.components != self._stored_comp:
755
+ if (self.ppsol is None) or (solution.components != self._stored_comp):
760
756
  self._destroy_ppsol()
761
757
  self._setup_ppsol(solution)
762
758
 
@@ -775,7 +771,7 @@ class PhreeqcEOS(NativeEOS):
775
771
 
776
772
  return ureg.Quantity(act, "dimensionless")
777
773
 
778
- def get_osmotic_coefficient(self, solution):
774
+ def get_osmotic_coefficient(self, solution: "Solution") -> ureg.Quantity:
779
775
  """
780
776
  Return the *molal scale* osmotic coefficient of solute, given a Solution
781
777
  object.
@@ -787,7 +783,7 @@ class PhreeqcEOS(NativeEOS):
787
783
  # TODO - find a way to access or calculate osmotic coefficient
788
784
  return ureg.Quantity(1, "dimensionless")
789
785
 
790
- def get_solute_volume(self, solution):
786
+ def get_solute_volume(self, solution: "Solution") -> ureg.Quantity:
791
787
  """Return the volume of the solutes."""
792
788
  # TODO - phreeqc seems to have no concept of volume, but it does calculate density
793
789
  return ureg.Quantity(0, "L")
pyEQL/solution.py CHANGED
@@ -92,11 +92,15 @@ class Solution(MSONable):
92
92
  -7 to +14. The default value corresponds to a pE value typical of natural
93
93
  waters in equilibrium with the atmosphere.
94
94
  balance_charge: The strategy for balancing charge during init and equilibrium calculations. Valid options
95
- are 'pH', which will adjust the solution pH to balance charge, 'pE' which will adjust the
96
- redox equilibrium to balance charge, or the name of a dissolved species e.g. 'Ca+2' or 'Cl-'
97
- that will be added/subtracted to balance charge. If set to None, no charge balancing will be
98
- performed either on init or when equilibrate() is called. Note that in this case, equilibrate()
99
- can distort the charge balance!
95
+ are
96
+ - 'pH', which will adjust the solution pH to balance charge,
97
+ - 'auto' which will use the majority cation or anion (i.e., that with the largest concentration)
98
+ as needed,
99
+ - 'pE' (not currently implemented) which will adjust the redox equilibrium to balance charge, or
100
+ the name of a dissolved species e.g. 'Ca+2' or 'Cl-' that will be added/subtracted to balance
101
+ charge.
102
+ - None (default), in which case no charge balancing will be performed either on init or when
103
+ equilibrate() is called. Note that in this case, equilibrate() can distort the charge balance!
100
104
  solvent: Formula of the solvent. Solvents other than water are not supported at this time.
101
105
  engine: Electrolyte modeling engine to use. See documentation for details on the available engines.
102
106
  database: path to a .json file (str or Path) or maggma Store instance that
@@ -171,7 +175,7 @@ class Solution(MSONable):
171
175
  self._pE = pE
172
176
  self._pH = pH
173
177
  self.pE = self._pE
174
- if isinstance(balance_charge, str) and balance_charge not in ["pH", "pE"]:
178
+ if isinstance(balance_charge, str) and balance_charge not in ["pH", "pE", "auto"]:
175
179
  self.balance_charge = standardize_formula(balance_charge)
176
180
  else:
177
181
  self.balance_charge = balance_charge #: Standardized formula of the species used for charge balancing.
@@ -257,33 +261,44 @@ class Solution(MSONable):
257
261
  for item in self._solutes:
258
262
  self.add_solute(*item)
259
263
 
260
- # adjust the charge balance, if necessary
264
+ # determine the species that will be used for charge balancing, when needed.
265
+ # this is necessary to do even if the composition is already electroneutral,
266
+ # because the appropriate species also needs to be passed to equilibrate
267
+ # to keep from distorting the charge balance.
261
268
  cb = self.charge_balance
269
+ if self.balance_charge is None:
270
+ self._cb_species = None
271
+ elif self.balance_charge == "pH":
272
+ self._cb_species = "H[+1]"
273
+ elif self.balance_charge == "pE":
274
+ raise NotImplementedError("Balancing charge via redox (pE) is not yet implemented!")
275
+ elif self.balance_charge == "auto":
276
+ # add the most abundant ion of the opposite charge
277
+ if cb <= 0:
278
+ self._cb_species = max(self.cations, key=self.cations.get)
279
+ elif cb > 0:
280
+ self._cb_species = max(self.anions, key=self.anions.get)
281
+ else:
282
+ ions = set().union(*[self.cations, self.anions]) # all ions
283
+ self._cb_species = self.balance_charge
284
+ if self._cb_species not in ions:
285
+ raise ValueError(
286
+ f"Charge balancing species {self._cb_species} was not found in the solution!. "
287
+ f"Species {ions} were found."
288
+ )
289
+
290
+ # adjust charge balance, if necessary
262
291
  if not np.isclose(cb, 0, atol=1e-8) and self.balance_charge is not None:
263
292
  balanced = False
264
293
  self.logger.info(
265
- f"Solution is not electroneutral (C.B. = {cb} eq/L). Adding {balance_charge} to compensate."
294
+ f"Solution is not electroneutral (C.B. = {cb} eq/L). Adding {self._cb_species} to compensate."
266
295
  )
267
- if self.balance_charge == "pH":
268
- self.components["H+"] += (
269
- -1 * cb * self.volume.to("L").magnitude
270
- ) # if C.B. is negative, we need to add cations. H+ is 1 eq/mol
296
+ z = self.get_property(self._cb_species, "charge")
297
+ self.components[self._cb_species] += -1 * cb / z * self.volume.to("L").magnitude
298
+ if np.isclose(self.charge_balance, 0, atol=1e-8):
271
299
  balanced = True
272
- elif self.balance_charge == "pE":
273
- raise NotImplementedError("Balancing charge via redox (pE) is not yet implemented!")
274
- else:
275
- ions = set().union(*[self.cations, self.anions]) # all ions
276
- if self.balance_charge not in ions:
277
- raise ValueError(
278
- f"Charge balancing species {self.balance_charge} was not found in the solution!. "
279
- f"Species {ions} were found."
280
- )
281
- z = self.get_property(balance_charge, "charge")
282
- self.components[balance_charge] += -1 * cb / z * self.volume.to("L").magnitude
283
- balanced = True
284
-
285
300
  if not balanced:
286
- warnings.warn(f"Unable to balance charge using species {self.balance_charge}")
301
+ warnings.warn(f"Unable to balance charge using species {self._cb_species}")
287
302
 
288
303
  @property
289
304
  def mass(self) -> Quantity:
@@ -1282,81 +1297,12 @@ class Solution(MSONable):
1282
1297
  Returns:
1283
1298
  Nothing. The concentration of solute is modified.
1284
1299
  """
1285
- # if units are given on a per-volume basis,
1286
- # iteratively solve for the amount of solute that will preserve the
1287
- # original volume and result in the desired concentration
1288
- if ureg.Quantity(amount).dimensionality in (
1289
- "[substance]/[length]**3",
1290
- "[mass]/[length]**3",
1291
- ):
1292
- # store the original volume for later
1293
- orig_volume = self.volume
1294
-
1295
- # change the amount of the solute present to match the desired amount
1296
- self.components[solute] += (
1297
- ureg.Quantity(amount)
1298
- .to(
1299
- "moles",
1300
- "chem",
1301
- mw=self.get_property(solute, "molecular_weight"),
1302
- volume=self.volume,
1303
- solvent_mass=self.solvent_mass,
1304
- )
1305
- .magnitude
1306
- )
1307
-
1308
- # set the amount to zero and log a warning if the desired amount
1309
- # change would result in a negative concentration
1310
- if self.get_amount(solute, "mol").magnitude < 0:
1311
- self.logger.error(
1312
- "Attempted to set a negative concentration for solute %s. Concentration set to 0" % solute
1313
- )
1314
- self.set_amount(solute, "0 mol")
1315
-
1316
- # calculate the volume occupied by all the solutes
1317
- solute_vol = self._get_solute_volume()
1318
-
1319
- # determine the volume of solvent that will preserve the original volume
1320
- target_vol = orig_volume - solute_vol
1321
-
1322
- # adjust the amount of solvent
1323
- # volume in L, density in kg/m3 = g/L
1324
- target_mass = target_vol * ureg.Quantity(self.water_substance.rho, "g/L")
1325
-
1326
- mw = self.get_property(self.solvent, "molecular_weight")
1327
- target_mol = target_mass / mw
1328
- self.components[self.solvent] = target_mol.magnitude
1329
-
1330
- else:
1331
- # change the amount of the solute present
1332
- self.components[solute] += (
1333
- ureg.Quantity(amount)
1334
- .to(
1335
- "moles",
1336
- "chem",
1337
- mw=self.get_property(solute, "molecular_weight"),
1338
- volume=self.volume,
1339
- solvent_mass=self.solvent_mass,
1340
- )
1341
- .magnitude
1342
- )
1343
-
1344
- # set the amount to zero and log a warning if the desired amount
1345
- # change would result in a negative concentration
1346
- if self.get_amount(solute, "mol").magnitude < 0:
1347
- self.logger.error(
1348
- "Attempted to set a negative concentration for solute %s. Concentration set to 0" % solute
1349
- )
1350
- self.set_amount(solute, "0 mol")
1351
-
1352
- # update the volume to account for the space occupied by all the solutes
1353
- # make sure that there is still solvent present in the first place
1354
- if self.solvent_mass <= ureg.Quantity(0, "kg"):
1355
- self.logger.error("All solvent has been depleted from the solution")
1356
- return
1357
-
1358
- # set the volume recalculation flag
1359
- self.volume_update_required = True
1300
+ # Get the current amount of the solute
1301
+ current_amt = self.get_amount(solute, amount.split(" ")[1])
1302
+ if current_amt.magnitude == 0:
1303
+ self.logger.warning(f"Add new solute {solute} to the solution")
1304
+ new_amt = ureg.Quantity(amount) + current_amt
1305
+ self.set_amount(solute, new_amt)
1360
1306
 
1361
1307
  def set_amount(self, solute: str, amount: str):
1362
1308
  """
@@ -2465,7 +2411,7 @@ class Solution(MSONable):
2465
2411
  """
2466
2412
  str_filename = str(filename)
2467
2413
  if not ("yaml" in str_filename.lower() or "json" in str_filename.lower()):
2468
- self.logger.error("Invalid file extension entered - %s" % str_filename)
2414
+ self.logger.error("Invalid file extension entered - {str_filename}")
2469
2415
  raise ValueError("File extension must be .json or .yaml")
2470
2416
  if "yaml" in str_filename.lower():
2471
2417
  solution_dict = self.as_dict()
pyEQL/utils.py CHANGED
@@ -60,6 +60,18 @@ def standardize_formula(formula: str):
60
60
  be enclosed in square brackets to remove any ambiguity in the meaning of the formula. For example, 'Na+',
61
61
  'Na+1', and 'Na[+]' will all standardize to "Na[+1]"
62
62
  """
63
+ # fix permuted sign and charge number (e.g. Co2+)
64
+ for str, rep in zip(["²⁺", "³⁺", "⁴⁺", "²⁻", "³⁻", "⁴⁻"], ["+2", "+3", "+4", "-2", "-3", "-4"]):
65
+ formula = formula.replace(str, rep)
66
+
67
+ # replace superscripts with non superscripts
68
+ for char, rep in zip("⁻⁺⁰¹²³⁴⁵⁶⁷⁸⁹", "-+0123456789"):
69
+ formula = formula.replace(char, rep)
70
+
71
+ # replace subscripts with non subscripts
72
+ for char, rep in zip("₀₁₂₃₄₅₆₇₈₉", "0123456789"):
73
+ formula = formula.replace(char, rep)
74
+
63
75
  sform = Ion.from_formula(formula).reduced_formula
64
76
 
65
77
  # TODO - manual formula adjustments. May be implemented upstream in pymatgen in the future
@@ -81,15 +93,49 @@ def standardize_formula(formula: str):
81
93
  # thiocyanate
82
94
  elif sform == "CSN[-1]":
83
95
  sform = "SCN[-1]"
84
- # triiodide
96
+ # triiodide, nitride, an phosphide
85
97
  elif sform == "I[-0.33333333]":
86
98
  sform = "I3[-1]"
99
+ elif sform == "N[-0.33333333]":
100
+ sform = "N3[-1]"
101
+ elif sform == "P[-0.33333333]":
102
+ sform = "P3[-1]"
87
103
  # formate
88
104
  elif sform == "HCOO[-1]":
89
105
  sform = "HCO2[-1]"
90
106
  # oxalate
91
107
  elif sform == "CO2[-1]":
92
108
  sform = "C2O4[-2]"
109
+ # triflate
110
+ elif sform == "CS(OF)3[-1]":
111
+ sform = "CF3SO3[-1]"
112
+ # haloacetic acids of F, Cl, Br, I
113
+ elif sform == "C2Cl3O2[-1]":
114
+ sform = "CCl3COO[-1]"
115
+ elif sform == "C2O2F3[-1]":
116
+ sform = "CF3COO[-1]"
117
+ elif sform == "C2I3O2[-1]":
118
+ sform = "CI3COO[-1]"
119
+ elif sform == "C2Br3O2[-1]":
120
+ sform = "CBr3COO[-1]"
121
+
122
+ # Cl+F
123
+ elif sform == "C2Cl2O2F[-1]":
124
+ sform = "CFCl2COO[-1]"
125
+ elif sform == "C2Cl(OF)2[-1]":
126
+ sform = "CF2ClCOO[-1]"
127
+
128
+ # Cl+Br
129
+ elif sform == "C2Br(ClO)2[-1]":
130
+ sform = "CBrCl2COO[-1]"
131
+ elif sform == "C2Br2ClO2[-1]":
132
+ sform = "CBr2ClCOO[-1]"
133
+
134
+ # Cl+I
135
+ elif sform == "C2I(ClO)2[-1]":
136
+ sform = "CICl2COO[-1]"
137
+ elif sform == "C2I2ClO2[-1]":
138
+ sform = "CI2ClCOO[-1]"
93
139
 
94
140
  # TODO - consider adding recognition of special formulas like MeOH for methanol or Cit for citrate
95
141
  return sform
@@ -1,20 +1,22 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyEQL
3
- Version: 1.1.0
4
- Summary: Python tools for solution chemistry
5
- Home-page: https://github.com/KingsburyLab/pyEQL
6
- Author: Ryan Kingsbury
7
- Author-email: kingsbury@princeton.edu
8
- License: LGPL3
9
- Project-URL: Documentation, https://pyeql.readthedocs.io/
10
- Platform: any
3
+ Version: 1.1.2
4
+ Summary: A python interface for solution chemistry
5
+ Author-email: Ryan Kingsbury <kingsbury@princeton.edu>
6
+ Project-URL: Docs, https://pyeql.readthedocs.io/
7
+ Project-URL: Repo, https://github.com/KingsburyLab/pyEQL
8
+ Project-URL: Package, https://pypi.org/project/pyEQL
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
11
14
  Classifier: Development Status :: 4 - Beta
12
- Classifier: Programming Language :: Python
13
15
  Classifier: Intended Audience :: Science/Research
16
+ Classifier: Operating System :: OS Independent
14
17
  Classifier: Topic :: Scientific/Engineering
15
- Classifier: Topic :: Software Development :: Libraries
16
18
  Requires-Python: >=3.9
17
- Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
19
+ Description-Content-Type: text/markdown
18
20
  License-File: LICENSE.txt
19
21
  License-File: COPYING
20
22
  License-File: AUTHORS.md
@@ -23,7 +25,7 @@ Requires-Dist: numpy <2
23
25
  Requires-Dist: scipy
24
26
  Requires-Dist: pymatgen ==2024.5.1
25
27
  Requires-Dist: iapws
26
- Requires-Dist: monty
28
+ Requires-Dist: monty >=2024.7.12
27
29
  Requires-Dist: maggma >=0.67.0
28
30
  Requires-Dist: phreeqpython
29
31
  Provides-Extra: docs
@@ -1,27 +1,26 @@
1
- pyEQL/__init__.py,sha256=JErflmaJVP373dm3-YGHUomFu05dgX0iXWS-Z5AgSTU,2118
1
+ pyEQL/__init__.py,sha256=OCp_PiQEPyVoi1VX0ursBzHJWN6nDS1Id6bTBOgqCYs,1999
2
2
  pyEQL/activity_correction.py,sha256=eOixjgTd5hTrTRD5s6aPCCG12lAIH7-lRN0Z1qHu678,37151
3
- pyEQL/engines.py,sha256=b9ay7FYqmnINmhSMyNpVmENAtZVW-gyBfXUPf1PEUoY,34946
3
+ pyEQL/engines.py,sha256=kE4ZtV2Z-zhOwtkH0cbv95oG1CETJmqMOoT0UfDuJeo,35221
4
4
  pyEQL/equilibrium.py,sha256=YCtoAJSgn1WC9NJnc3H4FTJdKQvogsvCuj7HqlKMtww,8307
5
5
  pyEQL/functions.py,sha256=nc-Hc61MmW-ELBR1PByJvQnELxM7PZexMHbU_O5-Bnw,10584
6
- pyEQL/pint_custom_units.txt,sha256=XHmcMlwVvqF9nEW7_e9Xgyq-xWEr-cDYqieas11T3eY,2882
7
6
  pyEQL/salt_ion_match.py,sha256=0nCZXmeo67VqcyYWQpPx-81hjSvnsg8HFB3fIyfjW_k,4070
8
7
  pyEQL/solute.py,sha256=no00Rc3tRfHmyht4wm2UXA1KZhKC45tWMO5QEkZY6yg,5140
9
- pyEQL/solution.py,sha256=XwtQmll3gPwdnnkXpQsnO7QebwTjplmlfhbEIU8rHDo,115940
10
- pyEQL/utils.py,sha256=unsY7zrrhl_1mOmt9_kumSKmLE5N5Hvh4bOErfS3nws,5168
8
+ pyEQL/solution.py,sha256=vtC2xRUt1y2wTv_L1o7Qs3wr8b_X7wvkUsp56499baQ,113898
9
+ pyEQL/utils.py,sha256=DWLtNm71qw5j4-jqBp5v3LssEjWgJnVvI6a_H60c5ic,6670
11
10
  pyEQL/database/geothermal.dat,sha256=kksnfcBtWdOTpNn4CLXU1Mz16cwas2WuVKpuMU8CaVI,234230
12
11
  pyEQL/database/llnl.dat,sha256=jN-a0kfUFbQlYMn2shTVRg1JX_ZhLa-tJ0lLw2YSpLU,751462
13
12
  pyEQL/database/phreeqc_license.txt,sha256=8W1r8VxC2kVptIMSU9sDFNASYqN7MdwKEtIWWfjTQuM,2906
14
- pyEQL/database/pyeql_db.json,sha256=TQKKofds7QBNd-Hw5QQuPwP6rQ8YWh_hHlRAtoQX0m8,1080793
13
+ pyEQL/database/pyeql_db.json,sha256=-7Z8tpAddXhPlvpxms7cFQKL_DSSbemzOzxm6L5vaVk,1080801
15
14
  pyEQL/presets/Ringers lactate.yaml,sha256=vtSnuvgALHR27XEjpDzC0xyw5-E6b2FSsF1EUEBiWpw,413
16
15
  pyEQL/presets/normal saline.yaml,sha256=i2znhnIeXfNx1iMFFSif7crMRCFRP6xN1m7Wp7USduM,318
17
16
  pyEQL/presets/rainwater.yaml,sha256=S0WHZNDfCJyjSSFxNFdkypjn2s3P0jJGCiYIxvi1ibA,337
18
17
  pyEQL/presets/seawater.yaml,sha256=oryc1CkhRz20RpWE6uiGiT93HoZnqlB0s-0PmBWr3-U,843
19
18
  pyEQL/presets/urine.yaml,sha256=0Njtc-H1fFRo7UhquHdiSTT4z-8VZJ1utDCk02qk28M,679
20
19
  pyEQL/presets/wastewater.yaml,sha256=jTTFBpmKxczaEtkCZb0xUULIPZt7wfC8eAJ6rthGnmw,502
21
- pyEQL-1.1.0.dist-info/AUTHORS.md,sha256=K9ZLhKFwZ2zLlFXwN62VuUYCpr5T6n4mOUCUHlytTUs,415
22
- pyEQL-1.1.0.dist-info/COPYING,sha256=Ww2oUywfFTn242v9ksCgQdIVSpcMXJiKKePn0GFm25E,7649
23
- pyEQL-1.1.0.dist-info/LICENSE.txt,sha256=2Zf1F7RzbpeposgIxUydpurqNCMoMgDi2gAB65_GjwQ,969
24
- pyEQL-1.1.0.dist-info/METADATA,sha256=1KbuGQ1gjYRWA-8cq2cb_OmZgW8DTpKoCZ6_6DpQ9jo,5889
25
- pyEQL-1.1.0.dist-info/WHEEL,sha256=0XQbNV6JE5ziJsWjIU8TRRv0N6SohNonLWgP86g5fiI,109
26
- pyEQL-1.1.0.dist-info/top_level.txt,sha256=QMOaZjCAm_lS4Njsjh4L0B5aWnJFGQMYKhuH88CG1co,6
27
- pyEQL-1.1.0.dist-info/RECORD,,
20
+ pyEQL-1.1.2.dist-info/AUTHORS.md,sha256=K9ZLhKFwZ2zLlFXwN62VuUYCpr5T6n4mOUCUHlytTUs,415
21
+ pyEQL-1.1.2.dist-info/COPYING,sha256=Ww2oUywfFTn242v9ksCgQdIVSpcMXJiKKePn0GFm25E,7649
22
+ pyEQL-1.1.2.dist-info/LICENSE.txt,sha256=2Zf1F7RzbpeposgIxUydpurqNCMoMgDi2gAB65_GjwQ,969
23
+ pyEQL-1.1.2.dist-info/METADATA,sha256=1mxAOqZ5yuBRO4iDo2RZN7gpAcJMXYOfY_KKFPFMzQQ,6096
24
+ pyEQL-1.1.2.dist-info/WHEEL,sha256=Wyh-_nZ0DJYolHNn1_hMa4lM7uDedD_RGVwbmTjyItk,91
25
+ pyEQL-1.1.2.dist-info/top_level.txt,sha256=QMOaZjCAm_lS4Njsjh4L0B5aWnJFGQMYKhuH88CG1co,6
26
+ pyEQL-1.1.2.dist-info/RECORD,,
@@ -1,6 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (70.2.0)
2
+ Generator: setuptools (71.1.0)
3
3
  Root-Is-Purelib: true
4
- Tag: py2-none-any
5
4
  Tag: py3-none-any
6
5
 
@@ -1,52 +0,0 @@
1
- # Units definition file for pint library
2
-
3
- # This file defines additional units and contexts to enable the pint
4
- # library to process solution chemistry units such as mol/L and mol/kg.
5
-
6
- @context(mw=0,volume=0,solvent_mass=0) chemistry = chem
7
- # mw is the molecular weight of the species
8
- # volume is the volume of the solution
9
- # solvent_mass is the mass of solvent in the solution
10
-
11
- # moles -> mass require the molecular weight
12
- [substance] -> [mass]: value * mw
13
- [mass] -> [substance]: value / mw
14
-
15
- # moles/volume -> mass/volume and moles/mass -> mass / mass
16
- # require the molecular weight
17
- [substance] / [volume] -> [mass] / [volume]: value * mw
18
- [mass] / [volume] -> [substance] / [volume]: value / mw
19
- [substance] / [mass] -> [mass] / [mass]: value * mw
20
- [mass] / [mass] -> [substance] / [mass]: value / mw
21
-
22
- # moles/volume -> moles requires the solution volume
23
- [substance] / [volume] -> [substance]: value * volume
24
- [substance] -> [substance] / [volume]: value / volume
25
-
26
- # moles/mass -> moles requires the solvent (usually water) mass
27
- [substance] / [mass] -> [substance]: value * solvent_mass
28
- [substance] -> [substance] / [mass]: value / solvent_mass
29
-
30
- # moles/mass -> moles/volume require the solvent mass and the volume
31
- [substance] / [mass] -> [substance]/[volume]: value * solvent_mass / volume
32
- [substance] / [volume] -> [substance] / [mass]: value / solvent_mass * volume
33
-
34
- @end
35
-
36
- @context electricity = elec
37
- [length] ** 2 * [mass] / [current] ** 2 / [time] ** 3 <-> [length] ** -2 * [mass] **-1 / [current] ** -2 / [time] ** -3: 1 / value
38
- @end
39
-
40
-
41
- #From the pint documentation:
42
-
43
- #@context(n=1) spectroscopy = sp
44
- # # n index of refraction of the medium.
45
- # [length] <-> [frequency]: speed_of_light / n / value
46
- # [frequency] -> [energy]: planck_constant * value
47
- # [energy] -> [frequency]: value / planck_constant
48
- #@end
49
-
50
- # The @context directive indicates the beginning of the transformations which are finished by the @end statement. You can optionally specify parameters for the context in parenthesis. All parameters are named and default values are mandatory. Multiple parameters are separated by commas (like in a python function definition). Finally, you provide the name of the context (e.g. spectroscopy) and, optionally, a short version of the name (e.g. sp) separated by an equal sign.
51
- # Conversions rules are specified by providing source and destination dimensions separated using a colon (:) from the equation. A special variable named value will be replaced by the source quantity. Other names will be looked first in the context arguments and then in registry.
52
- # A single forward arrow (->) indicates that the equations is used to transform from the first dimension to the second one. A double arrow (<->) is used to indicate that the transformation operates both ways.
File without changes