Beispiel #1
0
    def __init__(self, backend='noiseless', differentiator=None, **kwargs):
        """Instantiate this Layer.

        Create a layer that will output expectation values gained from
        simulating a quantum circuit.

        Args:
            backend: Optional Backend to use to simulate states. Defaults to
                the 'noiseless' simulator, options include {'noiseless',
                'noisy'}. In the noisy case a `repetitions` call argument
                must be provided. Users may also specify a preconfigured cirq
                object to use instead, which must inherit
                `cirq.sim.simulator.SimulatesExpectationValues`.
            differentiator: Optional Differentiator to use to calculate analytic
                derivative values of given operators_to_measure and circuit,
                which must inherit `tfq.differentiators.Differentiator` and
                implements `differentiate_analytic` method. Defaults to None,
                which uses `tfq.differentiators.ParameterShift()`. If
                `backend` is also 'noiseless' then default is
                `tfq.differentiators.Adjoint`.

        """
        super().__init__(**kwargs)

        # Ingest backend.
        if not isinstance(
            backend, cirq.sim.simulator.SimulatesExpectationValues) and \
                isinstance(backend, cirq.Sampler):
            raise TypeError("Backend implements cirq.Sampler but not "
                            "cirq.sim.simulator.SimulatesExpectationValues. "
                            "Please use SampledExpectation instead.")
        used_op = None
        self.noisy = False
        if backend == 'noiseless':
            backend = None

        # Ingest differentiator.
        if differentiator is None:
            differentiator = parameter_shift.ParameterShift()
            if backend is None:
                differentiator = adjoint.Adjoint()

        if not isinstance(differentiator, diff.Differentiator):
            raise TypeError("Differentiator must inherit from "
                            "tfq.differentiators.Differentiator")

        if backend == 'noisy':
            used_op = noisy_expectation_op.expectation
            self._expectation_op = differentiator.generate_differentiable_op(
                sampled_op=used_op)
            self.noisy = True
        else:
            used_op = circuit_execution_ops.get_expectation_op(backend=backend)
            self._expectation_op = differentiator.generate_differentiable_op(
                analytic_op=used_op)

        self._w = None
    def test_parameter_shift_sampled(self):
        """Test if ParameterShift.differentiate_sampled doesn't crash before
        running."""
        programs, names, values, ops, n_samples, true_f, true_g = \
        _simple_op_inputs()
        ps = parameter_shift.ParameterShift()
        op = ps.generate_differentiable_op(
            sampled_op=circuit_execution_ops.get_sampled_expectation_op())

        with tf.GradientTape() as g:
            g.watch(values)
            expectations = op(programs, names, values, ops, n_samples)
        grads = g.gradient(expectations, values)
        self.assertAllClose(expectations, true_f, atol=1e-1, rtol=1e-1)
        self.assertAllClose(grads, true_g, atol=1e-1, rtol=1e-1)
Beispiel #3
0
    def __init__(self, backend='noiseless', differentiator=None, **kwargs):
        """Instantiate this Layer.

        Create a layer that will output expectation values gained from
        simulating a quantum circuit.

        Args:
            backend: Optional Backend to use to simulate states. Can be either
                {'noiseless', 'noisy'} users may also
                specify a preconfigured cirq simulation object to use instead,
                which must inherit `cirq.Sampler`.
            differentiator: Optional Differentiator to use to calculate analytic
                derivative values of given operators_to_measure and circuit,
                which must inherit `tfq.differentiators.Differentiator`.
                Defaults to `parameter_shift.ParameterShift()` (None argument).

        """
        super().__init__(**kwargs)

        # Ingest differentiator.
        if differentiator is None:
            differentiator = parameter_shift.ParameterShift()

        if not isinstance(differentiator, diff.Differentiator):
            raise TypeError("Differentiator must inherit from "
                            "tfq.differentiators.Differentiator")

        # Ingest backend.
        if not isinstance(backend, cirq.Sampler) and \
                isinstance(backend, cirq.SimulatesFinalState):
            raise TypeError(
                "Backend implements cirq.SimulatesFinalState but "
                "not cirq.Sampler. Please use Expectation instead.")

        used_op = None
        if backend == 'noiseless':
            used_op = circuit_execution_ops.get_sampled_expectation_op(
                backend=None)
        elif backend == 'noisy':
            used_op = noisy_sampled_expectation_op.sampled_expectation
        else:
            used_op = circuit_execution_ops.get_sampled_expectation_op(
                backend=backend)

        self._expectation_op = differentiator.generate_differentiable_op(
            sampled_op=used_op)

        self._w = None
class GradientBenchmarksTest(tf.test.TestCase, parameterized.TestCase):
    """Test the Gradient benchmarking class."""
    @parameterized.parameters(
        list(
            util.kwargs_cartesian_product(
                **{
                    'diff': [
                        linear_combination.ForwardDifference(),
                        linear_combination.CentralDifference(),
                        parameter_shift.ParameterShift(),
                        stochastic_differentiator.SGDifferentiator(),
                    ],
                    'params': [TEST_PARAMS_1, TEST_PARAMS_2]
                })))
    def testBenchmarkGradient(self, diff, params):
        """Test that op constructs and runs correctly."""

        bench_name = "GradientBenchmarks.{}_{}_{}_{}_{}".format(
            diff.__class__.__name__, params.n_qubits, params.n_moments,
            params.batch_size, params.n_symbols)
        proto_file_path = os.path.join(SRC, "reports/",
                                       "{}".format(bench_name))
        self.addCleanup(os.remove, proto_file_path)

        bench = GradientBenchmarks(params=params)
        bench.setup()
        bench._benchmark_tfq_differentiator(diff, params)

        res = benchmark_util.read_benchmark_entry(proto_file_path)
        self.assertEqual(res.name, bench_name)
        self.assertEqual(
            res.extras.get("n_qubits").double_value, params.n_qubits)
        self.assertEqual(
            res.extras.get("n_moments").double_value, params.n_moments)
        self.assertEqual(
            res.extras.get("op_density").double_value, params.op_density)
        assert hasattr(res, 'iters')
        assert hasattr(res, 'wall_time')
Beispiel #5
0
import tensorflow as tf
from absl.testing import parameterized

import cirq
from tensorflow_quantum.python import util
from tensorflow_quantum.python.differentiators import adjoint
from tensorflow_quantum.python.differentiators import linear_combination
from tensorflow_quantum.python.differentiators import parameter_shift
from tensorflow_quantum.core.ops import circuit_execution_ops, batch_util

ANALYTIC_DIFFS = [
    linear_combination.ForwardDifference(grid_spacing=0.0001),
    linear_combination.ForwardDifference(error_order=2, grid_spacing=0.0001),
    linear_combination.CentralDifference(grid_spacing=0.0001),
    linear_combination.CentralDifference(error_order=4, grid_spacing=0.0001),
    parameter_shift.ParameterShift(),
]

SAMPLED_DIFFS = [
    linear_combination.ForwardDifference(grid_spacing=0.05),
    linear_combination.CentralDifference(grid_spacing=0.05),
    parameter_shift.ParameterShift(),
]

SAMPLED_DIFFS_TOLS = [0.5, 0.5, 0.2]

ANALYTIC_OPS = [
    circuit_execution_ops.get_expectation_op(cirq.sim.Simulator()),  # WF
    circuit_execution_ops.get_expectation_op()  # C++
]
 def benchmark_parameter_shift(self):
     """Benchmark the parameter shift gradient method."""
     diff = parameter_shift.ParameterShift()
     self._benchmark_tfq_differentiator(diff, self.params)
Beispiel #7
0
    def test_get_gradient_circuits(self):
        """Test that the correct objects are returned."""

        diff = parameter_shift.ParameterShift()

        # Circuits to differentiate.
        symbols = [sympy.Symbol("s0"), sympy.Symbol("s1")]
        q0 = cirq.GridQubit(0, 0)
        q1 = cirq.GridQubit(1, 2)
        input_programs = util.convert_to_tensor([
            cirq.Circuit(
                cirq.X(q0)**symbols[0],
                cirq.Y(q0)**symbols[0],
                cirq.ry(symbols[1])(q1)),
            cirq.Circuit(cirq.Y(q1)**symbols[1]),
        ])
        input_symbol_names = tf.constant([str(s) for s in symbols])
        input_symbol_values = tf.constant([[1.5, -2.7], [-0.3, 0.9]])

        # First, for each symbol `s`, check how many times `s` appears in each
        # program `p`, `n_ps`. Let `n_param_gates` be the maximum of `n_ps` over
        # all symbols and programs. Then, the shape of `batch_programs` will be
        # [n_programs, n_symbols * n_param_gates * n_shifts], where `n_shifts`
        # is 2 because we decompose into gates with 2 eigenvalues. For row index
        # `p` we have for column indices between `i * n_param_gates * n_shifts`
        # and `(i + 1) * n_param_gates * n_shifts`, the first `n_pi * 2`
        # programs are parameter shifted versions of `input_programs[p]` and the
        # remaining programs are empty.
        # Here, `n_param_gates` is 2.
        impurity_symbol_name = "_impurity_for_param_shift"
        impurity_symbol = sympy.Symbol(impurity_symbol_name)
        expected_batch_programs_0 = util.convert_to_tensor([
            cirq.Circuit(
                cirq.X(q0)**impurity_symbol,
                cirq.Y(q0)**symbols[0],
                cirq.ry(symbols[1])(q1)),
            cirq.Circuit(
                cirq.X(q0)**impurity_symbol,
                cirq.Y(q0)**symbols[0],
                cirq.ry(symbols[1])(q1)),
            cirq.Circuit(
                cirq.X(q0)**symbols[0],
                cirq.Y(q0)**impurity_symbol,
                cirq.ry(symbols[1])(q1)),
            cirq.Circuit(
                cirq.X(q0)**symbols[0],
                cirq.Y(q0)**impurity_symbol,
                cirq.ry(symbols[1])(q1)),
            cirq.Circuit(
                cirq.X(q0)**symbols[0],
                cirq.Y(q0)**symbols[0],
                cirq.ry(impurity_symbol)(q1)),
            cirq.Circuit(
                cirq.X(q0)**symbols[0],
                cirq.Y(q0)**symbols[0],
                cirq.ry(impurity_symbol)(q1)),
            cirq.Circuit(),
            cirq.Circuit()
        ])
        expected_batch_programs_1 = util.convert_to_tensor([
            cirq.Circuit(),
            cirq.Circuit(),
            cirq.Circuit(),
            cirq.Circuit(),
            cirq.Circuit(cirq.Y(q1)**impurity_symbol),
            cirq.Circuit(cirq.Y(q1)**impurity_symbol),
            cirq.Circuit(),
            cirq.Circuit()
        ])
        expected_batch_programs = tf.stack(
            [expected_batch_programs_0, expected_batch_programs_1])

        # The new symbols are the old ones, with an extra used for shifting.
        expected_new_symbol_names = tf.concat(
            [input_symbol_names,
             tf.constant([impurity_symbol_name])], 0)

        # The batch symbol values are the input symbol values, tiled and with
        # shifted values appended. Locations that have empty programs should
        # also have zero for the shift.
        # The shifted values are the original value plus 1/2 divided by the
        # `exponent_scalar` of the gate.
        expected_batch_symbol_values = tf.constant(
            [[[1.5, -2.7, 1.5 + 0.5], [1.5, -2.7, 1.5 - 0.5],
              [1.5, -2.7, 1.5 + 0.5], [1.5, -2.7, 1.5 - 0.5],
              [1.5, -2.7, -2.7 + np.pi / 2], [1.5, -2.7, -2.7 - np.pi / 2],
              [1.5, -2.7, -2.7], [1.5, -2.7, -2.7]],
             [[-0.3, 0.9, -0.3], [-0.3, 0.9, -0.3], [-0.3, 0.9, -0.3],
              [-0.3, 0.9, -0.3], [-0.3, 0.9, 0.9 + 0.5],
              [-0.3, 0.9, 0.9 - 0.5], [-0.3, 0.9, 0.9], [-0.3, 0.9, 0.9]]])

        # Empty program locations are given zero weight.
        expected_batch_weights = tf.constant(
            [[[np.pi / 2, -np.pi / 2, np.pi / 2, -np.pi / 2],
              [0.5, -0.5, 0.0, 0.0]],
             [[0.0, 0.0, 0.0, 0.0], [np.pi / 2, -np.pi / 2, 0.0, 0.0]]])

        expected_batch_mapper = tf.constant([[[0, 1, 2, 3], [4, 5, 6, 7]],
                                             [[0, 1, 2, 3], [4, 5, 6, 7]]])

        (test_batch_programs, test_new_symbol_names, test_batch_symbol_values,
         test_batch_weights, test_batch_mapper) = diff.get_gradient_circuits(
             input_programs, input_symbol_names, input_symbol_values)
        for i in range(tf.shape(input_programs)[0]):
            self.assertAllEqual(util.from_tensor(expected_batch_programs[i]),
                                util.from_tensor(test_batch_programs[i]))
        self.assertAllEqual(expected_new_symbol_names, test_new_symbol_names)
        self.assertAllClose(expected_batch_symbol_values,
                            test_batch_symbol_values,
                            atol=1e-5)
        self.assertAllClose(expected_batch_weights,
                            test_batch_weights,
                            atol=1e-5)
        self.assertAllEqual(expected_batch_mapper, test_batch_mapper)
    def __init__(self,
                 model_circuit,
                 operators,
                 *,
                 repetitions=None,
                 sample_based=None,
                 differentiator=None,
                 **kwargs):
        """Instantiate this layer.

        Create a layer that will output noisy expectation values of the given
        operators when fed quantum data to it's input layer. This layer will
        take two input tensors, one representing a quantum data source (these
        circuits must not contain any symbols) and the other representing
        control parameters for the model circuit that gets appended to the
        datapoints.

        model_circuit: `cirq.Circuit` containing `sympy.Symbols` that will be
            used as the model which will be fed quantum data inputs.
        operators: `cirq.PauliSum` or Python `list` of `cirq.PauliSum` objects
            used as observables at the end of the model circuit.
        repetitions: Python `int` indicating how many trajectories to use
            when estimating expectation values.
        sample_based: Python `bool` indicating whether to use sampling to
            estimate expectations or analytic calculations with each
            trajectory.
        differentiator: Optional `tfq.differentiator` object to specify how
            gradients of `model_circuit` should be calculated.
        """
        super().__init__(**kwargs)
        # Ingest model_circuit.
        if not isinstance(model_circuit, cirq.Circuit):
            raise TypeError("model_circuit must be a cirq.Circuit object."
                            " Given: ".format(model_circuit))

        self._symbols_list = list(
            sorted(util.get_circuit_symbols(model_circuit)))
        self._symbols = tf.constant([str(x) for x in self._symbols_list])

        self._circuit = util.convert_to_tensor([model_circuit])

        if len(self._symbols_list) == 0:
            raise ValueError("model_circuit has no sympy.Symbols. Please "
                             "provide a circuit that contains symbols so "
                             "that their values can be trained.")

        # Ingest operators.
        if isinstance(operators, (cirq.PauliString, cirq.PauliSum)):
            operators = [operators]

        if not isinstance(operators, (list, np.ndarray, tuple)):
            raise TypeError("operators must be a cirq.PauliSum or "
                            "cirq.PauliString, or a list, tuple, "
                            "or np.array containing them. "
                            "Got {}.".format(type(operators)))
        if not all([
                isinstance(op, (cirq.PauliString, cirq.PauliSum))
                for op in operators
        ]):
            raise TypeError("Each element in operators to measure "
                            "must be a cirq.PauliString"
                            " or cirq.PauliSum")

        self._operators = util.convert_to_tensor([operators])

        # Ingest and promote repetitions.
        if repetitions is None:
            raise ValueError("Value for repetitions must be provided when "
                             "using noisy simulation.")
        if not isinstance(repetitions, numbers.Integral):
            raise TypeError("repetitions must be a positive integer value."
                            " Given: ".format(repetitions))
        if repetitions <= 0:
            raise ValueError("Repetitions must be greater than zero.")

        self._repetitions = tf.constant(
            [[repetitions for _ in range(len(operators))]],
            dtype=tf.dtypes.int32)

        # Ingest differentiator.
        if differentiator is None:
            differentiator = parameter_shift.ParameterShift()

        # Ingest and promote sample based.
        if sample_based is None:
            raise ValueError("Please specify sample_based=False for analytic "
                             "calculations based on monte-carlo trajectories,"
                             " or sampled_based=True for measurement based "
                             "noisy estimates.")
        if not isinstance(sample_based, bool):
            raise TypeError("sample_based must be either True or False."
                            " received: {}".format(type(sample_based)))

        if not sample_based:
            self._executor = differentiator.generate_differentiable_op(
                sampled_op=noisy_expectation_op.expectation)
        else:
            self._executor = differentiator.generate_differentiable_op(
                sampled_op=noisy_sampled_expectation_op.sampled_expectation)

        self._append_layer = elementary.AddCircuit()
Beispiel #9
0
 def test_no_gradient_circuits(self):
     """Confirm ParameterShift differentiator has no gradient circuits."""
     dif = parameter_shift.ParameterShift()
     with self.assertRaisesRegex(NotImplementedError,
                                 expected_regex="not currently available"):
         _ = dif.get_gradient_circuits(None, None, None)
Beispiel #10
0
class ParameterShiftTest(tf.test.TestCase, parameterized.TestCase):
    """Test the ParameterShift Differentiator will run end to end."""
    def test_parameter_shift_analytic(self):
        """Test if ParameterShift.differentiate_analytical doesn't crash before
        running."""
        programs, names, values, ops, _, true_f, true_g = \
        _simple_op_inputs()

        ps = parameter_shift.ParameterShift()
        op = ps.generate_differentiable_op(
            analytic_op=circuit_execution_ops.get_expectation_op())

        with tf.GradientTape() as g:
            g.watch(values)
            expectations = op(programs, names, values, ops)
        grads = g.gradient(expectations, values)
        self.assertAllClose(expectations, true_f, atol=1e-2, rtol=1e-2)
        self.assertAllClose(grads, true_g, atol=1e-2, rtol=1e-2)

    def test_parameter_shift_sampled(self):
        """Test if ParameterShift.differentiate_sampled doesn't crash before
        running."""
        programs, names, values, ops, n_samples, true_f, true_g = \
        _simple_op_inputs()
        ps = parameter_shift.ParameterShift()
        op = ps.generate_differentiable_op(
            sampled_op=circuit_execution_ops.get_sampled_expectation_op())

        with tf.GradientTape() as g:
            g.watch(values)
            expectations = op(programs, names, values, ops, n_samples)
        grads = g.gradient(expectations, values)
        self.assertAllClose(expectations, true_f, atol=1e-1, rtol=1e-1)
        self.assertAllClose(grads, true_g, atol=1e-1, rtol=1e-1)

    def test_get_gradient_circuits(self):
        """Test that the correct objects are returned."""

        diff = parameter_shift.ParameterShift()

        # Circuits to differentiate.
        symbols = [sympy.Symbol("s0"), sympy.Symbol("s1")]
        q0 = cirq.GridQubit(0, 0)
        q1 = cirq.GridQubit(1, 2)
        input_programs = util.convert_to_tensor([
            cirq.Circuit(
                cirq.X(q0)**symbols[0],
                cirq.Y(q0)**symbols[0],
                cirq.ry(symbols[1])(q1)),
            cirq.Circuit(cirq.Y(q1)**symbols[1]),
        ])
        input_symbol_names = tf.constant([str(s) for s in symbols])
        input_symbol_values = tf.constant([[1.5, -2.7], [-0.3, 0.9]])

        # First, for each symbol `s`, check how many times `s` appears in each
        # program `p`, `n_ps`. Let `n_param_gates` be the maximum of `n_ps` over
        # all symbols and programs. Then, the shape of `batch_programs` will be
        # [n_programs, n_symbols * n_param_gates * n_shifts], where `n_shifts`
        # is 2 because we decompose into gates with 2 eigenvalues. For row index
        # `p` we have for column indices between `i * n_param_gates * n_shifts`
        # and `(i + 1) * n_param_gates * n_shifts`, the first `n_pi * 2`
        # programs are parameter shifted versions of `input_programs[p]` and the
        # remaining programs are empty.
        # Here, `n_param_gates` is 2.
        impurity_symbol_name = "_impurity_for_param_shift"
        impurity_symbol = sympy.Symbol(impurity_symbol_name)
        expected_batch_programs_0 = util.convert_to_tensor([
            cirq.Circuit(
                cirq.X(q0)**impurity_symbol,
                cirq.Y(q0)**symbols[0],
                cirq.ry(symbols[1])(q1)),
            cirq.Circuit(
                cirq.X(q0)**impurity_symbol,
                cirq.Y(q0)**symbols[0],
                cirq.ry(symbols[1])(q1)),
            cirq.Circuit(
                cirq.X(q0)**symbols[0],
                cirq.Y(q0)**impurity_symbol,
                cirq.ry(symbols[1])(q1)),
            cirq.Circuit(
                cirq.X(q0)**symbols[0],
                cirq.Y(q0)**impurity_symbol,
                cirq.ry(symbols[1])(q1)),
            cirq.Circuit(
                cirq.X(q0)**symbols[0],
                cirq.Y(q0)**symbols[0],
                cirq.ry(impurity_symbol)(q1)),
            cirq.Circuit(
                cirq.X(q0)**symbols[0],
                cirq.Y(q0)**symbols[0],
                cirq.ry(impurity_symbol)(q1)),
            cirq.Circuit(),
            cirq.Circuit()
        ])
        expected_batch_programs_1 = util.convert_to_tensor([
            cirq.Circuit(),
            cirq.Circuit(),
            cirq.Circuit(),
            cirq.Circuit(),
            cirq.Circuit(cirq.Y(q1)**impurity_symbol),
            cirq.Circuit(cirq.Y(q1)**impurity_symbol),
            cirq.Circuit(),
            cirq.Circuit()
        ])
        expected_batch_programs = tf.stack(
            [expected_batch_programs_0, expected_batch_programs_1])

        # The new symbols are the old ones, with an extra used for shifting.
        expected_new_symbol_names = tf.concat(
            [input_symbol_names,
             tf.constant([impurity_symbol_name])], 0)

        # The batch symbol values are the input symbol values, tiled and with
        # shifted values appended. Locations that have empty programs should
        # also have zero for the shift.
        # The shifted values are the original value plus 1/2 divided by the
        # `exponent_scalar` of the gate.
        expected_batch_symbol_values = tf.constant(
            [[[1.5, -2.7, 1.5 + 0.5], [1.5, -2.7, 1.5 - 0.5],
              [1.5, -2.7, 1.5 + 0.5], [1.5, -2.7, 1.5 - 0.5],
              [1.5, -2.7, -2.7 + np.pi / 2], [1.5, -2.7, -2.7 - np.pi / 2],
              [1.5, -2.7, -2.7], [1.5, -2.7, -2.7]],
             [[-0.3, 0.9, -0.3], [-0.3, 0.9, -0.3], [-0.3, 0.9, -0.3],
              [-0.3, 0.9, -0.3], [-0.3, 0.9, 0.9 + 0.5],
              [-0.3, 0.9, 0.9 - 0.5], [-0.3, 0.9, 0.9], [-0.3, 0.9, 0.9]]])

        # Empty program locations are given zero weight.
        expected_batch_weights = tf.constant(
            [[[np.pi / 2, -np.pi / 2, np.pi / 2, -np.pi / 2],
              [0.5, -0.5, 0.0, 0.0]],
             [[0.0, 0.0, 0.0, 0.0], [np.pi / 2, -np.pi / 2, 0.0, 0.0]]])

        expected_batch_mapper = tf.constant([[[0, 1, 2, 3], [4, 5, 6, 7]],
                                             [[0, 1, 2, 3], [4, 5, 6, 7]]])

        (test_batch_programs, test_new_symbol_names, test_batch_symbol_values,
         test_batch_weights, test_batch_mapper) = diff.get_gradient_circuits(
             input_programs, input_symbol_names, input_symbol_values)
        for i in range(tf.shape(input_programs)[0]):
            self.assertAllEqual(util.from_tensor(expected_batch_programs[i]),
                                util.from_tensor(test_batch_programs[i]))
        self.assertAllEqual(expected_new_symbol_names, test_new_symbol_names)
        self.assertAllClose(expected_batch_symbol_values,
                            test_batch_symbol_values,
                            atol=1e-5)
        self.assertAllClose(expected_batch_weights,
                            test_batch_weights,
                            atol=1e-5)
        self.assertAllEqual(expected_batch_mapper, test_batch_mapper)

    @parameterized.parameters(
        list(
            util.kwargs_cartesian_product(
                **{
                    'differentiator': [
                        parameter_shift.ParameterShift(),
                    ],
                    'n_qubits': [5],
                    'n_programs': [3],
                    'n_ops': [3],
                    'symbol_names': [['a', 'b']]
                })))
    def test_gradient_circuits_grad_comparison(self, differentiator, n_qubits,
                                               n_programs, n_ops,
                                               symbol_names):
        """Test that analytic gradient agrees with the one from grad circuits"""
        # Get random circuits to check.
        qubits = cirq.GridQubit.rect(1, n_qubits)
        circuit_batch, resolver_batch = \
            util.random_symbol_circuit_resolver_batch(
                cirq.GridQubit.rect(1, n_qubits), symbol_names, n_programs)
        psums = [
            util.random_pauli_sums(qubits, 1, n_ops) for _ in circuit_batch
        ]

        # Convert to tensors.
        symbol_names_array = np.array(symbol_names)
        symbol_values_array = np.array(
            [[resolver[symbol] for symbol in symbol_names]
             for resolver in resolver_batch],
            dtype=np.float32)
        symbol_names_tensor = tf.convert_to_tensor(symbol_names_array)
        symbol_values_tensor = tf.convert_to_tensor(symbol_values_array)
        programs = util.convert_to_tensor(circuit_batch)
        ops_tensor = util.convert_to_tensor(psums)

        # Get gradients using expectations of gradient circuits.
        (batch_programs, new_symbol_names, batch_symbol_values, batch_weights,
         batch_mapper) = differentiator.get_gradient_circuits(
             programs, symbol_names_tensor, symbol_values_tensor)
        analytic_op = circuit_execution_ops.get_expectation_op()
        batch_pauli_sums = tf.tile(tf.expand_dims(ops_tensor, 1),
                                   [1, tf.shape(batch_programs)[1], 1])
        n_batch_programs = tf.reduce_prod(tf.shape(batch_programs))
        n_symbols = tf.shape(new_symbol_names)[0]
        batch_expectations = analytic_op(
            tf.reshape(batch_programs, [n_batch_programs]), new_symbol_names,
            tf.reshape(batch_symbol_values, [n_batch_programs, n_symbols]),
            tf.reshape(batch_pauli_sums, [n_batch_programs, n_ops]))
        batch_expectations = tf.reshape(batch_expectations,
                                        tf.shape(batch_pauli_sums))
        batch_jacobian = tf.map_fn(
            lambda x: tf.einsum('km,kmp->kp', x[0], tf.gather(x[1], x[2])),
            (batch_weights, batch_expectations, batch_mapper),
            fn_output_signature=tf.float32)
        grad_manual = tf.reduce_sum(batch_jacobian, -1)

        # Get gradients using autodiff.
        differentiator.refresh()
        differentiable_op = differentiator.generate_differentiable_op(
            analytic_op=analytic_op)
        with tf.GradientTape() as g:
            g.watch(symbol_values_tensor)
            exact_outputs = differentiable_op(programs, symbol_names_tensor,
                                              symbol_values_tensor, ops_tensor)
        grad_auto = g.gradient(exact_outputs, symbol_values_tensor)
        self.assertAllClose(grad_manual, grad_auto, atol=1e-5)