Skip to content

Commit

Permalink
Merge pull request #145 from KingsburyLab/bugfix
Browse files Browse the repository at this point in the history
Fix incorrect display of NH4+, NH3, and other formulas
  • Loading branch information
rkingsbury authored Jul 9, 2024
2 parents 8d2a4b6 + e2446df commit f3ef9a6
Show file tree
Hide file tree
Showing 8 changed files with 72 additions and 17 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ 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).

## [1.0.2] - 2024-07-09

### Fixed

- `standardize_formula`: Fix incorrect display of ammonium/ammonia. Previously, their formulas
were shown as "H4N[+1]" and "H3N(aq)", respectively. They now correctly display as NH4 and NH3.
Similar fixes were implemented for HPO4[-2] / H2PO4[-1] / H3PO4, formate (HCOO[-1]), oxalate (C2O4[-2]), thicyanate (SCN[-1]), and triiodide (I3[-1]).
Fixes #136 (@xiaoxiaozhu123)

## [1.0.1] - 2024-06-17

### Added
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,5 @@ enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"]
warn_unreachable = true

[tool.codespell]
ignore-words-list = "nd"
ignore-words-list = "nd,formate"
skip = "tests/test_files/*,src/pyEQL/database/*"
6 changes: 3 additions & 3 deletions src/pyEQL/engines.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ class NativeEOS(EOS):
def __init__(
self,
phreeqc_db: Literal["vitens.dat", "wateq4f_PWN.dat", "pitzer.dat", "llnl.dat", "geothermal.dat"] = "llnl.dat",
):
) -> None:
"""
Args:
phreeqc_db: Name of the PHREEQC database file to use for solution thermodynamics
Expand Down Expand Up @@ -712,7 +712,7 @@ def equilibrate(self, solution):
# call to equilibrate can thus result in a slight change in the Solution mass.
solution.components[solution.solvent] = orig_solvent_moles

def __deepcopy__(self, memo):
def __deepcopy__(self, memo) -> "NativeEOS":
# custom deepcopy required because the PhreeqPython instance used by the Native and Phreeqc engines
# is not pickle-able.
import copy
Expand All @@ -736,7 +736,7 @@ def __init__(
phreeqc_db: Literal[
"vitens.dat", "wateq4f_PWN.dat", "pitzer.dat", "llnl.dat", "geothermal.dat"
] = "phreeqc.dat",
):
) -> None:
"""
Args:
phreeqc_db: Name of the PHREEQC database file to use for solution thermodynamics
Expand Down
2 changes: 1 addition & 1 deletion src/pyEQL/salt_ion_match.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
class Salt(MSONable):
"""Class to represent a salt."""

def __init__(self, cation, anion):
def __init__(self, cation, anion) -> None:
"""
Create a Salt object based on its component ions.
Expand Down
2 changes: 1 addition & 1 deletion src/pyEQL/solute.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def as_dict(self):
return dict(asdict(self).items())

# set output of the print() statement
def __str__(self):
def __str__(self) -> str:
return (
"Species "
+ str(self.formula)
Expand Down
12 changes: 6 additions & 6 deletions src/pyEQL/solution.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def __init__(
database: str | Path | Store | None = None,
default_diffusion_coeff: float = 1.6106e-9,
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = "ERROR",
):
) -> None:
"""
Instantiate a Solution from a composition.
Expand Down Expand Up @@ -2512,7 +2512,7 @@ def from_file(self, filename: str | Path) -> Solution:
return loadfn(filename)

# arithmetic operations
def __add__(self, other: Solution):
def __add__(self, other: Solution) -> Solution:
"""
Solution addition: mix two solutions together.
Expand Down Expand Up @@ -2600,18 +2600,18 @@ def __add__(self, other: Solution):
pE=mix_pE,
)

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

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

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

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

def __str__(self):
def __str__(self) -> str:
# set output of the print() statement for the solution
l1 = f"Volume: {self.volume:.3f~}"
l2 = f"Temperature: {self.temperature:.3f~}"
Expand Down
43 changes: 38 additions & 5 deletions src/pyEQL/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import logging
from collections import UserDict
from functools import lru_cache
from typing import Any

from iapws import IAPWS95, IAPWS97
from pymatgen.core.ion import Ion
Expand Down Expand Up @@ -59,7 +60,39 @@ def standardize_formula(formula: str):
be enclosed in square brackets to remove any ambiguity in the meaning of the formula. For example, 'Na+',
'Na+1', and 'Na[+]' will all standardize to "Na[+1]"
"""
return Ion.from_formula(formula).reduced_formula
sform = Ion.from_formula(formula).reduced_formula

# TODO - manual formula adjustments. May be implemented upstream in pymatgen in the future
# thanks to @xiaoxiaozhu123 for pointing out these issues in
# https://github.com/KingsburyLab/pyEQL/issues/136

# ammonia
if sform == "H4N[+1]":
sform = "NH4[+1]"
elif sform == "H3N(aq)":
sform = "NH3(aq)"
# phosphoric acid system
elif sform == "PH3O4(aq)":
sform = "H3PO4(aq)"
elif sform == "PHO4[-2]":
sform = "HPO4[-2]"
elif sform == "P(HO2)2[-1]":
sform = "H2PO4[-1]"
# thiocyanate
elif sform == "CSN[-1]":
sform = "SCN[-1]"
# triiodide
elif sform == "I[-0.33333333]":
sform = "I3[-1]"
# formate
elif sform == "HCOO[-1]":
sform = "HCO2[-1]"
# oxalate
elif sform == "CO2[-1]":
sform = "C2O4[-2]"

# TODO - consider adding recognition of special formulas like MeOH for methanol or Cit for citrate
return sform


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

def __getitem__(self, key):
def __getitem__(self, key) -> Any:
return super().__getitem__(standardize_formula(key))

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

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

def __delitem__(self, key):
def __delitem__(self, key) -> None:
super().__delitem__(standardize_formula(key))
13 changes: 13 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Tests of pyEQL.utils module
"""

from iapws import IAPWS95, IAPWS97
from pytest import raises

Expand All @@ -19,6 +20,18 @@ def test_standardize_formula():
assert standardize_formula("SO4--") == "SO4[-2]"
assert standardize_formula("Mg+2") == "Mg[+2]"
assert standardize_formula("O2") == "O2(aq)"
assert standardize_formula("NH4+") == "NH4[+1]"
assert standardize_formula("NH3") == "NH3(aq)"
assert standardize_formula("HPO4--") == "HPO4[-2]"
assert standardize_formula("H2PO4-") == "H2PO4[-1]"
assert standardize_formula("SCN-") == "SCN[-1]"
assert standardize_formula("I3-") == "I3[-1]"
assert standardize_formula("HCOO-") == "HCO2[-1]"
assert standardize_formula("CO2-1") == "C2O4[-2]"
assert standardize_formula("C2O4--") == "C2O4[-2]"
assert standardize_formula("H3PO4") == "H3PO4(aq)"
assert standardize_formula("H2SO4") == "H2SO4(aq)"
assert standardize_formula("HClO4") == "HClO4(aq)"


def test_formula_dict():
Expand Down

0 comments on commit f3ef9a6

Please sign in to comment.