Conversation
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 Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
c5b3a28 to
12b7b00
Compare
Signed-off-by: Ugochukwu Nwosu <ugn@sfu.ca>
12b7b00 to
8b8acf6
Compare
|
Awesome work @ugognw , thank you for digging into this. After reviewing again how to where Separately, since 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) |
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>
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>
2b9d39c to
f6de3eb
Compare
|
Thanks! Okay, this should be nearly good to go pending your feedback on the changes in |
rkingsbury
left a comment
There was a problem hiding this comment.
Looking great @ugognw . I made a comment to answer your HOH question and did some wordsmithing on the docstring.
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>
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 Interestingly, it seems like the test failure you're getting with |
|
Also, can you point me to a resource to better understand |
Yes, I think there is. There is also duplication in |
It's a sneaky one too. I only found it due to failing tests in
Yes, after calling |
Signed-off-by: Ugochukwu Nwosu <ugn@sfu.ca>
Signed-off-by: Ugochukwu Nwosu <ugn@sfu.ca>
So speciation actually shouldn't matter, because |
Signed-off-by: Ugochukwu Nwosu <ugn@sfu.ca>
Signed-off-by: Ugochukwu Nwosu <ugn@sfu.ca>
Signed-off-by: Ugochukwu Nwosu <ugognw@gmail.com>
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 On a side note:
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 |
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>
Correct! Although |
rkingsbury
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
| pE=mix_pE, | ||
| engine=self._engine, | ||
| solvent=self.solvent, | ||
| database=self.database, |
There was a problem hiding this comment.
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.
| @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( |
There was a problem hiding this comment.
With my other comment, this concentration will need to be lowered to 1e-10
| 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. |
There was a problem hiding this comment.
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')>
|
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 |
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:
I'll open a separate PR to finalize.
Oh, that's great to hear!
Will do! |
Summary
Major changes:
closes #250
Solution.get_salt_dictSolution.get_salt_dictto storeSaltobjects under"salt"keysSolution.get_salt_dictto no longer return an entry for waterSolution.get_saltto return None if nothing returned fromSolution.get_salt_dictSolution.get_salt_dictNativeEOSmethodsTodos
Saltobject can be instantiated from dictionaries inSalt.from_dict'mol'key are floatsChecklist
ruff. (For guidance in fixing rule violates, see rule list)mypy.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
Saltobject. However, the previous docstring states that the return value maps salt formulas toSaltobjects. 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_dictis added to the'mol'key which maps to the amount (in moles) of the salt. As a result, the dictionary cannot be directly passed toSalt.from_dictdue to the fact that theMSONablemethods are autogenerated from the constructor:Instead, to robustly obtain a
Saltobject, something awkward like this must be done:or:
Although so long as the
Saltconstructor requires onlycationandanionas arguments, one can just doProposed solutions for desired behaviour
Modify
Saltto include property tracking amountNo 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 bySolution.get_salt_dict. Alternatively, one could do something likeNote 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
Saltobject. Overall, it simplifies the construction ofSaltobjects from the output ofSolution.get_salt_dict(). The dictionaries can immediately be converted toSaltobjects withSalt.from_dict:I like this, but this may be muddying the constructor for
Salt, and expanding theSaltmodel with anamountattribute may be outside of the scope of what the class is meant to represent. Another alternative to this is to directly store the newSaltobject in the saltdict; however, doing so induces a breaking change.Store the
Saltobject under its own keyOtherwise, the
Saltobject could be nested under its own key:So that the output of
Salt.get_salt_dictis 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
Saltis robust and simple:The
'mol'key may be accessed as normal. But this is a major breaking change even for the library itself. Nearly everywhereSalt.get_salt_dictis used inpyEQL, the keysanionandcationare accessed prior to conversion into aSaltobject, 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):Retrieving the
SaltAgain, this is a breaking change. But it has the benefit of keeping the amount and
Saltentities programmatically distinct.If none of these options are appealing, then this patch includes edits to the docstring compatible with the current behaviour.