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

Equlibrate: fix bug with pH charge balancing; mypy #150

Merged
merged 2 commits into from
Jul 26, 2024
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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ 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).

## Unreleased
## [1.0.4] - 2024-07-26

### Fixed

- `equilibrate`: Fixed a bug that could cause an `AttributeError` when pH was used for charge
balancing.
- Database: `size.radius_ionic` was missing units for `Ni[+2]` and `Cr[+3]`. Correct units have been added.

### Changed
Expand Down
73 changes: 39 additions & 34 deletions src/pyEQL/engines.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import warnings
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Literal
from typing import TYPE_CHECKING, Literal

from phreeqpython import PhreeqPython

Expand All @@ -26,6 +26,9 @@

logger = logging.getLogger(__name__)

if TYPE_CHECKING:
from pyEQL import Solution

Check warning on line 30 in src/pyEQL/engines.py

View check run for this annotation

Codecov / codecov/patch

src/pyEQL/engines.py#L30

Added line #L30 was not covered by tests


class EOS(ABC):
"""
Expand All @@ -38,7 +41,7 @@
"""

@abstractmethod
def get_activity_coefficient(self, solution, solute):
def get_activity_coefficient(self, solution: "Solution", solute: str) -> ureg.Quantity:
"""
Return the *molal scale* activity coefficient of solute, given a Solution
object.
Expand All @@ -55,7 +58,7 @@
"""

@abstractmethod
def get_osmotic_coefficient(self, solution):
def get_osmotic_coefficient(self, solution: "Solution") -> ureg.Quantity:
"""
Return the *molal scale* osmotic coefficient of a Solution.

Expand All @@ -70,7 +73,7 @@
"""

@abstractmethod
def get_solute_volume(self):
def get_solute_volume(self, solution: "Solution") -> ureg.Quantity:
"""
Return the volume of only the solutes.

Expand All @@ -85,7 +88,7 @@
"""

@abstractmethod
def equilibrate(self, solution):
def equilibrate(self, solution: "Solution") -> None:
"""
Adjust the speciation and pH of a Solution object to achieve chemical equilibrium.

Expand All @@ -105,25 +108,25 @@
class IdealEOS(EOS):
"""Ideal solution equation of state engine."""

def get_activity_coefficient(self, solution, solute):
def get_activity_coefficient(self, solution: "Solution", solute: str) -> ureg.Quantity:
"""
Return the *molal scale* activity coefficient of solute, given a Solution
object.
"""
return ureg.Quantity(1, "dimensionless")

def get_osmotic_coefficient(self, solution):
def get_osmotic_coefficient(self, solution: "Solution") -> ureg.Quantity:
"""
Return the *molal scale* osmotic coefficient of solute, given a Solution
object.
"""
return ureg.Quantity(1, "dimensionless")

def get_solute_volume(self, solution):
def get_solute_volume(self, solution: "Solution") -> ureg.Quantity:
"""Return the volume of the solutes."""
return ureg.Quantity(0, "L")

def equilibrate(self, solution):
def equilibrate(self, solution: "Solution") -> None:
"""Adjust the speciation of a Solution object to achieve chemical equilibrium."""
warnings.warn("equilibrate() has no effect in IdealEOS!")
return
Expand All @@ -138,7 +141,9 @@

def __init__(
self,
phreeqc_db: Literal["vitens.dat", "wateq4f_PWN.dat", "pitzer.dat", "llnl.dat", "geothermal.dat"] = "llnl.dat",
phreeqc_db: Literal[
"phreeqc.dat", "vitens.dat", "wateq4f_PWN.dat", "pitzer.dat", "llnl.dat", "geothermal.dat"
] = "llnl.dat",
) -> None:
"""
Args:
Expand Down Expand Up @@ -172,7 +177,7 @@
# store the solution composition to see whether we need to re-instantiate the solution
self._stored_comp = None

def _setup_ppsol(self, solution):
def _setup_ppsol(self, solution: "Solution") -> None:
"""Helper method to set up a PhreeqPython solution for subsequent analysis."""
self._stored_comp = solution.components.copy()
solv_mass = solution.solvent_mass.to("kg").magnitude
Expand Down Expand Up @@ -244,13 +249,13 @@

self.ppsol = ppsol

def _destroy_ppsol(self):
"""Remove the PhreeqPython solution from memory"""
def _destroy_ppsol(self) -> None:
"""Remove the PhreeqPython solution from memory."""
if self.ppsol is not None:
self.ppsol.forget()
self.ppsol = None

def get_activity_coefficient(self, solution, solute):
def get_activity_coefficient(self, solution: "Solution", solute: str):
r"""
Whenever the appropriate parameters are available, the Pitzer model [may]_ is used.
If no Pitzer parameters are available, then the appropriate equations are selected
Expand Down Expand Up @@ -329,7 +334,7 @@

# show an error if no salt can be found that contains the solute
if salt is None:
logger.error("No salts found that contain solute %s. Returning unit activity coefficient." % solute)
logger.error(f"No salts found that contain solute {solute}. Returning unit activity coefficient.")

Check warning on line 337 in src/pyEQL/engines.py

View check run for this annotation

Codecov / codecov/patch

src/pyEQL/engines.py#L337

Added line #L337 was not covered by tests
return ureg.Quantity(1, "dimensionless")

# use the Pitzer model for higher ionic strength, if the parameters are available
Expand All @@ -344,14 +349,14 @@
# alpha1 and alpha2 based on charge
if salt.nu_cation >= 2 and salt.nu_anion <= -2:
if salt.nu_cation >= 3 or salt.nu_anion <= -3:
alpha1 = 2
alpha2 = 50
alpha1 = 2.0
alpha2 = 50.0

Check warning on line 353 in src/pyEQL/engines.py

View check run for this annotation

Codecov / codecov/patch

src/pyEQL/engines.py#L352-L353

Added lines #L352 - L353 were not covered by tests
else:
alpha1 = 1.4
alpha2 = 12
else:
alpha1 = 2
alpha2 = 0
alpha1 = 2.0
alpha2 = 0.0

# determine the average molality of the salt
# this is necessary for solutions inside e.g. an ion exchange
Expand Down Expand Up @@ -427,7 +432,7 @@

return molal

def get_osmotic_coefficient(self, solution):
def get_osmotic_coefficient(self, solution: "Solution") -> ureg.Quantity:
r"""
Return the *molal scale* osmotic coefficient of solute, given a Solution
object.
Expand Down Expand Up @@ -574,7 +579,7 @@
# this means the solution is empty
return 1

def get_solute_volume(self, solution):
def get_solute_volume(self, solution: "Solution") -> ureg.Quantity:
"""Return the volume of the solutes."""
# identify the predominant salt in the solution
salt = solution.get_salt()
Expand All @@ -596,14 +601,14 @@
# alpha1 and alpha2 based on charge
if salt.nu_cation >= 2 and salt.nu_anion >= 2:
if salt.nu_cation >= 3 or salt.nu_anion >= 3:
alpha1 = 2
alpha2 = 50
alpha1 = 2.0
alpha2 = 50.0

Check warning on line 605 in src/pyEQL/engines.py

View check run for this annotation

Codecov / codecov/patch

src/pyEQL/engines.py#L604-L605

Added lines #L604 - L605 were not covered by tests
else:
alpha1 = 1.4
alpha2 = 12
else:
alpha1 = 2
alpha2 = 0
alpha1 = 2.0
alpha2 = 0.0

apparent_vol = ac.get_apparent_volume_pitzer(
solution.ionic_strength,
Expand Down Expand Up @@ -633,7 +638,7 @@

pitzer_calc = True

logger.debug("Updated solution volume using Pitzer model for solute %s" % salt.formula)
logger.debug(f"Updated solution volume using Pitzer model for solute {salt.formula}")

# add the partial molar volume of any other solutes, except for water
# or the parent salt, which is already accounted for by the Pitzer parameters
Expand All @@ -649,7 +654,7 @@
part_vol = solution.get_property(solute, "size.molar_volume")
if part_vol is not None:
solute_vol += part_vol * ureg.Quantity(mol, "mol")
logger.debug("Updated solution volume using direct partial molar volume for solute %s" % solute)
logger.debug(f"Updated solution volume using direct partial molar volume for solute {solute}")

else:
logger.warning(
Expand All @@ -658,7 +663,7 @@

return solute_vol.to("L")

def equilibrate(self, solution):
def equilibrate(self, solution: "Solution") -> None:
"""Adjust the speciation of a Solution object to achieve chemical equilibrium."""
if self.ppsol is not None:
self.ppsol.forget()
Expand Down Expand Up @@ -699,7 +704,7 @@
if solution.balance_charge is None:
pass
elif solution.balance_charge == "pH":
solution.components["H+"] += charge_adjust.magnitude
solution.components["H+"] += charge_adjust

Check warning on line 707 in src/pyEQL/engines.py

View check run for this annotation

Codecov / codecov/patch

src/pyEQL/engines.py#L707

Added line #L707 was not covered by tests
elif solution.balance_charge == "pE":
raise NotImplementedError
else:
Expand Down Expand Up @@ -734,7 +739,7 @@
def __init__(
self,
phreeqc_db: Literal[
"vitens.dat", "wateq4f_PWN.dat", "pitzer.dat", "llnl.dat", "geothermal.dat"
"phreeqc.dat", "vitens.dat", "wateq4f_PWN.dat", "pitzer.dat", "llnl.dat", "geothermal.dat"
] = "phreeqc.dat",
) -> None:
"""
Expand All @@ -751,12 +756,12 @@
"""
super().__init__(phreeqc_db=phreeqc_db)

def get_activity_coefficient(self, solution, solute):
def get_activity_coefficient(self, solution: "Solution", solute: str) -> ureg.Quantity:
"""
Return the *molal scale* activity coefficient of solute, given a Solution
object.
"""
if self.ppsol is None or solution.components != self._stored_comp:
if (self.ppsol is None) or (solution.components != self._stored_comp):
self._destroy_ppsol()
self._setup_ppsol(solution)

Expand All @@ -775,7 +780,7 @@

return ureg.Quantity(act, "dimensionless")

def get_osmotic_coefficient(self, solution):
def get_osmotic_coefficient(self, solution: "Solution") -> ureg.Quantity:
"""
Return the *molal scale* osmotic coefficient of solute, given a Solution
object.
Expand All @@ -787,7 +792,7 @@
# TODO - find a way to access or calculate osmotic coefficient
return ureg.Quantity(1, "dimensionless")

def get_solute_volume(self, solution):
def get_solute_volume(self, solution: "Solution") -> ureg.Quantity:
"""Return the volume of the solutes."""
# TODO - phreeqc seems to have no concept of volume, but it does calculate density
return ureg.Quantity(0, "L")
Loading