Skip to content

Commit c73a451

Browse files
committed
Solution: fix serialization and copy bug
1 parent b1a333b commit c73a451

File tree

3 files changed

+35
-19
lines changed

3 files changed

+35
-19
lines changed

CHANGELOG.md

+11-1
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,20 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [0.8.1] - 2023-09-30
8+
## [0.8.1] - 2023-10-01
9+
10+
### Changed
11+
12+
- `from_dict` modified to avoid call to `super()`, making for more robust behavior if `Solution` is inherited.
13+
14+
### Removed
15+
16+
- `copy()` method was removed for consistency with `python` conventions (it returned a deep rather than a
17+
shallow copy). Use `copy.deepcopy(Solution)` instead.
918

1019
### Fixed
1120

21+
- Bugfix in `as_dict` in which the `solutes` attribute was saved with `Quantity` rather than `float`
1222
- Simplified `Solution.get_conductivity` to avoid errors in selected cases.
1323
- Required `pymatgen` version was incorrectly set at `2022.8.10` when it should be `2023.8.10`
1424
- Bug in `get_osmotic_coefficient` that caused a `ZeroDivisionError` with an empty solution.

src/pyEQL/solution.py

+11-14
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from iapws import IAPWS95
1818
from maggma.stores import JSONStore, Store
1919
from monty.dev import deprecated
20-
from monty.json import MSONable
20+
from monty.json import MontyDecoder, MSONable
2121
from pint import DimensionalityError, Quantity
2222
from pymatgen.core import Element
2323
from pymatgen.core.ion import Ion
@@ -69,13 +69,13 @@ def __init__(
6969
7070
Defaults to empty (pure solvent) if omitted
7171
volume : str, optional
72-
Volume of the solvent, including the ureg. Defaults to '1 L' if omitted.
72+
Volume of the solvent, including the unit. Defaults to '1 L' if omitted.
7373
Note that the total solution volume will be computed using partial molar
7474
volumes of the respective solutes as they are added to the solution.
7575
temperature : str, optional
7676
The solution temperature, including the ureg. Defaults to '25 degC' if omitted.
7777
pressure : Quantity, optional
78-
The ambient pressure of the solution, including the ureg.
78+
The ambient pressure of the solution, including the unit.
7979
Defaults to '1 atm' if omitted.
8080
pH : number, optional
8181
Negative log of H+ activity. If omitted, the solution will be
@@ -97,12 +97,13 @@ def __init__(
9797
contains serialized SoluteDocs. `None` (default) will use the built-in pyEQL database.
9898
9999
Examples:
100-
>>> s1 = pyEQL.Solution([['Na+','1 mol/L'],['Cl-','1 mol/L']],temperature='20 degC',volume='500 mL')
100+
>>> s1 = pyEQL.Solution({'Na+': '1 mol/L','Cl-': '1 mol/L'},temperature='20 degC',volume='500 mL')
101101
>>> print(s1)
102102
Components:
103-
['H2O', 'Cl-', 'H+', 'OH-', 'Na+']
104-
Volume: 0.5 l
105-
Density: 1.0383030844030992 kg/l
103+
Volume: 0.500 l
104+
Pressure: 1.000 atm
105+
Temperature: 293.150 K
106+
Components: ['H2O(aq)', 'H[+1]', 'OH[-1]', 'Na[+1]', 'Cl[-1]']
106107
"""
107108
# create a logger attached to this class
108109
# self.logger = logging.getLogger(type(self).__name__)
@@ -2350,11 +2351,6 @@ def _get_solute_volume(self):
23502351
"""Return the volume of only the solutes."""
23512352
return self.engine.get_solute_volume(self)
23522353

2353-
# copying and serialization
2354-
def copy(self) -> Solution:
2355-
"""Return a copy of the solution."""
2356-
return Solution.from_dict(self.as_dict())
2357-
23582354
def as_dict(self) -> dict:
23592355
"""
23602356
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:
23642360
self._update_volume()
23652361
d = super().as_dict()
23662362
# replace solutes with the current composition
2367-
d["solutes"] = {k: v * ureg.Quantity("1 mol") for k, v in self.components.items()}
2363+
d["solutes"] = {k: f"{v} mol" for k, v in self.components.items()}
23682364
# replace the engine with the associated str
23692365
d["engine"] = self._engine
23702366
return d
@@ -2379,7 +2375,8 @@ def from_dict(cls, d: dict) -> Solution:
23792375
# first we store the volume of the serialized solution
23802376
orig_volume = ureg.Quantity(d["volume"])
23812377
# then instantiate a new one
2382-
new_sol = super().from_dict(d)
2378+
decoded = {k: MontyDecoder().process_decoded(v) for k, v in d.items() if not k.startswith("@")}
2379+
new_sol = cls(**decoded)
23832380
# now determine how different the new solution volume is from the original
23842381
scale_factor = (orig_volume / new_sol.volume).magnitude
23852382
# reset the new solution volume to that of the original. In the process of

tests/test_solution.py

+13-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
used by pyEQL's Solution class
77
"""
88

9+
import copy
10+
911
import numpy as np
1012
import pytest
1113

@@ -36,13 +38,13 @@ def s4():
3638
@pytest.fixture()
3739
def s5():
3840
# 100 mg/L as CaCO3 ~ 1 mM
39-
return Solution([["Ca+2", "40.078 mg/L"], ["CO3-2", "60.0089 mg/L"]], volume="1 L")
41+
return Solution([["Ca+2", "40.078 mg/L"], ["CO3-2", "60.0089 mg/L"]])
4042

4143

4244
@pytest.fixture()
4345
def s5_pH():
4446
# 100 mg/L as CaCO3 ~ 1 mM
45-
return Solution([["Ca+2", "40.078 mg/L"], ["CO3-2", "60.0089 mg/L"]], volume="1 L", balance_charge="pH")
47+
return Solution([["Ca+2", "40.078 mg/L"], ["CO3-2", "60.0089 mg/L"]], balance_charge="pH")
4648

4749

4850
@pytest.fixture()
@@ -433,7 +435,7 @@ def test_conductivity(s1, s2):
433435

434436

435437
def test_arithmetic_and_copy(s2, s6):
436-
s6_scale = s6.copy()
438+
s6_scale = copy.deepcopy(s6)
437439
s6_scale *= 1.5
438440
assert s6_scale.volume == 1.5 * s6.volume
439441
assert s6_scale.pressure == s6.pressure
@@ -487,13 +489,17 @@ def test_arithmetic_and_copy(s2, s6):
487489
s2 + s_bad
488490

489491

490-
def test_serialization(s1, s2):
492+
def test_serialization(s1, s2, s5):
491493
assert isinstance(s1.as_dict(), dict)
492494
s1_new = Solution.from_dict(s1.as_dict())
493495
assert s1_new.volume.magnitude == 2
496+
assert s1_new._solutes["H[+1]"] == "2e-07 mol"
497+
assert s1_new.get_total_moles_solute() == s1.get_total_moles_solute()
494498
assert s1_new.components == s1.components
495499
assert np.isclose(s1_new.pH, s1.pH)
500+
assert np.isclose(s1_new._pH, s1._pH)
496501
assert np.isclose(s1_new.pE, s1.pE)
502+
assert np.isclose(s1_new._pE, s1._pE)
497503
assert s1_new.temperature == s1.temperature
498504
assert s1_new.pressure == s1.pressure
499505
assert s1_new.solvent == s1.solvent
@@ -510,8 +516,11 @@ def test_serialization(s1, s2):
510516
assert s2_new.components == s2.components
511517
# but not point to the same instances
512518
assert s2_new.components is not s2.components
519+
assert s2_new.get_total_moles_solute() == s2.get_total_moles_solute()
513520
assert np.isclose(s2_new.pH, s2.pH)
521+
assert np.isclose(s2_new._pH, s2._pH)
514522
assert np.isclose(s2_new.pE, s2.pE)
523+
assert np.isclose(s2_new._pE, s2._pE)
515524
assert s2_new.temperature == s2.temperature
516525
assert s2_new.pressure == s2.pressure
517526
assert s2_new.solvent == s2.solvent

0 commit comments

Comments
 (0)