Skip to content

Commit f3ef9a6

Browse files
authored
Merge pull request #145 from KingsburyLab/bugfix
Fix incorrect display of NH4+, NH3, and other formulas
2 parents 8d2a4b6 + e2446df commit f3ef9a6

8 files changed

+72
-17
lines changed

CHANGELOG.md

+9
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ 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+
## [1.0.2] - 2024-07-09
9+
10+
### Fixed
11+
12+
- `standardize_formula`: Fix incorrect display of ammonium/ammonia. Previously, their formulas
13+
were shown as "H4N[+1]" and "H3N(aq)", respectively. They now correctly display as NH4 and NH3.
14+
Similar fixes were implemented for HPO4[-2] / H2PO4[-1] / H3PO4, formate (HCOO[-1]), oxalate (C2O4[-2]), thicyanate (SCN[-1]), and triiodide (I3[-1]).
15+
Fixes #136 (@xiaoxiaozhu123)
16+
817
## [1.0.1] - 2024-06-17
918

1019
### Added

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -104,5 +104,5 @@ enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"]
104104
warn_unreachable = true
105105

106106
[tool.codespell]
107-
ignore-words-list = "nd"
107+
ignore-words-list = "nd,formate"
108108
skip = "tests/test_files/*,src/pyEQL/database/*"

src/pyEQL/engines.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ class NativeEOS(EOS):
139139
def __init__(
140140
self,
141141
phreeqc_db: Literal["vitens.dat", "wateq4f_PWN.dat", "pitzer.dat", "llnl.dat", "geothermal.dat"] = "llnl.dat",
142-
):
142+
) -> None:
143143
"""
144144
Args:
145145
phreeqc_db: Name of the PHREEQC database file to use for solution thermodynamics
@@ -712,7 +712,7 @@ def equilibrate(self, solution):
712712
# call to equilibrate can thus result in a slight change in the Solution mass.
713713
solution.components[solution.solvent] = orig_solvent_moles
714714

715-
def __deepcopy__(self, memo):
715+
def __deepcopy__(self, memo) -> "NativeEOS":
716716
# custom deepcopy required because the PhreeqPython instance used by the Native and Phreeqc engines
717717
# is not pickle-able.
718718
import copy
@@ -736,7 +736,7 @@ def __init__(
736736
phreeqc_db: Literal[
737737
"vitens.dat", "wateq4f_PWN.dat", "pitzer.dat", "llnl.dat", "geothermal.dat"
738738
] = "phreeqc.dat",
739-
):
739+
) -> None:
740740
"""
741741
Args:
742742
phreeqc_db: Name of the PHREEQC database file to use for solution thermodynamics

src/pyEQL/salt_ion_match.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
class Salt(MSONable):
2222
"""Class to represent a salt."""
2323

24-
def __init__(self, cation, anion):
24+
def __init__(self, cation, anion) -> None:
2525
"""
2626
Create a Salt object based on its component ions.
2727

src/pyEQL/solute.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def as_dict(self):
150150
return dict(asdict(self).items())
151151

152152
# set output of the print() statement
153-
def __str__(self):
153+
def __str__(self) -> str:
154154
return (
155155
"Species "
156156
+ str(self.formula)

src/pyEQL/solution.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def __init__(
5757
database: str | Path | Store | None = None,
5858
default_diffusion_coeff: float = 1.6106e-9,
5959
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = "ERROR",
60-
):
60+
) -> None:
6161
"""
6262
Instantiate a Solution from a composition.
6363
@@ -2512,7 +2512,7 @@ def from_file(self, filename: str | Path) -> Solution:
25122512
return loadfn(filename)
25132513

25142514
# arithmetic operations
2515-
def __add__(self, other: Solution):
2515+
def __add__(self, other: Solution) -> Solution:
25162516
"""
25172517
Solution addition: mix two solutions together.
25182518
@@ -2600,18 +2600,18 @@ def __add__(self, other: Solution):
26002600
pE=mix_pE,
26012601
)
26022602

2603-
def __sub__(self, other: Solution):
2603+
def __sub__(self, other: Solution) -> None:
26042604
raise NotImplementedError("Subtraction of solutions is not implemented.")
26052605

2606-
def __mul__(self, factor: float):
2606+
def __mul__(self, factor: float) -> None:
26072607
"""
26082608
Solution multiplication: scale all components by a factor. For example, Solution * 2 will double the moles of
26092609
every component (including solvent). No other properties will change.
26102610
"""
26112611
self.volume *= factor
26122612
return self
26132613

2614-
def __truediv__(self, factor: float):
2614+
def __truediv__(self, factor: float) -> None:
26152615
"""
26162616
Solution division: scale all components by a factor. For example, Solution / 2 will remove half of the moles
26172617
of every compoonents (including solvent). No other properties will change.
@@ -2657,7 +2657,7 @@ def print(
26572657

26582658
print(f"{i}:\t {amt:0.{places}f}")
26592659

2660-
def __str__(self):
2660+
def __str__(self) -> str:
26612661
# set output of the print() statement for the solution
26622662
l1 = f"Volume: {self.volume:.3f~}"
26632663
l2 = f"Temperature: {self.temperature:.3f~}"

src/pyEQL/utils.py

+38-5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import logging
1010
from collections import UserDict
1111
from functools import lru_cache
12+
from typing import Any
1213

1314
from iapws import IAPWS95, IAPWS97
1415
from pymatgen.core.ion import Ion
@@ -59,7 +60,39 @@ def standardize_formula(formula: str):
5960
be enclosed in square brackets to remove any ambiguity in the meaning of the formula. For example, 'Na+',
6061
'Na+1', and 'Na[+]' will all standardize to "Na[+1]"
6162
"""
62-
return Ion.from_formula(formula).reduced_formula
63+
sform = Ion.from_formula(formula).reduced_formula
64+
65+
# TODO - manual formula adjustments. May be implemented upstream in pymatgen in the future
66+
# thanks to @xiaoxiaozhu123 for pointing out these issues in
67+
# https://github.com/KingsburyLab/pyEQL/issues/136
68+
69+
# ammonia
70+
if sform == "H4N[+1]":
71+
sform = "NH4[+1]"
72+
elif sform == "H3N(aq)":
73+
sform = "NH3(aq)"
74+
# phosphoric acid system
75+
elif sform == "PH3O4(aq)":
76+
sform = "H3PO4(aq)"
77+
elif sform == "PHO4[-2]":
78+
sform = "HPO4[-2]"
79+
elif sform == "P(HO2)2[-1]":
80+
sform = "H2PO4[-1]"
81+
# thiocyanate
82+
elif sform == "CSN[-1]":
83+
sform = "SCN[-1]"
84+
# triiodide
85+
elif sform == "I[-0.33333333]":
86+
sform = "I3[-1]"
87+
# formate
88+
elif sform == "HCOO[-1]":
89+
sform = "HCO2[-1]"
90+
# oxalate
91+
elif sform == "CO2[-1]":
92+
sform = "C2O4[-2]"
93+
94+
# TODO - consider adding recognition of special formulas like MeOH for methanol or Cit for citrate
95+
return sform
6396

6497

6598
def format_solutes_dict(solute_dict: dict, units: str):
@@ -115,18 +148,18 @@ class FormulaDict(UserDict):
115148
formula notation (e.g., "Na+", "Na+1", "Na[+]" all have the same effect)
116149
"""
117150

118-
def __getitem__(self, key):
151+
def __getitem__(self, key) -> Any:
119152
return super().__getitem__(standardize_formula(key))
120153

121-
def __setitem__(self, key, value):
154+
def __setitem__(self, key, value) -> None:
122155
super().__setitem__(standardize_formula(key), value)
123156
# sort contents anytime an item is set
124157
self.data = dict(sorted(self.items(), key=lambda x: x[1], reverse=True))
125158

126159
# Necessary to define this so that .get() works properly in python 3.12+
127160
# see https://github.com/python/cpython/issues/105524
128-
def __contains__(self, key):
161+
def __contains__(self, key) -> bool:
129162
return standardize_formula(key) in self.data
130163

131-
def __delitem__(self, key):
164+
def __delitem__(self, key) -> None:
132165
super().__delitem__(standardize_formula(key))

tests/test_utils.py

+13
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Tests of pyEQL.utils module
33
44
"""
5+
56
from iapws import IAPWS95, IAPWS97
67
from pytest import raises
78

@@ -19,6 +20,18 @@ def test_standardize_formula():
1920
assert standardize_formula("SO4--") == "SO4[-2]"
2021
assert standardize_formula("Mg+2") == "Mg[+2]"
2122
assert standardize_formula("O2") == "O2(aq)"
23+
assert standardize_formula("NH4+") == "NH4[+1]"
24+
assert standardize_formula("NH3") == "NH3(aq)"
25+
assert standardize_formula("HPO4--") == "HPO4[-2]"
26+
assert standardize_formula("H2PO4-") == "H2PO4[-1]"
27+
assert standardize_formula("SCN-") == "SCN[-1]"
28+
assert standardize_formula("I3-") == "I3[-1]"
29+
assert standardize_formula("HCOO-") == "HCO2[-1]"
30+
assert standardize_formula("CO2-1") == "C2O4[-2]"
31+
assert standardize_formula("C2O4--") == "C2O4[-2]"
32+
assert standardize_formula("H3PO4") == "H3PO4(aq)"
33+
assert standardize_formula("H2SO4") == "H2SO4(aq)"
34+
assert standardize_formula("HClO4") == "HClO4(aq)"
2235

2336

2437
def test_formula_dict():

0 commit comments

Comments
 (0)