diff --git a/CHANGELOG.md b/CHANGELOG.md index 62a24b90..f452e677 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/creating.md b/docs/creating.md index be75ab74..6d2f8392 100644 --- a/docs/creating.md +++ b/docs/creating.md @@ -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') ``` diff --git a/src/pyEQL/__init__.py b/src/pyEQL/__init__.py index 5099e626..bb55277c 100644 --- a/src/pyEQL/__init__.py +++ b/src/pyEQL/__init__.py @@ -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: @@ -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 diff --git a/src/pyEQL/functions.py b/src/pyEQL/functions.py index 843943d2..f20fbb9f 100644 --- a/src/pyEQL/functions.py +++ b/src/pyEQL/functions.py @@ -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 @@ -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 @@ -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 @@ -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) diff --git a/tests/test_functions.py b/tests/test_functions.py new file mode 100644 index 00000000..6816b212 --- /dev/null +++ b/tests/test_functions.py @@ -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) diff --git a/tests/test_osmotic_coeff.py b/tests/test_osmotic_coeff.py index 9c19bcbc..d4d50a01 100644 --- a/tests/test_osmotic_coeff.py +++ b/tests/test_osmotic_coeff.py @@ -11,7 +11,7 @@ import numpy as np -import pyEQL +from pyEQL import Solution def test_osmotic_pressure(): @@ -21,9 +21,9 @@ def test_osmotic_pressure(): # TODO - at present this test is inaccurate because in the complex matrix # of seawater, pyEQL falls back to using an ideal solution model with # unit osmotic coefficient. - empty = pyEQL.Solution() + empty = Solution() assert np.isclose(empty.osmotic_pressure.to("atm").magnitude, 0, atol=1e-5) - sea = pyEQL.autogenerate("seawater") + sea = Solution.from_preset("seawater") assert np.isclose(sea.osmotic_pressure.to("atm").magnitude, 27, rtol=0.15) @@ -35,7 +35,7 @@ class Test_osmotic_pitzer: """ def test_dimensionality(self): - s1 = pyEQL.Solution([["Na+", "0.1 mol/L"], ["Cl-", "0.1 mol/L"]]) + s1 = Solution([["Na+", "0.1 mol/L"], ["Cl-", "0.1 mol/L"]]) assert s1.get_osmotic_coefficient().dimensionality == "" assert s1.get_osmotic_coefficient() >= 0 @@ -59,7 +59,7 @@ def test_osmotic_pitzer_ammoniumnitrate(self): for i, conc in enumerate(conc_list): conc = str(conc) + "mol/kg" - sol = pyEQL.Solution() + sol = Solution() sol.add_solute("NH4+", conc) sol.add_solute("NO3-", conc) result = sol.get_osmotic_coefficient() @@ -87,7 +87,7 @@ def test_osmotic_pitzer_coppersulfate(self): for i, conc in enumerate(conc_list): conc = str(conc) + "mol/kg" - sol = pyEQL.Solution() + sol = Solution() sol.add_solute("Cu+2", conc) sol.add_solute("SO4-2", conc) result = sol.get_osmotic_coefficient()