Repository: Qiskit/qiskit

Issue:
Remove opflow dependency in `EvolvedOperatorAnsatz`
### What should we add?

The `EvolvedOperatorAnsatz` class still depends on opflow expectations, and is not compatible with the new primitive-based workflow. This WIP PR #7275 already proposes an alternative to replace `EvolvedOperatorAnsatz` with `EvolutionGates`, so it might be a good starting point to realize this task.


Discussion:

Resolving patch:
diff --git a/qiskit/circuit/library/evolved_operator_ansatz.py b/qiskit/circuit/library/evolved_operator_ansatz.py
--- a/qiskit/circuit/library/evolved_operator_ansatz.py
+++ b/qiskit/circuit/library/evolved_operator_ansatz.py
@@ -12,12 +12,19 @@
 
 """The evolved operator ansatz."""
 
-from typing import Optional, Union, List
+from __future__ import annotations
+from collections.abc import Sequence
 
 import numpy as np
 
-from qiskit.circuit import Parameter, QuantumRegister, QuantumCircuit
+from qiskit.circuit.parameter import Parameter
+from qiskit.circuit.quantumregister import QuantumRegister
+from qiskit.circuit.quantumcircuit import QuantumCircuit
+from qiskit.exceptions import QiskitError
+from qiskit.quantum_info import Operator, Pauli, SparsePauliOp
+from qiskit.synthesis.evolution import LieTrotter
 
+from .pauli_evolution import PauliEvolutionGate
 from .n_local.n_local import NLocal
 
 
@@ -31,31 +38,28 @@ def __init__(
         evolution=None,
         insert_barriers: bool = False,
         name: str = "EvolvedOps",
-        parameter_prefix: Union[str, List[str]] = "t",
-        initial_state: Optional[QuantumCircuit] = None,
+        parameter_prefix: str | Sequence[str] = "t",
+        initial_state: QuantumCircuit | None = None,
     ):
         """
         Args:
-            operators (Optional[Union[OperatorBase, QuantumCircuit, list]): The operators to evolve.
-                If a circuit is passed, we assume it implements an already evolved operator and thus
-                the circuit is not evolved again. Can be a single operator (circuit) or a list of
-                operators (and circuits).
+            operators (BaseOperator | OperatorBase | QuantumCircuit | list | None): The operators
+                to evolve. If a circuit is passed, we assume it implements an already evolved
+                operator and thus the circuit is not evolved again. Can be a single operator
+                (circuit) or a list of operators (and circuits).
             reps: The number of times to repeat the evolved operators.
-            evolution (Optional[EvolutionBase]): An opflow converter object to construct the evolution.
-                Defaults to Trotterization.
+            evolution (EvolutionBase | EvolutionSynthesis | None):
+                A specification of which evolution synthesis to use for the
+                :class:`.PauliEvolutionGate`, if the operator is from :mod:`qiskit.quantum_info`
+                or an opflow converter object if the operator is from :mod:`qiskit.opflow`.
+                Defaults to first order Trotterization.
             insert_barriers: Whether to insert barriers in between each evolution.
             name: The name of the circuit.
             parameter_prefix: Set the names of the circuit parameters. If a string, the same prefix
                 will be used for each parameters. Can also be a list to specify a prefix per
                 operator.
-            initial_state: A `QuantumCircuit` object to prepend to the circuit.
+            initial_state: A :class:`.QuantumCircuit` object to prepend to the circuit.
         """
-        if evolution is None:
-            # pylint: disable=cyclic-import
-            from qiskit.opflow import PauliTrotterEvolution
-
-            evolution = PauliTrotterEvolution()
-
         super().__init__(
             initial_state=initial_state,
             parameter_prefix=parameter_prefix,
@@ -64,6 +68,7 @@ def __init__(
             name=name,
         )
         self._operators = None
+
         if operators is not None:
             self.operators = operators
 
@@ -99,8 +104,14 @@ def evolution(self):
         """The evolution converter used to compute the evolution.
 
         Returns:
-            EvolutionBase: The evolution converter used to compute the evolution.
+            EvolutionBase or EvolutionSynthesis: The evolution converter used to compute the evolution.
         """
+        if self._evolution is None:
+            # pylint: disable=cyclic-import
+            from qiskit.opflow import PauliTrotterEvolution
+
+            return PauliTrotterEvolution()
+
         return self._evolution
 
     @evolution.setter
@@ -108,7 +119,8 @@ def evolution(self, evol) -> None:
         """Sets the evolution converter used to compute the evolution.
 
         Args:
-            evol (EvolutionBase): An opflow converter object to construct the evolution.
+            evol (EvolutionBase | EvolutionSynthesis): An evolution synthesis object or
+                opflow converter object to construct the evolution.
         """
         self._invalidate()
         self._evolution = evol
@@ -152,6 +164,32 @@ def preferred_init_points(self):
             self._build()
             return np.zeros(self.reps * len(self.operators), dtype=float)
 
+    def _evolve_operator(self, operator, time):
+        from qiskit.opflow import OperatorBase, EvolutionBase
+        from qiskit.extensions import HamiltonianGate
+
+        if isinstance(operator, OperatorBase):
+            if not isinstance(self.evolution, EvolutionBase):
+                raise QiskitError(
+                    "If qiskit.opflow operators are evolved the evolution must be a "
+                    f"qiskit.opflow.EvolutionBase, not a {type(self.evolution)}."
+                )
+
+            evolved = self.evolution.convert((time * operator).exp_i())
+            return evolved.reduce().to_circuit()
+
+        # if the operator is specified as matrix use exact matrix exponentiation
+        if isinstance(operator, Operator):
+            gate = HamiltonianGate(operator, time)
+        # otherwise, use the PauliEvolutionGate
+        else:
+            evolution = LieTrotter() if self._evolution is None else self._evolution
+            gate = PauliEvolutionGate(operator, time, synthesis=evolution)
+
+        evolved = QuantumCircuit(operator.num_qubits)
+        evolved.append(gate, evolved.qubits)
+        return evolved
+
     def _build(self):
         if self._is_built:
             return
@@ -171,8 +209,8 @@ def _build(self):
                 if _is_pauli_identity(op):
                     continue
 
-                evolved_op = self.evolution.convert((coeff * op).exp_i()).reduce()
-                circuits.append(evolved_op.to_circuit())
+                evolved = self._evolve_operator(op, coeff)
+                circuits.append(evolved)
 
         self.rotation_blocks = []
         self.entanglement_blocks = circuits
@@ -206,6 +244,13 @@ def _is_pauli_identity(operator):
 
     if isinstance(operator, PauliSumOp):
         operator = operator.to_pauli_op()
+    if isinstance(operator, SparsePauliOp):
+        if len(operator.paulis) == 1:
+            operator = operator.paulis[0]  # check if the single Pauli is identity below
+        else:
+            return False
     if isinstance(operator, PauliOp):
-        return not np.any(np.logical_or(operator.primitive.x, operator.primitive.z))
+        operator = operator.primitive
+    if isinstance(operator, Pauli):
+        return not np.any(np.logical_or(operator.x, operator.z))
     return False
diff --git a/qiskit/circuit/library/n_local/qaoa_ansatz.py b/qiskit/circuit/library/n_local/qaoa_ansatz.py
--- a/qiskit/circuit/library/n_local/qaoa_ansatz.py
+++ b/qiskit/circuit/library/n_local/qaoa_ansatz.py
@@ -41,16 +41,16 @@ def __init__(
     ):
         r"""
         Args:
-            cost_operator (OperatorBase, optional): The operator representing the cost of
-                the optimization problem, denoted as :math:`U(C, \gamma)` in the original paper.
-                Must be set either in the constructor or via property setter.
+            cost_operator (BaseOperator or OperatorBase, optional): The operator
+                representing the cost of the optimization problem, denoted as :math:`U(C, \gamma)`
+                in the original paper. Must be set either in the constructor or via property setter.
             reps (int): The integer parameter p, which determines the depth of the circuit,
                 as specified in the original paper, default is 1.
             initial_state (QuantumCircuit, optional): An optional initial state to use.
                 If `None` is passed then a set of Hadamard gates is applied as an initial state
                 to all qubits.
-            mixer_operator (OperatorBase or QuantumCircuit, optional): An optional custom mixer
-                to use instead of the global X-rotations, denoted as :math:`U(B, \beta)`
+            mixer_operator (BaseOperator or OperatorBase or QuantumCircuit, optional): An optional
+                custom mixer to use instead of the global X-rotations, denoted as :math:`U(B, \beta)`
                 in the original paper. Can be an operator or an optionally parameterized quantum
                 circuit.
             name (str): A name of the circuit, default 'qaoa'
@@ -147,8 +147,8 @@ def operators(self) -> list:
         """The operators that are evolved in this circuit.
 
         Returns:
-             List[Union[OperatorBase, QuantumCircuit]]: The operators to be evolved (and circuits)
-                in this ansatz.
+             List[Union[BaseOperator, OperatorBase, QuantumCircuit]]: The operators to be evolved
+                (and circuits) in this ansatz.
         """
         return [self.cost_operator, self.mixer_operator]
 
@@ -157,7 +157,7 @@ def cost_operator(self):
         """Returns an operator representing the cost of the optimization problem.
 
         Returns:
-            OperatorBase: cost operator.
+            BaseOperator or OperatorBase: cost operator.
         """
         return self._cost_operator
 
@@ -166,7 +166,7 @@ def cost_operator(self, cost_operator) -> None:
         """Sets cost operator.
 
         Args:
-            cost_operator (OperatorBase, optional): cost operator to set.
+            cost_operator (BaseOperator or OperatorBase, optional): cost operator to set.
         """
         self._cost_operator = cost_operator
         self.qregs = [QuantumRegister(self.num_qubits, name="q")]
@@ -211,7 +211,7 @@ def mixer_operator(self):
         """Returns an optional mixer operator expressed as an operator or a quantum circuit.
 
         Returns:
-            OperatorBase or QuantumCircuit, optional: mixer operator or circuit.
+            BaseOperator or OperatorBase or QuantumCircuit, optional: mixer operator or circuit.
         """
         if self._mixer is not None:
             return self._mixer
@@ -239,8 +239,8 @@ def mixer_operator(self, mixer_operator) -> None:
         """Sets mixer operator.
 
         Args:
-            mixer_operator (OperatorBase or QuantumCircuit, optional): mixer operator or circuit
-                to set.
+            mixer_operator (BaseOperator or OperatorBase or QuantumCircuit, optional): mixer
+                operator or circuit to set.
         """
         self._mixer = mixer_operator
         self._invalidate()
diff --git a/qiskit/synthesis/evolution/matrix_synthesis.py b/qiskit/synthesis/evolution/matrix_synthesis.py
--- a/qiskit/synthesis/evolution/matrix_synthesis.py
+++ b/qiskit/synthesis/evolution/matrix_synthesis.py
@@ -27,22 +27,20 @@ class MatrixExponential(EvolutionSynthesis):
     """
 
     def synthesize(self, evolution):
-        from scipy.linalg import expm
+        from qiskit.extensions import HamiltonianGate
 
         # get operators and time to evolve
         operators = evolution.operator
         time = evolution.time
 
-        # construct the evolution circuit
-        evolution_circuit = QuantumCircuit(operators[0].num_qubits)
-
         if not isinstance(operators, list):
             matrix = operators.to_matrix()
         else:
             matrix = sum(op.to_matrix() for op in operators)
 
-        exp = expm(-1j * time * matrix)
-
-        evolution_circuit.unitary(exp, evolution_circuit.qubits)
+        # construct the evolution circuit
+        evolution_circuit = QuantumCircuit(operators[0].num_qubits)
+        gate = HamiltonianGate(matrix, time)
+        evolution_circuit.append(gate, evolution_circuit.qubits)
 
         return evolution_circuit


Test patch:
diff --git a/test/python/circuit/library/test_evolution_gate.py b/test/python/circuit/library/test_evolution_gate.py
--- a/test/python/circuit/library/test_evolution_gate.py
+++ b/test/python/circuit/library/test_evolution_gate.py
@@ -12,6 +12,7 @@
 
 """Test the evolution gate."""
 
+import unittest
 import numpy as np
 import scipy
 from ddt import ddt, data, unpack
@@ -313,3 +314,7 @@ def test_labels_and_name(self):
                 evo = PauliEvolutionGate(op)
                 self.assertEqual(evo.name, "PauliEvolution")
                 self.assertEqual(evo.label, f"exp(-it {label})")
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/test/python/circuit/library/test_evolved_op_ansatz.py b/test/python/circuit/library/test_evolved_op_ansatz.py
--- a/test/python/circuit/library/test_evolved_op_ansatz.py
+++ b/test/python/circuit/library/test_evolved_op_ansatz.py
@@ -12,22 +12,32 @@
 
 """Test the evolved operator ansatz."""
 
+from ddt import ddt, data
+import numpy as np
 
 from qiskit.circuit import QuantumCircuit
 from qiskit.opflow import X, Y, Z, I, MatrixEvolution
+from qiskit.quantum_info import SparsePauliOp, Operator, Pauli
 
 from qiskit.circuit.library import EvolvedOperatorAnsatz
+from qiskit.synthesis.evolution import MatrixExponential
 from qiskit.test import QiskitTestCase
 
 
+@ddt
 class TestEvolvedOperatorAnsatz(QiskitTestCase):
     """Test the evolved operator ansatz."""
 
-    def test_evolved_op_ansatz(self):
+    @data(True, False)
+    def test_evolved_op_ansatz(self, use_opflow):
         """Test the default evolution."""
         num_qubits = 3
 
-        ops = [Z ^ num_qubits, Y ^ num_qubits, X ^ num_qubits]
+        if use_opflow:
+            ops = [Z ^ num_qubits, Y ^ num_qubits, X ^ num_qubits]
+        else:
+            ops = [Pauli("Z" * num_qubits), Pauli("Y" * num_qubits), Pauli("X" * num_qubits)]
+
         strings = ["z" * num_qubits, "y" * num_qubits, "x" * num_qubits] * 2
 
         evo = EvolvedOperatorAnsatz(ops, 2)
@@ -39,18 +49,29 @@ def test_evolved_op_ansatz(self):
 
         self.assertEqual(evo.decompose().decompose(), reference)
 
-    def test_custom_evolution(self):
+    @data(True, False)
+    def test_custom_evolution(self, use_opflow):
         """Test using another evolution than the default (e.g. matrix evolution)."""
+        if use_opflow:
+            op = X ^ I ^ Z
+            matrix = op.to_matrix()
+            evolution = MatrixEvolution()
+        else:
+            op = SparsePauliOp(["ZIX"])
+            matrix = np.array(op)
+            evolution = MatrixExponential()
 
-        op = X ^ I ^ Z
-        matrix = op.to_matrix()
-        evo = EvolvedOperatorAnsatz(op, evolution=MatrixEvolution())
+        evo = EvolvedOperatorAnsatz(op, evolution=evolution)
 
         parameters = evo.parameters
         reference = QuantumCircuit(3)
         reference.hamiltonian(matrix, parameters[0], [0, 1, 2])
 
-        self.assertEqual(evo.decompose(), reference)
+        decomposed = evo.decompose()
+        if not use_opflow:
+            decomposed = decomposed.decompose()
+
+        self.assertEqual(decomposed, reference)
 
     def test_changing_operators(self):
         """Test rebuilding after the operators changed."""
@@ -87,6 +108,12 @@ def test_empty_build_fails(self):
         with self.assertRaises(ValueError):
             _ = evo.draw()
 
+    def test_matrix_operator(self):
+        """Test passing a quantum_info.Operator uses the HamiltonianGate."""
+        unitary = Operator([[0, 1], [1, 0]])
+        evo = EvolvedOperatorAnsatz(unitary, reps=3).decompose()
+        self.assertEqual(evo.count_ops()["hamiltonian"], 3)
+
 
 def evolve(pauli_string, time):
     """Get the reference evolution circuit for a single Pauli string."""
diff --git a/test/python/circuit/library/test_qaoa_ansatz.py b/test/python/circuit/library/test_qaoa_ansatz.py
--- a/test/python/circuit/library/test_qaoa_ansatz.py
+++ b/test/python/circuit/library/test_qaoa_ansatz.py
@@ -18,7 +18,8 @@
 from qiskit.circuit import QuantumCircuit, Parameter
 from qiskit.circuit.library import HGate, RXGate, YGate, RYGate, RZGate
 from qiskit.circuit.library.n_local.qaoa_ansatz import QAOAAnsatz
-from qiskit.opflow import I, Y, Z, PauliSumOp
+from qiskit.opflow import I, Y, Z
+from qiskit.quantum_info import Pauli, SparsePauliOp
 from qiskit.test import QiskitTestCase
 
 
@@ -156,15 +157,15 @@ def test_empty_op(self):
     def test_num_qubits(self, num_qubits):
         """Test num_qubits with {num_qubits} qubits"""
 
-        circuit = QAOAAnsatz(cost_operator=I ^ num_qubits, reps=5)
+        circuit = QAOAAnsatz(cost_operator=Pauli("I" * num_qubits), reps=5)
         self.assertEqual(circuit.num_qubits, num_qubits)
 
     def test_identity(self):
         """Test construction with identity"""
         reps = 4
         num_qubits = 3
-        pauli_sum_op = PauliSumOp.from_list([("I" * num_qubits, 1)])
-        pauli_op = I ^ num_qubits
+        pauli_sum_op = SparsePauliOp.from_list([("I" * num_qubits, 1)])
+        pauli_op = Pauli("I" * num_qubits)
         for cost in [pauli_op, pauli_sum_op]:
             for mixer in [None, pauli_op, pauli_sum_op]:
                 with self.subTest(f"cost: {type(cost)}, mixer:{type(mixer)}"):