Repository: Qiskit/qiskit (python)

Issue:
`XXPlusYYGate.to_matrix()` fails after qasm2 roundtrip
### Environment

- **Qiskit version**: 1.0.2
- **Python version**: 3.11.0
- **Operating system**: Linux


### What is happening?

After a qasm2 round-trip, a `XXPlusYYGate`'s `to_matrix()` method no longer works.

~~This problem seems somewhat special to `XXPlusYYGate`; it does not exist for most other two-qubit gates we have tried.~~

I've been able to reproduce this problem as well for `CSGate`, but not `CXGate` or `CZGate`.

### How can we reproduce the issue?

```python
from qiskit import qasm2, QuantumCircuit
from qiskit.circuit.library import XXPlusYYGate

# Build a circuit with a single XXPlusYYGate
qc = QuantumCircuit(2)
qc.append(XXPlusYYGate(0.1), [0, 1])

# Perform a qasm2 round trip
program = qasm2.dumps(qc)
qc2 = QuantumCircuit.from_qasm_str(program)

# Call to_matrix on original circuit instructions (works)
for inst in qc.data:
    inst.operation.to_matrix()

# Call to_matrix on circuit instructions after serialization (fails)
for inst in qc2.data:
    inst.operation.to_matrix()
```

throws the exception

```
File ~/serverless/.direnv/python-3.11.0/lib/python3.11/site-packages/qiskit/circuit/gate.py:63, in Gate.to_matrix(self)
     61 if hasattr(self, "__array__"):
     62     return self.__array__(dtype=complex)
---> 63 raise CircuitError(f"to_matrix not defined for this {type(self)}")

CircuitError: "to_matrix not defined for this <class 'qiskit.qasm2.parse._DefinedGate'>"
```

### What should happen?

it should work the same as the circuit that did not undergo a round trip.

### Any suggestions?

_No response_

Pull request:
Implement `__array__` for `qasm2._DefinedGate` (backport #12119)
### Summary

Gates defined from OpenQASM 2 should have a well-defined matrix form (up to global phase) whenever all the constituent parts of the definition do. This manually makes such a matrix available.



### Details and comments

Fix #12100.
<hr>This is an automatic backport of pull request #12119 done by [Mergify](https://mergify.com).

Patch:
diff --git a/qiskit/qasm2/parse.py b/qiskit/qasm2/parse.py
index 5cb8137b5..30c85843a 100644
--- a/qiskit/qasm2/parse.py
+++ b/qiskit/qasm2/parse.py
@@ -16,6 +16,8 @@ import dataclasses
 import math
 from typing import Iterable, Callable
 
+import numpy as np
+
 from qiskit.circuit import (
     Barrier,
     CircuitInstruction,
@@ -30,6 +32,7 @@ from qiskit.circuit import (
     Reset,
     library as lib,
 )
+from qiskit.quantum_info import Operator
 from qiskit._accelerate.qasm2 import (
     OpCode,
     UnaryOpCode,
@@ -315,6 +318,11 @@ class _DefinedGate(Gate):
                 raise ValueError(f"received invalid bytecode to build gate: {op}")
         self._definition = qc
 
+    def __array__(self, dtype=None, copy=None):
+        if copy is False:
+            raise ValueError("unable to avoid copy while creating an array as requested")
+        return np.asarray(Operator(self.definition), dtype=dtype)
+
     # It's fiddly to implement pickling for PyO3 types (the bytecode stream), so instead if we need
     # to pickle ourselves, we just eagerly create the definition and pickle that.
 
diff --git a/releasenotes/notes/qasm2-to-matrix-c707fe1e61b3987f.yaml b/releasenotes/notes/qasm2-to-matrix-c707fe1e61b3987f.yaml
new file mode 100644
index 000000000..84cfd1a1d
--- /dev/null
+++ b/releasenotes/notes/qasm2-to-matrix-c707fe1e61b3987f.yaml
@@ -0,0 +1,9 @@
+---
+fixes:
+  - |
+    Custom gates (those stemming from a ``gate`` statement) in imported OpenQASM 2 programs will now
+    have an :meth:`.Gate.to_matrix` implementation.  Previously they would have no matrix definition,
+    meaning that roundtrips through OpenQASM 2 could needlessly lose the ability to derive the gate
+    matrix.  Note, though, that the matrix is calculated by recursively finding the matrices of the
+    inner gate definitions, as :class:`.Operator` does, which might be less performant than before
+    the round-trip.


Test patch:
diff --git a/test/python/qasm2/test_structure.py b/test/python/qasm2/test_structure.py
index 141d3c0f8..22eff30b3 100644
--- a/test/python/qasm2/test_structure.py
+++ b/test/python/qasm2/test_structure.py
@@ -22,6 +22,7 @@ import shutil
 import tempfile
 
 import ddt
+import numpy as np
 
 import qiskit.qasm2
 from qiskit import qpy
@@ -34,6 +35,7 @@ from qiskit.circuit import (
     Qubit,
     library as lib,
 )
+from qiskit.quantum_info import Operator
 from test import QiskitTestCase  # pylint: disable=wrong-import-order
 
 from . import gate_builder
@@ -906,6 +908,30 @@ class TestMeasure(QiskitTestCase):
         )
         self.assertEqual(parsed, qc)
 
+    def test_has_to_matrix(self):
+        program = """
+            OPENQASM 2.0;
+            include "qelib1.inc";
+            qreg qr[1];
+            gate my_gate(a) q {
+                rz(a) q;
+                rx(pi / 2) q;
+                rz(-a) q;
+            }
+            my_gate(1.0) qr[0];
+        """
+        parsed = qiskit.qasm2.loads(program)
+        expected = (
+            lib.RZGate(-1.0).to_matrix()
+            @ lib.RXGate(math.pi / 2).to_matrix()
+            @ lib.RZGate(1.0).to_matrix()
+        )
+        defined_gate = parsed.data[0].operation
+        self.assertEqual(defined_gate.name, "my_gate")
+        np.testing.assert_allclose(defined_gate.to_matrix(), expected, atol=1e-14, rtol=0)
+        # Also test that the standard `Operator` method on the whole circuit still works.
+        np.testing.assert_allclose(Operator(parsed), expected, atol=1e-14, rtol=0)
+
 
 class TestReset(QiskitTestCase):
     def test_single(self):