Skip to content

Commit

Permalink
Merge pull request #83 from KingsburyLab/housekeeping
Browse files Browse the repository at this point in the history
Add tests for gibbs_mix and entropy_mix
  • Loading branch information
rkingsbury authored Nov 21, 2023
2 parents 6e74b88 + 81cfa88 commit 2405f14
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 110 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ 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

### Added

- Add tests for `gibbs_mix` and `entropy_mix` functions. Format docstrings in Google style.

### Removed

- `functions.py` is no longer imported into the root namespace. You'll now need to say `from pyEQL.functions import gibbs_mix`
instead of `from pyEQL import gibbs_mix`

## [0.11.0] - 2023-11-20

### Changed
Expand Down
6 changes: 3 additions & 3 deletions docs/creating.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ Finally, you can manually create a solution with any list of solutes, temperatur

## Using a preset

Alternatively, you can use the `pyEQL.functions.autogenerate()` function to easily create common solutions like seawater:
Alternatively, you can use the `Solution.from_preset()` classmethod to easily create common solutions like seawater:

```
>>> from pyEQL.functions import autogenerate
>>> s2 = autogenerate('seawater')
>>> from pyEQL import Solution
>>> s2 = Solution.from_preset('seawater')
<pyEQL.solution.Solution object at 0x7f057de6b0a0>
```

Expand Down
5 changes: 2 additions & 3 deletions src/pyEQL/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
:license: LGPL, see LICENSE for more details.
"""
from importlib.metadata import PackageNotFoundError, version # pragma: no cover

from pint import UnitRegistry
from pathlib import Path

from maggma.stores import JSONStore
from pint import UnitRegistry
from pkg_resources import resource_filename

try:
Expand Down Expand Up @@ -50,5 +50,4 @@
# instantiated Store in memory, which should speed up instantiation of Solution objects.
IonDB.connect()

from pyEQL.functions import * # noqa: E402, F403
from pyEQL.solution import Solution # noqa: E402
177 changes: 79 additions & 98 deletions src/pyEQL/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,51 +10,44 @@

from monty.dev import deprecated

import pyEQL
from pyEQL import ureg
from pyEQL import Solution, ureg
from pyEQL.logging_system import logger


def gibbs_mix(Solution1, Solution2):
def gibbs_mix(solution1: Solution, solution2: Solution):
r"""
Return the Gibbs energy change associated with mixing two solutions.
Parameters
----------
Solution1, Solution2 : Solution objects
The two solutions to be mixed.
Args:
solution1, solution2: The two solutions to be mixed.
Returns
-------
Quantity
Returns:
The change in Gibbs energy associated with complete mixing of the
Solutions, in Joules.
Notes
-----
The Gibbs energy of mixing is calculated as follows
Notes:
The Gibbs energy of mixing is calculated as follows
.. math::
.. math::
\\Delta_{mix} G = \\sum_i (n_c + n_d) R T \\ln a_b - \\sum_i n_c R T \\ln a_c - \\sum_i n_d R T \\ln a_d
\\Delta_{mix} G = \\sum_i (n_c + n_d) R T \\ln a_b - \\sum_i n_c R T \\ln a_c - \\sum_i n_d R T \\ln a_d
Where :math:`n` is the number of moles of substance, :math:`T` is the temperature in kelvin,
and subscripts :math:`b`, :math:`c`, and :math:`d` refer to the concentrated, dilute, and blended
Solutions, respectively.
Where :math:`n` is the number of moles of substance, :math:`T` is the temperature in kelvin,
and subscripts :math:`b`, :math:`c`, and :math:`d` refer to the concentrated, dilute, and blended
Solutions, respectively.
Note that dissociated ions must be counted as separate components,
so a simple salt dissolved in water is a three component solution (cation,
anion, and water).
Note that dissociated ions must be counted as separate components,
so a simple salt dissolved in water is a three component solution (cation,
anion, and water).
References
----------
Koga, Yoshikata, 2007. *Solution Thermodynamics and its Application to Aqueous Solutions:
A differential approach.* Elsevier, 2007, pp. 23-37.
References:
Koga, Yoshikata, 2007. *Solution Thermodynamics and its Application to Aqueous Solutions:
A differential approach.* Elsevier, 2007, pp. 23-37.
"""
concentrate = Solution1
dilute = Solution2
blend = Solution1 + Solution2
concentrate = solution1
dilute = solution2
blend = solution1 + solution2
term_list = {concentrate: 0, dilute: 0, blend: 0}

# calculate the entropy change and number of moles solute for each solution
Expand All @@ -68,46 +61,40 @@ def gibbs_mix(Solution1, Solution2):
)


def entropy_mix(Solution1, Solution2):
def entropy_mix(solution1: Solution, solution2: Solution):
r"""
Return the ideal mixing entropy associated with mixing two solutions.
Parameters
----------
Solution1, Solution2 : Solution objects
The two solutions to be mixed.
Parameters:
solution1, solution2: The two solutions to be mixed.
Returns
-------
Quantity
Returns:
The ideal mixing entropy associated with complete mixing of the
Solutions, in Joules.
Notes
-----
The ideal entropy of mixing is calculated as follows
Notes:
The ideal entropy of mixing is calculated as follows
.. math::
.. math::
\\Delta_{mix} S = \\sum_i (n_c + n_d) R T \\ln x_b - \\sum_i n_c R T \\ln x_c - \\sum_i n_d R T \\ln x_d
\\Delta_{mix} S = \\sum_i (n_c + n_d) R T \\ln x_b - \\sum_i n_c R T \\ln x_c - \\sum_i n_d R T \\ln x_d
Where :math:`n` is the number of moles of substance, :math:`T` is the temperature in kelvin,
and subscripts :math:`b`, :math:`c`, and :math:`d` refer to the concentrated, dilute, and blended
Solutions, respectively.
Where :math:`n` is the number of moles of substance, :math:`T` is the temperature in kelvin,
and subscripts :math:`b`, :math:`c`, and :math:`d` refer to the concentrated, dilute, and blended
Solutions, respectively.
Note that dissociated ions must be counted as separate components,
so a simple salt dissolved in water is a three component solution (cation,
anion, and water).
Note that dissociated ions must be counted as separate components,
so a simple salt dissolved in water is a three component solution (cation,
anion, and water).
References
----------
Koga, Yoshikata, 2007. *Solution Thermodynamics and its Application to Aqueous Solutions:
A differential approach.* Elsevier, 2007, pp. 23-37.
References:
Koga, Yoshikata, 2007. *Solution Thermodynamics and its Application to Aqueous Solutions:
A differential approach.* Elsevier, 2007, pp. 23-37.
"""
concentrate = Solution1
dilute = Solution2
blend = Solution1 + Solution2
concentrate = solution1
dilute = solution2
blend = solution1 + solution2
term_list = {concentrate: 0, dilute: 0, blend: 0}

# calculate the entropy change and number of moles solute for each solution
Expand All @@ -123,67 +110,61 @@ def entropy_mix(Solution1, Solution2):
)


def donnan_eql(solution, fixed_charge):
def donnan_eql(solution: Solution, fixed_charge: str):
"""
Return a solution object in equilibrium with fixed_charge.
Parameters
----------
solution : Solution object
The external solution to be brought into equilibrium with the fixed
charges
fixed_charge : str quantity
String representing the concentration of fixed charges, including sign.
May be specified in mol/L or mol/kg units. e.g. '1 mol/kg'
Parameters:
solution : Solution object
The external solution to be brought into equilibrium with the fixed
charges
fixed_charge : str quantity
String representing the concentration of fixed charges, including sign.
May be specified in mol/L or mol/kg units. e.g. '1 mol/kg'
Returns
-------
Solution
A solution that has established Donnan equilibrium with the external
Returns:
A Solution that has established Donnan equilibrium with the external
(input) Solution
Notes
-----
The general equation representing the equilibrium between an external
electrolyte solution and an ion-exchange medium containing fixed charges
is
Notes:
The general equation representing the equilibrium between an external
electrolyte solution and an ion-exchange medium containing fixed charges
is
.. math::
.. math::
\\frac{a_{-}}{\\bar a_{-}}^{\\frac{1}{z_{-}} \\frac{\\bar a_{+}}{a_{+}}^{\\frac{1}{z_{+}} \
= exp(\\frac{\\Delta \\pi \\bar V}{{RT z_{+} \\nu_{+}}})
\\frac{a_{-}}{\\bar a_{-}}^{\\frac{1}{z_{-}} \\frac{\\bar a_{+}}{a_{+}}^{\\frac{1}{z_{+}} \
= exp(\\frac{\\Delta \\pi \\bar V}{{RT z_{+} \\nu_{+}}})
Where subscripts :math:`+` and :math:`-` indicate the cation and anion, respectively,
the overbar indicates the membrane phase,
:math:`a` represents activity, :math:`z` represents charge, :math:`\\nu` represents the stoichiometric
coefficient, :math:`V` represents the partial molar volume of the salt, and
:math:`\\Delta \\pi` is the difference in osmotic pressure between the membrane and the
solution phase.
Where subscripts :math:`+` and :math:`-` indicate the cation and anion, respectively,
the overbar indicates the membrane phase,
:math:`a` represents activity, :math:`z` represents charge, :math:`\\nu` represents the stoichiometric
coefficient, :math:`V` represents the partial molar volume of the salt, and
:math:`\\Delta \\pi` is the difference in osmotic pressure between the membrane and the
solution phase.
In addition, electroneutrality must prevail within the membrane phase:
In addition, electroneutrality must prevail within the membrane phase:
.. math:: \\bar C_{+} z_{+} + \\bar X + \\bar C_{-} z_{-} = 0
.. math:: \\bar C_{+} z_{+} + \\bar X + \\bar C_{-} z_{-} = 0
Where :math:`C` represents concentration and :math:`X` is the fixed charge concentration
in the membrane or ion exchange phase.
Where :math:`C` represents concentration and :math:`X` is the fixed charge concentration
in the membrane or ion exchange phase.
This function solves these two equations simultaneously to arrive at the
concentrations of the cation and anion in the membrane phase. It returns
a solution equal to the input solution except that the concentrations of
the predominant cation and anion have been adjusted according to this
equilibrium.
This function solves these two equations simultaneously to arrive at the
concentrations of the cation and anion in the membrane phase. It returns
a solution equal to the input solution except that the concentrations of
the predominant cation and anion have been adjusted according to this
equilibrium.
NOTE that this treatment is only capable of equilibrating a single salt.
This salt is identified by the get_salt() method.
NOTE that this treatment is only capable of equilibrating a single salt.
This salt is identified by the get_salt() method.
References
----------
Strathmann, Heiner, ed. *Membrane Science and Technology* vol. 9, 2004. Chapter 2, p. 51.
References:
Strathmann, Heiner, ed. *Membrane Science and Technology* vol. 9, 2004. Chapter 2, p. 51.
http://dx.doi.org/10.1016/S0927-5193(04)80033-0
See Also:
--------
get_salt()
get_salt()
"""
# identify the salt
Expand Down Expand Up @@ -434,4 +415,4 @@ def autogenerate(
logger.error("Invalid solution entered - %s" % solution)
return None

return pyEQL.Solution(solutes, temperature=temperature, pressure=pressure, pH=pH)
return Solution(solutes, temperature=temperature, pressure=pressure, pH=pH)
85 changes: 85 additions & 0 deletions tests/test_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""
Tests of pyEQL.functions module
"""
import numpy as np
import pytest

from pyEQL import Solution
from pyEQL.functions import entropy_mix, gibbs_mix


@pytest.fixture()
def s1():
return Solution(volume="2 L")


@pytest.fixture()
def s2():
return Solution({"Na+": "1 mol/L", "Cl-": "1 mol/L"}, volume="10 L")


@pytest.fixture()
def s1_p():
return Solution(volume="2 L", engine="phreeqc")


@pytest.fixture()
def s2_p():
return Solution({"Na+": "1 mol/L", "Cl-": "1 mol/L"}, volume="10 L", engine="phreeqc")


@pytest.fixture()
def s1_i():
return Solution(volume="2 L", engine="ideal")


@pytest.fixture()
def s2_i():
return Solution({"Na+": "1 mol/L", "Cl-": "1 mol/L"}, volume="10 L", engine="ideal")


def test_mixing_functions(s1, s2, s1_p, s2_p, s1_i, s2_i):
# mixing energy and entropy of any solution with itself should be zero
assert np.isclose(gibbs_mix(s1, s1).magnitude, 0)
assert np.isclose(entropy_mix(s2, s2).magnitude, 0)
assert np.isclose(gibbs_mix(s1_p, s1_p).magnitude, 0)
assert np.isclose(entropy_mix(s2_p, s2_p).magnitude, 0)
assert np.isclose(gibbs_mix(s1_i, s1_i).magnitude, 0, atol=1e-6)
assert np.isclose(entropy_mix(s2_i, s2_i).magnitude, 0)

# TODO - I have not tested how equilibrate() affects the results
for dil, conc in zip([s1, s1_p, s1_i], [s2, s2_p, s2_i]):
# for mixing 1 and 2, we should have
# H20: 55.5 * 2 mol + 55.5 * 10 mol, x1 = 0.9999 x2 = 0.9645, mixture = 0.9703 = approximately -9043 J
s_theoretical = (
8.314
* 298.15
* (
(dil + conc).get_amount("H2O", "mol").magnitude
* np.log((dil + conc).get_amount("H2O", "fraction").magnitude)
+ (dil + conc).get_amount("Na+", "mol").magnitude
* np.log((dil + conc).get_amount("Na+", "fraction").magnitude)
+ (dil + conc).get_amount("Cl-", "mol").magnitude
* np.log((dil + conc).get_amount("Cl-", "fraction").magnitude)
- dil.get_amount("H2O", "mol").magnitude * np.log(dil.get_amount("H2O", "fraction").magnitude)
- conc.get_amount("H2O", "mol").magnitude * np.log(conc.get_amount("H2O", "fraction").magnitude)
- conc.get_amount("Na+", "mol").magnitude * np.log(conc.get_amount("Na+", "fraction").magnitude)
- conc.get_amount("Cl-", "mol").magnitude * np.log(conc.get_amount("Cl-", "fraction").magnitude)
)
)
assert np.isclose(entropy_mix(dil, conc).magnitude, s_theoretical, rtol=0.005)
g_theoretical = (
8.314
* 298.15
* (
(dil + conc).get_amount("H2O", "mol").magnitude * np.log((dil + conc).get_activity("H2O").magnitude)
+ (dil + conc).get_amount("Na+", "mol").magnitude * np.log((dil + conc).get_activity("Na+").magnitude)
+ (dil + conc).get_amount("Cl-", "mol").magnitude * np.log((dil + conc).get_activity("Cl-").magnitude)
- dil.get_amount("H2O", "mol").magnitude * np.log(dil.get_activity("H2O").magnitude)
- conc.get_amount("H2O", "mol").magnitude * np.log(conc.get_activity("H2O").magnitude)
- conc.get_amount("Na+", "mol").magnitude * np.log(conc.get_activity("Na+").magnitude)
- conc.get_amount("Cl-", "mol").magnitude * np.log(conc.get_activity("Cl-").magnitude)
)
)
assert np.isclose(gibbs_mix(dil, conc).magnitude, g_theoretical, rtol=0.005)
Loading

0 comments on commit 2405f14

Please sign in to comment.