pyEQL 1.1.2__tar.gz → 1.1.3__tar.gz

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.
Files changed (128) hide show
  1. {pyeql-1.1.2 → pyeql-1.1.3}/CHANGELOG.md +14 -0
  2. {pyeql-1.1.2/src/pyEQL.egg-info → pyeql-1.1.3}/PKG-INFO +1 -1
  3. {pyeql-1.1.2 → pyeql-1.1.3}/docs/changelog.md +14 -0
  4. {pyeql-1.1.2 → pyeql-1.1.3}/src/pyEQL/engines.py +2 -14
  5. {pyeql-1.1.2 → pyeql-1.1.3}/src/pyEQL/solution.py +54 -12
  6. {pyeql-1.1.2 → pyeql-1.1.3/src/pyEQL.egg-info}/PKG-INFO +1 -1
  7. {pyeql-1.1.2 → pyeql-1.1.3}/tests/test_solution.py +32 -6
  8. {pyeql-1.1.2 → pyeql-1.1.3}/.coveragerc +0 -0
  9. {pyeql-1.1.2 → pyeql-1.1.3}/.gitattributes +0 -0
  10. {pyeql-1.1.2 → pyeql-1.1.3}/.github/dependabot.yml +0 -0
  11. {pyeql-1.1.2 → pyeql-1.1.3}/.github/pull_request_template.md +0 -0
  12. {pyeql-1.1.2 → pyeql-1.1.3}/.github/release.yml +0 -0
  13. {pyeql-1.1.2 → pyeql-1.1.3}/.github/workflows/post-process.yml +0 -0
  14. {pyeql-1.1.2 → pyeql-1.1.3}/.github/workflows/release.yml +0 -0
  15. {pyeql-1.1.2 → pyeql-1.1.3}/.github/workflows/testing.yaml +0 -0
  16. {pyeql-1.1.2 → pyeql-1.1.3}/.github/workflows/upgrade_dependencies.yml +0 -0
  17. {pyeql-1.1.2 → pyeql-1.1.3}/.gitignore +0 -0
  18. {pyeql-1.1.2 → pyeql-1.1.3}/.pre-commit-config.yaml +0 -0
  19. {pyeql-1.1.2 → pyeql-1.1.3}/.readthedocs.yml +0 -0
  20. {pyeql-1.1.2 → pyeql-1.1.3}/.zenodo.json +0 -0
  21. {pyeql-1.1.2 → pyeql-1.1.3}/AUTHORS.md +0 -0
  22. {pyeql-1.1.2 → pyeql-1.1.3}/CITATION.cff +0 -0
  23. {pyeql-1.1.2 → pyeql-1.1.3}/COPYING +0 -0
  24. {pyeql-1.1.2 → pyeql-1.1.3}/LICENSE.txt +0 -0
  25. {pyeql-1.1.2 → pyeql-1.1.3}/MANIFEST.in +0 -0
  26. {pyeql-1.1.2 → pyeql-1.1.3}/README.md +0 -0
  27. {pyeql-1.1.2 → pyeql-1.1.3}/docs/Makefile +0 -0
  28. {pyeql-1.1.2 → pyeql-1.1.3}/docs/_static/.gitignore +0 -0
  29. {pyeql-1.1.2 → pyeql-1.1.3}/docs/amounts.md +0 -0
  30. {pyeql-1.1.2 → pyeql-1.1.3}/docs/arithmetic.md +0 -0
  31. {pyeql-1.1.2 → pyeql-1.1.3}/docs/authors.md +0 -0
  32. {pyeql-1.1.2 → pyeql-1.1.3}/docs/chemistry.md +0 -0
  33. {pyeql-1.1.2 → pyeql-1.1.3}/docs/class_solution.md +0 -0
  34. {pyeql-1.1.2 → pyeql-1.1.3}/docs/conf.py +0 -0
  35. {pyeql-1.1.2 → pyeql-1.1.3}/docs/contributing.md +0 -0
  36. {pyeql-1.1.2 → pyeql-1.1.3}/docs/creating.md +0 -0
  37. {pyeql-1.1.2 → pyeql-1.1.3}/docs/database.md +0 -0
  38. {pyeql-1.1.2 → pyeql-1.1.3}/docs/engines.md +0 -0
  39. {pyeql-1.1.2 → pyeql-1.1.3}/docs/examples/.ipynb_checkpoints/pyEQL_demo_1-checkpoint.ipynb +0 -0
  40. {pyeql-1.1.2 → pyeql-1.1.3}/docs/examples/.ipynb_checkpoints/pyeql_demo-checkpoint.ipynb +0 -0
  41. {pyeql-1.1.2 → pyeql-1.1.3}/docs/examples/.ipynb_checkpoints/pyeql_tutorial_database-checkpoint.ipynb +0 -0
  42. {pyeql-1.1.2 → pyeql-1.1.3}/docs/examples/.ipynb_checkpoints/pyeql_tutorial_osmotic_pressure-checkpoint.ipynb +0 -0
  43. {pyeql-1.1.2 → pyeql-1.1.3}/docs/examples/.ipynb_checkpoints/speedup-checkpoint.ipynb +0 -0
  44. {pyeql-1.1.2 → pyeql-1.1.3}/docs/examples/pyEQL_demo_1.ipynb +0 -0
  45. {pyeql-1.1.2 → pyeql-1.1.3}/docs/examples/pyeql_demo.ipynb +0 -0
  46. {pyeql-1.1.2 → pyeql-1.1.3}/docs/examples/pyeql_tutorial_database.ipynb +0 -0
  47. {pyeql-1.1.2 → pyeql-1.1.3}/docs/examples/pyeql_tutorial_osmotic_pressure.ipynb +0 -0
  48. {pyeql-1.1.2 → pyeql-1.1.3}/docs/examples/speedup.ipynb +0 -0
  49. {pyeql-1.1.2 → pyeql-1.1.3}/docs/index.md +0 -0
  50. {pyeql-1.1.2 → pyeql-1.1.3}/docs/installation.md +0 -0
  51. {pyeql-1.1.2 → pyeql-1.1.3}/docs/internal.md +0 -0
  52. {pyeql-1.1.2 → pyeql-1.1.3}/docs/license.md +0 -0
  53. {pyeql-1.1.2 → pyeql-1.1.3}/docs/mixing.md +0 -0
  54. {pyeql-1.1.2 → pyeql-1.1.3}/docs/quickstart.md +0 -0
  55. {pyeql-1.1.2 → pyeql-1.1.3}/docs/readme.md +0 -0
  56. {pyeql-1.1.2 → pyeql-1.1.3}/docs/requirements.txt +0 -0
  57. {pyeql-1.1.2 → pyeql-1.1.3}/docs/serialization.md +0 -0
  58. {pyeql-1.1.2 → pyeql-1.1.3}/docs/tutorials.md +0 -0
  59. {pyeql-1.1.2 → pyeql-1.1.3}/docs/units.md +0 -0
  60. {pyeql-1.1.2 → pyeql-1.1.3}/pyeql-logo.png +0 -0
  61. {pyeql-1.1.2 → pyeql-1.1.3}/pyeql-logo.svg +0 -0
  62. {pyeql-1.1.2 → pyeql-1.1.3}/pyproject.toml +0 -0
  63. {pyeql-1.1.2 → pyeql-1.1.3}/requirements/macos-latest_py3.10.txt +0 -0
  64. {pyeql-1.1.2 → pyeql-1.1.3}/requirements/macos-latest_py3.10_extras.txt +0 -0
  65. {pyeql-1.1.2 → pyeql-1.1.3}/requirements/macos-latest_py3.11.txt +0 -0
  66. {pyeql-1.1.2 → pyeql-1.1.3}/requirements/macos-latest_py3.11_extras.txt +0 -0
  67. {pyeql-1.1.2 → pyeql-1.1.3}/requirements/macos-latest_py3.12.txt +0 -0
  68. {pyeql-1.1.2 → pyeql-1.1.3}/requirements/macos-latest_py3.12_extras.txt +0 -0
  69. {pyeql-1.1.2 → pyeql-1.1.3}/requirements/macos-latest_py3.9.txt +0 -0
  70. {pyeql-1.1.2 → pyeql-1.1.3}/requirements/macos-latest_py3.9_extras.txt +0 -0
  71. {pyeql-1.1.2 → pyeql-1.1.3}/requirements/ubuntu-latest_py3.10.txt +0 -0
  72. {pyeql-1.1.2 → pyeql-1.1.3}/requirements/ubuntu-latest_py3.10_extras.txt +0 -0
  73. {pyeql-1.1.2 → pyeql-1.1.3}/requirements/ubuntu-latest_py3.11.txt +0 -0
  74. {pyeql-1.1.2 → pyeql-1.1.3}/requirements/ubuntu-latest_py3.11_extras.txt +0 -0
  75. {pyeql-1.1.2 → pyeql-1.1.3}/requirements/ubuntu-latest_py3.12.txt +0 -0
  76. {pyeql-1.1.2 → pyeql-1.1.3}/requirements/ubuntu-latest_py3.12_extras.txt +0 -0
  77. {pyeql-1.1.2 → pyeql-1.1.3}/requirements/ubuntu-latest_py3.9.txt +0 -0
  78. {pyeql-1.1.2 → pyeql-1.1.3}/requirements/ubuntu-latest_py3.9_extras.txt +0 -0
  79. {pyeql-1.1.2 → pyeql-1.1.3}/requirements/windows-latest_py3.10.txt +0 -0
  80. {pyeql-1.1.2 → pyeql-1.1.3}/requirements/windows-latest_py3.10_extras.txt +0 -0
  81. {pyeql-1.1.2 → pyeql-1.1.3}/requirements/windows-latest_py3.11.txt +0 -0
  82. {pyeql-1.1.2 → pyeql-1.1.3}/requirements/windows-latest_py3.11_extras.txt +0 -0
  83. {pyeql-1.1.2 → pyeql-1.1.3}/requirements/windows-latest_py3.12.txt +0 -0
  84. {pyeql-1.1.2 → pyeql-1.1.3}/requirements/windows-latest_py3.12_extras.txt +0 -0
  85. {pyeql-1.1.2 → pyeql-1.1.3}/requirements/windows-latest_py3.9.txt +0 -0
  86. {pyeql-1.1.2 → pyeql-1.1.3}/requirements/windows-latest_py3.9_extras.txt +0 -0
  87. {pyeql-1.1.2 → pyeql-1.1.3}/setup.cfg +0 -0
  88. {pyeql-1.1.2 → pyeql-1.1.3}/setup.py +0 -0
  89. {pyeql-1.1.2 → pyeql-1.1.3}/src/pyEQL/__init__.py +0 -0
  90. {pyeql-1.1.2 → pyeql-1.1.3}/src/pyEQL/activity_correction.py +0 -0
  91. {pyeql-1.1.2 → pyeql-1.1.3}/src/pyEQL/database/geothermal.dat +0 -0
  92. {pyeql-1.1.2 → pyeql-1.1.3}/src/pyEQL/database/llnl.dat +0 -0
  93. {pyeql-1.1.2 → pyeql-1.1.3}/src/pyEQL/database/phreeqc_license.txt +0 -0
  94. {pyeql-1.1.2 → pyeql-1.1.3}/src/pyEQL/database/pyeql_db.json +0 -0
  95. {pyeql-1.1.2 → pyeql-1.1.3}/src/pyEQL/equilibrium.py +0 -0
  96. {pyeql-1.1.2 → pyeql-1.1.3}/src/pyEQL/functions.py +0 -0
  97. {pyeql-1.1.2 → pyeql-1.1.3}/src/pyEQL/presets/Ringers lactate.yaml +0 -0
  98. {pyeql-1.1.2 → pyeql-1.1.3}/src/pyEQL/presets/normal saline.yaml +0 -0
  99. {pyeql-1.1.2 → pyeql-1.1.3}/src/pyEQL/presets/rainwater.yaml +0 -0
  100. {pyeql-1.1.2 → pyeql-1.1.3}/src/pyEQL/presets/seawater.yaml +0 -0
  101. {pyeql-1.1.2 → pyeql-1.1.3}/src/pyEQL/presets/urine.yaml +0 -0
  102. {pyeql-1.1.2 → pyeql-1.1.3}/src/pyEQL/presets/wastewater.yaml +0 -0
  103. {pyeql-1.1.2 → pyeql-1.1.3}/src/pyEQL/salt_ion_match.py +0 -0
  104. {pyeql-1.1.2 → pyeql-1.1.3}/src/pyEQL/solute.py +0 -0
  105. {pyeql-1.1.2 → pyeql-1.1.3}/src/pyEQL/utils.py +0 -0
  106. {pyeql-1.1.2 → pyeql-1.1.3}/src/pyEQL.egg-info/SOURCES.txt +0 -0
  107. {pyeql-1.1.2 → pyeql-1.1.3}/src/pyEQL.egg-info/dependency_links.txt +0 -0
  108. {pyeql-1.1.2 → pyeql-1.1.3}/src/pyEQL.egg-info/requires.txt +0 -0
  109. {pyeql-1.1.2 → pyeql-1.1.3}/src/pyEQL.egg-info/top_level.txt +0 -0
  110. {pyeql-1.1.2 → pyeql-1.1.3}/tests/conftest.py +0 -0
  111. {pyeql-1.1.2 → pyeql-1.1.3}/tests/test_activity.py +0 -0
  112. {pyeql-1.1.2 → pyeql-1.1.3}/tests/test_bulk_properties.py +0 -0
  113. {pyeql-1.1.2 → pyeql-1.1.3}/tests/test_debye_length.py +0 -0
  114. {pyeql-1.1.2 → pyeql-1.1.3}/tests/test_density.py +0 -0
  115. {pyeql-1.1.2 → pyeql-1.1.3}/tests/test_dielectric.py +0 -0
  116. {pyeql-1.1.2 → pyeql-1.1.3}/tests/test_effective_pitzer.py +0 -0
  117. {pyeql-1.1.2 → pyeql-1.1.3}/tests/test_equilibrium.py +0 -0
  118. {pyeql-1.1.2 → pyeql-1.1.3}/tests/test_functions.py +0 -0
  119. {pyeql-1.1.2 → pyeql-1.1.3}/tests/test_logging.py +0 -0
  120. {pyeql-1.1.2 → pyeql-1.1.3}/tests/test_mixed_electrolyte_activity.py +0 -0
  121. {pyeql-1.1.2 → pyeql-1.1.3}/tests/test_osmotic_coeff.py +0 -0
  122. {pyeql-1.1.2 → pyeql-1.1.3}/tests/test_phreeqc.py +0 -0
  123. {pyeql-1.1.2 → pyeql-1.1.3}/tests/test_salt_matching.py +0 -0
  124. {pyeql-1.1.2 → pyeql-1.1.3}/tests/test_solute.py +0 -0
  125. {pyeql-1.1.2 → pyeql-1.1.3}/tests/test_solute_properties.py +0 -0
  126. {pyeql-1.1.2 → pyeql-1.1.3}/tests/test_utils.py +0 -0
  127. {pyeql-1.1.2 → pyeql-1.1.3}/tests/test_volume_concentration.py +0 -0
  128. {pyeql-1.1.2 → pyeql-1.1.3}/tox.ini +0 -0
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.1.3] - 2024-07-28
9
+
10
+ ### Fixed
11
+
12
+ - `Solution`: Fix a bug in which setting `balance_charge` to `pH` could result in
13
+ negative concentration errors when charge balancing or after `equilibrate` was
14
+ called. `Solution` now correctly enforces the ion product of water (Kw=1e-14)
15
+ whenever adjusting the amounts of H+ or OH- for charge balancing.
16
+
17
+ ### Added
18
+
19
+ - `Solution._adjust_charge_balance`: Added a privat helper method to consolidate charge
20
+ balancing code used in `__init__` and `equilibrate`.
21
+
8
22
  ## [1.1.2] - 2024-07-28
9
23
 
10
24
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyEQL
3
- Version: 1.1.2
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/
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.1.3] - 2024-07-28
9
+
10
+ ### Fixed
11
+
12
+ - `Solution`: Fix a bug in which setting `balance_charge` to `pH` could result in
13
+ negative concentration errors when charge balancing or after `equilibrate` was
14
+ called. `Solution` now correctly enforces the ion product of water (Kw=1e-14)
15
+ whenever adjusting the amounts of H+ or OH- for charge balancing.
16
+
17
+ ### Added
18
+
19
+ - `Solution._adjust_charge_balance`: Added a privat helper method to consolidate charge
20
+ balancing code used in `__init__` and `equilibrate`.
21
+
8
22
  ## [1.1.2] - 2024-07-28
9
23
 
10
24
  ### Fixed
@@ -687,20 +687,8 @@ class NativeEOS(EOS):
687
687
  )
688
688
 
689
689
  # re-adjust charge balance for any missing species
690
- # note that if balance_charge is set, it will have been passed to PHREEQC, so we only need to adjust
691
- # for any missing species here.
692
- charge_adjust = 0
693
- for s in missing_species:
694
- charge_adjust += -1 * solution.get_amount(s, "eq").magnitude
695
- if charge_adjust != 0:
696
- logger.warning(
697
- "After equilibration, the charge balance of the solution was not electroneutral."
698
- f" {charge_adjust} eq of charge were added via {solution._cb_species}"
699
- )
700
-
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")
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()
704
692
 
705
693
  # rescale the solvent mass to ensure the total mass of solution does not change
706
694
  # this is important because PHREEQC and the pyEQL database may use slightly different molecular
@@ -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
@@ -288,17 +289,7 @@ class Solution(MSONable):
288
289
  )
289
290
 
290
291
  # adjust charge balance, if necessary
291
- if not np.isclose(cb, 0, atol=1e-8) and self.balance_charge is not None:
292
- balanced = False
293
- self.logger.info(
294
- f"Solution is not electroneutral (C.B. = {cb} eq/L). Adding {self._cb_species} to compensate."
295
- )
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):
299
- balanced = True
300
- if not balanced:
301
- warnings.warn(f"Unable to balance charge using species {self._cb_species}")
292
+ self._adjust_charge_balance()
302
293
 
303
294
  @property
304
295
  def mass(self) -> Quantity:
@@ -2293,6 +2284,57 @@ class Solution(MSONable):
2293
2284
 
2294
2285
  return distance.to("nm")
2295
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
+
2296
2338
  def _update_volume(self):
2297
2339
  """Recalculate the solution volume based on composition."""
2298
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.2
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/
@@ -202,8 +202,8 @@ def test_init_engines():
202
202
 
203
203
 
204
204
  def test_component_subsets(s2):
205
- assert s2.cations == {"Na[+1]": 8, "H[+1]": 2e-7}
206
- assert s2.anions == {"Cl[-1]": 8, "OH[-1]": 2e-7}
205
+ assert list(s2.cations.keys()) == ["Na[+1]", "H[+1]"]
206
+ assert list(s2.anions.keys()) == ["Cl[-1]", "OH[-1]"]
207
207
  assert list(s2.neutrals.keys()) == ["H2O(aq)"]
208
208
 
209
209
 
@@ -285,6 +285,32 @@ def test_charge_balance(s3, s5, s5_pH, s6, s6_Ca):
285
285
  assert s._cb_species == "Cl[-1]"
286
286
  assert np.isclose(s.charge_balance, 0, atol=1e-8)
287
287
 
288
+ # check 'pH' when the solution needs to be made more POSITIVE
289
+ s = Solution({"Na+": "2 mM", "Cl-": "1 mM"}, balance_charge="pH", pH=4)
290
+ assert s.balance_charge == "pH"
291
+ assert s._cb_species == "H[+1]"
292
+ assert np.isclose(s.charge_balance, 0, atol=1e-8)
293
+ assert s.pH > 4
294
+ s.equilibrate()
295
+ assert s.balance_charge == "pH"
296
+ assert s._cb_species == "H[+1]"
297
+ assert np.isclose(s.charge_balance, 0, atol=1e-8)
298
+
299
+ # check 'pH' when the imbalance is extreme
300
+ s = Solution({"Na+": "2 mM", "Cl-": "1 M"}, balance_charge="pH", pH=4)
301
+ assert s.balance_charge == "pH"
302
+ assert s._cb_species == "H[+1]"
303
+ assert np.isclose(s.charge_balance, 0, atol=1e-8)
304
+ assert np.isclose(s.pH, 0, atol=0.1)
305
+ s.equilibrate()
306
+ assert s.balance_charge == "pH"
307
+ assert s._cb_species == "H[+1]"
308
+ assert np.isclose(s.charge_balance, 0, atol=1e-8)
309
+
310
+ # check warning when there isn't enough to balance
311
+ s = Solution({"Na+": "1 M", "K+": "2 mM", "Cl-": "2 mM"}, balance_charge="K+")
312
+ assert s.get_amount("K+", "mol/L") == 0
313
+
288
314
  # check "auto" with an electroneutral solution
289
315
  s = Solution({"Na+": "2 mM", "Cl-": "2 mM"}, balance_charge="auto")
290
316
  assert s.balance_charge == "auto"
@@ -405,16 +431,16 @@ def test_components_by_element(s1, s2):
405
431
  assert s1.get_components_by_element() == {
406
432
  "H(1.0)": [
407
433
  "H2O(aq)",
408
- "H[+1]",
409
434
  "OH[-1]",
435
+ "H[+1]",
410
436
  ],
411
437
  "O(-2.0)": ["H2O(aq)", "OH[-1]"],
412
438
  }
413
439
  assert s2.get_components_by_element() == {
414
440
  "H(1.0)": [
415
441
  "H2O(aq)",
416
- "H[+1]",
417
442
  "OH[-1]",
443
+ "H[+1]",
418
444
  ],
419
445
  "O(-2.0)": ["H2O(aq)", "OH[-1]"],
420
446
  "Na(1.0)": ["Na[+1]"],
@@ -498,8 +524,8 @@ def test_equilibrate(s1, s2, s5_pH):
498
524
  orig_solv_mass = s5_pH.solvent_mass.magnitude
499
525
  set(s5_pH.components.keys())
500
526
  s5_pH.equilibrate()
501
- assert np.isclose(s5_pH.get_total_amount("Ca", "mol").magnitude, 0.001)
502
- assert np.isclose(s5_pH.get_total_amount("C(4)", "mol").magnitude, 0.001)
527
+ assert np.isclose(s5_pH.get_total_amount("Ca", "mol").magnitude, 0.001, atol=1e-7)
528
+ assert np.isclose(s5_pH.get_total_amount("C(4)", "mol").magnitude, 0.001, atol=1e-7)
503
529
  # due to the large pH shift, water mass and density need not be perfectly conserved
504
530
  assert np.isclose(s5_pH.solvent_mass.magnitude, orig_solv_mass, atol=1e-3)
505
531
  assert np.isclose(s5_pH.density.magnitude, orig_density, atol=1e-3)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes