Skip to content

Fix get salt dict type#258

Merged
rkingsbury merged 85 commits intoKingsburyLab:mainfrom
ugognw:fix-get-salt-dict-type
Aug 8, 2025
Merged

Fix get salt dict type#258
rkingsbury merged 85 commits intoKingsburyLab:mainfrom
ugognw:fix-get-salt-dict-type

Conversation

@ugognw
Copy link
Copy Markdown
Contributor

@ugognw ugognw commented Jun 24, 2025

Summary

Major changes:

closes #250

  • Fixes type annotations in Solution.get_salt_dict
  • minor refactoring to resolve mypy errors
  • update docstring to represent changes
  • changes return value of Solution.get_salt_dict to store Salt objects under "salt" keys
  • modifies Solution.get_salt_dict to no longer return an entry for water
  • modifies Solution.get_salt to return None if nothing returned from Solution.get_salt_dict
  • fixes a few bugs in Solution.get_salt_dict
  • fixes solution addition bugs
  • fixes bugs in NativeEOS methods
  • Introduces comprehensive unit test framework for testing

Todos

  • update changelog
  • decide on return type
  • add unit tests
    • check that Salt object can be instantiated from dictionaries in Salt.from_dict
    • check that values corresponding to the 'mol' key are floats
  • reduce test parametrization

Checklist

  • Google format doc strings added.
  • Code linted with ruff. (For guidance in fixing rule violates, see rule list)
  • Type annotations included. Check with mypy.
  • Tests added for new features/fixes.
  • I have run the tests locally and they passed.

Note that I have changed the docstring in this patch to reflect the current function behaviour. The current patch still maps salt formulas to a dictionary that can be used to construct a representative Salt object. However, the previous docstring states that the return value maps salt formulas to Salt objects. Further, that this is the desired behaviour has been mentioned previously. So, I think this a good place to address this before the function's previous intent is disappeared into the ghost of git log's past. Hopefully, this can be resolved in this PR.

The current behaviour

To the output of Salt.as_dict is added to the 'mol' key which maps to the amount (in moles) of the salt. As a result, the dictionary cannot be directly passed to Salt.from_dict due to the fact that the MSONable methods are autogenerated from the constructor:

>>> from pyEQL import Solution
>>> from pyEQL.salt_ion_match import Salt
>>> s1 = Solution()
>>> salt_dict = s1.get_salt_dict()
>>> Salt.from_dict(salt_dict['HOH'])
...
TypeError: Salt.__init__() got an unexpected keyword argument 'mol'

Instead, to robustly obtain a Salt object, something awkward like this must be done:

>>> Salt.from_dict({k: v for k, v in salt_dict["HOH"] if k != 'mol'})
<pyEQL.salt_ion_match.Salt object at ...>

or:

>>> amount = salt_dict["HOH"].pop('mol')
>>> Salt.from_dict(salt_dict["HOH"])
<pyEQL.salt_ion_match.Salt object at ...>

Although so long as the Salt constructor requires only cation and anion as arguments, one can just do

>>> Salt.from_dict(salt_dict["HOH"]["cation"], salt_dict["HOH"]["anion"])
<pyEQL.salt_ion_match.Salt object at ...>

Proposed solutions for desired behaviour

Modify Salt to include property tracking amount

class Salt(MSONable):
    ...
    def __init__(self, cation, anion, *, mol: float | None = None) -> None:...
        ... 
        self.amount = mol
        ....

No changes are required to Salt.get_salt_dict.

I'm not in love with the choice of the parameter name 'mol' here, but it's chosen to minimize the amount of refactoring required since the amount is stored under the 'mol' key by Solution.get_salt_dict. Alternatively, one could do something like

class Salt(MSONable):
    ...
    def __init__(self, cation, anion, *, amount: float | None = None, **kwargs) -> None:...
        ... 
        self.amount = amount or kwargs.get('mol')
        ...

Note that this doesn't quite achieve the desired behaviour since the dictionary would still map salt formulas to dictionaries, but this is intended so as to minimize breaking things for anything that accesses the data as a dictionary before conversion to a Salt object. Overall, it simplifies the construction of Salt objects from the output of Solution.get_salt_dict(). The dictionaries can immediately be converted to Salt objects with Salt.from_dict:

>>> Salt.from_dict(salt_dict['HOH"])
<pyEQL.salt_ion_match.Salt object at ...>. # no TypeError!

I like this, but this may be muddying the constructor for Salt, and expanding the Salt model with an amount attribute may be outside of the scope of what the class is meant to represent. Another alternative to this is to directly store the new Salt object in the salt dict; however, doing so induces a breaking change.

Store the Salt object under its own key

Otherwise, the Salt object could be nested under its own key:

class Solution(MSONable):
    ...
    def get_salt_dict(self) -> dict[str, dict[str, str] | dict[str, float]]:
        ...
        x = Salt(...)
        ...
        salt_dict.update({x.formula: {'salt': x}}). # remove call to Salt.as_dict()
        ...

So that the output of Salt.get_salt_dict is something like:

{'HOH': {'salt': {'@module': 'pyEQL.salt_ion_match',
        '@class': 'Salt',
        '@version': ...,
        'cation': 'H[+1]',
        'anion': 'OH[-1]'},
        'mol': 1e-07}}

Then, retrieving a given Salt is robust and simple:

>>> salt_dict['HOH']['salt']
<pyEQL.salt_ion_match.Salt object at ...>

The 'mol' key may be accessed as normal. But this is a major breaking change even for the library itself. Nearly everywhere Salt.get_salt_dict is used in pyEQL, the keys anion and cation are accessed prior to conversion into a Salt object, so this change would require refactoring everywhere.

Change the type of the values in the returned dictionary

Alternatively, the salt formulas could map to a tuple (salt, amount):

class Solution(MSONable):
    ...
    def get_salt_dict(self) -> dict[str, tuple[dict[str, str], float]]:
        ...
        salt_dict.update({x.formula: (x, amount)}).
        ...

Retrieving the Salt

>>> salt_dict['HOH'][0]
<pyEQL.salt_ion_match.Salt object at ...>

Again, this is a breaking change. But it has the benefit of keeping the amount and Salt entities programmatically distinct.

If none of these options are appealing, then this patch includes edits to the docstring compatible with the current behaviour.

ugognw added 2 commits June 23, 2025 17:15
The type hints are now consistent within the function and return signature.

The edits to _adjust_charge_balance and __add__ were triggered by the ruff formatter.

Signed-off-by: Ugochukwu Nwosu <ugn@sfu.ca>
Reordering the factors in the product changes how mypy validates types.
"Any" (the return type of Solution._get_property) has all methods supports
all types as arguments.

The return of Solution.get_amount is a Quantity, so the magnitude of the
number of moles must be extracted.

Signed-off-by: Ugochukwu Nwosu <ugn@sfu.ca>
@codecov
Copy link
Copy Markdown

codecov bot commented Jun 24, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 83.58%. Comparing base (0a36ec6) to head (a0cc4c1).
⚠️ Report is 9 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #258      +/-   ##
==========================================
+ Coverage   81.81%   83.58%   +1.76%     
==========================================
  Files           9        9              
  Lines        1496     1456      -40     
  Branches      258      248      -10     
==========================================
- Hits         1224     1217       -7     
+ Misses        226      210      -16     
+ Partials       46       29      -17     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ugognw ugognw force-pushed the fix-get-salt-dict-type branch from c5b3a28 to 12b7b00 Compare June 24, 2025 00:16
Signed-off-by: Ugochukwu Nwosu <ugn@sfu.ca>
@ugognw ugognw force-pushed the fix-get-salt-dict-type branch from 12b7b00 to 8b8acf6 Compare June 24, 2025 02:24
@rkingsbury
Copy link
Copy Markdown
Member

Awesome work @ugognw , thank you for digging into this.

After reviewing again how get_salt_dict() is used, I think the most straightforward way to address is to store the Salt under its own key, as you suggest in approach No. 2. This will only require refactoring in 2 places in engines.py (assuming we deprecated list_salts - see below) and has the benefit that the Salt object does not have to be serialized and re-built when get_activity_coefficient is called. I think all you need to do is change, e.g.,

v["cation"]

to

v["salt"].cation

where v is a Salt object.

Separately, since list_salts has been marked for deprecation for a long time now. please go ahead and remove it as part of this PR.

Finally, please update the CHANGELOG to include your changes here and add yourself to AUTHORS.md! (Before the next release I will also update the changelog with additional recent changes)

ugognw and others added 12 commits June 27, 2025 16:05
Tests cover all public Solution methods which set values in Solution.components.

Signed-off-by: Ugochukwu Nwosu <ugn@sfu.ca>
The docstring has been updated as well.

Signed-off-by: Ugochukwu Nwosu <ugn@sfu.ca>
Signed-off-by: Ugochukwu Nwosu <ugn@sfu.ca>
Signed-off-by: Ugochukwu Nwosu <ugn@sfu.ca>
Note that there is also a change in the logic of NativeEOS.get_activity_coefficient.
Previously, when the for loop iterated over solution.get_salt_dict().values(),
the check `v == 'HOH'` was used. However, this condition would always evaluate to
false as v would have been a dictionary. This patch changes the conditional expression
to correctly compare the formula of the salt to 'HOH'. A regression test should also
be included.

Signed-off-by: Ugochukwu Nwosu <ugn@sfu.ca>
This patch reverts the behaviour of NativeEOS.get_activity_coefficient
such that activity coefficients can be recovered for H+ and OH-.

Signed-off-by: Ugochukwu Nwosu <ugognw@gmail.com>
Creating a test superclass prevents duplication of code.

The new type check condition (e.g., is float vs. isinstance(.., float)) ensures
that all values are of the same type.

Regression tests are also added for the Solution.get_salt_dict return values.

Signed-off-by: Ugochukwu Nwosu <ugognw@gmail.com>
In a previous commit (5fa5a31), the type checks on Solution.components
values and the values corresponding to the 'mol' keys in the return value
of Solution.get_salt_dict() were made stricter to fail with subclasses of float.
This was in anticipation of correcting a minor bug originating from the use of
numpy.float64's in certain functions (e.g., Solution.water_substance.rho,
activity_correction.get_apparent_volume_pitzer). For example,

>>> from pyEQL import Solution
>>> sol = Solution()
>>> sol.components
{'H2O(aq)': np.float64(55.34457595832858), 'OH[-1]': 1.0000000000000001e-07, 'H[+1]': 1e-07}
>>> sol.get_salt_dict()
{'HOH': {'salt': <pyEQL.salt_ion_match.Salt object at 0x111f32590>, 'mol': np.float64(55.34457595832858)}}

That is, the number of moles of the solvent is stored as a numpy.float64, but the number of moles of any
other component is stored as an ordinary Python float. This is somewhat harmless since
numpy.float64's are subclasses of Python floats, but it might be unexpected. In any case,
correcting this behaviour is beyond the scope of the current changes and requires
changes in many modules, so the unit tests checking for this behaviour have been removed.

Signed-off-by: Ugochukwu Nwosu <ugognw@gmail.com>
Signed-off-by: Ugochukwu Nwosu <ugognw@gmail.com>
Signed-off-by: Ugochukwu Nwosu <ugognw@gmail.com>
Signed-off-by: Ugochukwu Nwosu <ugognw@gmail.com>
Signed-off-by: Ugochukwu Nwosu <ugognw@gmail.com>
ugognw added 2 commits June 28, 2025 23:53
test_should_store_mol_as_floats now tests every branch in which the 'mol' key is set.

Signed-off-by: Ugochukwu Nwosu <ugognw@gmail.com>
The concentration units have no bearing on how the 'mol' key is set.

Signed-off-by: Ugochukwu Nwosu <ugognw@gmail.com>
@ugognw ugognw force-pushed the fix-get-salt-dict-type branch from 2b9d39c to f6de3eb Compare June 29, 2025 06:53
@ugognw
Copy link
Copy Markdown
Contributor Author

ugognw commented Jun 29, 2025

Thanks! Okay, this should be nearly good to go pending your feedback on the changes in NativeEOS.get_activity_coefficient

Copy link
Copy Markdown
Member

@rkingsbury rkingsbury left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking great @ugognw . I made a comment to answer your HOH question and did some wordsmithing on the docstring.

ugognw and others added 4 commits June 30, 2025 10:35
This reverts commit 07906e1.

Signed-off-by: Ugochukwu Nwosu <ugn@sfu.ca>
Signed-off-by: Ugochukwu Nwosu <ugn@sfu.ca>
A number of unit tests for Solution.get_salt_dict are introduced to improve
method coverage to 100%. Tests are parametrized to cover combinations of
uni-, di-, and trivalent ions as well as empty solutions.

test_should_not_include_water is tentatively included pending the proposed
change to Solution.get_salt_dict (i.e., ignore water).

The following tests are failing due to bugs in the code:
- test_should_order_salts_by_amount
- test_should_not_return_salts_with_concentration_below_cutoff

Signed-off-by: Ugochukwu Nwosu <ugognw@gmail.com>
Pytest fixtures have been renamed for clarity, and type hints have been
corrected.

Parametrization over solution volumes has been added to enable unit testing of
a particular fail-mode of get_salt_dict and its implementation of the "cutoff"
parameter: get_salt_dict calculates ion equivalents based on
Solution.components-the number of moles of each component; however, "cutoff" is
in units of concentration. It follows that "cutoff" may not be strictly
respected when Solution.volume deviates from 1 L.

TestSaltDictTypes tests have been parametrized over cation_scale to ensure that
the values corresponding to the 'salt' and 'mol' keys are correctly set in each
of the three 'if' branches within the while loop of get_salt_dict.

Fixtures have been refactored to explicitly cast types to appease mypy.

"reasons" have been added to pytest.mark.xfail statements for posterity.

New test: test_should_calculate_correct_concentration_for_salts

Signed-off-by: Ugochukwu Nwosu <ugn@sfu.ca>
@rkingsbury
Copy link
Copy Markdown
Member

Summary of Most Recent Changes

In trying to resolve failing tests and writing additional unit tests, I found several apparent bugs and subtle changes due to this patch. The main takeaways are summarized below:

Outstanding work. I am really glad you found the bug about engine not getting inherited on summation. I remember being very careful to implement that inheritance in from_dict / as_dict and I must have just overlooked in the summation stuff.

Interestingly, it seems like the test failure you're getting with MgCl2 is because get_salt is not correctly identifying the salt

[2025-07-11 13:43:42,149] [   ERROR] --- No salts found that contain solute Mg+2. Returning unit activity coefficient. (engines.py:327)
[2025-07-11 13:43:42,183] [   ERROR] --- No salts found that contain solute Cl-. Returning unit activity coefficient. (engines.py:327)

@rkingsbury
Copy link
Copy Markdown
Member

Also, can you point me to a resource to better understand conftest.py? I see you use it to parameterize a bunch of fixtures for use in other test files. Is there any opportunity to consolidate some of the fixtures in the salt matching tests with the ones defined in conftest.py?

@ugognw
Copy link
Copy Markdown
Contributor Author

ugognw commented Jul 11, 2025

Also, can you point me to a resource to better understand conftest.py?

👉🏾 conftest.py reference

Is there any opportunity to consolidate some of the fixtures in the salt matching tests with the ones defined in [conftest.py](http://conftest.py/)?

Yes, I think there is. There is also duplication in test_activity.py, test_osmotic_coeff.py, and test_solution.py that I'll address.

@ugognw
Copy link
Copy Markdown
Contributor Author

ugognw commented Jul 11, 2025

Outstanding work. I am really glad you found the bug about engine not getting inherited on summation. I remember being very careful to implement that inheritance in from_dict / as_dict and I must have just overlooked in the summation stuff.

It's a sneaky one too. I only found it due to failing tests in test_mixing_functions.py–completely unrelated.

Interestingly, it seems like the test failure you're getting with MgCl2 is because get_salt is not correctly identifying the salt

Yes, after calling Solution.equilibrate, the concentrations of Mg[+2] and Cl[-1] decrease slightly due to speciation (into e.g., MgOH[+1], MgCl[+1], ClO[-1], etc.). The concentration of MgCl2 as determined by Solution.get_salt_dict falls just ~0.2 attomoles/kg below the cutoff.

ugognw added 2 commits July 11, 2025 16:41
Signed-off-by: Ugochukwu Nwosu <ugn@sfu.ca>
Signed-off-by: Ugochukwu Nwosu <ugn@sfu.ca>
@rkingsbury
Copy link
Copy Markdown
Member

Yes, after calling Solution.equilibrate, the concentrations of Mg[+2] and Cl[-1] decrease slightly due to speciation (into e.g., MgOH[+1], MgCl[+1], ClO[-1], etc.). The concentration of MgCl2 as determined by Solution.get_salt_dict falls just ~0.2 attomoles/kg below the cutoff.

So speciation actually shouldn't matter, because get_salt_dict should be using the TOTAL element concentrations (use_totals=True).

ugognw and others added 4 commits July 14, 2025 17:21
Signed-off-by: Ugochukwu Nwosu <ugn@sfu.ca>
Signed-off-by: Ugochukwu Nwosu <ugn@sfu.ca>
Signed-off-by: Ugochukwu Nwosu <ugognw@gmail.com>
@ugognw
Copy link
Copy Markdown
Contributor Author

ugognw commented Jul 15, 2025

So speciation actually shouldn't matter, because get_salt_dict should be using the TOTAL element concentrations (use_totals=True).

Ah, yes! I thought this was related to oxidation states, but it appears to be a rounding error (?) introduced by speciation.

>>> s1 = Solution(solutes={'Mg[+2]': '1e-3 mol/kg', 'Cl[-1]': '2e-3 mol/kg'})
>>> before = s1.get_total_amount('Cl(-1.0)', 'mol').m
>>> s1.equilibrate()
>>> after = s1.get_total_amount('Cl(-1.0)', 'mol').m
>>> after - before
np.float64(–4.336808689942018e-19)

This corresponds to the ~0.2 attomoles/kg decrease in MgCl2 concentration. This seems to be a problem with NativeEOS.equilibrate. As a fix, I've introduced a small tolerance (~1e-16) around the cutoff in get_salt_dict. Tests are now passing.

On a side note:

get_salt_dict should be using the TOTAL element concentrations (use_totals=True).

But different oxidation states of the same element are to be treated different, yes? For example, in a solution of 1 M NaCl and 0.5 M NaClO4, should entries for both NaCl and NaClO4 be returned by get_salt_dict? I've just added tests assuming the affirmative.

Tests are parametrized over all combinations of pairs of Cl (oxy-)anions
and the first 3 cations in the module-level variable, _CATIONS.

Signed-off-by: Ugochukwu Nwosu <ugognw@gmail.com>
@rkingsbury
Copy link
Copy Markdown
Member

But different oxidation states of the same element are to be treated different, yes? For example, in a solution of 1 M NaCl and 0.5 M NaClO4, should entries for both NaCl and NaClO4 be returned by get_salt_dict? I've just added tests assuming the affirmative.

Correct! Although get_total_amount can operate both ways, the default behavior is to return totals by oxidation state, which is the appropriate behavior in the context of salt matching. See the docstring.

Copy link
Copy Markdown
Member

@rkingsbury rkingsbury left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great @ugognw . I'm ready to merge pending a few small comments - let me know if you have questions. Thank you again for your diligent work on this!

cation_list = [[k, v] for k, v in cation_equiv.items()]
anion_list = [[k, v] for k, v in anion_equiv.items()]
solvent_mass = self.solvent_mass.to("kg").m
_atol = 1e-16
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super minor comment, for future maintainability - can you move the definition of _atol down close to where it is used (c. line 1646) and add a comment explaining what it does? As I understand it, this is a numerical tolerance to account for edge cases where calling equilibrate slightly changes the total amount of an element.

Comment on lines 2554 to +2583
pE=mix_pE,
engine=self._engine,
solvent=self.solvent,
database=self.database,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If not too much trouble, can you please move this fix into a separate PR? Since it's a distinct issue that's being resolved, that keeps the history a bit cleaner and easier to track down issues.

Comment on lines +671 to +672
@pytest.mark.parametrize(("salt_conc_units", "salt_conc", "engine"), [("mol/kg", 1e-4, "native")])
def test_should_return_unit_activity_for_all_solutes_when_salt_concentration_below_cutoff(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With my other comment, this concentration will need to be lowered to 1e-10

Comment on lines +1525 to +1527
Args:
cutoff: Lowest molal concentration to consider. No salts below this value will be included in the output.
Useful for excluding analysis of trace anions. Defaults to 1e-3.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please lower the default cutoff to 1e-9 (1 part per billion). I know that seems extremely low, but the Debye-Huckel model will return an activity coefficient different from 1 (0.99996) even at this low level. I think 1 ppb makes sense as a "practical lower limit" below which it isn't really sensible to talk about an activity coefficient different from one.

If you encounter numerical or performance problems with that low a cutoff, we can use 1e-6 (1ppm ) instead, using the same logic.

>>> I = pyEQL.ureg('1e-9 mol/kg')
>>> pyEQL.activity_correction.get_activity_coefficient_debyehuckel(I)
<Quantity(0.9999628818205379, 'dimensionless')>

@rkingsbury
Copy link
Copy Markdown
Member

Hi @ugognw just checking in. I'm ready to get this merged. I see from the "thumbs ups" that you were planning to take care of my remaining small changes; will you be able to do it soon? If not, let me know and we can possibly jump in.

You should know that your work has already had an impact. Last week our group got an inquiry from an engineer at a major water services company who is using pyEQL and noticed some discrepancies in osmotic coefficients for (NH4)2SO4. Your PR fixed it, and they were able to move on with their analysis! So thank you again for the great work spotting that stoichiometry bug.

Finally, if you're interested, I'd be happy to meet with you on Zoom to learn a little more about the work you're doing with pyEQL. Feel free to email me kingsbury princeton <.> edu

@rkingsbury rkingsbury added enhancement fix Bug Fixes breaking includes breaking changes labels Aug 8, 2025
@rkingsbury
Copy link
Copy Markdown
Member

Thank you again for the amazing work here @ugognw ! I'm going to go ahead and merge. There is a new test failure that was introduced by my changes in #201 , which I will address (along with the minor edits I requested before) in a separate PR shortly.

Closes #250

@rkingsbury rkingsbury closed this Aug 8, 2025
@rkingsbury rkingsbury reopened this Aug 8, 2025
@rkingsbury rkingsbury merged commit 96bd5c5 into KingsburyLab:main Aug 8, 2025
6 of 19 checks passed
@ugognw
Copy link
Copy Markdown
Contributor Author

ugognw commented Aug 13, 2025

Hi @ugognw just checking in. I'm ready to get this merged. I see from the "thumbs ups" that you were planning to take care of my remaining small changes; will you be able to do it soon? If not, let me know and we can possibly jump in.

Hi, sorry, for the delay! A few deadlines pulled me away from this in recent weeks. I do plan on finalizing the remaining edits. From what I can recall, the outstanding includes:

  • consolidate solute parametrization in test fixtures (contest.py, test_activity.py, test_osmotic_coeff.py, and test_solution.py)
  • ensure that concentration tolerances are consistent where changed (1e-10)
  • redefine _atol closer to its use in Solution.get_salt_dict

I'll open a separate PR to finalize.

You should know that your work has already had an impact. Last week our group got an inquiry from an engineer at a major water services company who is using pyEQL and noticed some discrepancies in osmotic coefficients for (NH4)2SO4. Your PR fixed it, and they were able to move on with their analysis! So thank you again for the great work spotting that stoichiometry bug.

Oh, that's great to hear!

Finally, if you're interested, I'd be happy to meet with you on Zoom to learn a little more about the work you're doing with pyEQL. Feel free to email me kingsbury princeton <.> edu

Will do!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking includes breaking changes enhancement fix Bug Fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Type hint in Solution.get_salt_dict is inconsistent

2 participants