Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions scimath/units/compare_units.py
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):
Copy link
Member

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 eps to be a quantity of the same unit class as x1 and x2 here, 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-9 would mean in the case that x1 and x2 are comparable, but have different actual units, and the implementation here looks as though it would give asymmetric results:

>>> x = UnitScalar(1.0, units="um")
>>> y = UnitScalar(1002, units="nm")
>>> unit_scalars_almost_equal(x, y, eps=1e-2)
True
>>> unit_scalars_almost_equal(y, x, eps=1e-2)
False

Copy link
Contributor Author

@jonathanrocher jonathanrocher Apr 18, 2019

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...

Copy link
Member

@mdickinson mdickinson Apr 18, 2019

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 float eps 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 given unit_scalars_almost_equal call passes for some length length1 and fails for a length2 that represents exactly the same length (so length1 == length2 gives True), but happens to use different units.

Copy link
Member

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.

Copy link
Contributor Author

@jonathanrocher jonathanrocher Apr 18, 2019

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

from scimath.units.testing.assertion_utils import assert_unit_scalar_almost_equal
from scimath.units.api import UnitScalar
assert_unit_scalar_almost_equal(x, y, eps=UnitScalar(1e-12, units="SOME UNIT HERE"))

is quite verbose. What I meant to do with the current implementation is:

assert_unit_scalar_almost_equal(x, y, eps=UnitScalar(1e-12, units=x.units))

What about automatically treating a float eps like UnitScalar(eps, units=x.units) inside unit_scalars_almost_equal? Or change eps to a non-dimensional "rtol" which would be compared to float(abs(x1-x2)/abs(x2)) (after unit conversion) similar to https://docs.scipy.org/doc/numpy-1.13.0/reference/generated/numpy.allclose.html ?

""" 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.
40 changes: 40 additions & 0 deletions scimath/units/testing/assertion_utils.py
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)
25 changes: 25 additions & 0 deletions scimath/units/testing/tests/test_assertion_utils.py
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"))
94 changes: 94 additions & 0 deletions scimath/units/tests/test_compare_units.py
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))