Skip to content

Commit 089d954

Browse files
authored
Merge pull request #36 from rkingsbury/elements
improvements in element / chemistry handling
2 parents 58e28cd + 8662321 commit 089d954

File tree

8 files changed

+100
-7
lines changed

8 files changed

+100
-7
lines changed

CHANGELOG.md

+11
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ 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+
## [Unreleased]
9+
10+
### Added
11+
12+
- `Solution`: new properties `elements` and `chemical_system`, new function `get_el_amt_dict` to compute the total
13+
number of moles of each element present in the Solution.
14+
15+
### Fixed
16+
17+
- Two issues with the formatting of the `H2O(aq)` entry in the database, `pyeql_db.json`
18+
819
## [0.7.0] - 2023-08-22
920

1021
### Changed

setup.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ install_requires =
5454
numpy
5555
scipy
5656
pint
57-
pymatgen>=2022.0.17
57+
pymatgen>2022.8.10
5858
iapws
5959
monty
6060
maggma

src/pyEQL/database/pyeql_db.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
"formula": "H2O(aq)",
44
"charge": 0,
55
"molecular_weight": "18.01528 g/mol",
6-
"elements": "[Element H, Element O]",
6+
"elements": ["H", "O"],
77
"chemsys": "H-O",
8-
"pmg_ion": "H2 O1 (aq)",
8+
"pmg_ion": {"H": 2.0, "O": 1.0, "charge": 0.0},
99
"formula_html": "H<sub>2</sub>O",
1010
"formula_latex": "H$_{2}$O",
1111
"formula_hill": "H2 O",

src/pyEQL/solute.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import numpy as np
2020
from pymatgen.core.ion import Ion
2121

22+
from pyEQL.utils import standardize_formula
23+
2224

2325
@dataclass
2426
class Datum:
@@ -105,14 +107,15 @@ def from_formula(cls, formula: str):
105107
of the IonDoc.
106108
"""
107109
pmg_ion = Ion.from_formula(formula)
108-
f = pmg_ion.reduced_formula
110+
f, factor = pmg_ion.get_reduced_formula_and_factor()
111+
rform = standardize_formula(formula)
109112
charge = int(pmg_ion.charge)
110113
els = [str(el) for el in pmg_ion.elements]
111-
mw = f"{float(pmg_ion.weight)} g/mol" # weight is a FloatWithUnit
114+
mw = f"{float(pmg_ion.weight / factor)} g/mol" # weight is a FloatWithUnit
112115
chemsys = pmg_ion.chemical_system
113116

114117
return cls(
115-
f,
118+
rform,
116119
charge=charge,
117120
molecular_weight=mw,
118121
elements=els,

src/pyEQL/solution.py

+52-1
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,25 @@ def dielectric_constant(self) -> Quantity:
429429

430430
return ureg.Quantity(di_water / denominator, "dimensionless")
431431

432+
@property
433+
def chemical_system(self) -> str:
434+
"""
435+
Return the chemical system of the Solution as a "-" separated list of elements, sorted alphabetically. For
436+
example, a solution containing CaCO3 would have a chemical system of "C-Ca-H-O".
437+
"""
438+
return "-".join(self.elements)
439+
440+
@property
441+
def elements(self) -> list:
442+
"""
443+
Return a list of elements that are present in the solution. For example,
444+
a solution containing CaCO3 would return ["C", "Ca", "H", "O"]
445+
"""
446+
els = []
447+
for s in self.components:
448+
els.extend(self.get_property(s, "elements"))
449+
return sorted(set(els))
450+
432451
# TODO - need tests for viscosity
433452
@property
434453
def viscosity_dynamic(self) -> Quantity:
@@ -1020,6 +1039,31 @@ def get_amount(self, solute: str, units: str = "mol/L") -> Quantity:
10201039

10211040
raise ValueError(f"Unsupported unit {units} specified for get_amount")
10221041

1042+
def get_el_amt_dict(self):
1043+
"""
1044+
Return a dict of Element: amount in mol
1045+
1046+
Elements (keys) are suffixed with their oxidation state in parentheses,
1047+
e.g. "Fe(2)", "Cl(-1)".
1048+
"""
1049+
d = {}
1050+
for s, mol in self.components.items():
1051+
elements = self.get_property(s, "elements")
1052+
pmg_ion_dict = self.get_property(s, "pmg_ion")
1053+
oxi_states = self.get_property(s, "oxi_state_guesses")[0]
1054+
1055+
for el in elements:
1056+
# stoichiometric coefficient, mol element per mol solute
1057+
stoich = pmg_ion_dict.get(el)
1058+
oxi_state = oxi_states.get(el)
1059+
key = f"{el}({oxi_state})"
1060+
if d.get(key):
1061+
d[key] += stoich * mol
1062+
else:
1063+
d[key] = stoich * mol
1064+
1065+
return d
1066+
10231067
def get_total_amount(self, element: str, units) -> Quantity:
10241068
"""
10251069
Return the total amount of 'element' (across all solutes) in the solution.
@@ -1825,8 +1869,15 @@ def _get_property(self, solute: str, name: str) -> Any | None:
18251869
return doc["model_parameters"]["molar_volume_pitzer"]
18261870
return None
18271871

1872+
if name == "molecular_weight":
1873+
return ureg.Quantity(doc.get(name))
1874+
18281875
# for parameters not named above, just return the base value
1829-
val = doc.get(name) if not isinstance(doc.get(name), dict) else doc[name].get("value")
1876+
if name == "pmg_ion" or not isinstance(doc.get(name), dict):
1877+
# if the queried value is not a dict, it is a root level key and should be returned as is
1878+
return doc.get(name)
1879+
1880+
val = doc[name].get("value")
18301881
# logger.warning("%s has not been corrected for solution conditions" % name)
18311882
if val is not None:
18321883
return ureg.Quantity(val)

tests/test_solute.py

+3
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@ def test_from_formula():
1717
assert s.n_elements == 1
1818
assert s.oxi_state_guesses == ({"Mg": 2.0},)
1919
assert s.molecular_weight == "24.305 g/mol"
20+
s2 = Solute.from_formula("O6")
21+
assert s2.formula == "O3(aq)"
22+
assert s2.molecular_weight == "47.9982 g/mol"

tests/test_solution.py

+24
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,30 @@ def test_pressure_temperature(s5):
171171
assert s5.volume < intermediate_V
172172

173173

174+
def test_elements(s5, s6):
175+
assert s6.elements == sorted({"Ag", "Br", "C", "Ca", "H", "Mg", "Na", "O", "S"})
176+
assert s6.chemical_system == "-".join(s6.elements)
177+
assert s5.chemical_system == "C-Ca-H-O"
178+
179+
180+
def test_get_el_amt_dict(s6):
181+
""" """
182+
water_mol = s6.components["H2O(aq)"]
183+
# scale volume to 8L
184+
s6 *= 8
185+
d = s6.get_el_amt_dict()
186+
for el, amt in zip(
187+
["H(1)", "O(-2)", "Ca(2)", "Mg(2)", "Na(1)", "Ag(1)", "C(4)", "S(6)", "Br(-1)"],
188+
[water_mol * 2 * 8, (water_mol + 0.018 + 0.24) * 8, 0.008, 0.040, 0.08, 0.08, 0.048, 0.48, 0.16],
189+
):
190+
assert np.isclose(d[el], amt, atol=1e-3)
191+
192+
s = Solution({"Fe+2": "1 mM", "Fe+3": "5 mM", "FeCl2": "1 mM", "FeCl3": "5 mM"})
193+
d = s.get_el_amt_dict()
194+
for el, amt in zip(["Fe(2)", "Fe(3)", "Cl(-1)"], [0.002, 0.01, 0.002 + 0.015]):
195+
assert np.isclose(d[el], amt, atol=1e-3)
196+
197+
174198
def test_p(s2):
175199
assert np.isclose(s2.p("Na+"), -1 * np.log10(s2.get_activity("Na+")))
176200
assert np.isclose(s2.p("Na+", activity=False), -1 * np.log10(s2.get_amount("Na+", "M").magnitude))

tests/test_utils.py

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ def test_standardize_formula():
1515
assert standardize_formula("Na[+]") == "Na[+1]"
1616
assert standardize_formula("SO4--") == "SO4[-2]"
1717
assert standardize_formula("Mg+2") == "Mg[+2]"
18+
assert standardize_formula("O2") == "O2(aq)"
1819

1920

2021
def test_formula_dict():

0 commit comments

Comments
 (0)