Repository: Qiskit/qiskit — pull request #12986 (python)
Title: Fix `UnitaryGate.repeat()` method

### Summary\r\nCloses #11990 : error when calling the `UnitaryGate.repeat()` method (inherited from the `Gate` class).\r\n\r\n### Details and comments\r\nThe method `repeat()` internally calls the private method `_return_repeat()` which is inherited from the `Gate` class.\r\nIt creates a new `Gate` object with arguments `params=self.params`, which is expected to be of type `ParameterExpression` or `float`. However the parameters are of type `numpy.ndarray` for a `UnitaryGate`. \r\nA simple fix is to omit these parameters (`params=[]`) as they are not directly used by the returned `Gate`. Actually these parameters are still stored in the `repeat()` method and used only there.\r\n

Resolved issues:
title: Invalid param type <class 'numpy.ndarray'> for gate unitary*1
body: ### Environment\n\n- **Qiskit version**: 1.0.2\r\n- **Python version**: 3.11.8\r\n- **Operating system**: Ubuntu 22.04.3 LTS\n\n### What is happening?\n\nRunning the below code triggered a qiskit.circuit.exceptions.CircuitError: "Invalid param type <class 'numpy.ndarray'> for gate unitary*1." I found this bug happens for some other Gates as well. Below is a minimal reproduction example using CUGate.\n\n### How can we reproduce the issue?\n\n```\r\nfrom qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister\r\nfrom qiskit.circuit.library.standard_gates import CUGate\r\n\r\nqr = QuantumRegister(2, name='qr')\r\ncr = ClassicalRegister(2, name='cr')\r\nqc = QuantumCircuit(qr, cr, name='qc')\r\n\r\nqc.append(CUGate(0, 0, 0, 0).power(1).repeat(1), qargs=qr)\r\n```\n\n### What should happen?\n\n```\r\nTraceback (most recent call last):\r\n  File "/mnt/tests/reproduction/reproduce_invalid_param_type.py", line 8, in <module>\r\n    qc.append(CUGate(0, 0, 0, 0).power(1).repeat(1), qargs=qr)\r\n              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n  File "/root/anaconda3/envs/qiskit1.0.2/lib/python3.11/site-packages/qiskit/circuit/instruction.py", line 591, in repeat\r\n    instruction = self._return_repeat(n)\r\n                  ^^^^^^^^^^^^^^^^^^^^^^\r\n  File "/root/anaconda3/envs/qiskit1.0.2/lib/python3.11/site-packages/qiskit/circuit/gate.py", line 87, in _return_repeat\r\n    return Gate(name=f"{self.name}*{exponent}", num_qubits=self.num_qubits, params=self.params)\r\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n  File "/root/anaconda3/envs/qiskit1.0.2/lib/python3.11/site-packages/qiskit/circuit/gate.py", line 46, in __init__\r\n    super().__init__(name, num_qubits, 0, params, label=label, duration=duration, unit=unit)\r\n  File "/root/anaconda3/envs/qiskit1.0.2/lib/python3.11/site-packages/qiskit/circuit/instruction.py", line 108, in __init__\r\n    self.params = params  # must be at last (other properties may be required for validation)\r\n    ^^^^^^^^^^^\r\n  File "/root/anaconda3/envs/qiskit1.0.2/lib/python3.11/site-packages/qiskit/circuit/instruction.py", line 287, in params\r\n    self._params.append(self.validate_parameter(single_param))\r\n                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n  File "/root/anaconda3/envs/qiskit1.0.2/lib/python3.11/site-packages/qiskit/circuit/gate.py", line 242, in validate_parameter\r\n    raise CircuitError(f"Invalid param type {type(parameter)} for gate {self.name}.")\r\nqiskit.circuit.exceptions.CircuitError: "Invalid param type <class 'numpy.ndarray'> for gate unitary*1."\r\n```\n\n### Any suggestions?\n\n_No response_

Hints:

Fix patch:
diff --git a/qiskit/circuit/gate.py b/qiskit/circuit/gate.py\nindex 37fd19e2022a..2f1c09668252 100644\n--- a/qiskit/circuit/gate.py\n+++ b/qiskit/circuit/gate.py\n@@ -97,7 +97,10 @@ def __pow__(self, exponent: float) -> "Gate":\n         return self.power(exponent)\n \n     def _return_repeat(self, exponent: float) -> "Gate":\n-        return Gate(name=f"{self.name}*{exponent}", num_qubits=self.num_qubits, params=self.params)\n+        gate = Gate(name=f"{self.name}*{exponent}", num_qubits=self.num_qubits, params=[])\n+        gate.validate_parameter = self.validate_parameter\n+        gate.params = self.params\n+        return gate\n \n     def control(\n         self,\ndiff --git a/releasenotes/notes/fix_11990-8551c7250207fc76.yaml b/releasenotes/notes/fix_11990-8551c7250207fc76.yaml\nnew file mode 100644\nindex 000000000000..68912403cd39\n--- /dev/null\n+++ b/releasenotes/notes/fix_11990-8551c7250207fc76.yaml\n@@ -0,0 +1,6 @@\n+---\n+fixes:\n+  - |\n+    Fixes an error when calling the method :meth:`.UnitaryGate.repeat`. \n+    Refer to `#11990 <https://github.com/Qiskit/qiskit/issues/11990>`_ for more\n+    details.\n\\ No newline at end of file\n

Test patch:
diff --git a/test/python/circuit/library/test_linear_function.py b/test/python/circuit/library/test_linear_function.py\nindex a3df1e9664a2..b7756faa7aa7 100644\n--- a/test/python/circuit/library/test_linear_function.py\n+++ b/test/python/circuit/library/test_linear_function.py\n@@ -502,6 +502,19 @@ def test_clifford_linear_function_equivalence(self, num_qubits):\n         self.assertEqual(Clifford(qc_to_linear_function), qc_to_clifford)\n         self.assertEqual(qc_to_linear_function, LinearFunction(qc_to_clifford))\n \n+    @data(2, 3)\n+    def test_repeat_method(self, num_qubits):\n+        """Test the repeat() method."""\n+        rng = np.random.default_rng(127)\n+        for num_gates, seed in zip(\n+            [0, 5, 5 * num_qubits], rng.integers(100000, size=10, dtype=np.uint64)\n+        ):\n+            # create a random linear circuit\n+            linear_circuit = random_linear_circuit(num_qubits, num_gates, seed=seed)\n+            operator = Operator(linear_circuit)\n+            linear_function = LinearFunction(linear_circuit)\n+            self.assertTrue(Operator(linear_function.repeat(2)), operator @ operator)\n+\n \n if __name__ == "__main__":\n     unittest.main()\ndiff --git a/test/python/circuit/library/test_permutation.py b/test/python/circuit/library/test_permutation.py\nindex 30d504d7dc8f..a2ec59431fe3 100644\n--- a/test/python/circuit/library/test_permutation.py\n+++ b/test/python/circuit/library/test_permutation.py\n@@ -104,6 +104,12 @@ def test_inverse(self):\n         expected_inverse_perm = PermutationGate([3, 0, 5, 1, 4, 2])\n         self.assertTrue(np.array_equal(inverse_perm.pattern, expected_inverse_perm.pattern))\n \n+    def test_repeat(self):\n+        """Test the ``repeat`` method."""\n+        pattern = [2, 4, 1, 3, 0]\n+        perm = PermutationGate(pattern)\n+        self.assertTrue(np.allclose(Operator(perm.repeat(2)), Operator(perm) @ Operator(perm)))\n+\n \n class TestPermutationGatesOnCircuit(QiskitTestCase):\n     """Tests for quantum circuits containing permutations."""\ndiff --git a/test/python/circuit/test_diagonal_gate.py b/test/python/circuit/test_diagonal_gate.py\nindex de8d53e12d3b..bb9259999576 100644\n--- a/test/python/circuit/test_diagonal_gate.py\n+++ b/test/python/circuit/test_diagonal_gate.py\n@@ -83,6 +83,19 @@ def test_npcomplex_params_conversion(self):\n                 all(isinstance(p, complex) and not isinstance(p, np.number) for p in params)\n             )\n \n+    def test_repeat(self):\n+        """Test the repeat() method."""\n+        for phases in [\n+            [0, 0],\n+            np.array([0, 0.8, 1, 0]),\n+            (2 * np.pi * np.random.rand(2**3)).tolist(),\n+        ]:\n+            with self.subTest(phases=phases):\n+                diag = [np.exp(1j * ph) for ph in phases]\n+                gate = DiagonalGate(diag)\n+                operator = Operator(gate)\n+                self.assertTrue(np.allclose(Operator(gate.repeat(2)), operator @ operator))\n+\n \n def _get_diag_gate_matrix(diag):\n     return np.diagflat(diag)\ndiff --git a/test/python/circuit/test_gate_definitions.py b/test/python/circuit/test_gate_definitions.py\nindex 950b0c478ed8..914ac8bab985 100644\n--- a/test/python/circuit/test_gate_definitions.py\n+++ b/test/python/circuit/test_gate_definitions.py\n@@ -54,6 +54,8 @@\n     CZGate,\n     RYYGate,\n     PhaseGate,\n+    PauliGate,\n+    UCPauliRotGate,\n     CPhaseGate,\n     UGate,\n     CUGate,\n@@ -184,6 +186,18 @@ def test_xx_minus_yy_definition(self):\n         self.assertTrue(len(decomposed_circuit) > len(circuit))\n         self.assertTrue(Operator(circuit).equiv(Operator(decomposed_circuit), atol=1e-7))\n \n+    def test_pauligate_repeat(self):\n+        """Test `repeat` method for `PauliGate`."""\n+        gate = PauliGate("XYZ")\n+        operator = Operator(gate)\n+        self.assertTrue(np.allclose(Operator(gate.repeat(2)), operator @ operator))\n+\n+    def test_ucpaulirotgate_repeat(self):\n+        """Test `repeat` method for `UCPauliRotGate`."""\n+        gate = UCPauliRotGate([0.3, 0.5], "X")\n+        operator = Operator(gate)\n+        self.assertTrue(np.allclose(Operator(gate.repeat(2)), operator @ operator))\n+\n \n @ddt\n class TestStandardGates(QiskitTestCase):\ndiff --git a/test/python/circuit/test_hamiltonian_gate.py b/test/python/circuit/test_hamiltonian_gate.py\nindex 60fb1e9a90a9..e65e2f07ba5e 100644\n--- a/test/python/circuit/test_hamiltonian_gate.py\n+++ b/test/python/circuit/test_hamiltonian_gate.py\n@@ -62,6 +62,12 @@ def test_adjoint(self):\n             ham.adjoint().to_matrix(), np.transpose(np.conj(ham.to_matrix()))\n         )\n \n+    def test_repeat(self):\n+        """test repeat operation"""\n+        ham = HamiltonianGate(np.array([[1, 0.5 + 4j], [0.5 - 4j, -0.2]]), np.pi * 0.143)\n+        operator = Operator(ham)\n+        self.assertTrue(np.allclose(Operator(ham.repeat(2)), operator @ operator))\n+\n \n class TestHamiltonianCircuit(QiskitTestCase):\n     """Hamiltonian gate circuit tests."""\ndiff --git a/test/python/circuit/test_initializer.py b/test/python/circuit/test_initializer.py\nindex 2507fbc6c139..990df5755d59 100644\n--- a/test/python/circuit/test_initializer.py\n+++ b/test/python/circuit/test_initializer.py\n@@ -473,6 +473,16 @@ def test_gates_to_uncompute(self):\n         vec = Statevector(qc)\n         self.assertTrue(vec == Statevector(desired_vector))\n \n+    def test_repeat(self):\n+        """Test the repeat() method."""\n+        desired_vector = np.array([0.5, 0.5, 0.5, 0.5])\n+        initialize = Initialize(desired_vector)\n+        qr = QuantumRegister(2)\n+        qc = QuantumCircuit(qr)\n+        qc.append(initialize.repeat(2), qr)\n+        statevector = Statevector(qc)\n+        self.assertTrue(np.allclose(statevector, desired_vector))\n+\n \n class TestInstructionParam(QiskitTestCase):\n     """Test conversion of numpy type parameters."""\ndiff --git a/test/python/circuit/test_isometry.py b/test/python/circuit/test_isometry.py\nindex 35ff639cedd5..ff497c76f48a 100644\n--- a/test/python/circuit/test_isometry.py\n+++ b/test/python/circuit/test_isometry.py\n@@ -133,6 +133,21 @@ def test_isometry_inverse(self, iso):\n         result = Operator(qc)\n         np.testing.assert_array_almost_equal(result.data, np.identity(result.dim[0]))\n \n+    @data(\n+        np.eye(2, 2),\n+        random_unitary(2, seed=297102).data,\n+        np.eye(4, 4),\n+        random_unitary(4, seed=123642).data,\n+        random_unitary(8, seed=568288).data,\n+    )\n+    def test_isometry_repeat(self, iso):\n+        """Tests for the repeat of isometries from n to n qubits"""\n+        iso_gate = Isometry(iso, 0, 0)\n+\n+        op = Operator(iso_gate)\n+        op_double = Operator(iso_gate.repeat(2))\n+        np.testing.assert_array_almost_equal(op @ op, op_double)\n+\n \n if __name__ == "__main__":\n     unittest.main()\ndiff --git a/test/python/circuit/test_uc.py b/test/python/circuit/test_uc.py\nindex 87541d349c3f..0277c4afb43d 100644\n--- a/test/python/circuit/test_uc.py\n+++ b/test/python/circuit/test_uc.py\n@@ -100,6 +100,13 @@ def test_inverse_ucg(self):\n \n         self.assertTrue(np.allclose(unitary_desired, unitary))\n \n+    def test_repeat(self):\n+        """test repeat operation"""\n+        gates = [random_unitary(2, seed=seed).data for seed in [124435, 876345, 687462, 928365]]\n+\n+        uc = UCGate(gates, up_to_diagonal=False)\n+        self.assertTrue(np.allclose(Operator(uc.repeat(2)), Operator(uc) @ Operator(uc)))\n+\n \n def _get_ucg_matrix(squs):\n     return block_diag(*squs)\ndiff --git a/test/python/circuit/test_unitary.py b/test/python/circuit/test_unitary.py\nindex 63edbdaffbf9..6ab4a93c3f7d 100644\n--- a/test/python/circuit/test_unitary.py\n+++ b/test/python/circuit/test_unitary.py\n@@ -67,6 +67,11 @@ def test_adjoint(self):\n         uni = UnitaryGate([[0, 1j], [-1j, 0]])\n         self.assertTrue(numpy.array_equal(uni.adjoint().to_matrix(), uni.to_matrix()))\n \n+    def test_repeat(self):\n+        """test repeat operation"""\n+        uni = UnitaryGate([[1, 0], [0, 1j]])\n+        self.assertTrue(numpy.array_equal(Operator(uni.repeat(2)), Operator(uni) @ Operator(uni)))\n+\n \n class TestUnitaryCircuit(QiskitTestCase):\n     """Matrix gate circuit tests."""\n