-
Notifications
You must be signed in to change notification settings - Fork 15
Comparison tools for UnitScalars/UnitArrays #85
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jonathanrocher
wants to merge
6
commits into
main
Choose a base branch
from
feature/unit_scal_arr_comparisons
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 3 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
6e5b3b4
Add assertion and test functions to compare 2 UnitScalars or 2 UnitAr…
jonathanrocher 9279ca7
Flake8
jonathanrocher a609341
Clean up some tests.
jonathanrocher 3692c4f
Simplify the folder structure and import path.
jonathanrocher 640b0cb
Change precision argument meaning from absolute to relative. Add a sh…
jonathanrocher fbbc66e
One more conversion eps -> rtol, and update test module.
jonathanrocher File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| """ Utilities around unit comparisons. | ||
| """ | ||
| import numpy as np | ||
|
|
||
| from scimath.units.api import convert, UnitArray, UnitScalar | ||
| from scimath.units.unit import InvalidConversion | ||
|
|
||
|
|
||
| def unit_scalars_almost_equal(x1, x2, eps=1e-9): | ||
| """ Returns whether 2 UnitScalars are almost equal. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| x1 : UnitScalar | ||
| First unit scalar to compare. | ||
|
|
||
| x2 : UnitScalar | ||
| Second unit scalar to compare. | ||
|
|
||
| eps : float | ||
| Absolute precision of the comparison. | ||
| """ | ||
| if not isinstance(x1, UnitScalar): | ||
| msg = "x1 is supposed to be a UnitScalar but a {} was passed." | ||
| msg = msg.format(type(x1)) | ||
| raise ValueError(msg) | ||
|
|
||
| if not isinstance(x2, UnitScalar): | ||
| msg = "x2 is supposed to be a UnitScalar but a {} was passed." | ||
| msg = msg.format(type(x2)) | ||
| raise ValueError(msg) | ||
|
|
||
| a1 = float(x1) | ||
| try: | ||
| a2 = convert(float(x2), from_unit=x2.units, to_unit=x1.units) | ||
| except InvalidConversion: | ||
| return False | ||
| return np.abs(a1 - a2) < eps | ||
|
|
||
|
|
||
| def unit_arrays_almost_equal(uarr1, uarr2, eps=1e-9): | ||
| """ Returns whether 2 UnitArrays are almost equal. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| uarr1 : UnitArray | ||
| First unit array to compare. | ||
|
|
||
| uarr2 : UnitArray | ||
| Second unit array to compare. | ||
|
|
||
| eps : float | ||
| Absolute precision of the comparison. | ||
| """ | ||
| if not isinstance(uarr1, UnitArray): | ||
| msg = "uarr1 is supposed to be a UnitArray but a {} was passed." | ||
| msg = msg.format(type(uarr1)) | ||
| raise ValueError(msg) | ||
|
|
||
| if not isinstance(uarr2, UnitArray): | ||
| msg = "uarr2 is supposed to be a UnitArray but a {} was passed." | ||
| msg = msg.format(type(uarr2)) | ||
| raise ValueError(msg) | ||
|
|
||
| a1 = np.array(uarr1) | ||
| try: | ||
| a2 = convert(np.array(uarr2), from_unit=uarr2.units, | ||
| to_unit=uarr1.units) | ||
| except InvalidConversion: | ||
| return False | ||
| return np.all(np.abs(a1 - a2) < eps) | ||
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| # -*- coding: utf-8 -*- | ||
| """ Utilities providing assertions to support unit tests involving UnitScalars | ||
| and UnitArrays. | ||
| """ | ||
| from nose.tools import assert_false, assert_true | ||
|
|
||
| from scimath.units.compare_units import unit_arrays_almost_equal, \ | ||
| unit_scalars_almost_equal | ||
|
|
||
|
|
||
| def assert_unit_scalar_almost_equal(val1, val2, eps=1.e-9, msg=None): | ||
| if msg is None: | ||
| msg = "{} and {} are not almost equal with precision {}" | ||
| msg = msg.format(val1, val2, eps) | ||
|
|
||
| assert_true(unit_scalars_almost_equal(val1, val2, eps=eps), msg=msg) | ||
|
|
||
|
|
||
| def assert_unit_scalar_not_almost_equal(val1, val2, eps=1.e-9, msg=None): | ||
| if msg is None: | ||
| msg = "{} and {} unexpectedly almost equal with precision {}" | ||
| msg = msg.format(val1, val2, eps) | ||
|
|
||
| assert_false(unit_scalars_almost_equal(val1, val2, eps=eps), msg=msg) | ||
|
|
||
|
|
||
| def assert_unit_array_almost_equal(uarr1, uarr2, eps=1e-9, msg=None): | ||
| if msg is None: | ||
| msg = "{} and {} are not almost equal with precision {}" | ||
| msg = msg.format(uarr1, uarr2, eps) | ||
|
|
||
| assert_true(unit_arrays_almost_equal(uarr1, uarr2, eps=eps), msg=msg) | ||
|
|
||
|
|
||
| def assert_unit_array_not_almost_equal(uarr1, uarr2, eps=1e-9, msg=None): | ||
| if msg is None: | ||
| msg = "{} and {} are almost equal with precision {}" | ||
| msg = msg.format(uarr1, uarr2, eps) | ||
|
|
||
| assert_false(unit_arrays_almost_equal(uarr1, uarr2, eps=eps), msg=msg) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| from unittest import TestCase | ||
|
|
||
| from scimath.units.api import UnitArray, UnitScalar | ||
| from scimath.units.testing.assertion_utils import \ | ||
| assert_unit_array_almost_equal, assert_unit_scalar_almost_equal | ||
|
|
||
|
|
||
| class TestAssertUnitScalarEqual(TestCase): | ||
| def test_same_unit_scalar(self): | ||
| assert_unit_scalar_almost_equal(UnitScalar(1, units="s"), | ||
| UnitScalar(1, units="s")) | ||
|
|
||
| def test_equivalent_unit_scalar(self): | ||
| assert_unit_scalar_almost_equal(UnitScalar(1, units="m"), | ||
| UnitScalar(100, units="cm")) | ||
|
|
||
|
|
||
| class TestAssertUnitArrayEqual(TestCase): | ||
| def test_same_unit_array(self): | ||
| assert_unit_array_almost_equal(UnitArray([1, 2], units="s"), | ||
| UnitArray([1, 2], units="s")) | ||
|
|
||
| def test_equivalent_unit_array(self): | ||
| assert_unit_array_almost_equal(UnitArray([1, 2], units="m"), | ||
| UnitArray([100, 200], units="cm")) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| from unittest import TestCase | ||
|
|
||
| from scimath.units.api import dimensionless, UnitArray, UnitScalar | ||
| from scimath.units.compare_units import unit_arrays_almost_equal, \ | ||
| unit_scalars_almost_equal | ||
|
|
||
|
|
||
| class TestUnitScalarAlmostEqual(TestCase): | ||
| def test_values_identical(self): | ||
| val1 = UnitScalar(1., units="m") | ||
| self.assertTrue(unit_scalars_almost_equal(val1, val1)) | ||
|
|
||
| def test_wrong_type1(self): | ||
| val1 = 1 | ||
| val2 = UnitScalar(1., units="m") | ||
| with self.assertRaises(ValueError): | ||
| unit_scalars_almost_equal(val1, val2) | ||
|
|
||
| def test_wrong_type2(self): | ||
| val1 = UnitScalar(1., units="m") | ||
| val2 = 1 | ||
| with self.assertRaises(ValueError): | ||
| unit_scalars_almost_equal(val1, val2) | ||
|
|
||
| def test_values_identical_in_diff_units(self): | ||
| val1 = UnitScalar(1., units="m") | ||
| val2 = UnitScalar(100., units="cm") | ||
| self.assertTrue(unit_scalars_almost_equal(val1, val2)) | ||
|
|
||
| def test_dimensionless(self): | ||
| val1 = UnitScalar(1., units=dimensionless) | ||
| val2 = UnitScalar(1., units="cm") | ||
| self.assertFalse(unit_scalars_almost_equal(val1, val2)) | ||
|
|
||
| def test_2_dimensionless(self): | ||
| val1 = UnitScalar(1., units=dimensionless) | ||
| val2 = UnitScalar(1., units="BLAH") | ||
| val3 = UnitScalar(100., units="BLAH") | ||
| self.assertTrue(unit_scalars_almost_equal(val1, val1)) | ||
| self.assertTrue(unit_scalars_almost_equal(val1, val2)) | ||
| self.assertFalse(unit_scalars_almost_equal(val1, val3)) | ||
|
|
||
| def test_values_close_enough(self): | ||
| val1 = UnitScalar(1., units="m") | ||
| val2 = val1 + UnitScalar(1.e-5, units="m") | ||
| self.assertFalse(unit_scalars_almost_equal(val1, val2)) | ||
| self.assertTrue(unit_scalars_almost_equal(val1, val2, eps=1e-4)) | ||
|
|
||
|
|
||
| class TestUnitArraysAlmostEqual(TestCase): | ||
| def test_wrong_type1(self): | ||
| val1 = 1 | ||
| val2 = UnitArray([1.], units="m") | ||
| with self.assertRaises(ValueError): | ||
| unit_arrays_almost_equal(val1, val2) | ||
|
|
||
| def test_wrong_type2(self): | ||
| val1 = UnitArray([1.], units="m") | ||
| val2 = 1 | ||
| with self.assertRaises(ValueError): | ||
| unit_arrays_almost_equal(val1, val2) | ||
|
|
||
| def test_values_identical(self): | ||
| val1 = UnitArray([1., 2.], units="m") | ||
| self.assertTrue(unit_arrays_almost_equal(val1, val1)) | ||
|
|
||
| def test_values_identical_in_diff_units(self): | ||
| val1 = UnitArray([1., 2.], units="m") | ||
| val2 = UnitArray([100., 200.], units="cm") | ||
| self.assertTrue(unit_arrays_almost_equal(val1, val2)) | ||
|
|
||
| def test_dimensionless(self): | ||
| val1 = UnitArray([1.], units=dimensionless) | ||
| val2 = UnitArray([1.], units="cm") | ||
| self.assertFalse(unit_arrays_almost_equal(val1, val2)) | ||
|
|
||
| def test_2_dimensionless(self): | ||
| val1 = UnitArray([1.], units=dimensionless) | ||
| val2 = UnitArray([1.], units="BLAH") | ||
| val3 = UnitArray([100.], units="BLAH") | ||
| self.assertTrue(unit_arrays_almost_equal(val1, val1)) | ||
| self.assertTrue(unit_arrays_almost_equal(val1, val2)) | ||
| self.assertFalse(unit_arrays_almost_equal(val1, val3)) | ||
|
|
||
| def test_values_close_enough(self): | ||
| val1 = UnitArray([1., 2.], units="m") | ||
| val2 = val1 + UnitArray([1.e-5, 1.e-6], units="m") | ||
| self.assertFalse(unit_arrays_almost_equal(val1, val2)) | ||
| self.assertTrue(unit_arrays_almost_equal(val1, val2, eps=1e-4)) | ||
|
|
||
| def test_values_not_close_enough(self): | ||
| val1 = UnitArray([1., 2.], units="m") | ||
| val3 = val1 + UnitArray([1.e-2, 1.e-6], units="m") | ||
| self.assertFalse(unit_arrays_almost_equal(val1, val3, eps=1e-4)) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we really want
epsto be a quantity of the same unit class asx1andx2here, and I'd suggest removing the default value and making it a required argument. It just doesn't make sense conceptually to ask whether two lengths (for example) are equal up to two decimal places, while it absolutely makes sense to ask whether two lengths are equal to within a margin of 2cm, or 20nm, or 1/8 inch, or ...It's particularly unclear what comparing to
1e-9would mean in the case thatx1andx2are comparable, but have different actual units, and the implementation here looks as though it would give asymmetric results:Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the valuable feedback @mdickinson . I totally agree with you on eps needing to be unitted, and for it to be a required argument. I would only suggest to allow for eps to be a float if the 2 values have the same unit: seems reasonable to you? I will push an update...
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd go further, and only allow use of a
floateps if both quantities being compared are actually unitless. Otherwise you're again comparing a unitful quantity with a unitless quantity, which seems like a category error to me. It would seem wrong to me if a givenunit_scalars_almost_equalcall passes for some lengthlength1and fails for alength2that represents exactly the same length (solength1 == length2givesTrue), but happens to use different units.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IOW, I'd want the check to be checking a property of the length itself, not a property of the representation of that length.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I see your point. At the same time, I would like to keep it simple to use. What you propose would lead to this for the usage
is quite verbose. What I meant to do with the current implementation is:
What about automatically treating a float
epslikeUnitScalar(eps, units=x.units)insideunit_scalars_almost_equal? Or changeepsto a non-dimensional "rtol" which would be compared tofloat(abs(x1-x2)/abs(x2))(after unit conversion) similar to https://docs.scipy.org/doc/numpy-1.13.0/reference/generated/numpy.allclose.html ?