Repository: quantumlib/Cirq

Issue:
Approximately equal circuits with symbols not detected by cirq.approx_eq
**Description of the issue**
Circuits with symbols are determined to be unequal by `cirq.approx_eq` when they should be approximately equal.

**How to reproduce the issue**

```python
q = cirq.GridQubit(0, 0)
s = sympy.Symbol("s")

no_symbol_1 = cirq.Circuit(cirq.rz(1.515)(q))
no_symbol_2 = cirq.Circuit(cirq.rz(1.510)(q))
print(cirq.approx_eq(no_symbol_1, no_symbol_2, atol=0.1))

symbol_1 = cirq.Circuit(cirq.rz(1.515*s)(q))
symbol_2 = cirq.Circuit(cirq.rz(1.510*s)(q))
print(cirq.approx_eq(symbol_1, symbol_2, atol=0.1))
```
First output is `True`, second output is `False`

**Cirq version**
0.8.2




Discussion:
Thank you for filing this @zaqqwerty - this should work. Either approx_eq should throw an error if the arguments are parametrized that it's not supported or support it properly. This behavior is a silent failure. Marked it as a bug. 

It also seems like a self contained issue, perfect for contributors to pick up! 
Is help still needed on this? I am looking for a good first issue.
Thanks for offering to help!  Of the options Balint listed, most helpful would be to figure out how to make:
```python
symbol_1 = cirq.Circuit(cirq.rz(1.515*s)(q))
symbol_2 = cirq.Circuit(cirq.rz(1.510*s)(q))
print(cirq.approx_eq(symbol_1, symbol_2, atol=0.1))
```
output `True`.
It's all yours @tonybruguier-google - thanks for volunteering!

Resolving patch:
diff --git a/cirq/protocols/approximate_equality_protocol.py b/cirq/protocols/approximate_equality_protocol.py
--- a/cirq/protocols/approximate_equality_protocol.py
+++ b/cirq/protocols/approximate_equality_protocol.py
@@ -18,6 +18,7 @@
 
 import numbers
 import numpy as np
+import sympy
 
 from typing_extensions import Protocol
 
@@ -94,6 +95,14 @@ def approx_eq(val: Any, other: Any, *, atol: Union[int, float] = 1e-8) -> bool:
     if isinstance(val, str):
         return val == other
 
+    if isinstance(val, sympy.Basic) or isinstance(other, sympy.Basic):
+        delta = sympy.Abs(other - val).simplify()
+        if not delta.is_number:
+            raise AttributeError('Insufficient information to decide whether '
+                                 'expressions are approximately equal '
+                                 f'[{val}] vs [{other}]')
+        return sympy.LessThan(delta, atol) == sympy.true
+
     # If the values are iterable, try comparing recursively on items.
     if isinstance(val, Iterable) and isinstance(other, Iterable):
         return _approx_eq_iterables(val, other, atol=atol)


Test patch:
diff --git a/cirq/ops/eigen_gate_test.py b/cirq/ops/eigen_gate_test.py
--- a/cirq/ops/eigen_gate_test.py
+++ b/cirq/ops/eigen_gate_test.py
@@ -12,6 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import re
+
 import numpy as np
 import pytest
 import sympy
@@ -132,22 +134,24 @@ def test_approx_eq():
         ZGateDef(exponent=1.5),
         atol=0.1
     )
-    assert not cirq.approx_eq(
-        ZGateDef(exponent=1.5),
-        ZGateDef(exponent=sympy.Symbol('a')),
-        atol=0.1
-    )
-
+    with pytest.raises(TypeError,
+                       match=re.escape("unsupported operand type(s) for"
+                                       " -: 'Symbol' and 'PeriodicValue'")):
+        cirq.approx_eq(ZGateDef(exponent=1.5),
+                       ZGateDef(exponent=sympy.Symbol('a')),
+                       atol=0.1)
     assert cirq.approx_eq(
         CExpZinGate(sympy.Symbol('a')),
         CExpZinGate(sympy.Symbol('a')),
         atol=0.1
     )
-    assert not cirq.approx_eq(
-        CExpZinGate(sympy.Symbol('a')),
-        CExpZinGate(sympy.Symbol('b')),
-        atol=0.1
-    )
+    with pytest.raises(
+            AttributeError,
+            match="Insufficient information to decide whether expressions are "
+            "approximately equal .* vs .*"):
+        assert not cirq.approx_eq(CExpZinGate(sympy.Symbol('a')),
+                                  CExpZinGate(sympy.Symbol('b')),
+                                  atol=0.1)
 
 
 def test_approx_eq_periodic():
diff --git a/cirq/protocols/approximate_equality_protocol_test.py b/cirq/protocols/approximate_equality_protocol_test.py
--- a/cirq/protocols/approximate_equality_protocol_test.py
+++ b/cirq/protocols/approximate_equality_protocol_test.py
@@ -16,6 +16,8 @@
 from decimal import Decimal
 from numbers import Number
 import numpy as np
+import pytest
+import sympy
 import cirq
 
 
@@ -152,6 +154,32 @@ def test_approx_eq_list():
     assert not cirq.approx_eq([1.1, 1.2, 1.3], [1, 1, 1], atol=0.2)
 
 
+def test_approx_eq_symbol():
+    q = cirq.GridQubit(0, 0)
+    s = sympy.Symbol("s")
+    t = sympy.Symbol("t")
+
+    assert not cirq.approx_eq(t + 1.51 + s, t + 1.50 + s, atol=0.005)
+    assert cirq.approx_eq(t + 1.51 + s, t + 1.50 + s, atol=0.020)
+
+    with pytest.raises(
+            AttributeError,
+            match="Insufficient information to decide whether expressions are "
+            "approximately equal .* vs .*"):
+        cirq.approx_eq(t, 0.0, atol=0.005)
+
+    symbol_1 = cirq.Circuit(cirq.rz(1.515 + s)(q))
+    symbol_2 = cirq.Circuit(cirq.rz(1.510 + s)(q))
+    assert cirq.approx_eq(symbol_1, symbol_2, atol=0.2)
+
+    symbol_3 = cirq.Circuit(cirq.rz(1.510 + t)(q))
+    with pytest.raises(
+            AttributeError,
+            match="Insufficient information to decide whether expressions are "
+            "approximately equal .* vs .*"):
+        cirq.approx_eq(symbol_1, symbol_3, atol=0.2)
+
+
 def test_approx_eq_default():
     assert cirq.approx_eq(1.0, 1.0 + 1e-9)
     assert cirq.approx_eq(1.0, 1.0 - 1e-9)