Repository: quantumlib/Cirq

Issue:
ParamResolver is slow
Reconstructing a circuit op-wise and replacing all parametrized ops with new ops containing parameter updates is 10-50x faster than using `cirq.ParamResolver` with sympy  symbols...

Example code to reproduce:
```
from timeit import default_timer as timer
import numpy as np
import sympy
import cirq


def update_params(circuit, params):
    """Competitor method to cirq.ParamResolver."""
    new_op_tree = []
    for op, param in zip(circuit.all_operations(), params):
        new_op_tree.append(op.gate._with_exponent(param/np.pi)(*op.qubits))
    return cirq.Circuit.from_ops(new_op_tree)


trials = 100
for depth in [10, 15, 20]:
    sympy_circuit = cirq.Circuit.from_ops([cirq.Rx(sympy.Symbol(str(k)))(cirq.LineQubit(0)) for k in range(depth)])
    random_params = np.random.randn(trials, depth)
    # time twenty runs
    start = timer()
    for j in range(trials):
        resolver = dict(zip([str(k) for k in range(depth)], random_params[j]))
        wf1 = cirq.Simulator().simulate(sympy_circuit, param_resolver=resolver).final_state
    end = timer() - start
    print(f"{depth} parameters, {trials} trials using Sympy+ParamResolver: {end} seconds")

    start = timer()
    for j in range(trials):
        float_circuit = update_params(sympy_circuit, random_params[j])
        wf2 = cirq.Simulator().simulate(float_circuit).final_state
    end = timer() - start
    print(f"{depth} parameters, {trials} trials using reconstructed circuit: {end} seconds")
```
produced (cirq v0.5.0, Windows 10 + core i7 gen 7 processor)
```
>>> 10 parameters, 100 trials using Sympy+ParamResolver: 2.408036000095308 seconds
>>> 10 parameters, 100 trials using reconstructed circuit: 0.1671589999459684 seconds
>>> 15 parameters, 100 trials using Sympy+ParamResolver: 4.347879000008106 seconds
>>> 15 parameters, 100 trials using reconstructed circuit: 0.25207799999043345 seconds
>>> 20 parameters, 100 trials using Sympy+ParamResolver: 7.1194350000005215 seconds
>>> 20 parameters, 100 trials using reconstructed circuit: 0.31734399998094887 seconds
```
Some plots of how this scales with large numbers of parameters (generated from a different script using the same `update_params` method):
![image](https://user-images.githubusercontent.com/14928221/62160823-b23ca400-b2c9-11e9-9073-666989fdea44.png)



Discussion:
Interesting.  We should narrow this down to whether it is something in the Simulator or the ParamResolver or the protocol.  A first glance at code suggest that hashing in ParamResolver, that doesn't seem like a great idea. 
Some thoughts:
1) ParamResolver initialization might be potentially heavy, but afaics it is not supposed to be re-initialized in the recursive calls.
2) Recursion itself might potentially be a problem, which is a known issue in Python. The winning solution doesn't do recursion for parameter resolving.
@Ashalynd could you say a little more about how recursion is used in the resolver?
The following line where sympy.subs https://github.com/quantumlib/Cirq/blob/57b0beb5d625cb44d467b70cc46fba24fff160f0/cirq/study/resolver.py#L74 consumes most of the time

sympy.subs is known to be very slow, and is called very often (and includes, as @Ashalynd  said, a lot of recursions).

After having a look in subs I came up wit the following code to replace the line mentioned above

```
v = value
for pr in value.args:
    str_pr = str(pr)
    if str_pr in self.param_dict:
        v = v.subs(str_pr, self.param_dict[str_pr])
```

On my computer it is now 3 times faster, but still approx. 30 times slower than the method without sympy.

Would a solution be to use lambdification?
Here is the [page](https://docs.sympy.org/latest/modules/numeric-computation.html) on `sub` being slow and lambification

We could do this:

```
def Rz(rads: value.TParamVal) -> ZPowGate:
    """Returns a gate with the matrix e^{-i Z rads / 2}."""
    if isinstance(rads, sympy.Symbol):
        return ZPowGate(exponent=FastOverPi(rads), global_shift=-0.5)
    pi = sympy.pi if protocols.is_parameterized(rads) else np.pi
    return ZPowGate(exponent=rads / pi, global_shift=-0.5)

class FastOverPi(sympy.Mul):
    def __init__(self, v: sympy.Symbol):
        super().__init__(v, 1 / sympy.pi)
        self.v = v

    def subs(self, *args):
        if isinstance(args[0], dict):
            return args[0].get(str(self.v), self.v) / np.pi
        return super().subs(*args)
```
@karlunho 
sympy is generally slow, and everything that touches sympy becomes slow.

Would it be possible to have simple parameterized gates (e.g. the Rz rotations uses sympy only to multiply a number by pi) without sympy? If the symbol in the constructor stands just for a number, sympy is overkill, if the symbol is an expression per se, maybe the sympy overhead is good price.

I took a stab at short-circuiting sympy for common cases for PR #2394 .  It does not help the above code as written, but, if flattened or if using XPowGate (which doesn't add the "x/pi" formula), this speeds up the code significantly.


```
10 parameters, 100 trials using flattened circuit: 0.1600349259097129 seconds
10 parameters, 100 trials using xpow circuit: 0.17739228205755353 seconds
10 parameters, 100 trials using reconstructed circuit: 0.16374025191180408 seconds:
15 parameters, 100 trials using flattened circuit: 0.2257283239159733 seconds
15 parameters, 100 trials using xpow circuit: 0.2516175350174308 seconds
15 parameters, 100 trials using reconstructed circuit: 0.23388241999782622 seconds:
20 parameters, 100 trials using flattened circuit: 0.2913768459111452 seconds
20 parameters, 100 trials using xpow circuit: 0.32354101818054914 seconds
20 parameters, 100 trials using reconstructed circuit: 0.29962173686362803 seconds:
```


The changed code is below, for reference:
```
trials = 100
for depth in [10, 15, 20]:
    global sympy_circuit
    global resolver
    sympy_circuit1 = cirq.Circuit.from_ops([cirq.Rx(sympy.Symbol(str(k)))(cirq.LineQubit(0)) for k in range(depth)])
    sympy_circuit2 = cirq.Circuit.from_ops([cirq.XPowGate(exponent=sympy.Symbol(str(k)))(cirq.LineQubit(0)) for k in range(depth)])
    sympy_circuit, expr_map = cirq.flatten(sympy_circuit1)
    random_params = np.random.randn(trials, depth)
    resolver = expr_map.transform_params(dict(zip([str(k) for k in range(depth)], random_params[0])))
    # time twenty runs
    start = timer()
    for j in range(trials):
        wf = cirq.Simulator().simulate(sympy_circuit, param_resolver=resolver).final_state
    end = timer() - start
    print(f"{depth} parameters, {trials} trials using flattened circuit: {end} seconds")

    # time twenty runs
    start = timer()
    for j in range(trials):
        resolver = dict(zip([str(k) for k in range(depth)], random_params[j]))
        wf = cirq.Simulator().simulate(sympy_circuit2, param_resolver=resolver).final_state
    end = timer() - start
    print(f"{depth} parameters, {trials} trials using xpow circuit: {end} seconds")

    start = timer()
    for j in range(trials):
        float_circuit = update_params(sympy_circuit, random_params[j])
        wf2 = cirq.Simulator().simulate(float_circuit).final_state
    end = timer() - start
    print(f"{depth} parameters, {trials} trials using reconstructed circuit: {end} seconds:")

```
I'm still facing a problem. In my case, new theta values are generated in a different scope and delivered to the ParamSolver. The next value of thetas would be unknown to me. I will only know it after I ParamSolver and generate the unitary of the circuit for the current thetas. So for me, it has to be one theta set at a time. Any suggestions?
@aminjahanpour Can you provide a simplified example of the problem?  Otherwise, I am not sure I can reproduce your issue.

One suggestion would be to reduce the use of formulas as much as possible, since sympy formula resolution is extremely slow.  If you are using the Rz/Ry/Rx gates, you may want to consider using ZPowGate,YPowGate,XPowGate instead and divide by pi yourself rather than rely on sympy to resolve it.
I'll write an example. Before that, I am using custom two-qubit gates on my circuit. I am not aware of a way to convert them to PowGates.
here is an example but I am facing another issue: the resolver does not work with my custom unitary gate.
```import cirq
import numpy as np
import sympy


class tbr_x(cirq.TwoQubitGate):
    def __init__(self, theta):
        self.theta = theta
    def _unitary_(self):
        return np.sin(self.theta) * np.eye(4) - 1j * np.cos(self.theta) * np.array([
            [0, 1, 0, 1],
            [-1, 0, 1, 0],
            [0, 1, 0, -1],
            [1, 0, 1, 0]
        ])

def generate_unitary_by_rebuilding_the_circuit(theta):
    n_qubits = 2
    qubits = cirq.LineQubit.range(n_qubits)
    moments = []

    moments.append(tbr_x(theta[0])(qubits[0], qubits[1]))
    moments.append(tbr_x(theta[1])(qubits[0], qubits[1]))

    my_circuit = cirq.Circuit(moments)

    return my_circuit.unitary()

def generate_unitary_by_resolving_thetas(sympy_circuit, theta):
    resolver_dict = {'theta0': theta[0],
                     'theta1': theta[1]}
    resolver = cirq.ParamResolver(resolver_dict)

    my_circuit = cirq.resolve_parameters(sympy_circuit, resolver)

    return my_circuit.unitary()


def search_for_optimum_theta():
    n_qubits = 2
    qubits = cirq.LineQubit.range(n_qubits)
    moments = []
    moments.append(tbr_x(sympy.Symbol('theta_0'))(qubits[0], qubits[1]))
    moments.append(tbr_x(sympy.Symbol('theta_1'))(qubits[0], qubits[1]))
    sympy_circuit = cirq.Circuit(moments)


    for i in range(100):
        """
        here I simply use random numbers but theta values are actually generated
        based on the unitary of the circuit built with the previous theta set.
        """
        thetas = np.random.uniform(size=(2))
        U1 = generate_unitary_by_rebuilding_the_circuit(thetas)
        U2 = generate_unitary_by_resolving_thetas(sympy_circuit, thetas)


if __name__ == '__main__':
    search_for_optimum_theta()

Can you say how it fails?  I think you need to implement the `_is_parameterized_` protocol.
When Cirq it trying to build the unitary of the Sympy circuit, apparently it is getting confused by the formulation:
```
AttributeError: 'Symbol' object has no attribute 'sin'
line 11, in _unitary_
    return np.sin(self.theta) * np.eye(4) - 1j * np.cos(self.theta) * np.array([
TypeError: loop of ufunc does not support argument 0 of type Symbol which has no callable sin method
@aminjahanpour You have to be careful which functions you call if you are trying to do formula resolution with symbols.  I think it will work if you use sympy.sin instead of numpy.sin.  Same with cos().
I modified the gate class to comply with Sympy:
```

    def _unitary_(self):
        return sympy.sin(self.theta) * sympy.eye(4) - 1j * sympy.cos(self.theta) * sympy.Array([
            [0, 1, 0, 1],
            [-1, 0, 1, 0],
            [0, 1, 0, -1],
            [1, 0, 1, 0]
        ])
```
but now I can not produce the circuit unitary:
```File "/anaconda3/envs/opt_api/lib/python3.7/site-packages/sympy/matrices/matrices.py", line 2141, in __getattr__
    "%s has no attribute %s." % (self.__class__.__name__, attr))
AttributeError: MutableDenseMatrix has no attribute astype.
```
I guess I have to first deal with this issue before getting at the issue of speed. Not sure here is the right place to focus on this issue?

Resolving patch:
diff --git a/cirq/study/resolver.py b/cirq/study/resolver.py
--- a/cirq/study/resolver.py
+++ b/cirq/study/resolver.py
@@ -15,6 +15,7 @@
 """Resolves ParameterValues to assigned values."""
 
 from typing import Any, Dict, Iterator, Optional, TYPE_CHECKING, Union, cast
+import numpy as np
 import sympy
 from cirq._compat import proper_repr
 from cirq._doc import document
@@ -113,6 +114,27 @@ def value_of(self,
             if isinstance(param_value, (float, int)):
                 return param_value
 
+        # The following resolves common sympy expressions
+        # If sympy did its job and wasn't slower than molasses,
+        # we wouldn't need the following block.
+        if isinstance(value, sympy.Add):
+            summation = self.value_of(value.args[0])
+            for addend in value.args[1:]:
+                summation += self.value_of(addend)
+            return summation
+        if isinstance(value, sympy.Mul):
+            product = self.value_of(value.args[0])
+            for factor in value.args[1:]:
+                product *= self.value_of(factor)
+            return product
+        if isinstance(value, sympy.Pow) and len(value.args) == 2:
+            return np.power(self.value_of(value.args[0]),
+                            self.value_of(value.args[1]))
+        if value == sympy.pi:
+            return np.pi
+        if value == sympy.S.NegativeOne:
+            return -1
+
         # Input is either a sympy formula or the dictionary maps to a
         # formula.  Use sympy to resolve the value.
         # Note that sympy.subs() is slow, so we want to avoid this and


Test patch:
diff --git a/cirq/study/resolver_test.py b/cirq/study/resolver_test.py
--- a/cirq/study/resolver_test.py
+++ b/cirq/study/resolver_test.py
@@ -37,6 +37,7 @@ def test_value_of():
 
     assert r.value_of(sympy.pi) == np.pi
     assert r.value_of(2 * sympy.pi) == 2 * np.pi
+    assert r.value_of(4**sympy.Symbol('a') + sympy.Symbol('b') * 10) == 3
     assert r.value_of('c') == 1 + 1j
     assert r.value_of(sympy.I * sympy.pi) == np.pi * 1j