From 8c33aebf740b3c9381e2daef91fad9e35d049949 Mon Sep 17 00:00:00 2001 From: Ryan Date: Mon, 16 Feb 2026 13:41:24 -0500 Subject: [PATCH 1/3] equilibrate: exclude H and O from element check --- src/pyEQL/engines.py | 47 ++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/pyEQL/engines.py b/src/pyEQL/engines.py index 1b6fbc56..b5282fab 100644 --- a/src/pyEQL/engines.py +++ b/src/pyEQL/engines.py @@ -366,29 +366,30 @@ def equilibrate( new_el_dict = solution.get_el_amt_dict(nested=True) for el in orig_el_dict: - orig_el_amount = sum([orig_el_dict[el][k] for k in orig_el_dict[el]]) - new_el_amount = sum([new_el_dict[el][k] for k in new_el_dict.get(el, [])]) - - # If this element went "missing", add back all components that - # contain this element (for any valence value) - if new_el_amount == 0 and orig_el_amount > 0: - logger.info( - f"PHREEQC discarded element {el} during equilibration. Adding all components for this element." - ) - solution.components.update( - { - component: self._stored_comp[component] - for components in orig_components_by_element[el].values() - for component in components - if component not in solution.components - } - ) - elif abs(orig_el_amount - new_el_amount) / orig_el_amount > _rtol: - logger.error( - f"PHREEQC returned a total Element {el} concentration of {new_el_amount} mol, " - f"which differs from the original concentration of {orig_el_amount}. This " - "should never occur and indicates an error in the PHREEQC database or calculation." - ) + if el not in ['H', 'O']: # skip H and O since they are part of H2O and always handled by PHREEQC. + orig_el_amount = sum([orig_el_dict[el][k] for k in orig_el_dict[el]]) + new_el_amount = sum([new_el_dict[el][k] for k in new_el_dict.get(el, [])]) + + # If this element went "missing", add back all components that + # contain this element (for any valence value) + if new_el_amount == 0 and orig_el_amount > 0: + logger.info( + f"PHREEQC discarded element {el} during equilibration. Adding all components for this element." + ) + solution.components.update( + { + component: self._stored_comp[component] + for components in orig_components_by_element[el].values() + for component in components + if component not in solution.components + } + ) + elif abs(orig_el_amount - new_el_amount) / orig_el_amount > _rtol: + logger.error( + f"PHREEQC returned a total Element {el} concentration of {new_el_amount} mol, " + f"which differs from the original concentration of {orig_el_amount}. This " + "should never occur and indicates an error in the PHREEQC database or calculation." + ) # re-adjust charge balance for any missing species # note that if balance_charge is set, it will have been passed to PHREEQC, so the only reason to re-adjust charge balance here is to account for any missing species. From 31b808e75fdfefe121d428e7b8729f5c31b1bf41 Mon Sep 17 00:00:00 2001 From: Ryan Date: Mon, 16 Feb 2026 16:15:48 -0500 Subject: [PATCH 2/3] equilibrate: revert H, O exclusion; move volume rescaling up --- src/pyEQL/engines.py | 47 ++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/src/pyEQL/engines.py b/src/pyEQL/engines.py index b5282fab..1b6fbc56 100644 --- a/src/pyEQL/engines.py +++ b/src/pyEQL/engines.py @@ -366,30 +366,29 @@ def equilibrate( new_el_dict = solution.get_el_amt_dict(nested=True) for el in orig_el_dict: - if el not in ['H', 'O']: # skip H and O since they are part of H2O and always handled by PHREEQC. - orig_el_amount = sum([orig_el_dict[el][k] for k in orig_el_dict[el]]) - new_el_amount = sum([new_el_dict[el][k] for k in new_el_dict.get(el, [])]) - - # If this element went "missing", add back all components that - # contain this element (for any valence value) - if new_el_amount == 0 and orig_el_amount > 0: - logger.info( - f"PHREEQC discarded element {el} during equilibration. Adding all components for this element." - ) - solution.components.update( - { - component: self._stored_comp[component] - for components in orig_components_by_element[el].values() - for component in components - if component not in solution.components - } - ) - elif abs(orig_el_amount - new_el_amount) / orig_el_amount > _rtol: - logger.error( - f"PHREEQC returned a total Element {el} concentration of {new_el_amount} mol, " - f"which differs from the original concentration of {orig_el_amount}. This " - "should never occur and indicates an error in the PHREEQC database or calculation." - ) + orig_el_amount = sum([orig_el_dict[el][k] for k in orig_el_dict[el]]) + new_el_amount = sum([new_el_dict[el][k] for k in new_el_dict.get(el, [])]) + + # If this element went "missing", add back all components that + # contain this element (for any valence value) + if new_el_amount == 0 and orig_el_amount > 0: + logger.info( + f"PHREEQC discarded element {el} during equilibration. Adding all components for this element." + ) + solution.components.update( + { + component: self._stored_comp[component] + for components in orig_components_by_element[el].values() + for component in components + if component not in solution.components + } + ) + elif abs(orig_el_amount - new_el_amount) / orig_el_amount > _rtol: + logger.error( + f"PHREEQC returned a total Element {el} concentration of {new_el_amount} mol, " + f"which differs from the original concentration of {orig_el_amount}. This " + "should never occur and indicates an error in the PHREEQC database or calculation." + ) # re-adjust charge balance for any missing species # note that if balance_charge is set, it will have been passed to PHREEQC, so the only reason to re-adjust charge balance here is to account for any missing species. From ad350822ba7d830e8087d40fb46200818a21c68a Mon Sep 17 00:00:00 2001 From: Ryan Date: Mon, 16 Feb 2026 16:16:43 -0500 Subject: [PATCH 3/3] equilibrate: revert H, O exclusion; move volume rescaling up --- src/pyEQL/engines.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/pyEQL/engines.py b/src/pyEQL/engines.py index 1b6fbc56..93abea09 100644 --- a/src/pyEQL/engines.py +++ b/src/pyEQL/engines.py @@ -359,6 +359,16 @@ def equilibrate( "by PHREEQC. These species are likely absent from its database." ) + # rescale the solvent mass to ensure the total mass of solution does not change + # this is important because PHREEQC and the pyEQL database may use slightly different molecular + # weights for water. Since water amount is passed to PHREEQC in kg but returned in moles, each + # call to equilibrate can thus result in a slight change in the Solution mass. + # NOTE - a second reason for doing this here is that the PHREEQC2026 wrapper does not include + # H2O(aq) in the list of components. pyEQL adds it back in the line below. If this is not done + # before the "missing element" check, then it can cause false positive errors because it will + # appear that H and O have "disappeared" from the solution. + solution.components[solution.solvent] = orig_solvent_moles + # tolerance (in moles) for detecting cases where an element amount # is no longer balanced because of species that are not recognized # by PHREEQC. @@ -394,11 +404,6 @@ def equilibrate( # note that if balance_charge is set, it will have been passed to PHREEQC, so the only reason to re-adjust charge balance here is to account for any missing species. solution._adjust_charge_balance() - # rescale the solvent mass to ensure the total mass of solution does not change - # this is important because PHREEQC and the pyEQL database may use slightly different molecular - # weights for water. Since water amount is passed to PHREEQC in kg but returned in moles, each - # call to equilibrate can thus result in a slight change in the Solution mass. - solution.components[solution.solvent] = orig_solvent_moles def get_activity_coefficient(self, solution: "solution.Solution", solute: str) -> ureg.Quantity: """