Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

serialization and copy bugfixes and enhancements #45

Merged
merged 1 commit into from
Oct 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
25 changes: 11 additions & 14 deletions src/pyEQL/solution.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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__)
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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
Expand Down
17 changes: 13 additions & 4 deletions tests/test_solution.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
used by pyEQL's Solution class
"""

import copy

import numpy as np
import pytest

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading