pyEQL 1.1.1__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.
- {pyeql-1.1.1 → pyeql-1.1.3}/CHANGELOG.md +22 -0
- {pyeql-1.1.1/src/pyEQL.egg-info → pyeql-1.1.3}/PKG-INFO +1 -1
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/changelog.md +22 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/src/pyEQL/engines.py +3 -24
- {pyeql-1.1.1 → pyeql-1.1.3}/src/pyEQL/solution.py +79 -32
- {pyeql-1.1.1 → pyeql-1.1.3/src/pyEQL.egg-info}/PKG-INFO +1 -1
- {pyeql-1.1.1 → pyeql-1.1.3}/tests/test_solution.py +55 -10
- {pyeql-1.1.1 → pyeql-1.1.3}/.coveragerc +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/.gitattributes +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/.github/dependabot.yml +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/.github/pull_request_template.md +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/.github/release.yml +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/.github/workflows/post-process.yml +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/.github/workflows/release.yml +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/.github/workflows/testing.yaml +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/.github/workflows/upgrade_dependencies.yml +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/.gitignore +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/.pre-commit-config.yaml +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/.readthedocs.yml +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/.zenodo.json +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/AUTHORS.md +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/CITATION.cff +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/COPYING +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/LICENSE.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/MANIFEST.in +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/README.md +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/Makefile +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/_static/.gitignore +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/amounts.md +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/arithmetic.md +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/authors.md +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/chemistry.md +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/class_solution.md +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/conf.py +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/contributing.md +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/creating.md +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/database.md +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/engines.md +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/examples/.ipynb_checkpoints/pyEQL_demo_1-checkpoint.ipynb +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/examples/.ipynb_checkpoints/pyeql_demo-checkpoint.ipynb +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/examples/.ipynb_checkpoints/pyeql_tutorial_database-checkpoint.ipynb +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/examples/.ipynb_checkpoints/pyeql_tutorial_osmotic_pressure-checkpoint.ipynb +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/examples/.ipynb_checkpoints/speedup-checkpoint.ipynb +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/examples/pyEQL_demo_1.ipynb +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/examples/pyeql_demo.ipynb +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/examples/pyeql_tutorial_database.ipynb +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/examples/pyeql_tutorial_osmotic_pressure.ipynb +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/examples/speedup.ipynb +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/index.md +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/installation.md +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/internal.md +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/license.md +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/mixing.md +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/quickstart.md +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/readme.md +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/requirements.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/serialization.md +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/tutorials.md +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/docs/units.md +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/pyeql-logo.png +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/pyeql-logo.svg +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/pyproject.toml +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/requirements/macos-latest_py3.10.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/requirements/macos-latest_py3.10_extras.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/requirements/macos-latest_py3.11.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/requirements/macos-latest_py3.11_extras.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/requirements/macos-latest_py3.12.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/requirements/macos-latest_py3.12_extras.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/requirements/macos-latest_py3.9.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/requirements/macos-latest_py3.9_extras.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/requirements/ubuntu-latest_py3.10.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/requirements/ubuntu-latest_py3.10_extras.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/requirements/ubuntu-latest_py3.11.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/requirements/ubuntu-latest_py3.11_extras.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/requirements/ubuntu-latest_py3.12.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/requirements/ubuntu-latest_py3.12_extras.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/requirements/ubuntu-latest_py3.9.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/requirements/ubuntu-latest_py3.9_extras.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/requirements/windows-latest_py3.10.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/requirements/windows-latest_py3.10_extras.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/requirements/windows-latest_py3.11.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/requirements/windows-latest_py3.11_extras.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/requirements/windows-latest_py3.12.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/requirements/windows-latest_py3.12_extras.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/requirements/windows-latest_py3.9.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/requirements/windows-latest_py3.9_extras.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/setup.cfg +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/setup.py +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/src/pyEQL/__init__.py +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/src/pyEQL/activity_correction.py +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/src/pyEQL/database/geothermal.dat +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/src/pyEQL/database/llnl.dat +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/src/pyEQL/database/phreeqc_license.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/src/pyEQL/database/pyeql_db.json +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/src/pyEQL/equilibrium.py +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/src/pyEQL/functions.py +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/src/pyEQL/presets/Ringers lactate.yaml +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/src/pyEQL/presets/normal saline.yaml +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/src/pyEQL/presets/rainwater.yaml +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/src/pyEQL/presets/seawater.yaml +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/src/pyEQL/presets/urine.yaml +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/src/pyEQL/presets/wastewater.yaml +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/src/pyEQL/salt_ion_match.py +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/src/pyEQL/solute.py +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/src/pyEQL/utils.py +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/src/pyEQL.egg-info/SOURCES.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/src/pyEQL.egg-info/dependency_links.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/src/pyEQL.egg-info/requires.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/src/pyEQL.egg-info/top_level.txt +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/tests/conftest.py +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/tests/test_activity.py +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/tests/test_bulk_properties.py +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/tests/test_debye_length.py +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/tests/test_density.py +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/tests/test_dielectric.py +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/tests/test_effective_pitzer.py +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/tests/test_equilibrium.py +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/tests/test_functions.py +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/tests/test_logging.py +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/tests/test_mixed_electrolyte_activity.py +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/tests/test_osmotic_coeff.py +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/tests/test_phreeqc.py +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/tests/test_salt_matching.py +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/tests/test_solute.py +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/tests/test_solute_properties.py +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/tests/test_utils.py +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/tests/test_volume_concentration.py +0 -0
- {pyeql-1.1.1 → pyeql-1.1.3}/tox.ini +0 -0
|
@@ -5,6 +5,28 @@ 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
|
+
|
|
22
|
+
## [1.1.2] - 2024-07-28
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- `Solution`: Fix a bug in which setting `balance_charge` to `auto` when the initial
|
|
27
|
+
composition was electroneutral would cause errors and/or improper charge balancing
|
|
28
|
+
after `equilibrate` was called.
|
|
29
|
+
|
|
8
30
|
## [1.1.1] - 2024-07-27
|
|
9
31
|
|
|
10
32
|
### Fixed
|
|
@@ -5,6 +5,28 @@ 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
|
+
|
|
22
|
+
## [1.1.2] - 2024-07-28
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- `Solution`: Fix a bug in which setting `balance_charge` to `auto` when the initial
|
|
27
|
+
composition was electroneutral would cause errors and/or improper charge balancing
|
|
28
|
+
after `equilibrate` was called.
|
|
29
|
+
|
|
8
30
|
## [1.1.1] - 2024-07-27
|
|
9
31
|
|
|
10
32
|
### Fixed
|
|
@@ -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
|
|
694
|
-
|
|
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
|
|
@@ -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 *
|
|
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
|
-
#
|
|
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
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
elif
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
296
|
-
|
|
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()
|
|
@@ -202,8 +202,8 @@ def test_init_engines():
|
|
|
202
202
|
|
|
203
203
|
|
|
204
204
|
def test_component_subsets(s2):
|
|
205
|
-
assert s2.cations ==
|
|
206
|
-
assert s2.anions ==
|
|
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
|
|
|
@@ -268,16 +268,61 @@ def test_charge_balance(s3, s5, s5_pH, s6, s6_Ca):
|
|
|
268
268
|
volume="1 L",
|
|
269
269
|
balance_charge="auto",
|
|
270
270
|
)
|
|
271
|
-
assert s.balance_charge == "
|
|
271
|
+
assert s.balance_charge == "auto"
|
|
272
|
+
assert s._cb_species == "Na[+1]"
|
|
272
273
|
assert np.isclose(s.charge_balance, 0, atol=1e-8)
|
|
273
274
|
s.equilibrate()
|
|
274
|
-
assert s.balance_charge == "
|
|
275
|
+
assert s.balance_charge == "auto"
|
|
276
|
+
assert s._cb_species == "Na[+1]"
|
|
277
|
+
assert np.isclose(s.charge_balance, 0, atol=1e-8)
|
|
275
278
|
|
|
276
279
|
s = Solution({"Na+": "2 mM", "Cl-": "1 mM"}, balance_charge="auto")
|
|
277
|
-
assert s.balance_charge == "
|
|
280
|
+
assert s.balance_charge == "auto"
|
|
281
|
+
assert s._cb_species == "Cl[-1]"
|
|
282
|
+
assert np.isclose(s.charge_balance, 0, atol=1e-8)
|
|
283
|
+
s.equilibrate()
|
|
284
|
+
assert s.balance_charge == "auto"
|
|
285
|
+
assert s._cb_species == "Cl[-1]"
|
|
286
|
+
assert np.isclose(s.charge_balance, 0, atol=1e-8)
|
|
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
|
+
|
|
314
|
+
# check "auto" with an electroneutral solution
|
|
315
|
+
s = Solution({"Na+": "2 mM", "Cl-": "2 mM"}, balance_charge="auto")
|
|
316
|
+
assert s.balance_charge == "auto"
|
|
317
|
+
assert s._cb_species == "Na[+1]"
|
|
278
318
|
assert np.isclose(s.charge_balance, 0, atol=1e-8)
|
|
279
319
|
s.equilibrate()
|
|
280
|
-
assert s.balance_charge == "
|
|
320
|
+
assert s.balance_charge == "auto"
|
|
321
|
+
assert s._cb_species == "Na[+1]"
|
|
322
|
+
assert np.isclose(s.charge_balance, 0, atol=1e-8)
|
|
323
|
+
|
|
324
|
+
with pytest.raises(ValueError, match=r"Charge balancing species Zr\[\+4\] was not found"):
|
|
325
|
+
s = Solution({"Na+": "2 mM", "Cl-": "2 mM"}, balance_charge="Zr[+4]")
|
|
281
326
|
|
|
282
327
|
|
|
283
328
|
def test_alkalinity_hardness(s3, s5, s6):
|
|
@@ -386,16 +431,16 @@ def test_components_by_element(s1, s2):
|
|
|
386
431
|
assert s1.get_components_by_element() == {
|
|
387
432
|
"H(1.0)": [
|
|
388
433
|
"H2O(aq)",
|
|
389
|
-
"H[+1]",
|
|
390
434
|
"OH[-1]",
|
|
435
|
+
"H[+1]",
|
|
391
436
|
],
|
|
392
437
|
"O(-2.0)": ["H2O(aq)", "OH[-1]"],
|
|
393
438
|
}
|
|
394
439
|
assert s2.get_components_by_element() == {
|
|
395
440
|
"H(1.0)": [
|
|
396
441
|
"H2O(aq)",
|
|
397
|
-
"H[+1]",
|
|
398
442
|
"OH[-1]",
|
|
443
|
+
"H[+1]",
|
|
399
444
|
],
|
|
400
445
|
"O(-2.0)": ["H2O(aq)", "OH[-1]"],
|
|
401
446
|
"Na(1.0)": ["Na[+1]"],
|
|
@@ -479,8 +524,8 @@ def test_equilibrate(s1, s2, s5_pH):
|
|
|
479
524
|
orig_solv_mass = s5_pH.solvent_mass.magnitude
|
|
480
525
|
set(s5_pH.components.keys())
|
|
481
526
|
s5_pH.equilibrate()
|
|
482
|
-
assert np.isclose(s5_pH.get_total_amount("Ca", "mol").magnitude, 0.001)
|
|
483
|
-
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)
|
|
484
529
|
# due to the large pH shift, water mass and density need not be perfectly conserved
|
|
485
530
|
assert np.isclose(s5_pH.solvent_mass.magnitude, orig_solv_mass, atol=1e-3)
|
|
486
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
|
|
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
|