Skip to content

Commit ddb8025

Browse files
authored
Correctly handle non-UnitaryGate gates named "unitary" (#14109)
Normally creating a custom gate class that overloads the name of a Qiskit defined operation is not valid and not allowed. The names have meaning and are often used as identifiers and this overloading the name will prevent Qiskit from correctly identifying an operation. However as was discovered in #14103 there are some paths available around serialization and I/O where Qiskit does this itself. For example, qasm (both 2 and 3) is a lossy serialization format and qasm2 doesn't have a representation of a UnitaryGate. So when the qasm2 exporter encounteres a `UnitaryGate` it is serialized as a custom gate definition with the name "unitary" in the output qasm2 and the definition is a decomposition of the unitary from the `UnitaryGate`. When that qasm2 program is subsequently deserialized by qiskit parser the custom gate named "unitary" is added as a `_DefinedGate` subclass which includes an `__array__` implementation which computes the unitary from the definition using the quantum info Operator class. This makes the custom gate parsed from qasm2 look like a `UnitaryGate` despite not actually one so this is typically fine for most use cases. However, since #13759 trying to add that not `UnitaryGate` object named "unitary" would cause the Python -> Rust translation to panic (which happens as part of qasm2 desierailzation). because the conversion was expecting a gate named `unitary` to be a `UnitaryGate` as is prescribed by the data model. This commit fixes this by gracefully handling the lack of a matrix parameter as it not actually being a `UnitaryGate` and instead the object gets treated as a `PyGate` in rust which is the expected behavior. Related to #14103
1 parent 4de7c93 commit ddb8025

3 files changed

Lines changed: 59 additions & 1 deletion

File tree

crates/circuit/src/circuit_instruction.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -578,7 +578,7 @@ impl<'py> FromPyObject<'py> for OperationFromPython {
578578
// We need to check by name here to avoid a circular import during initial loading
579579
if ob.getattr(intern!(py, "name"))?.extract::<String>()? == "unitary" {
580580
let params = extract_params()?;
581-
if let Param::Obj(data) = &params[0] {
581+
if let Some(Param::Obj(data)) = params.first() {
582582
let py_matrix: PyReadonlyArray2<Complex64> = data.extract(py)?;
583583
let matrix: Option<MatrixView2<Complex64>> = py_matrix.try_as_matrix();
584584
if let Some(x) = matrix {

test/python/qasm2/test_structure.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1836,3 +1836,19 @@ def test_single_quoted_path(self):
18361836
qc = QuantumCircuit(QuantumRegister(1, "q"))
18371837
qc.h(0)
18381838
self.assertEqual(parsed, qc)
1839+
1840+
def test_unitary_qasm(self):
1841+
"""Test that UnitaryGate can be loaded by OQ2 correctly."""
1842+
qc = QuantumCircuit(1)
1843+
qc.unitary([[1, 0], [0, 1]], 0)
1844+
qasm = """
1845+
OPENQASM 2.0;
1846+
include "qelib1.inc";
1847+
gate unitary q0 { U(0,0,0) q0; }
1848+
qreg q[1];
1849+
unitary q[0];
1850+
"""
1851+
parsed = qiskit.qasm2.loads(qasm)
1852+
self.assertIsInstance(parsed, QuantumCircuit)
1853+
self.assertIsInstance(parsed.data[0].operation, qiskit.qasm2.parse._DefinedGate)
1854+
self.assertEqual(Operator.from_circuit(parsed), Operator.from_circuit(qc))

test/python/transpiler/test_split_2q_unitaries.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
"""
1414
Tests for the Split2QUnitaries transpiler pass.
1515
"""
16+
17+
import io
18+
1619
from math import pi
1720
from test import QiskitTestCase
1821
import numpy as np
@@ -26,6 +29,7 @@
2629
from qiskit.quantum_info.operators.predicates import matrix_equal
2730
from qiskit.transpiler.passes import Collect2qBlocks, ConsolidateBlocks
2831
from qiskit.transpiler.passes.optimization.split_2q_unitaries import Split2QUnitaries
32+
from qiskit import qpy
2933

3034

3135
class TestSplit2QUnitaries(QiskitTestCase):
@@ -377,3 +381,41 @@ def test_2q_swap_with_large_circuit(self):
377381
) # the original 2-qubit unitary should be split into 2 1-qubit unitaries.
378382
self.assertTrue(expected_op.equiv(res_op))
379383
self.assertTrue(matrix_equal(expected_op.data, res_op.data, ignore_phase=False))
384+
385+
def test_overloaded_unitary_name_from_qasm(self):
386+
"""Test that an otherwise invalid custom gate named unitary created via valid Qiskit
387+
API calls doesn't crash the pass
388+
389+
See: https://github.com/Qiskit/qiskit/issues/14103
390+
"""
391+
392+
qasm_str = """OPENQASM 2.0;
393+
include "qelib1.inc";
394+
gate cx_o0 q0,q1 { x q0; cx q0,q1; x q0; }
395+
gate unitary q0,q1 { u(pi/2,0.6763483147328913,0) q0; u(1.6719020266110614,-pi/2,0) q1; cx q0,q1; u(pi,-0.9111063207475532,3.1249343449042435) q0; u(1.6719020266110616,0,-pi/2) q1; }
396+
qreg v__0__0_[0];
397+
qreg l___0__0___1_[2];
398+
qreg l___0__0___2_[2];
399+
qreg v__0__1_[0];
400+
qreg l___0__1___1_[2];
401+
qreg v__1__0_[0];
402+
qreg l___1__0___1_[2];
403+
qreg l___1__0___2_[2];
404+
qreg v__1__1_[0];
405+
qreg l___1__1___1_[2];
406+
creg meas[12];
407+
unitary l___0__0___2_[0],l___0__1___1_[0];
408+
"""
409+
# Parse qasm string to get custom unitary gate that's not a UnitaryGate (but has matrix defined)
410+
qc = QuantumCircuit.from_qasm_str(qasm_str)
411+
# Roundtrip QPY to lose the custom matrix definition from qasm2
412+
# unitary
413+
with io.BytesIO() as buf:
414+
qpy.dump(qc, buf)
415+
# Rewind back to the beginning of the "file".
416+
buf.seek(0)
417+
qc = qpy.load(buf)[0]
418+
# Run split unitaries pass with custom gate named unitary that has
419+
# no matrix.
420+
res = Split2QUnitaries()(qc)
421+
self.assertEqual(res, qc)

0 commit comments

Comments
 (0)