From 3ca607615ee582fab526172524f06b945704db8d Mon Sep 17 00:00:00 2001 From: Ryan Date: Fri, 26 Jul 2024 12:00:30 -0400 Subject: [PATCH 01/12] testing: tmpdir -> tmp_path --- CHANGELOG.md | 1 + tests/test_solution.py | 16 +++++++--------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f518df19..db985fbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Unit tests: update `tmpdir` to `tmp_path` text fixture. - CI: Small updates to pre-commit and GitHub actions per scientific python [repo review](https://scientific-python.github.io/repo-review/?repo=kingsburylab%2FpyEQL&branch=main). ## [1.0.3] - 2024-07-20 diff --git a/tests/test_solution.py b/tests/test_solution.py index df8ee41d..3583d85c 100644 --- a/tests/test_solution.py +++ b/tests/test_solution.py @@ -621,11 +621,11 @@ def test_as_from_dict(s1, s2): # assert s2_new.database != s2.database -def test_serialization(s1, s2, tmpdir): +def test_serialization(s1, s2, tmp_path): from monty.serialization import dumpfn, loadfn - dumpfn(s1, str(tmpdir / "s1.json")) - s1_new = loadfn(str(tmpdir / "s1.json")) + dumpfn(s1, str(tmp_path / "s1.json")) + s1_new = loadfn(str(tmp_path / "s1.json")) assert s1_new.volume.magnitude == 2 assert s1_new._solutes["H[+1]"] == "2e-07 mol" assert s1_new.get_total_moles_solute() == s1.get_total_moles_solute() @@ -644,8 +644,8 @@ def test_serialization(s1, s2, tmpdir): # TODO currently this test will fail due to a bug in maggma's __eq__ # assert s1_new.database != s1.database - dumpfn(s2, str(tmpdir / "s2.json")) - s2_new = loadfn(str(tmpdir / "s2.json")) + dumpfn(s2, str(tmp_path / "s2.json")) + s2_new = loadfn(str(tmp_path / "s2.json")) assert s2_new.volume == s2.volume # components concentrations should be the same assert s2_new.components == s2.components @@ -667,7 +667,7 @@ def test_serialization(s1, s2, tmpdir): # assert s2_new.database != s2.database -def test_from_preset(tmpdir): +def test_from_preset(tmp_path): from monty.serialization import dumpfn preset_name = "seawater" @@ -685,7 +685,6 @@ def test_from_preset(tmpdir): with pytest.raises(FileNotFoundError): Solution.from_preset("nonexistent_preset") # test json as preset - tmp_path = Path(tmpdir) json_preset = tmp_path / "test.json" dumpfn(solution, json_preset) solution_json = Solution.from_preset(tmp_path / "test") @@ -695,8 +694,7 @@ def test_from_preset(tmpdir): assert np.isclose(solution_json.pH, data["pH"], atol=0.01) -def test_test_to_from_file(tmpdir, s1): - tmp_path = Path(tmpdir) +def test_to_from_file(tmp_path, s1): for f in ["test.json", "test.yaml"]: filename = tmp_path / f s1.to_file(filename) From 372744a43a1aacfaf360857c6dd2841f904ab21f Mon Sep 17 00:00:00 2001 From: Ryan Date: Fri, 26 Jul 2024 12:19:36 -0400 Subject: [PATCH 02/12] add_amount: refactor to allow absent solutes --- CHANGELOG.md | 3 ++ src/pyEQL/solution.py | 80 +++--------------------------- tests/test_volume_concentration.py | 7 +++ 3 files changed, 16 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db985fbd..89c95b2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- `Solution.add_amount`: This method will now add solutes that are absent from the Solution. Previously, calling, e.g., + `add_amount('Na+', '1 mol')` on a `Solution` that did not contain any sodium would result in an error. A warning + is logged if the method has to add a new solute. - Unit tests: update `tmpdir` to `tmp_path` text fixture. - CI: Small updates to pre-commit and GitHub actions per scientific python [repo review](https://scientific-python.github.io/repo-review/?repo=kingsburylab%2FpyEQL&branch=main). diff --git a/src/pyEQL/solution.py b/src/pyEQL/solution.py index 0a4740a3..f726a642 100644 --- a/src/pyEQL/solution.py +++ b/src/pyEQL/solution.py @@ -1282,81 +1282,13 @@ def add_amount(self, solute: str, amount: str): Returns: Nothing. The concentration of solute is modified. """ - # if units are given on a per-volume basis, - # iteratively solve for the amount of solute that will preserve the - # original volume and result in the desired concentration - if ureg.Quantity(amount).dimensionality in ( - "[substance]/[length]**3", - "[mass]/[length]**3", - ): - # store the original volume for later - orig_volume = self.volume - - # change the amount of the solute present to match the desired amount - self.components[solute] += ( - ureg.Quantity(amount) - .to( - "moles", - "chem", - mw=self.get_property(solute, "molecular_weight"), - volume=self.volume, - solvent_mass=self.solvent_mass, - ) - .magnitude - ) - - # set the amount to zero and log a warning if the desired amount - # change would result in a negative concentration - if self.get_amount(solute, "mol").magnitude < 0: - self.logger.error( - "Attempted to set a negative concentration for solute %s. Concentration set to 0" % solute - ) - self.set_amount(solute, "0 mol") + # Get the current amount of the solute + current_amt = self.get_amount(solute, amount.split(" ")[1]) + if current_amt.magnitude == 0: + self.logger.warning(f"Add new solute {solute} to the solution") + new_amt = ureg.Quantity(amount) + current_amt + self.set_amount(solute, new_amt) - # calculate the volume occupied by all the solutes - solute_vol = self._get_solute_volume() - - # determine the volume of solvent that will preserve the original volume - target_vol = orig_volume - solute_vol - - # adjust the amount of solvent - # volume in L, density in kg/m3 = g/L - target_mass = target_vol * ureg.Quantity(self.water_substance.rho, "g/L") - - mw = self.get_property(self.solvent, "molecular_weight") - target_mol = target_mass / mw - self.components[self.solvent] = target_mol.magnitude - - else: - # change the amount of the solute present - self.components[solute] += ( - ureg.Quantity(amount) - .to( - "moles", - "chem", - mw=self.get_property(solute, "molecular_weight"), - volume=self.volume, - solvent_mass=self.solvent_mass, - ) - .magnitude - ) - - # set the amount to zero and log a warning if the desired amount - # change would result in a negative concentration - if self.get_amount(solute, "mol").magnitude < 0: - self.logger.error( - "Attempted to set a negative concentration for solute %s. Concentration set to 0" % solute - ) - self.set_amount(solute, "0 mol") - - # update the volume to account for the space occupied by all the solutes - # make sure that there is still solvent present in the first place - if self.solvent_mass <= ureg.Quantity(0, "kg"): - self.logger.error("All solvent has been depleted from the solution") - return - - # set the volume recalculation flag - self.volume_update_required = True def set_amount(self, solute: str, amount: str): """ diff --git a/tests/test_volume_concentration.py b/tests/test_volume_concentration.py index 1350f211..9011876b 100644 --- a/tests/test_volume_concentration.py +++ b/tests/test_volume_concentration.py @@ -216,6 +216,13 @@ def test_add_amount_11(self, s2): s2.add_amount("Cl-", "-2 mol") assert np.allclose(s2.get_amount("Na+", "mol").magnitude, 6) + def test_add_amount_12(self, s2): + # test behavior when the solute is initially absent from the solution + s2.add_amount("Ca+2", "1 mol") + s2.add_amount("Br-", "1 mol") + assert np.allclose(s2.get_amount("Ca+2", "mol/L").magnitude, 0.5, atol=0.002) + assert np.allclose(s2.get_amount("Br-", "mol/L").magnitude, 0.5, atol=0.002) + class Test_get_amount: """ From 800eb886b029197a2076a8a2275857bd8056d476 Mon Sep 17 00:00:00 2001 From: Ryan Date: Fri, 26 Jul 2024 12:38:09 -0400 Subject: [PATCH 03/12] add auto charge balancing --- CHANGELOG.md | 8 +++++++- src/pyEQL/solution.py | 26 ++++++++++++++++++-------- tests/test_solution.py | 17 +++++++++++++++++ 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89c95b2c..d3acdaf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.0.4] - 2024-07-26 +## [1.1.0] - 2024-07-26 ### Fixed @@ -13,6 +13,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 balancing. - Database: `size.radius_ionic` was missing units for `Ni[+2]` and `Cr[+3]`. Correct units have been added. +### Added + +- `Solution`: New automatic charge balancing method will automatically identify the majority (highest concentration) + cation or anion as appropriate (depending on the charge balance) for charge balancing. To use this mode, set + `balance_charge='auto'` when instantiating a `Solution`. + ### Changed - `Solution.add_amount`: This method will now add solutes that are absent from the Solution. Previously, calling, e.g., diff --git a/src/pyEQL/solution.py b/src/pyEQL/solution.py index f726a642..12320c7c 100644 --- a/src/pyEQL/solution.py +++ b/src/pyEQL/solution.py @@ -92,11 +92,15 @@ def __init__( -7 to +14. The default value corresponds to a pE value typical of natural waters in equilibrium with the atmosphere. balance_charge: The strategy for balancing charge during init and equilibrium calculations. Valid options - are 'pH', which will adjust the solution pH to balance charge, 'pE' which will adjust the - redox equilibrium to balance charge, or the name of a dissolved species e.g. 'Ca+2' or 'Cl-' - that will be added/subtracted to balance charge. If set to None, no charge balancing will be - performed either on init or when equilibrate() is called. Note that in this case, equilibrate() - can distort the charge balance! + are + - 'pH', which will adjust the solution pH to balance charge, + - 'auto' which will use the majority cation or anion (i.e., that with the largest concentration) + as needed, + - 'pE' (not currently implemented) which will adjust the redox equilibrium to balance charge, or + the name of a dissolved species e.g. 'Ca+2' or 'Cl-' that will be added/subtracted to balance + charge. + - None (default), in which case no charge balancing will be performed either on init or when + equilibrate() is called. Note that in this case, equilibrate() can distort the charge balance! solvent: Formula of the solvent. Solvents other than water are not supported at this time. engine: Electrolyte modeling engine to use. See documentation for details on the available engines. database: path to a .json file (str or Path) or maggma Store instance that @@ -171,7 +175,7 @@ def __init__( self._pE = pE self._pH = pH self.pE = self._pE - if isinstance(balance_charge, str) and balance_charge not in ["pH", "pE"]: + if isinstance(balance_charge, str) and balance_charge not in ["pH", "pE", "auto"]: self.balance_charge = standardize_formula(balance_charge) else: self.balance_charge = balance_charge #: Standardized formula of the species used for charge balancing. @@ -273,13 +277,19 @@ def __init__( raise NotImplementedError("Balancing charge via redox (pE) is not yet implemented!") else: ions = set().union(*[self.cations, self.anions]) # all ions + if self.balance_charge == "auto": + # add the most abundant ion of the opposite charge + if cb < 0: + self.balance_charge = max(self.cations, key=self.cations.get) + elif cb >0: + self.balance_charge = max(self.anions, key=self.anions.get) if self.balance_charge not in ions: raise ValueError( f"Charge balancing species {self.balance_charge} was not found in the solution!. " f"Species {ions} were found." ) - z = self.get_property(balance_charge, "charge") - self.components[balance_charge] += -1 * cb / z * self.volume.to("L").magnitude + z = self.get_property(self.balance_charge, "charge") + self.components[self.balance_charge] += -1 * cb / z * self.volume.to("L").magnitude balanced = True if not balanced: diff --git a/tests/test_solution.py b/tests/test_solution.py index 3583d85c..a13582ca 100644 --- a/tests/test_solution.py +++ b/tests/test_solution.py @@ -255,6 +255,23 @@ def test_charge_balance(s3, s5, s5_pH, s6, s6_Ca): assert np.isclose(s6.charge_balance, -0.12) assert np.isclose(s6_Ca.charge_balance, 0, atol=1e-8) + # test auto charge balance + s=Solution( + [ + ["Ca+2", "1 mM"], # 2 meq/L + ["Mg+2", "5 mM"], # 10 meq/L + ["Na+1", "10 mM"], # 10 meq/L + ["Ag+1", "10 mM"], # no contribution to alk or hardness + ["CO3-2", "6 mM"], # no contribution to alk or hardness + ["SO4-2", "60 mM"], # -120 meq/L + ["Br-", "20 mM"], + ], # -20 meq/L + volume="1 L", + balance_charge="auto", + ) + assert s.balance_charge == 'Na[+1]' + assert np.isclose(s.charge_balance, 0, atol=1e-8) + def test_alkalinity_hardness(s3, s5, s6): assert np.isclose(s3.hardness, 0) From 027b1b701b73e458019549e983e1841f22eb1ba7 Mon Sep 17 00:00:00 2001 From: Ryan Date: Sat, 27 Jul 2024 15:59:51 -0400 Subject: [PATCH 04/12] pre-commit autoupdate --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 61a45ce8..8c7f3a9f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,18 +12,18 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.8 + rev: v0.5.5 hooks: - id: ruff args: [--fix, --ignore, "D,E501", "--show-fixes"] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.2.0 + rev: 24.4.2 hooks: - id: black - repo: https://github.com/codespell-project/codespell - rev: v2.2.6 + rev: v2.3.0 hooks: - id: codespell stages: [commit, commit-msg] @@ -31,7 +31,7 @@ repos: additional_dependencies: [tomli] # needed to read pyproject.toml below py3.11 - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: check-case-conflict - id: check-symlinks From 37c8a1f37fbba0193714fb66a7cd9cb194862fd5 Mon Sep 17 00:00:00 2001 From: Ryan Date: Sat, 27 Jul 2024 16:01:10 -0400 Subject: [PATCH 05/12] bump monty; streamline as_dict() --- pyproject.toml | 4 ++-- src/pyEQL/solution.py | 15 +++++---------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6c2cc13c..f70acfc5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "pyEQL" readme = "README.md" dynamic = ["version"] -description="A python interace for solution chemistry" +description="A python interface for solution chemistry" authors =[ {name = "Ryan Kingsbury", email = "kingsbury@princeton.edu"} ] @@ -25,7 +25,7 @@ dependencies = [ "scipy", "pymatgen==2024.5.1", "iapws", - "monty", + "monty>=2024.7.12", "maggma>=0.67.0", "phreeqpython", ] diff --git a/src/pyEQL/solution.py b/src/pyEQL/solution.py index 12320c7c..41ffa697 100644 --- a/src/pyEQL/solution.py +++ b/src/pyEQL/solution.py @@ -92,13 +92,13 @@ def __init__( -7 to +14. The default value corresponds to a pE value typical of natural waters in equilibrium with the atmosphere. balance_charge: The strategy for balancing charge during init and equilibrium calculations. Valid options - are + are - 'pH', which will adjust the solution pH to balance charge, - 'auto' which will use the majority cation or anion (i.e., that with the largest concentration) as needed, - 'pE' (not currently implemented) which will adjust the redox equilibrium to balance charge, or the name of a dissolved species e.g. 'Ca+2' or 'Cl-' that will be added/subtracted to balance - charge. + charge. - None (default), in which case no charge balancing will be performed either on init or when equilibrate() is called. Note that in this case, equilibrate() can distort the charge balance! solvent: Formula of the solvent. Solvents other than water are not supported at this time. @@ -279,9 +279,9 @@ def __init__( ions = set().union(*[self.cations, self.anions]) # all ions if self.balance_charge == "auto": # add the most abundant ion of the opposite charge - if cb < 0: + if cb <= 0: self.balance_charge = max(self.cations, key=self.cations.get) - elif cb >0: + elif cb > 0: self.balance_charge = max(self.anions, key=self.anions.get) if self.balance_charge not in ions: raise ValueError( @@ -1299,7 +1299,6 @@ def add_amount(self, solute: str, amount: str): new_amt = ureg.Quantity(amount) + current_amt self.set_amount(solute, new_amt) - def set_amount(self, solute: str, amount: str): """ Set the amount of 'solute' in the parent solution. @@ -2310,10 +2309,6 @@ def as_dict(self) -> dict: if self.volume_update_required: self._update_volume() d = super().as_dict() - for k, v in d.items(): - # convert all Quantity to str - if isinstance(v, Quantity): - d[k] = str(v) # replace solutes with the current composition d["solutes"] = {k: f"{v} mol" for k, v in self.components.items()} # replace the engine with the associated str @@ -2407,7 +2402,7 @@ def to_file(self, filename: str | Path) -> None: """ str_filename = str(filename) if not ("yaml" in str_filename.lower() or "json" in str_filename.lower()): - self.logger.error("Invalid file extension entered - %s" % str_filename) + self.logger.error("Invalid file extension entered - {str_filename}") raise ValueError("File extension must be .json or .yaml") if "yaml" in str_filename.lower(): solution_dict = self.as_dict() From 4eba2e096ffb4947d72970986c0fce560e58eeb1 Mon Sep 17 00:00:00 2001 From: Ryan Date: Sat, 27 Jul 2024 16:01:59 -0400 Subject: [PATCH 06/12] expand tests; lint --- src/pyEQL/engines.py | 3 +++ tests/test_solution.py | 7 ++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pyEQL/engines.py b/src/pyEQL/engines.py index 4f7a30ea..ebb757d8 100644 --- a/src/pyEQL/engines.py +++ b/src/pyEQL/engines.py @@ -709,6 +709,9 @@ def equilibrate(self, solution: "Solution") -> None: raise NotImplementedError else: z = solution.get_property(solution.balance_charge, "charge") + from icecream import ic + + ic(charge_adjust, solution.balance_charge) solution.add_amount(solution.balance_charge, f"{charge_adjust/z} mol") # rescale the solvent mass to ensure the total mass of solution does not change diff --git a/tests/test_solution.py b/tests/test_solution.py index a13582ca..305589d3 100644 --- a/tests/test_solution.py +++ b/tests/test_solution.py @@ -9,7 +9,6 @@ import copy import os import platform -from pathlib import Path import numpy as np import pytest @@ -256,7 +255,7 @@ def test_charge_balance(s3, s5, s5_pH, s6, s6_Ca): assert np.isclose(s6_Ca.charge_balance, 0, atol=1e-8) # test auto charge balance - s=Solution( + s = Solution( [ ["Ca+2", "1 mM"], # 2 meq/L ["Mg+2", "5 mM"], # 10 meq/L @@ -269,8 +268,10 @@ def test_charge_balance(s3, s5, s5_pH, s6, s6_Ca): volume="1 L", balance_charge="auto", ) - assert s.balance_charge == 'Na[+1]' + assert s.balance_charge == "Na[+1]" assert np.isclose(s.charge_balance, 0, atol=1e-8) + s.equilibrate() + assert s.balance_charge == "Na[+1]" def test_alkalinity_hardness(s3, s5, s6): From 68e1482c29cf0b4ceef8dd4faed3d2f7b089fb67 Mon Sep 17 00:00:00 2001 From: Ryan Date: Sat, 27 Jul 2024 16:03:29 -0400 Subject: [PATCH 07/12] changelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3acdaf2..aa74a6ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.1.0] - 2024-07-26 +## [1.1.0] - 2024-07-27 ### Fixed @@ -24,6 +24,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Solution.add_amount`: This method will now add solutes that are absent from the Solution. Previously, calling, e.g., `add_amount('Na+', '1 mol')` on a `Solution` that did not contain any sodium would result in an error. A warning is logged if the method has to add a new solute. +- `pre-commit autoupdate` +- Misc. linting and code quality improvements. - Unit tests: update `tmpdir` to `tmp_path` text fixture. - CI: Small updates to pre-commit and GitHub actions per scientific python [repo review](https://scientific-python.github.io/repo-review/?repo=kingsburylab%2FpyEQL&branch=main). From 8a50214563276758a45dfe0149f8d445da920a1b Mon Sep 17 00:00:00 2001 From: Ryan Date: Sat, 27 Jul 2024 16:13:57 -0400 Subject: [PATCH 08/12] restore Quantity conversion need to wait for next monty release --- src/pyEQL/solution.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pyEQL/solution.py b/src/pyEQL/solution.py index 41ffa697..c45b31b5 100644 --- a/src/pyEQL/solution.py +++ b/src/pyEQL/solution.py @@ -2309,6 +2309,10 @@ def as_dict(self) -> dict: if self.volume_update_required: self._update_volume() d = super().as_dict() + for k, v in d.items(): + # convert all Quantity to str + if isinstance(v, Quantity): + d[k] = str(v) # replace solutes with the current composition d["solutes"] = {k: f"{v} mol" for k, v in self.components.items()} # replace the engine with the associated str From 10ae1d8edfd081fdb1a57874be422bbceec3c8b2 Mon Sep 17 00:00:00 2001 From: Ryan Date: Sat, 27 Jul 2024 16:23:15 -0400 Subject: [PATCH 09/12] rm debugging statement --- src/pyEQL/engines.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pyEQL/engines.py b/src/pyEQL/engines.py index ebb757d8..4f7a30ea 100644 --- a/src/pyEQL/engines.py +++ b/src/pyEQL/engines.py @@ -709,9 +709,6 @@ def equilibrate(self, solution: "Solution") -> None: raise NotImplementedError else: z = solution.get_property(solution.balance_charge, "charge") - from icecream import ic - - ic(charge_adjust, solution.balance_charge) solution.add_amount(solution.balance_charge, f"{charge_adjust/z} mol") # rescale the solvent mass to ensure the total mass of solution does not change From 05cad1a17677203313ebe5b1d04a8fff9e8b3c5b Mon Sep 17 00:00:00 2001 From: Ryan Date: Sat, 27 Jul 2024 17:13:20 -0400 Subject: [PATCH 10/12] use upstream pint chemistry context --- MANIFEST.in | 1 - src/pyEQL/__init__.py | 5 +--- src/pyEQL/pint_custom_units.txt | 52 --------------------------------- 3 files changed, 1 insertion(+), 57 deletions(-) delete mode 100644 src/pyEQL/pint_custom_units.txt diff --git a/MANIFEST.in b/MANIFEST.in index 1fb89d73..67f28a87 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,6 +2,5 @@ include README COPYING CHANGELOG LICENSE README.md README.rst README.txt CHANGES recursive-include src/pyEQL/database/ * recursive-include src/pyEQL/presets/ * recursive-include docs/ * -include src/pyEQL/pint_custom_units.txt prune docs/build global-exclude *.pyc *~ .DS_Store *__pycache__* *.pyo diff --git a/src/pyEQL/__init__.py b/src/pyEQL/__init__.py index 64bc4e87..6c1807f6 100644 --- a/src/pyEQL/__init__.py +++ b/src/pyEQL/__init__.py @@ -37,11 +37,8 @@ # convert "offset units" so that, e.g. Quantity('25 degC') works without error # see https://pint.readthedocs.io/en/0.22/user/nonmult.html?highlight=offset#temperature-conversion ureg.autoconvert_offset_to_baseunit = True -# append custom unit definitions and contexts -fname = files("pyEQL") / "pint_custom_units.txt" -ureg.load_definitions(fname) # activate the "chemistry" context globally -ureg.enable_contexts("chem") +ureg.enable_contexts("chemistry") # set the default string formatting for pint quantities ureg.default_format = "P~" diff --git a/src/pyEQL/pint_custom_units.txt b/src/pyEQL/pint_custom_units.txt deleted file mode 100644 index cde2fbdf..00000000 --- a/src/pyEQL/pint_custom_units.txt +++ /dev/null @@ -1,52 +0,0 @@ -# Units definition file for pint library - -# This file defines additional units and contexts to enable the pint -# library to process solution chemistry units such as mol/L and mol/kg. - -@context(mw=0,volume=0,solvent_mass=0) chemistry = chem - # mw is the molecular weight of the species - # volume is the volume of the solution - # solvent_mass is the mass of solvent in the solution - - # moles -> mass require the molecular weight - [substance] -> [mass]: value * mw - [mass] -> [substance]: value / mw - - # moles/volume -> mass/volume and moles/mass -> mass / mass - # require the molecular weight - [substance] / [volume] -> [mass] / [volume]: value * mw - [mass] / [volume] -> [substance] / [volume]: value / mw - [substance] / [mass] -> [mass] / [mass]: value * mw - [mass] / [mass] -> [substance] / [mass]: value / mw - - # moles/volume -> moles requires the solution volume - [substance] / [volume] -> [substance]: value * volume - [substance] -> [substance] / [volume]: value / volume - - # moles/mass -> moles requires the solvent (usually water) mass - [substance] / [mass] -> [substance]: value * solvent_mass - [substance] -> [substance] / [mass]: value / solvent_mass - - # moles/mass -> moles/volume require the solvent mass and the volume - [substance] / [mass] -> [substance]/[volume]: value * solvent_mass / volume - [substance] / [volume] -> [substance] / [mass]: value / solvent_mass * volume - -@end - -@context electricity = elec - [length] ** 2 * [mass] / [current] ** 2 / [time] ** 3 <-> [length] ** -2 * [mass] **-1 / [current] ** -2 / [time] ** -3: 1 / value -@end - - -#From the pint documentation: - -#@context(n=1) spectroscopy = sp -# # n index of refraction of the medium. -# [length] <-> [frequency]: speed_of_light / n / value -# [frequency] -> [energy]: planck_constant * value -# [energy] -> [frequency]: value / planck_constant -#@end - -# 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. -# 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. -# 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. From 96793337495a8facf084d29bc071fe3c3823f4ab Mon Sep 17 00:00:00 2001 From: Ryan Date: Sat, 27 Jul 2024 17:32:24 -0400 Subject: [PATCH 11/12] expand auto charge balance test --- tests/test_solution.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_solution.py b/tests/test_solution.py index 305589d3..c52ea155 100644 --- a/tests/test_solution.py +++ b/tests/test_solution.py @@ -273,6 +273,12 @@ def test_charge_balance(s3, s5, s5_pH, s6, s6_Ca): s.equilibrate() assert s.balance_charge == "Na[+1]" + s = Solution({"Na+": "2 mM", "Cl-": "1 mM"}, balance_charge="auto") + assert s.balance_charge == "Cl[-1]" + assert np.isclose(s.charge_balance, 0, atol=1e-8) + s.equilibrate() + assert s.balance_charge == "Cl[-1]" + def test_alkalinity_hardness(s3, s5, s6): assert np.isclose(s3.hardness, 0) From 2afe6ef6100af8f49f43c211fda68c8ed56fa8ff Mon Sep 17 00:00:00 2001 From: Ryan Date: Sat, 27 Jul 2024 17:33:07 -0400 Subject: [PATCH 12/12] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa74a6ac..f1c04964 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Solution.add_amount`: This method will now add solutes that are absent from the Solution. Previously, calling, e.g., `add_amount('Na+', '1 mol')` on a `Solution` that did not contain any sodium would result in an error. A warning is logged if the method has to add a new solute. +- Units: use the upstream chemistry context from `pint` instead of the custom one from 2013. - `pre-commit autoupdate` - Misc. linting and code quality improvements. - Unit tests: update `tmpdir` to `tmp_path` text fixture.