Repository: quantumlib/Cirq

Issue:
Complex numbers can be approximately equal to integers
but approx_eq incorrectly disagrees


Discussion:

Resolving patch:
diff --git a/cirq/protocols/approximate_equality.py b/cirq/protocols/approximate_equality.py
--- a/cirq/protocols/approximate_equality.py
+++ b/cirq/protocols/approximate_equality.py
@@ -13,7 +13,10 @@
 # limitations under the License.
 
 from typing import Any, Union
+from fractions import Fraction
+from decimal import Decimal
 
+import numbers
 import numpy as np
 
 from typing_extensions import Protocol
@@ -86,15 +89,12 @@ def approx_eq(val: Any, other: Any, *, atol: Union[int, float] = 1e-8) -> bool:
             return result
 
     # Compare primitive types directly.
-    if isinstance(val, (int, float)):
-        if not isinstance(other, (int, float)):
+    if isinstance(val, numbers.Number):
+        if not isinstance(other, numbers.Number):
             return False
-        return _isclose(val, other, atol=atol)
-
-    if isinstance(val, complex):
-        if not isinstance(other, complex):
-            return False
-        return _isclose(val, other, atol=atol)
+        result = _isclose(val, other, atol=atol)
+        if result is not NotImplemented:
+            return result
 
     # Try to compare source and target recursively, assuming they're iterable.
     result = _approx_eq_iterables(val, other, atol=atol)
@@ -160,4 +160,19 @@ def get_iter(iterable):
 
 def _isclose(a: Any, b: Any, *, atol: Union[int, float]) -> bool:
     """Convenience wrapper around np.isclose."""
-    return True if np.isclose([a], [b], atol=atol, rtol=0.0)[0] else False
+
+    # support casting some standard numeric types
+    x1 = np.asarray([a])
+    if isinstance(a, (Fraction, Decimal)):
+        x1 = x1.astype(np.float64)
+    x2 = np.asarray([b])
+    if isinstance(b, (Fraction, Decimal)):
+        x2 = x2.astype(np.float64)
+
+    # workaround np.isfinite type limitations. Cast to bool to avoid np.bool_
+    try:
+        result = bool(np.isclose(x1, x2, atol=atol, rtol=0.0)[0])
+    except TypeError:
+        return NotImplemented
+
+    return result


Test patch:
diff --git a/cirq/protocols/approximate_equality_test.py b/cirq/protocols/approximate_equality_test.py
--- a/cirq/protocols/approximate_equality_test.py
+++ b/cirq/protocols/approximate_equality_test.py
@@ -12,10 +12,15 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from fractions import Fraction
+from decimal import Decimal
+from numbers import Number
+import numpy as np
 import cirq
 
 
 def test_approx_eq_primitives():
+    assert not cirq.approx_eq(1, 2, atol=1e-01)
     assert cirq.approx_eq(1.0, 1.0 + 1e-10, atol=1e-09)
     assert not cirq.approx_eq(1.0, 1.0 + 1e-10, atol=1e-11)
     assert cirq.approx_eq(0.0, 1e-10, atol=1e-09)
@@ -24,6 +29,64 @@ def test_approx_eq_primitives():
     assert not cirq.approx_eq(complex(1, 1), complex(1.1, 1.2), atol=0.1)
 
 
+def test_approx_eq_mixed_primitives():
+    assert cirq.approx_eq(complex(1, 1e-10), 1, atol=1e-09)
+    assert not cirq.approx_eq(complex(1, 1e-4), 1, atol=1e-09)
+    assert cirq.approx_eq(complex(1, 1e-10), 1.0, atol=1e-09)
+    assert not cirq.approx_eq(complex(1, 1e-8), 1.0, atol=1e-09)
+    assert cirq.approx_eq(1, 1.0 + 1e-10, atol=1e-9)
+    assert not cirq.approx_eq(1, 1.0 + 1e-10, atol=1e-11)
+
+
+def test_numpy_dtype_compatibility():
+    i_a, i_b, i_c = 0, 1, 2
+    i_types = [np.intc, np.intp, np.int0, np.int8, np.int16, np.int32, np.int64]
+    for i_type in i_types:
+        assert cirq.approx_eq(i_type(i_a), i_type(i_b), atol=1)
+        assert not cirq.approx_eq(i_type(i_a), i_type(i_c), atol=1)
+    u_types = [np.uint, np.uint0, np.uint8, np.uint16, np.uint32, np.uint64]
+    for u_type in u_types:
+        assert cirq.approx_eq(u_type(i_a), u_type(i_b), atol=1)
+        assert not cirq.approx_eq(u_type(i_a), u_type(i_c), atol=1)
+
+    f_a, f_b, f_c = 0, 1e-8, 1
+    f_types = [np.float16, np.float32, np.float64, np.float128]
+    for f_type in f_types:
+        assert cirq.approx_eq(f_type(f_a), f_type(f_b), atol=1e-8)
+        assert not cirq.approx_eq(f_type(f_a), f_type(f_c), atol=1e-8)
+
+    c_a, c_b, c_c = 0, 1e-8j, 1j
+    c_types = [np.complex64, np.complex128, np.complex256]
+    for c_type in c_types:
+        assert cirq.approx_eq(c_type(c_a), c_type(c_b), atol=1e-8)
+        assert not cirq.approx_eq(c_type(c_a), c_type(c_c), atol=1e-8)
+
+
+def test_fractions_compatibility():
+    assert cirq.approx_eq(Fraction(0), Fraction(1, int(1e10)), atol=1e-9)
+    assert not cirq.approx_eq(Fraction(0), Fraction(1, int(1e7)), atol=1e-9)
+
+
+def test_decimal_compatibility():
+    assert cirq.approx_eq(Decimal('0'), Decimal('0.0000000001'), atol=1e-9)
+    assert not cirq.approx_eq(Decimal('0'), Decimal('0.00000001'), atol=1e-9)
+    assert not cirq.approx_eq(Decimal('NaN'), Decimal('-Infinity'), atol=1e-9)
+
+
+def test_approx_eq_mixed_types():
+    assert cirq.approx_eq(np.float32(1), 1.0 + 1e-10, atol=1e-9)
+    assert cirq.approx_eq(np.float64(1), np.complex64(1 + 1e-8j), atol=1e-4)
+    assert cirq.approx_eq(np.uint8(1), np.complex64(1 + 1e-8j), atol=1e-4)
+    assert cirq.approx_eq(np.complex256(1), complex(1, 1e-8), atol=1e-4)
+    assert cirq.approx_eq(np.int32(1), 1, atol=1e-9)
+    assert cirq.approx_eq(complex(0.5, 0), Fraction(1, 2), atol=0.0)
+    assert cirq.approx_eq(0.5 + 1e-4j, Fraction(1, 2), atol=1e-4)
+    assert cirq.approx_eq(0, Fraction(1, 100000000), atol=1e-8)
+    assert cirq.approx_eq(np.uint16(1), Decimal('1'), atol=0.0)
+    assert cirq.approx_eq(np.float64(1.0), Decimal('1.00000001'), atol=1e-8)
+    assert not cirq.approx_eq(np.complex64(1e-5j), Decimal('0.001'), atol=1e-4)
+
+
 def test_approx_eq_special_numerics():
     assert not cirq.approx_eq(float('nan'), 0, atol=0.0)
     assert not cirq.approx_eq(float('nan'), float('nan'), atol=0.0)
@@ -33,6 +96,32 @@ def test_approx_eq_special_numerics():
     assert cirq.approx_eq(float('inf'), float('inf'), atol=0.0)
 
 
+class X(Number):
+    """Subtype of Number that can fallback to __eq__"""
+
+    def __init__(self, val):
+        self.val = val
+
+    def __eq__(self, other):
+        if not isinstance(self, type(other)):
+            return NotImplemented
+        return self.val == other.val
+
+
+class Y(Number):
+    """Subtype of Number that cannot fallback to __eq__"""
+
+    def __init__(self):
+        pass
+
+
+def test_approx_eq_number_uses__eq__():
+    assert cirq.approx_eq(C(0), C(0), atol=0.0)
+    assert not cirq.approx_eq(X(0), X(1), atol=0.0)
+    assert not cirq.approx_eq(X(0), 0, atol=0.0)
+    assert not cirq.approx_eq(Y(), 1, atol=0.0)
+
+
 def test_approx_eq_tuple():
     assert cirq.approx_eq((1, 1), (1, 1), atol=0.0)
     assert not cirq.approx_eq((1, 1), (1, 1, 1), atol=0.0)
@@ -121,7 +210,5 @@ def test_approx_eq_types_mismatch():
     assert not cirq.approx_eq(A(0), B(0), atol=0.0)
     assert not cirq.approx_eq(C(0), A(0), atol=0.0)
     assert not cirq.approx_eq(A(0), C(0), atol=0.0)
-    assert not cirq.approx_eq(complex(0, 0), 0, atol=0.0)
-    assert not cirq.approx_eq(0, complex(0, 0), atol=0.0)
     assert not cirq.approx_eq(0, [0], atol=1.0)
     assert not cirq.approx_eq([0], 0, atol=0.0)