diff --git a/CHANGELOG.md b/CHANGELOG.md index 342ae535..5c39198a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,20 @@ 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). -## [0.8.1] - 2023-09-30 +## [0.8.1] - 2023-10-01 + +### Changed + +- `from_dict` modified to avoid call to `super()`, making for more robust behavior if `Solution` is inherited. + +### Removed + +- `copy()` method was removed for consistency with `python` conventions (it returned a deep rather than a + shallow copy). Use `copy.deepcopy(Solution)` instead. ### Fixed +- Bugfix in `as_dict` in which the `solutes` attribute was saved with `Quantity` rather than `float` - Simplified `Solution.get_conductivity` to avoid errors in selected cases. - Required `pymatgen` version was incorrectly set at `2022.8.10` when it should be `2023.8.10` - Bug in `get_osmotic_coefficient` that caused a `ZeroDivisionError` with an empty solution. diff --git a/src/pyEQL/solution.py b/src/pyEQL/solution.py index c4137265..0e4c6f6e 100644 --- a/src/pyEQL/solution.py +++ b/src/pyEQL/solution.py @@ -17,7 +17,7 @@ from iapws import IAPWS95 from maggma.stores import JSONStore, Store from monty.dev import deprecated -from monty.json import MSONable +from monty.json import MontyDecoder, MSONable from pint import DimensionalityError, Quantity from pymatgen.core import Element from pymatgen.core.ion import Ion @@ -69,13 +69,13 @@ def __init__( Defaults to empty (pure solvent) if omitted volume : str, optional - Volume of the solvent, including the ureg. Defaults to '1 L' if omitted. + Volume of the solvent, including the unit. Defaults to '1 L' if omitted. Note that the total solution volume will be computed using partial molar volumes of the respective solutes as they are added to the solution. temperature : str, optional The solution temperature, including the ureg. Defaults to '25 degC' if omitted. pressure : Quantity, optional - The ambient pressure of the solution, including the ureg. + The ambient pressure of the solution, including the unit. Defaults to '1 atm' if omitted. pH : number, optional Negative log of H+ activity. If omitted, the solution will be @@ -97,12 +97,13 @@ def __init__( contains serialized SoluteDocs. `None` (default) will use the built-in pyEQL database. Examples: - >>> s1 = pyEQL.Solution([['Na+','1 mol/L'],['Cl-','1 mol/L']],temperature='20 degC',volume='500 mL') + >>> s1 = pyEQL.Solution({'Na+': '1 mol/L','Cl-': '1 mol/L'},temperature='20 degC',volume='500 mL') >>> print(s1) Components: - ['H2O', 'Cl-', 'H+', 'OH-', 'Na+'] - Volume: 0.5 l - Density: 1.0383030844030992 kg/l + Volume: 0.500 l + Pressure: 1.000 atm + Temperature: 293.150 K + Components: ['H2O(aq)', 'H[+1]', 'OH[-1]', 'Na[+1]', 'Cl[-1]'] """ # create a logger attached to this class # self.logger = logging.getLogger(type(self).__name__) @@ -2350,11 +2351,6 @@ def _get_solute_volume(self): """Return the volume of only the solutes.""" return self.engine.get_solute_volume(self) - # copying and serialization - def copy(self) -> Solution: - """Return a copy of the solution.""" - return Solution.from_dict(self.as_dict()) - def as_dict(self) -> dict: """ Convert the Solution into a dict representation that can be serialized to .json or other format. @@ -2364,7 +2360,7 @@ def as_dict(self) -> dict: self._update_volume() d = super().as_dict() # replace solutes with the current composition - d["solutes"] = {k: v * ureg.Quantity("1 mol") for k, v in self.components.items()} + d["solutes"] = {k: f"{v} mol" for k, v in self.components.items()} # replace the engine with the associated str d["engine"] = self._engine return d @@ -2379,7 +2375,8 @@ def from_dict(cls, d: dict) -> Solution: # first we store the volume of the serialized solution orig_volume = ureg.Quantity(d["volume"]) # then instantiate a new one - new_sol = super().from_dict(d) + decoded = {k: MontyDecoder().process_decoded(v) for k, v in d.items() if not k.startswith("@")} + new_sol = cls(**decoded) # now determine how different the new solution volume is from the original scale_factor = (orig_volume / new_sol.volume).magnitude # reset the new solution volume to that of the original. In the process of diff --git a/tests/test_solution.py b/tests/test_solution.py index 96c5780e..523b74b1 100644 --- a/tests/test_solution.py +++ b/tests/test_solution.py @@ -6,6 +6,8 @@ used by pyEQL's Solution class """ +import copy + import numpy as np import pytest @@ -36,13 +38,13 @@ def s4(): @pytest.fixture() def s5(): # 100 mg/L as CaCO3 ~ 1 mM - return Solution([["Ca+2", "40.078 mg/L"], ["CO3-2", "60.0089 mg/L"]], volume="1 L") + return Solution([["Ca+2", "40.078 mg/L"], ["CO3-2", "60.0089 mg/L"]]) @pytest.fixture() def s5_pH(): # 100 mg/L as CaCO3 ~ 1 mM - return Solution([["Ca+2", "40.078 mg/L"], ["CO3-2", "60.0089 mg/L"]], volume="1 L", balance_charge="pH") + return Solution([["Ca+2", "40.078 mg/L"], ["CO3-2", "60.0089 mg/L"]], balance_charge="pH") @pytest.fixture() @@ -433,7 +435,7 @@ def test_conductivity(s1, s2): def test_arithmetic_and_copy(s2, s6): - s6_scale = s6.copy() + s6_scale = copy.deepcopy(s6) s6_scale *= 1.5 assert s6_scale.volume == 1.5 * s6.volume assert s6_scale.pressure == s6.pressure @@ -487,13 +489,17 @@ def test_arithmetic_and_copy(s2, s6): s2 + s_bad -def test_serialization(s1, s2): +def test_serialization(s1, s2, s5): assert isinstance(s1.as_dict(), dict) s1_new = Solution.from_dict(s1.as_dict()) 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() assert s1_new.components == s1.components assert np.isclose(s1_new.pH, s1.pH) + assert np.isclose(s1_new._pH, s1._pH) assert np.isclose(s1_new.pE, s1.pE) + assert np.isclose(s1_new._pE, s1._pE) assert s1_new.temperature == s1.temperature assert s1_new.pressure == s1.pressure assert s1_new.solvent == s1.solvent @@ -510,8 +516,11 @@ def test_serialization(s1, s2): assert s2_new.components == s2.components # but not point to the same instances assert s2_new.components is not s2.components + assert s2_new.get_total_moles_solute() == s2.get_total_moles_solute() assert np.isclose(s2_new.pH, s2.pH) + assert np.isclose(s2_new._pH, s2._pH) assert np.isclose(s2_new.pE, s2.pE) + assert np.isclose(s2_new._pE, s2._pE) assert s2_new.temperature == s2.temperature assert s2_new.pressure == s2.pressure assert s2_new.solvent == s2.solvent