Repository: Qiskit/qiskit-terra (python)

Issue:
Support unparameterized opaque gates in the gradients
### What should we add?

If a gate is opaque but unparameterized, the estimator and sampler gradients can still construct the gradient circuits. Currently, this is not supported as e.g. the LCU gradients internally attempt to unroll the whole circuit to a fixed basis set. This unrolling can be restricted to only parameterized gates and (1) enable handling custom, opaque gates and (2) doesn't prevent any optimizations the compiler might do on abstract blocks.

Pull request description:
Translate parameterized gates only in gradient calculations
<!--
⚠️ If you do not respect this template, your pull request will be closed.
⚠️ Your pull request title should be short detailed and understandable for all.
⚠️ Also, please add a release note file using reno if the change needs to be
  documented in the release notes.
⚠️ If your pull request fixes an open issue, please link to the issue.

- [ ] I have added the tests to cover my changes.
- [ ] I have updated the documentation accordingly.
- [ ] I have read the CONTRIBUTING document.
-->

### Summary

Closes #9054.

### Details and comments

All gradient now preserve unparameterized operations instead of attempting to unroll them. 
This 
* allows to evaluate gradients on custom, opaque gates that individual primitives can handle
* keeps a higher level of abstraction for optimized synthesis and compilation after the gradient circuits have been constructed.

Patch:
diff --git a/qiskit/algorithms/gradients/base_estimator_gradient.py b/qiskit/algorithms/gradients/base_estimator_gradient.py
index 2b1d169ec..95176bdc1 100644
--- a/qiskit/algorithms/gradients/base_estimator_gradient.py
+++ b/qiskit/algorithms/gradients/base_estimator_gradient.py
@@ -22,7 +22,6 @@ from copy import copy
 
 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
@@ -30,6 +29,7 @@ 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 (
@@ -160,18 +160,15 @@ class BaseEstimatorGradient(ABC):
             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(
diff --git a/qiskit/algorithms/gradients/base_sampler_gradient.py b/qiskit/algorithms/gradients/base_sampler_gradient.py
index a7276feff..cd348bfa3 100644
--- a/qiskit/algorithms/gradients/base_sampler_gradient.py
+++ b/qiskit/algorithms/gradients/base_sampler_gradient.py
@@ -21,12 +21,12 @@ from collections import defaultdict
 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 (
@@ -141,18 +141,15 @@ class BaseSamplerGradient(ABC):
             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(
diff --git a/qiskit/algorithms/gradients/utils.py b/qiskit/algorithms/gradients/utils.py
index a828106f4..8950de5cc 100644
--- a/qiskit/algorithms/gradients/utils.py
+++ b/qiskit/algorithms/gradients/utils.py
@@ -23,7 +23,6 @@ from dataclasses import dataclass
 
 import numpy as np
 
-from qiskit import transpile
 from qiskit.circuit import (
     ClassicalRegister,
     Gate,
@@ -46,6 +45,7 @@ from qiskit.circuit.library.standard_gates import (
     RZZGate,
     XGate,
 )
+from qiskit.transpiler.passes.basis import TranslateParameterizedGates
 
 ################################################################################
 ## Gradient circuits and Enum
@@ -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())
 
diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py
index c9c5ec33e..dd4689827 100644
--- a/qiskit/transpiler/passes/__init__.py
+++ b/qiskit/transpiler/passes/__init__.py
@@ -59,6 +59,7 @@ Basis Change
    Decompose
    UnrollCustomDefinitions
    BasisTranslator
+   TranslateParameterizedGates
 
 Optimizations
 =============
@@ -205,6 +206,7 @@ from .basis import Unroller
 from .basis import UnrollCustomDefinitions
 from .basis import Unroll3qOrMore
 from .basis import BasisTranslator
+from .basis import TranslateParameterizedGates
 
 # optimization
 from .optimization import Optimize1qGates
diff --git a/qiskit/transpiler/passes/basis/__init__.py b/qiskit/transpiler/passes/basis/__init__.py
index 620834e7b..57c094a2a 100644
--- a/qiskit/transpiler/passes/basis/__init__.py
+++ b/qiskit/transpiler/passes/basis/__init__.py
@@ -17,3 +17,4 @@ from .unroller import Unroller
 from .unroll_custom_definitions import UnrollCustomDefinitions
 from .unroll_3q_or_more import Unroll3qOrMore
 from .basis_translator import BasisTranslator
+from .translate_parameterized import TranslateParameterizedGates
diff --git a/qiskit/transpiler/passes/basis/translate_parameterized.py b/qiskit/transpiler/passes/basis/translate_parameterized.py
new file mode 100644
index 000000000..9e6e555a4
--- /dev/null
+++ b/qiskit/transpiler/passes/basis/translate_parameterized.py
@@ -0,0 +1,177 @@
+# 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.
+
+"""Translate parameterized gates only, and leave others as they are."""
+
+from __future__ import annotations
+
+from qiskit.circuit import Instruction, ParameterExpression, Qubit, Clbit
+from qiskit.converters import circuit_to_dag
+from qiskit.dagcircuit import DAGCircuit, DAGOpNode
+from qiskit.circuit.equivalence_library import EquivalenceLibrary
+from qiskit.exceptions import QiskitError
+from qiskit.transpiler import Target
+
+from qiskit.transpiler.basepasses import TransformationPass
+
+from .basis_translator import BasisTranslator
+
+
+class TranslateParameterizedGates(TransformationPass):
+    """Translate parameterized gates to a supported basis set.
+
+    Once a parameterized instruction is found that is not in the ``supported_gates`` list,
+    the instruction is decomposed one level and the parameterized sub-blocks are recursively
+    decomposed. The recursion is stopped once all parameterized gates are in ``supported_gates``,
+    or if a gate has no definition and a translation to the basis is attempted (this might happen
+    e.g. for the ``UGate`` if it's not in the specified gate list).
+
+    Example:
+
+        The following, multiply nested circuit::
+
+            from qiskit.circuit import QuantumCircuit, ParameterVector
+            from qiskit.transpiler.passes import TranslateParameterizedGates
+
+            x = ParameterVector("x", 4)
+            block1 = QuantumCircuit(1)
+            block1.rx(x[0], 0)
+
+            sub_block = QuantumCircuit(2)
+            sub_block.cx(0, 1)
+            sub_block.rz(x[2], 0)
+
+            block2 = QuantumCircuit(2)
+            block2.ry(x[1], 0)
+            block2.append(sub_block.to_gate(), [0, 1])
+
+            block3 = QuantumCircuit(3)
+            block3.ccx(0, 1, 2)
+
+            circuit = QuantumCircuit(3)
+            circuit.append(block1.to_gate(), [1])
+            circuit.append(block2.to_gate(), [0, 1])
+            circuit.append(block3.to_gate(), [0, 1, 2])
+            circuit.cry(x[3], 0, 2)
+
+            supported_gates = ["rx", "ry", "rz", "cp", "crx", "cry", "crz"]
+            unrolled = TranslateParameterizedGates(supported_gates)(circuit)
+
+        is decomposed to::
+
+                 ┌──────────┐     ┌──────────┐┌─────────────┐
+            q_0: ┤ Ry(x[1]) ├──■──┤ Rz(x[2]) ├┤0            ├─────■──────
+                 ├──────────┤┌─┴─┐└──────────┘│             │     │
+            q_1: ┤ Rx(x[0]) ├┤ X ├────────────┤1 circuit-92 ├─────┼──────
+                 └──────────┘└───┘            │             │┌────┴─────┐
+            q_2: ─────────────────────────────┤2            ├┤ Ry(x[3]) ├
+                                              └─────────────┘└──────────┘
+
+    """
+
+    def __init__(
+        self,
+        supported_gates: list[str] | None = None,
+        equivalence_library: EquivalenceLibrary | None = None,
+        target: Target | None = None,
+    ) -> None:
+        """
+        Args:
+            supported_gates: A list of suppported basis gates specified as string. If ``None``,
+                a ``target`` must be provided.
+            equivalence_library: The equivalence library to translate the gates. Defaults
+                to the equivalence library of all Qiskit standard gates.
+            target: A :class:`.Target` containing the supported operations. If ``None``,
+                ``supported_gates`` must be set. Note that this argument takes precedence over
+                ``supported_gates``, if both are set.
+
+        Raises:
+            ValueError: If neither of ``supported_gates`` and ``target`` are passed.
+        """
+        super().__init__()
+
+        # get the default equivalence library, if none has been set
+        if equivalence_library is None:
+            from qiskit.circuit.library.standard_gates.equivalence_library import _sel
+
+            equivalence_library = _sel
+
+        # The target takes precedence over the supported_gates argument. If neither are set,
+        # raise an error.
+        if target is not None:
+            supported_gates = target.operation_names
+        elif supported_gates is None:
+            raise ValueError("One of ``supported_gates`` or ``target`` must be specified.")
+
+        self._supported_gates = supported_gates
+        self._target = target
+        self._translator = BasisTranslator(equivalence_library, supported_gates, target=target)
+
+    def run(self, dag: DAGCircuit) -> DAGCircuit:
+        """Run the transpiler pass.
+
+        Args:
+            dag: The DAG circuit in which the parameterized gates should be unrolled.
+
+        Returns:
+            A DAG where the parameterized gates have been unrolled.
+
+        Raises:
+            QiskitError: If the circuit cannot be unrolled.
+        """
+        for node in dag.op_nodes():
+            # check whether it is parameterized and we need to decompose it
+            if _is_parameterized(node.op) and not _is_supported(
+                node, self._supported_gates, self._target
+            ):
+                definition = node.op.definition
+
+                if definition is not None:
+                    # recurse to unroll further parameterized blocks
+                    unrolled = self.run(circuit_to_dag(definition))
+                else:
+                    # if we hit a base case, try to translate to the specified basis
+                    try:
+                        unrolled = self._translator.run(_instruction_to_dag(node.op))
+                    except Exception as exc:
+                        raise QiskitError("Failed to translate final block.") from exc
+
+                # replace with unrolled (or translated) dag
+                dag.substitute_node_with_dag(node, unrolled)
+
+        return dag
+
+
+def _is_parameterized(op: Instruction) -> bool:
+    return any(
+        isinstance(param, ParameterExpression) and len(param.parameters) > 0 for param in op.params
+    )
+
+
+def _is_supported(node: DAGOpNode, supported_gates: list[str], target: Target | None) -> bool:
+    """Check whether the node is supported.
+
+    If the target is provided, check using the target, otherwise the supported_gates are used.
+    """
+    if target is not None:
+        return target.instruction_supported(node.op.name)
+
+    return node.op.name in supported_gates
+
+
+def _instruction_to_dag(op: Instruction) -> DAGCircuit:
+    dag = DAGCircuit()
+    dag.add_qubits([Qubit() for _ in range(op.num_qubits)])
+    dag.add_qubits([Clbit() for _ in range(op.num_clbits)])
+    dag.apply_operation_back(op, dag.qubits, dag.clbits)
+
+    return dag
diff --git a/releasenotes/notes/gradients-preserve-unparameterized-8ebff145b6c96fa3.yaml b/releasenotes/notes/gradients-preserve-unparameterized-8ebff145b6c96fa3.yaml
new file mode 100644
index 000000000..d23814893
--- /dev/null
+++ b/releasenotes/notes/gradients-preserve-unparameterized-8ebff145b6c96fa3.yaml
@@ -0,0 +1,12 @@
+---
+features:
+  - |
+    All gradient classes in :mod:`qiskit.algorithms.gradients` now preserve unparameterized
+    operations instead of attempting to unroll them. This allows to evaluate gradients on
+    custom, opaque gates that individual primitives can handle and keeps a higher
+    level of abstraction for optimized synthesis and compilation after the gradient circuits
+    have been constructed.
+  - |
+    Added a :class:`.TranslateParameterizedGates` pass to map only parameterized gates in a 
+    circuit to a specified basis, but leave unparameterized gates untouched. The pass first
+    attempts unrolling and finally translates if a parameterized gate cannot be further unrolled.


Test patch:
diff --git a/test/python/algorithms/gradients/logging_primitives.py b/test/python/algorithms/gradients/logging_primitives.py
new file mode 100644
index 000000000..efd062f4b
--- /dev/null
+++ b/test/python/algorithms/gradients/logging_primitives.py
@@ -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)
diff --git a/test/python/algorithms/gradients/test_estimator_gradient.py b/test/python/algorithms/gradients/test_estimator_gradient.py
index feea14423..5ee28890f 100644
--- a/test/python/algorithms/gradients/test_estimator_gradient.py
+++ b/test/python/algorithms/gradients/test_estimator_gradient.py
@@ -14,7 +14,6 @@
 """Test Estimator Gradients"""
 
 import unittest
-from test import combine
 
 import numpy as np
 from ddt import ddt, data, unpack
@@ -35,6 +34,8 @@ from qiskit.quantum_info import Operator, SparsePauliOp, Pauli
 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"),
@@ -48,7 +49,7 @@ gradient_factories = [
 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()
@@ -70,7 +71,7 @@ class TestEstimatorGradient(QiskitTestCase):
         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()
@@ -86,7 +87,7 @@ class TestEstimatorGradient(QiskitTestCase):
         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()
@@ -104,7 +105,7 @@ class TestEstimatorGradient(QiskitTestCase):
             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()
@@ -125,7 +126,7 @@ class TestEstimatorGradient(QiskitTestCase):
             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()
@@ -153,7 +154,7 @@ class TestEstimatorGradient(QiskitTestCase):
             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()
@@ -178,7 +179,7 @@ class TestEstimatorGradient(QiskitTestCase):
                 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()
@@ -199,7 +200,7 @@ class TestEstimatorGradient(QiskitTestCase):
             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()
@@ -218,7 +219,7 @@ class TestEstimatorGradient(QiskitTestCase):
             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()
@@ -257,9 +258,7 @@ class TestEstimatorGradient(QiskitTestCase):
         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()
@@ -318,7 +317,7 @@ class TestEstimatorGradient(QiskitTestCase):
         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)
@@ -370,13 +369,11 @@ class TestEstimatorGradient(QiskitTestCase):
         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"""
@@ -427,6 +424,45 @@ class TestEstimatorGradient(QiskitTestCase):
             # 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()
diff --git a/test/python/algorithms/gradients/test_qfi.py b/test/python/algorithms/gradients/test_qfi.py
index 276b1c7c3..16d1e94b6 100644
--- a/test/python/algorithms/gradients/test_qfi.py
+++ b/test/python/algorithms/gradients/test_qfi.py
@@ -26,6 +26,8 @@ from qiskit.circuit.parametervector import ParameterVector
 from qiskit.primitives import Estimator
 from qiskit.test import QiskitTestCase
 
+from .logging_primitives import LoggingEstimator
+
 
 class TestQFI(QiskitTestCase):
     """Test QFI"""
@@ -251,6 +253,34 @@ class TestQFI(QiskitTestCase):
             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()
diff --git a/test/python/algorithms/gradients/test_sampler_gradient.py b/test/python/algorithms/gradients/test_sampler_gradient.py
index da5e71f04..7cff9c545 100644
--- a/test/python/algorithms/gradients/test_sampler_gradient.py
+++ b/test/python/algorithms/gradients/test_sampler_gradient.py
@@ -14,11 +14,10 @@
 """Test Sampler Gradients"""
 
 import unittest
-from test import combine
 from typing import List
 
 import numpy as np
-from ddt import ddt
+from ddt import ddt, data
 
 from qiskit import QuantumCircuit
 from qiskit.algorithms.gradients import (
@@ -34,6 +33,8 @@ from qiskit.primitives import Sampler
 from qiskit.result import QuasiDistribution
 from qiskit.test import QiskitTestCase
 
+from .logging_primitives import LoggingSampler
+
 gradient_factories = [
     lambda sampler: FiniteDiffSamplerGradient(sampler, epsilon=1e-6, method="central"),
     lambda sampler: FiniteDiffSamplerGradient(sampler, epsilon=1e-6, method="forward"),
@@ -47,7 +48,7 @@ gradient_factories = [
 class TestSamplerGradient(QiskitTestCase):
     """Test Sampler Gradient"""
 
-    @combine(grad=gradient_factories)
+    @data(*gradient_factories)
     def test_single_circuit(self, grad):
         """Test the sampler gradient for a single circuit"""
         sampler = Sampler()
@@ -70,7 +71,7 @@ class TestSamplerGradient(QiskitTestCase):
                 for k in quasi_dist:
                     self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3)
 
-    @combine(grad=gradient_factories)
+    @data(*gradient_factories)
     def test_gradient_p(self, grad):
         """Test the sampler gradient for p"""
         sampler = Sampler()
@@ -93,7 +94,7 @@ class TestSamplerGradient(QiskitTestCase):
                 for k in quasi_dist:
                     self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3)
 
-    @combine(grad=gradient_factories)
+    @data(*gradient_factories)
     def test_gradient_u(self, grad):
         """Test the sampler gradient for u"""
         sampler = Sampler()
@@ -117,7 +118,7 @@ class TestSamplerGradient(QiskitTestCase):
                 for k in quasi_dist:
                     self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3)
 
-    @combine(grad=gradient_factories)
+    @data(*gradient_factories)
     def test_gradient_efficient_su2(self, grad):
         """Test the sampler gradient for EfficientSU2"""
         sampler = Sampler()
@@ -211,7 +212,7 @@ class TestSamplerGradient(QiskitTestCase):
                 for k in quasi_dist:
                     self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3)
 
-    @combine(grad=gradient_factories)
+    @data(*gradient_factories)
     def test_gradient_2qubit_gate(self, grad):
         """Test the sampler gradient for 2 qubit gates"""
         sampler = Sampler()
@@ -239,7 +240,7 @@ class TestSamplerGradient(QiskitTestCase):
                 for k in quasi_dist:
                     self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3)
 
-    @combine(grad=gradient_factories)
+    @data(*gradient_factories)
     def test_gradient_parameter_coefficient(self, grad):
         """Test the sampler gradient for parameter variables with coefficients"""
         sampler = Sampler()
@@ -313,7 +314,7 @@ class TestSamplerGradient(QiskitTestCase):
                 for k in quasi_dist:
                     self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 2)
 
-    @combine(grad=gradient_factories)
+    @data(*gradient_factories)
     def test_gradient_parameters(self, grad):
         """Test the sampler gradient for parameters"""
         sampler = Sampler()
@@ -334,7 +335,7 @@ class TestSamplerGradient(QiskitTestCase):
                 for k in quasi_dist:
                     self.assertAlmostEqual(quasi_dist[k], correct_results[i][j][k], 3)
 
-    @combine(grad=gradient_factories)
+    @data(*gradient_factories)
     def test_gradient_multi_arguments(self, grad):
         """Test the sampler gradient for multiple arguments"""
         sampler = Sampler()
@@ -379,7 +380,7 @@ class TestSamplerGradient(QiskitTestCase):
                 for k in q_dists:
                     self.assertAlmostEqual(q_dists[k], correct_results[i][j][k], 3)
 
-    @combine(grad=[FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient])
+    @data(FiniteDiffSamplerGradient, ParamShiftSamplerGradient, LinCombSamplerGradient)
     def test_gradient_validation(self, grad):
         """Test sampler gradient's validation"""
         sampler = Sampler()
@@ -476,7 +477,7 @@ class TestSamplerGradient(QiskitTestCase):
                 for k in q_dists:
                     self.assertAlmostEqual(q_dists[k], correct_results3[i][j][k], 3)
 
-    @combine(grad=[ParamShiftSamplerGradient, LinCombSamplerGradient])
+    @data(ParamShiftSamplerGradient, LinCombSamplerGradient)
     def test_gradient_random_parameters(self, grad):
         """Test param shift and lin comb w/ random parameters"""
         rng = np.random.default_rng(123)
@@ -517,13 +518,11 @@ class TestSamplerGradient(QiskitTestCase):
             array2 = _quasi2array(res2, num_qubits)
             np.testing.assert_allclose(array1, array2, rtol=1e-4)
 
-    @combine(
-        grad=[
-            FiniteDiffSamplerGradient,
-            ParamShiftSamplerGradient,
-            LinCombSamplerGradient,
-            SPSASamplerGradient,
-        ],
+    @data(
+        FiniteDiffSamplerGradient,
+        ParamShiftSamplerGradient,
+        LinCombSamplerGradient,
+        SPSASamplerGradient,
     )
     def test_options(self, grad):
         """Test sampler gradient's run options"""
@@ -574,6 +573,46 @@ class TestSamplerGradient(QiskitTestCase):
             # Only default + sampler options. Not run.
             self.assertEqual(options.get("shots"), 200)
 
+    @data(
+        FiniteDiffSamplerGradient,
+        ParamShiftSamplerGradient,
+        LinCombSamplerGradient,
+        SPSASamplerGradient,
+    )
+    def test_operations_preserved(self, gradient_cls):
+        """Test non-parameterized instructions are preserved and not unrolled."""
+        x = Parameter("x")
+        circuit = QuantumCircuit(2)
+        circuit.initialize(np.array([1, 1, 0, 0]) / np.sqrt(2))  # this should remain as initialize
+        circuit.crx(x, 0, 1)  # this should get unrolled
+        circuit.measure_all()
+
+        values = [np.pi / 2]
+        expect = [{0: 0, 1: -0.25, 2: 0, 3: 0.25}]
+
+        ops = []
+
+        def operations_callback(op):
+            ops.append(op)
+
+        sampler = LoggingSampler(operations_callback=operations_callback)
+
+        if gradient_cls in [SPSASamplerGradient, FiniteDiffSamplerGradient]:
+            gradient = gradient_cls(sampler, epsilon=0.01)
+        else:
+            gradient = gradient_cls(sampler)
+
+        job = gradient.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"):
+            array1 = _quasi2array(result.gradients[0], num_qubits=2)
+            array2 = _quasi2array(expect, num_qubits=2)
+            np.testing.assert_allclose(array1, array2, atol=1e-5)
+
 
 def _quasi2array(quasis: List[QuasiDistribution], num_qubits: int) -> np.ndarray:
     ret = np.zeros((len(quasis), 2**num_qubits))
diff --git a/test/python/transpiler/test_parameterizedgate_translator.py b/test/python/transpiler/test_parameterizedgate_translator.py
new file mode 100644
index 000000000..28fa9f140
--- /dev/null
+++ b/test/python/transpiler/test_parameterizedgate_translator.py
@@ -0,0 +1,99 @@
+# 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 the TranslateParameterizedGates pass"""
+
+import unittest
+
+from qiskit.test import QiskitTestCase
+from qiskit.circuit import ParameterVector, Parameter, Gate, QuantumCircuit
+from qiskit.circuit.library import TwoLocal
+from qiskit.exceptions import QiskitError
+from qiskit.transpiler.passes import TranslateParameterizedGates
+from qiskit.providers.fake_provider import FakeAthensV2
+
+
+class TestTranslateParameterized(QiskitTestCase):
+    """Test the pass to translate parameterized gates, but keep others as is."""
+
+    def test_only_parameterized_is_unrolled(self):
+        """Test only parameterized gates are unrolled."""
+        x = ParameterVector("x", 4)
+        block1 = QuantumCircuit(1)
+        block1.rx(x[0], 0)
+
+        sub_block = QuantumCircuit(2)
+        sub_block.cx(0, 1)
+        sub_block.rz(x[2], 0)
+
+        block2 = QuantumCircuit(2)
+        block2.ry(x[1], 0)
+        block2.append(sub_block.to_gate(), [0, 1])
+
+        block3 = QuantumCircuit(3)
+        block3.ccx(0, 1, 2)
+
+        circuit = QuantumCircuit(3)
+        circuit.append(block1.to_gate(), [1])
+        circuit.append(block2.to_gate(), [0, 1])
+        circuit.append(block3.to_gate(), [0, 1, 2])
+        circuit.cry(x[3], 0, 2)
+
+        supported_gates = ["rx", "ry", "rz", "cp", "crx", "cry", "crz"]
+        unroller = TranslateParameterizedGates(supported_gates)
+        unrolled = unroller(circuit)
+
+        expected = QuantumCircuit(3)
+        expected.rx(x[0], 1)
+        expected.ry(x[1], 0)
+        expected.cx(0, 1)
+        expected.rz(x[2], 0)
+        expected.append(block3.to_gate(), [0, 1, 2])
+        expected.cry(x[3], 0, 2)
+
+        self.assertEqual(unrolled, expected)
+
+    def test_target(self):
+        """Test unrolling with a target."""
+        target = FakeAthensV2().target
+        circuit = TwoLocal(2, "rz", "cx", reps=2, entanglement="linear")
+
+        translator = TranslateParameterizedGates(target=target)
+        translated = translator(circuit)
+
+        expected_ops = {"cx": 2, "rz": 6}
+
+        self.assertEqual(translated.count_ops(), expected_ops)
+
+    def test_no_supported_gates_or_target(self):
+        """Test an error is raised if neither of ``supported_gates`` and ``target`` is supported."""
+        with self.assertRaises(ValueError):
+            _ = TranslateParameterizedGates(supported_gates=None, target=None)
+
+    def test_translation_impossible(self):
+        """Test translating a parameterized gate without definition does not work."""
+
+        x = Parameter("x")
+        mission_actually_impossible = Gate("mysterious", 1, [x])
+
+        circuit = QuantumCircuit(1)
+        circuit.rx(x, 0)
+        circuit.append(mission_actually_impossible, [0])
+
+        translator = TranslateParameterizedGates(["rx", "ry", "rz"])
+
+        with self.assertRaises(QiskitError):
+            _ = translator(circuit)
+
+
+if __name__ == "__main__":
+    unittest.main()