pyEQL 1.1.1__py3-none-any.whl → 1.1.3__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/engines.py CHANGED
@@ -229,10 +229,7 @@ class NativeEOS(EOS):
229
229
  d[key] = str(mol / solv_mass)
230
230
 
231
231
  # tell PHREEQC which species to use for charge balance
232
- if (
233
- solution.balance_charge is not None
234
- and solution.balance_charge in solution.get_components_by_element()[el]
235
- ):
232
+ if solution.balance_charge is not None and solution._cb_species in solution.get_components_by_element()[el]:
236
233
  d[key] += " charge"
237
234
 
238
235
  # create the PHREEQC solution object
@@ -690,26 +687,8 @@ class NativeEOS(EOS):
690
687
  )
691
688
 
692
689
  # re-adjust charge balance for any missing species
693
- # note that if balance_charge is set, it will have been passed to PHREEQC, so we only need to adjust
694
- # for any missing species here.
695
- charge_adjust = 0
696
- for s in missing_species:
697
- charge_adjust += -1 * solution.get_amount(s, "eq").magnitude
698
- if charge_adjust != 0:
699
- logger.warning(
700
- "After equilibration, the charge balance of the solution was not electroneutral."
701
- f" {charge_adjust} eq of charge were added via {solution.balance_charge}"
702
- )
703
-
704
- if solution.balance_charge is None:
705
- pass
706
- elif solution.balance_charge == "pH":
707
- solution.components["H+"] += charge_adjust
708
- elif solution.balance_charge == "pE":
709
- raise NotImplementedError
710
- else:
711
- z = solution.get_property(solution.balance_charge, "charge")
712
- solution.add_amount(solution.balance_charge, f"{charge_adjust/z} mol")
690
+ # note that if balance_charge is set, it will have been passed to PHREEQC, so the only reason to re-adjust charge balance here is to account for any missing species.
691
+ solution._adjust_charge_balance()
713
692
 
714
693
  # rescale the solvent mass to ensure the total mass of solution does not change
715
694
  # this is important because PHREEQC and the pyEQL database may use slightly different molecular
pyEQL/solution.py CHANGED
@@ -35,6 +35,7 @@ from pyEQL.utils import FormulaDict, create_water_substance, interpret_units, st
35
35
  EQUIV_WT_CACO3 = ureg.Quantity(100.09 / 2, "g/mol")
36
36
  # string to denote unknown oxidation states
37
37
  UNKNOWN_OXI_STATE = "unk"
38
+ K_W = 1e-14 # ion product of water at 25 degC
38
39
 
39
40
 
40
41
  class Solution(MSONable):
@@ -242,7 +243,7 @@ class Solution(MSONable):
242
243
 
243
244
  # set the pH with H+ and OH-
244
245
  self.add_solute("H+", str(10 ** (-1 * pH)) + "mol/L")
245
- self.add_solute("OH-", str(10 ** (-1 * (14 - pH))) + "mol/L")
246
+ self.add_solute("OH-", str(K_W / (10 ** (-1 * pH))) + "mol/L")
246
247
 
247
248
  # populate the other solutes
248
249
  self._solutes = solutes
@@ -261,39 +262,34 @@ class Solution(MSONable):
261
262
  for item in self._solutes:
262
263
  self.add_solute(*item)
263
264
 
264
- # adjust the charge balance, if necessary
265
+ # determine the species that will be used for charge balancing, when needed.
266
+ # this is necessary to do even if the composition is already electroneutral,
267
+ # because the appropriate species also needs to be passed to equilibrate
268
+ # to keep from distorting the charge balance.
265
269
  cb = self.charge_balance
266
- if not np.isclose(cb, 0, atol=1e-8) and self.balance_charge is not None:
267
- balanced = False
268
- self.logger.info(
269
- f"Solution is not electroneutral (C.B. = {cb} eq/L). Adding {balance_charge} to compensate."
270
- )
271
- if self.balance_charge == "pH":
272
- self.components["H+"] += (
273
- -1 * cb * self.volume.to("L").magnitude
274
- ) # if C.B. is negative, we need to add cations. H+ is 1 eq/mol
275
- balanced = True
276
- elif self.balance_charge == "pE":
277
- raise NotImplementedError("Balancing charge via redox (pE) is not yet implemented!")
278
- else:
279
- ions = set().union(*[self.cations, self.anions]) # all ions
280
- if self.balance_charge == "auto":
281
- # add the most abundant ion of the opposite charge
282
- if cb <= 0:
283
- self.balance_charge = max(self.cations, key=self.cations.get)
284
- elif cb > 0:
285
- self.balance_charge = max(self.anions, key=self.anions.get)
286
- if self.balance_charge not in ions:
287
- raise ValueError(
288
- f"Charge balancing species {self.balance_charge} was not found in the solution!. "
289
- f"Species {ions} were found."
290
- )
291
- z = self.get_property(self.balance_charge, "charge")
292
- self.components[self.balance_charge] += -1 * cb / z * self.volume.to("L").magnitude
293
- balanced = True
270
+ if self.balance_charge is None:
271
+ self._cb_species = None
272
+ elif self.balance_charge == "pH":
273
+ self._cb_species = "H[+1]"
274
+ elif self.balance_charge == "pE":
275
+ raise NotImplementedError("Balancing charge via redox (pE) is not yet implemented!")
276
+ elif self.balance_charge == "auto":
277
+ # add the most abundant ion of the opposite charge
278
+ if cb <= 0:
279
+ self._cb_species = max(self.cations, key=self.cations.get)
280
+ elif cb > 0:
281
+ self._cb_species = max(self.anions, key=self.anions.get)
282
+ else:
283
+ ions = set().union(*[self.cations, self.anions]) # all ions
284
+ self._cb_species = self.balance_charge
285
+ if self._cb_species not in ions:
286
+ raise ValueError(
287
+ f"Charge balancing species {self._cb_species} was not found in the solution!. "
288
+ f"Species {ions} were found."
289
+ )
294
290
 
295
- if not balanced:
296
- warnings.warn(f"Unable to balance charge using species {self.balance_charge}")
291
+ # adjust charge balance, if necessary
292
+ self._adjust_charge_balance()
297
293
 
298
294
  @property
299
295
  def mass(self) -> Quantity:
@@ -2288,6 +2284,57 @@ class Solution(MSONable):
2288
2284
 
2289
2285
  return distance.to("nm")
2290
2286
 
2287
+ def _adjust_charge_balance(self, atol=1e-8) -> None:
2288
+ """Helper method to adjust the charge balance of the Solution."""
2289
+ cb = self.charge_balance
2290
+ if not np.isclose(cb, 0, atol=atol):
2291
+ self.logger.info(f"Solution is not electroneutral (C.B. = {cb} eq/L).")
2292
+ if self.balance_charge is None:
2293
+ # Nothing to do.
2294
+ self.logger.info("balance_charge is None, so no charge balancing will be performed.")
2295
+ return
2296
+
2297
+ self.logger.info(
2298
+ f"Solution is not electroneutral (C.B. = {cb} eq/L). Adjusting {self._cb_species} to compensate."
2299
+ )
2300
+
2301
+ if self.balance_charge == "pH":
2302
+ # the charge imbalance associated with the H+ / OH- system can be expressed
2303
+ # as ([H+] - [OH-]) or ([H+] - K_W/[H+]). If we adjust H+, we also have to
2304
+ # adjust OH- to maintain water equilibrium.
2305
+ C_hplus = self.get_amount("H+", "mol/L").magnitude
2306
+ start_imbalance = C_hplus - K_W / C_hplus
2307
+ new_imbalance = start_imbalance - cb
2308
+ # calculate the new concentration of H+ that will balance the charge
2309
+ # solve H^2 - new_imbalance H - K_W = 0, so a=1, b=-new_imbalance, c=-K_W
2310
+ # check b^2 - 4ac; are there any real roots?
2311
+ if new_imbalance**2 - 4 * 1 * K_W < 0:
2312
+ self.logger.error("Cannot balance charge by adjusting pH. The imbalance is too large.")
2313
+ return
2314
+ new_hplus = max(
2315
+ [
2316
+ (new_imbalance + np.sqrt(new_imbalance**2 + 4 * 1 * K_W)) / 2,
2317
+ (new_imbalance - np.sqrt(new_imbalance**2 + 4 * 1 * K_W)) / 2,
2318
+ ]
2319
+ )
2320
+ self.set_amount("H+", f"{new_hplus} mol/L")
2321
+ self.set_amount("OH-", f"{K_W/new_hplus} mol/L")
2322
+ assert np.isclose(self.charge_balance, 0, atol=atol), f"{self.charge_balance}"
2323
+ return
2324
+
2325
+ z = self.get_property(self._cb_species, "charge")
2326
+ try:
2327
+ self.add_amount(self._cb_species, f"{-1*cb/z} mol")
2328
+ return
2329
+ except ValueError:
2330
+ # if the concentration is negative, it must mean there is not enough present.
2331
+ # remove everything that's present and log an error.
2332
+ self.components[self._cb_species] = 0
2333
+ self.logger.error(
2334
+ f"There is not enough {self._cb_species} present to balance the charge. Try a different species."
2335
+ )
2336
+ return
2337
+
2291
2338
  def _update_volume(self):
2292
2339
  """Recalculate the solution volume based on composition."""
2293
2340
  self._volume = self._get_solvent_volume() + self._get_solute_volume()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyEQL
3
- Version: 1.1.1
3
+ Version: 1.1.3
4
4
  Summary: A python interface for solution chemistry
5
5
  Author-email: Ryan Kingsbury <kingsbury@princeton.edu>
6
6
  Project-URL: Docs, https://pyeql.readthedocs.io/
@@ -1,11 +1,11 @@
1
1
  pyEQL/__init__.py,sha256=OCp_PiQEPyVoi1VX0ursBzHJWN6nDS1Id6bTBOgqCYs,1999
2
2
  pyEQL/activity_correction.py,sha256=eOixjgTd5hTrTRD5s6aPCCG12lAIH7-lRN0Z1qHu678,37151
3
- pyEQL/engines.py,sha256=9Pfo6YATahB9EUdBpUkE3xBAyDQqp1MjG9LO-zD4fAg,35517
3
+ pyEQL/engines.py,sha256=VbdQSPKlNehW96U1XxWYwjTy6k6WDpZZEx_Y4l3qZv4,34686
4
4
  pyEQL/equilibrium.py,sha256=YCtoAJSgn1WC9NJnc3H4FTJdKQvogsvCuj7HqlKMtww,8307
5
5
  pyEQL/functions.py,sha256=nc-Hc61MmW-ELBR1PByJvQnELxM7PZexMHbU_O5-Bnw,10584
6
6
  pyEQL/salt_ion_match.py,sha256=0nCZXmeo67VqcyYWQpPx-81hjSvnsg8HFB3fIyfjW_k,4070
7
7
  pyEQL/solute.py,sha256=no00Rc3tRfHmyht4wm2UXA1KZhKC45tWMO5QEkZY6yg,5140
8
- pyEQL/solution.py,sha256=WET68HPDaleEsIxK2ObtAlNR7oolunNU5mNjxNeGG8U,113696
8
+ pyEQL/solution.py,sha256=LwlfY6Q19QEcY8MJpP_mcJgEw0ohNaGfAqAiqhdPmU8,116022
9
9
  pyEQL/utils.py,sha256=DWLtNm71qw5j4-jqBp5v3LssEjWgJnVvI6a_H60c5ic,6670
10
10
  pyEQL/database/geothermal.dat,sha256=kksnfcBtWdOTpNn4CLXU1Mz16cwas2WuVKpuMU8CaVI,234230
11
11
  pyEQL/database/llnl.dat,sha256=jN-a0kfUFbQlYMn2shTVRg1JX_ZhLa-tJ0lLw2YSpLU,751462
@@ -17,10 +17,10 @@ pyEQL/presets/rainwater.yaml,sha256=S0WHZNDfCJyjSSFxNFdkypjn2s3P0jJGCiYIxvi1ibA,
17
17
  pyEQL/presets/seawater.yaml,sha256=oryc1CkhRz20RpWE6uiGiT93HoZnqlB0s-0PmBWr3-U,843
18
18
  pyEQL/presets/urine.yaml,sha256=0Njtc-H1fFRo7UhquHdiSTT4z-8VZJ1utDCk02qk28M,679
19
19
  pyEQL/presets/wastewater.yaml,sha256=jTTFBpmKxczaEtkCZb0xUULIPZt7wfC8eAJ6rthGnmw,502
20
- pyEQL-1.1.1.dist-info/AUTHORS.md,sha256=K9ZLhKFwZ2zLlFXwN62VuUYCpr5T6n4mOUCUHlytTUs,415
21
- pyEQL-1.1.1.dist-info/COPYING,sha256=Ww2oUywfFTn242v9ksCgQdIVSpcMXJiKKePn0GFm25E,7649
22
- pyEQL-1.1.1.dist-info/LICENSE.txt,sha256=2Zf1F7RzbpeposgIxUydpurqNCMoMgDi2gAB65_GjwQ,969
23
- pyEQL-1.1.1.dist-info/METADATA,sha256=5EWAVR_RKxcnBz3XHpl-OOCBfUo2sJMMOyJATkAZXIw,6096
24
- pyEQL-1.1.1.dist-info/WHEEL,sha256=Wyh-_nZ0DJYolHNn1_hMa4lM7uDedD_RGVwbmTjyItk,91
25
- pyEQL-1.1.1.dist-info/top_level.txt,sha256=QMOaZjCAm_lS4Njsjh4L0B5aWnJFGQMYKhuH88CG1co,6
26
- pyEQL-1.1.1.dist-info/RECORD,,
20
+ pyEQL-1.1.3.dist-info/AUTHORS.md,sha256=K9ZLhKFwZ2zLlFXwN62VuUYCpr5T6n4mOUCUHlytTUs,415
21
+ pyEQL-1.1.3.dist-info/COPYING,sha256=Ww2oUywfFTn242v9ksCgQdIVSpcMXJiKKePn0GFm25E,7649
22
+ pyEQL-1.1.3.dist-info/LICENSE.txt,sha256=2Zf1F7RzbpeposgIxUydpurqNCMoMgDi2gAB65_GjwQ,969
23
+ pyEQL-1.1.3.dist-info/METADATA,sha256=aDLyZrhXd2laVUiJ6drgt4RFSvlptOPxLEo9xrkaR40,6096
24
+ pyEQL-1.1.3.dist-info/WHEEL,sha256=Wyh-_nZ0DJYolHNn1_hMa4lM7uDedD_RGVwbmTjyItk,91
25
+ pyEQL-1.1.3.dist-info/top_level.txt,sha256=QMOaZjCAm_lS4Njsjh4L0B5aWnJFGQMYKhuH88CG1co,6
26
+ pyEQL-1.1.3.dist-info/RECORD,,
File without changes
File without changes