Skip to content

Commit

Permalink
Translate parameterized gates only in gradient calculations (Qiskit/q…
Browse files Browse the repository at this point in the history
…iskit#9067)

* translate parameterized gates only

* TranslateParameterized to proper pass

* update reno w/ new trafo pass

* Only add cregs if required

* rm unused import

* directly construct DAG

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>

* target support

* qubit-wise support of unrolling

* globally check for supported gates in target

that's because this pass is run before qubit mapping

* lint!

* updates after merge from main

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
3 people committed Jan 6, 2023
1 parent 7326947 commit c82f3f7
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 59 deletions.
11 changes: 4 additions & 7 deletions qiskit_algorithms/gradients/base_estimator_gradient.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@

import numpy as np

from qiskit import transpile
from qiskit.algorithms import AlgorithmJob
from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit
from qiskit.opflow import PauliSumOp
from qiskit.primitives import BaseEstimator
from qiskit.primitives.utils import _circuit_key
from qiskit.providers import Options
from qiskit.quantum_info.operators.base_operator import BaseOperator
from qiskit.transpiler.passes import TranslateParameterizedGates

from .estimator_gradient_result import EstimatorGradientResult
from .utils import (
Expand Down Expand Up @@ -160,18 +160,15 @@ def _preprocess(
The list of gradient circuits, the list of parameter values, and the list of parameters.
parameter_values and parameters are updated to match the gradient circuit.
"""
translator = TranslateParameterizedGates(supported_gates)
g_circuits, g_parameter_values, g_parameter_sets = [], [], []
for circuit, parameter_value_, parameter_set in zip(
circuits, parameter_values, parameter_sets
):
circuit_key = _circuit_key(circuit)
if circuit_key not in self._gradient_circuit_cache:
transpiled_circuit = transpile(
circuit, basis_gates=supported_gates, optimization_level=0
)
self._gradient_circuit_cache[circuit_key] = _assign_unique_parameters(
transpiled_circuit
)
unrolled = translator(circuit)
self._gradient_circuit_cache[circuit_key] = _assign_unique_parameters(unrolled)
gradient_circuit = self._gradient_circuit_cache[circuit_key]
g_circuits.append(gradient_circuit.gradient_circuit)
g_parameter_values.append(
Expand Down
11 changes: 4 additions & 7 deletions qiskit_algorithms/gradients/base_sampler_gradient.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@
from collections.abc import Sequence
from copy import copy

from qiskit import transpile
from qiskit.algorithms import AlgorithmJob
from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit
from qiskit.primitives import BaseSampler
from qiskit.primitives.utils import _circuit_key
from qiskit.providers import Options
from qiskit.transpiler.passes import TranslateParameterizedGates

from .sampler_gradient_result import SamplerGradientResult
from .utils import (
Expand Down Expand Up @@ -141,18 +141,15 @@ def _preprocess(
The list of gradient circuits, the list of parameter values, and the list of parameters.
parameter_values and parameters are updated to match the gradient circuit.
"""
translator = TranslateParameterizedGates(supported_gates)
g_circuits, g_parameter_values, g_parameter_sets = [], [], []
for circuit, parameter_value_, parameter_set in zip(
circuits, parameter_values, parameter_sets
):
circuit_key = _circuit_key(circuit)
if circuit_key not in self._gradient_circuit_cache:
transpiled_circuit = transpile(
circuit, basis_gates=supported_gates, optimization_level=0
)
self._gradient_circuit_cache[circuit_key] = _assign_unique_parameters(
transpiled_circuit
)
unrolled = translator(circuit)
self._gradient_circuit_cache[circuit_key] = _assign_unique_parameters(unrolled)
gradient_circuit = self._gradient_circuit_cache[circuit_key]
g_circuits.append(gradient_circuit.gradient_circuit)
g_parameter_values.append(
Expand Down
13 changes: 8 additions & 5 deletions qiskit_algorithms/gradients/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@

import numpy as np

from qiskit import transpile
from qiskit.circuit import (
ClassicalRegister,
Gate,
Expand All @@ -46,6 +45,7 @@
RZZGate,
XGate,
)
from qiskit.transpiler.passes.basis import TranslateParameterizedGates

################################################################################
## Gradient circuits and Enum
Expand Down Expand Up @@ -211,13 +211,16 @@ def _make_lin_comb_qfi_circuit(
"y",
"z",
]

circuit2 = transpile(circuit, basis_gates=supported_gates, optimization_level=0)
unroller = TranslateParameterizedGates(supported_gates)
circuit2 = unroller(circuit)

qr_aux = QuantumRegister(1, "aux")
cr_aux = ClassicalRegister(1, "aux")
circuit2.add_register(qr_aux)
circuit2.add_bits(cr_aux)

if add_measurement:
cr_aux = ClassicalRegister(1, "aux")
circuit2.add_bits(cr_aux)

circuit2.h(qr_aux)
circuit2.data.insert(0, circuit2.data.pop())

Expand Down
49 changes: 49 additions & 0 deletions test/gradients/logging_primitives.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2022.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Test primitives that check what kind of operations are in the circuits they execute."""

from qiskit.primitives import Estimator, Sampler


class LoggingEstimator(Estimator):
"""An estimator checking what operations were in the circuits it executed."""

def __init__(
self,
circuits=None,
observables=None,
parameters=None,
options=None,
operations_callback=None,
):
super().__init__(circuits, observables, parameters, options)
self.operations_callback = operations_callback

def _run(self, circuits, observables, parameter_values, **run_options):
if self.operations_callback is not None:
ops = [circuit.count_ops() for circuit in circuits]
self.operations_callback(ops)
return super()._run(circuits, observables, parameter_values, **run_options)


class LoggingSampler(Sampler):
"""A sampler checking what operations were in the circuits it executed."""

def __init__(self, operations_callback):
super().__init__()
self.operations_callback = operations_callback

def _run(self, circuits, parameter_values, **run_options):
ops = [circuit.count_ops() for circuit in circuits]
self.operations_callback(ops)
return super()._run(circuits, parameter_values, **run_options)
78 changes: 57 additions & 21 deletions test/gradients/test_estimator_gradient.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
"""Test Estimator Gradients"""

import unittest
from test import combine

import numpy as np
from ddt import ddt, data, unpack
Expand All @@ -35,6 +34,8 @@
from qiskit.quantum_info.random import random_pauli_list
from qiskit.test import QiskitTestCase

from .logging_primitives import LoggingEstimator

gradient_factories = [
lambda estimator: FiniteDiffEstimatorGradient(estimator, epsilon=1e-6, method="central"),
lambda estimator: FiniteDiffEstimatorGradient(estimator, epsilon=1e-6, method="forward"),
Expand All @@ -48,7 +49,7 @@
class TestEstimatorGradient(QiskitTestCase):
"""Test Estimator Gradient"""

@combine(grad=gradient_factories)
@data(*gradient_factories)
def test_gradient_operators(self, grad):
"""Test the estimator gradient for different operators"""
estimator = Estimator()
Expand All @@ -70,7 +71,7 @@ def test_gradient_operators(self, grad):
value = gradient.run([qc], [op], [param]).result().gradients[0]
self.assertAlmostEqual(value[0], correct_result, 3)

@combine(grad=gradient_factories)
@data(*gradient_factories)
def test_single_circuit_observable(self, grad):
"""Test the estimator gradient for a single circuit and observable"""
estimator = Estimator()
Expand All @@ -86,7 +87,7 @@ def test_single_circuit_observable(self, grad):
value = gradient.run(qc, op, [param]).result().gradients[0]
self.assertAlmostEqual(value[0], correct_result, 3)

@combine(grad=gradient_factories)
@data(*gradient_factories)
def test_gradient_p(self, grad):
"""Test the estimator gradient for p"""
estimator = Estimator()
Expand All @@ -104,7 +105,7 @@ def test_gradient_p(self, grad):
for j, value in enumerate(gradients):
self.assertAlmostEqual(value, correct_results[i][j], 3)

@combine(grad=gradient_factories)
@data(*gradient_factories)
def test_gradient_u(self, grad):
"""Test the estimator gradient for u"""
estimator = Estimator()
Expand All @@ -125,7 +126,7 @@ def test_gradient_u(self, grad):
for j, value in enumerate(gradients):
self.assertAlmostEqual(value, correct_results[i][j], 3)

@combine(grad=gradient_factories)
@data(*gradient_factories)
def test_gradient_efficient_su2(self, grad):
"""Test the estimator gradient for EfficientSU2"""
estimator = Estimator()
Expand Down Expand Up @@ -153,7 +154,7 @@ def test_gradient_efficient_su2(self, grad):
gradients = gradient.run([qc], [op], [param]).result().gradients[0]
np.testing.assert_allclose(gradients, correct_results[i], atol=1e-3)

@combine(grad=gradient_factories)
@data(*gradient_factories)
def test_gradient_2qubit_gate(self, grad):
"""Test the estimator gradient for 2 qubit gates"""
estimator = Estimator()
Expand All @@ -178,7 +179,7 @@ def test_gradient_2qubit_gate(self, grad):
gradients = gradient.run([qc], [op], [param]).result().gradients[0]
np.testing.assert_allclose(gradients, correct_results[i], atol=1e-3)

@combine(grad=gradient_factories)
@data(*gradient_factories)
def test_gradient_parameter_coefficient(self, grad):
"""Test the estimator gradient for parameter variables with coefficients"""
estimator = Estimator()
Expand All @@ -199,7 +200,7 @@ def test_gradient_parameter_coefficient(self, grad):
gradients = gradient.run([qc], [op], [param]).result().gradients[0]
np.testing.assert_allclose(gradients, correct_results[i], atol=1e-3)

@combine(grad=gradient_factories)
@data(*gradient_factories)
def test_gradient_parameters(self, grad):
"""Test the estimator gradient for parameters"""
estimator = Estimator()
Expand All @@ -218,7 +219,7 @@ def test_gradient_parameters(self, grad):
gradients = gradient.run([qc], [op], [param], parameters=[[a]]).result().gradients[0]
np.testing.assert_allclose(gradients, correct_results[i], atol=1e-3)

@combine(grad=gradient_factories)
@data(*gradient_factories)
def test_gradient_multi_arguments(self, grad):
"""Test the estimator gradient for multiple arguments"""
estimator = Estimator()
Expand Down Expand Up @@ -257,9 +258,7 @@ def test_gradient_multi_arguments(self, grad):
np.testing.assert_allclose(gradients2[1], correct_results2[1], atol=1e-3)
np.testing.assert_allclose(gradients2[2], correct_results2[2], atol=1e-3)

@combine(
grad=[FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient]
)
@data(FiniteDiffEstimatorGradient, ParamShiftEstimatorGradient, LinCombEstimatorGradient)
def test_gradient_validation(self, grad):
"""Test estimator gradient's validation"""
estimator = Estimator()
Expand Down Expand Up @@ -318,7 +317,7 @@ def test_spsa_gradient(self):
gradients = gradient.run([qc], [op], param_list).result().gradients
np.testing.assert_allclose(gradients, correct_results, atol=1e-3)

@combine(grad=[ParamShiftEstimatorGradient, LinCombEstimatorGradient])
@data(ParamShiftEstimatorGradient, LinCombEstimatorGradient)
def test_gradient_random_parameters(self, grad):
"""Test param shift and lin comb w/ random parameters"""
rng = np.random.default_rng(123)
Expand Down Expand Up @@ -370,13 +369,11 @@ def test_lin_comb_imag_gradient(self, derivative_type, expected_gradient_value):
result = gradient.run([c], [Pauli("I")], [[0.0]]).result()
self.assertAlmostEqual(result.gradients[0][0], expected_gradient_value)

@combine(
grad=[
FiniteDiffEstimatorGradient,
ParamShiftEstimatorGradient,
LinCombEstimatorGradient,
SPSAEstimatorGradient,
],
@data(
FiniteDiffEstimatorGradient,
ParamShiftEstimatorGradient,
LinCombEstimatorGradient,
SPSAEstimatorGradient,
)
def test_options(self, grad):
"""Test estimator gradient's run options"""
Expand Down Expand Up @@ -427,6 +424,45 @@ def test_options(self, grad):
# Only default + estimator options. Not run.
self.assertEqual(options.get("shots"), 200)

@data(
FiniteDiffEstimatorGradient,
ParamShiftEstimatorGradient,
LinCombEstimatorGradient,
SPSAEstimatorGradient,
)
def test_operations_preserved(self, gradient_cls):
"""Test non-parameterized instructions are preserved and not unrolled."""
x = Parameter("x")
circuit = QuantumCircuit(2)
circuit.initialize([0.5, 0.5, 0.5, 0.5]) # this should remain as initialize
circuit.crx(x, 0, 1) # this should get unrolled

values = [np.pi / 2]
expect = -1 / (2 * np.sqrt(2))

observable = SparsePauliOp(["XX"])

ops = []

def operations_callback(op):
ops.append(op)

estimator = LoggingEstimator(operations_callback=operations_callback)

if gradient_cls in [SPSAEstimatorGradient, FiniteDiffEstimatorGradient]:
gradient = gradient_cls(estimator, epsilon=0.01)
else:
gradient = gradient_cls(estimator)

job = gradient.run([circuit], [observable], [values])
result = job.result()

with self.subTest(msg="assert initialize is preserved"):
self.assertTrue(all("initialize" in ops_i[0].keys() for ops_i in ops))

with self.subTest(msg="assert result is correct"):
self.assertAlmostEqual(result.gradients[0].item(), expect, places=5)


if __name__ == "__main__":
unittest.main()
30 changes: 30 additions & 0 deletions test/gradients/test_qfi.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
from qiskit.primitives import Estimator
from qiskit.test import QiskitTestCase

from .logging_primitives import LoggingEstimator


class TestQFI(QiskitTestCase):
"""Test QFI"""
Expand Down Expand Up @@ -251,6 +253,34 @@ def test_options(self):
self.assertEqual(result.options.get("shots"), 300)
self.assertEqual(options.get("shots"), 200)

def test_operations_preserved(self):
"""Test non-parameterized instructions are preserved and not unrolled."""
x, y = Parameter("x"), Parameter("y")
circuit = QuantumCircuit(2)
circuit.initialize([0.5, 0.5, 0.5, 0.5]) # this should remain as initialize
circuit.crx(x, 0, 1) # this should get unrolled
circuit.ry(y, 0)

values = [np.pi / 2, np.pi]
expect = np.diag([0.25, 0.5])

ops = []

def operations_callback(op):
ops.append(op)

estimator = LoggingEstimator(operations_callback=operations_callback)
qfi = LinCombQFI(estimator)

job = qfi.run([circuit], [values])
result = job.result()

with self.subTest(msg="assert initialize is preserved"):
self.assertTrue(all("initialize" in ops_i[0].keys() for ops_i in ops))

with self.subTest(msg="assert result is correct"):
np.testing.assert_allclose(result.qfis[0], expect, atol=1e-5)


if __name__ == "__main__":
unittest.main()
Loading

0 comments on commit c82f3f7

Please sign in to comment.