Skip to content
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

460 element equality #462

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

precicely
Copy link
Contributor

Element now has __eq__ method that works with numpy array as well a is_close method that useful for approximate equality testing.

Copy link

codecov bot commented Aug 31, 2024

Codecov Report

Attention: Patch coverage is 96.66667% with 1 line in your changes missing coverage. Please review.

Project coverage is 71.94%. Comparing base (f2dfa28) to head (5277edc).
Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
kwave/utils/kwave_array.py 96.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #462      +/-   ##
==========================================
+ Coverage   71.84%   71.94%   +0.09%     
==========================================
  Files          46       46              
  Lines        6744     6768      +24     
  Branches     1496     1506      +10     
==========================================
+ Hits         4845     4869      +24     
  Misses       1333     1333              
  Partials      566      566              
Flag Coverage Δ
3.10 72.14% <96.66%> (+0.09%) ⬆️
3.11 72.14% <96.66%> (+0.09%) ⬆️
3.12 72.14% <96.66%> (+0.09%) ⬆️
3.9 71.91% <96.66%> (+0.09%) ⬆️
macos-latest 71.88% <96.66%> (+0.10%) ⬆️
ubuntu-latest 71.91% <96.66%> (+0.09%) ⬆️
windows-latest 71.92% <96.66%> (+0.09%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

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

@waltsims
Copy link
Owner

waltsims commented Sep 1, 2024

Hi @precicely,

thanks for the PR. It looks great. Before I review the PR, it would be great if you could extend the test coverage for the new functionality.

Thanks,
Walter

@precicely
Copy link
Contributor Author

No problem @waltsims. I've tweaked the code a bit and improved the coverage.

Copy link
Owner

@waltsims waltsims left a comment

Choose a reason for hiding this comment

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

Thanks for the PR! Only minor changes.

Comment on lines +122 to +142
def __eq__(self, other):
"""Equality operator that handles all fields but specifically numpy
arrays.

:param other: an instance of Element
:raises ValueError: when other object is not an instance of Element
:return: bool
"""
if not isinstance(other, type(self)):
raise TypeError(f"{other} with {type(other)} is not of type Element")

for field in fields(self):
self_attr = getattr(self, field.name)
other_attr = getattr(other, field.name)
if isinstance(self_attr, np.ndarray):
if not np.array_equal(self_attr, other_attr):
return False
else:
if self_attr != other_attr:
return False
return True
Copy link
Owner

Choose a reason for hiding this comment

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

please move eq next to post_init to keep the dunder methods together.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will do.

Comment on lines +14 to +35
def test_element():
element1 = Element(group_id=0, type="rect", dim=2, active=True, measure=1, position=[1, 1, 1])
# element2 has a very similar position to element 1 but with a small numerical difference
element2 = Element(group_id=0, type="rect", dim=2, active=True, measure=1, position=[1, 1, 1.000001])
# element3 is the same as element 1 but belongs to a differnet group
element3 = Element(group_id=1, type="rect", dim=2, active=True, measure=2, position=[1, 1, 1])
# element4 is the same as element 1 but has a differnet position
element4 = Element(group_id=0, type="rect", dim=2, active=True, measure=2, position=[2, 2, 2])
# element 4 is not an element
element5 = "not an element"

assert element1 == element1
assert element1 != element2
assert element1.is_close(element2)
assert element1 != element3
assert not element1.is_close(element3)
assert element1 != element4
assert not element1.is_close(element4)
with pytest.raises(TypeError):
element1 != element5
with pytest.raises(TypeError):
element1.is_close(element5)
Copy link
Owner

Choose a reason for hiding this comment

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

Minor nitpick and not blocking: It would be great for clarity if you breakout the tests into their own functions. Here is some inspiration:

Suggested change
def test_element():
element1 = Element(group_id=0, type="rect", dim=2, active=True, measure=1, position=[1, 1, 1])
# element2 has a very similar position to element 1 but with a small numerical difference
element2 = Element(group_id=0, type="rect", dim=2, active=True, measure=1, position=[1, 1, 1.000001])
# element3 is the same as element 1 but belongs to a differnet group
element3 = Element(group_id=1, type="rect", dim=2, active=True, measure=2, position=[1, 1, 1])
# element4 is the same as element 1 but has a differnet position
element4 = Element(group_id=0, type="rect", dim=2, active=True, measure=2, position=[2, 2, 2])
# element 4 is not an element
element5 = "not an element"
assert element1 == element1
assert element1 != element2
assert element1.is_close(element2)
assert element1 != element3
assert not element1.is_close(element3)
assert element1 != element4
assert not element1.is_close(element4)
with pytest.raises(TypeError):
element1 != element5
with pytest.raises(TypeError):
element1.is_close(element5)
def test_element_is_close():
base_element = Element(group_id=0, type="rect", dim=2, active=True, measure=1, position=[1, 1, 1])
# Test cases
assert base_element.is_close(Element(group_id=0, type="rect", dim=2, active=True, measure=1, position=[1, 1, 1]))
assert base_element.is_close(Element(group_id=0, type="rect", dim=2, active=True, measure=1, position=[1, 1, 1.000001]))
assert not base_element.is_close(Element(group_id=1, type="rect", dim=2, active=True, measure=2, position=[1, 1, 1]))
assert not base_element.is_close(Element(group_id=0, type="rect", dim=2, active=True, measure=1, position=[2, 2, 2]))
assert not base_element.is_close(Element(group_id=0, type="rect", dim=2, active=True, measure=1, position=[1, 1, float('nan')]))
assert not base_element.is_close(Element(group_id=0, type="rect", dim=2, active=True, measure=1, position=[1, 1, float('inf')]))
assert not base_element.is_close(Element(group_id=0, type="annular", dim=2, active=True, measure=1, position=[1, 1, 1]))
def test_element_is_close_nan():
nan_element = Element(group_id=0, type="rect", dim=2, active=True, measure=1, position=[1, 1, float('nan')])
assert nan_element.is_close(nan_element, equal_nan=True)
assert not nan_element.is_close(nan_element, equal_nan=False)
def test_element_is_close_boundary():
base_element = Element(group_id=0, type="rect", dim=2, active=True, measure=1, position=[1, 1, 1])
boundary_element = Element(group_id=0, type="rect", dim=2, active=True, measure=1, position=[1, 1, 1 + 1.1e-5])
assert not base_element.is_close(boundary_element)
assert base_element.is_close(boundary_element, rtol=1.2e-5)
def test_element_is_close_consistency():
base_element = Element(group_id=0, type="rect", dim=2, active=True, measure=1, position=[1, 1, 1])
other_element = Element(group_id=0, type="rect", dim=2, active=True, measure=1, position=[1, 1, 1.000001])
assert base_element.is_close(other_element) == np.allclose(base_element.position, other_element.position)
assert base_element.is_close(other_element) == base_element.is_close(other_element, rtol=1e-05, atol=1e-08, equal_nan=False)
def test_element_is_close_type_error():
element = Element(group_id=0, type="rect", dim=2, active=True, measure=1, position=[1, 1, 1])
with pytest.raises(TypeError, match=r"not an element with <class 'str'> is not of type Element"):
element.is_close("not an element")
def test_element_equality():
element1 = Element(group_id=0, type="rect", dim=2, active=True, measure=1, position=[1, 1, 1])
element2 = Element(group_id=0, type="rect", dim=2, active=True, measure=1, position=[1, 1, 1.000001])
element3 = Element(group_id=1, type="rect", dim=2, active=True, measure=2, position=[1, 1, 1])
assert element1 == element1
assert element1 != element2
assert element1 != element3

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will do.

Comment on lines +94 to +96
"""Compares 2 Elements to a certain precision for their numerical
fields. This differs from the __eq__ method which requires a perfect
match between numerical fields. Works with ints, floats and numpy
Copy link
Owner

Choose a reason for hiding this comment

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

Please specify behavior for non-numerical fields as well in docstring. Namely, is_close allows numerical values to vary within the rtol but only when non-numerical fields are the same. When is_close returns False, it could be due to variations in numerical fields beyond rtol, OR because other properties differ.

Copy link
Owner

Choose a reason for hiding this comment

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

is_approximately_equal might be a more accurate name.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will do. The thinking with is_close was that it mirrors numpy's allclose and is_close but happy to change it to is_approximately_equal.

Copy link
Owner

Choose a reason for hiding this comment

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

Please move Element tests to a new file directly under tests. This directory is for the comparison of Python outputs and collected outputs from MATLAB.

thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants