Skip to content

Conversation

mtreinish
Copy link
Member

Summary

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, if you then qpy serialize the circuit that contains a custom _DefinedGate instance that's named "unitary", QPY doesn't understand what _DefinedGate is and assumes that it can not fully recreate it because it's outside the standard Qiskit data model. In these cases qpy serializes the fields from the standard data model defined in Qiskit which it knows about: name, params, definition, etc and encodes that as a custom gate. When deserializing that custom gate it populates the standard fields, but anything defined outside of that is lost through the serialization because it's defined outside of Qiskit. This means the custom __array__ implementation is missing from the qpy round-tripped, previously qasm2 round-tripped, UnitaryGate instance in a circuit. This was then causing the transpiler to panic (which is a hard crash) in the Split2QUnitaries pass because when it saw a gate named "unitary" it rightly assumed it was a UnitaryGate and there is always a matrix available (because a UnitaryGate is a gate defined solely by it's matrix). This is an example of why overloading gate names is typically not allowed and is considered invalid usage of Qiskit. However, in this case since the resulting gate came from valid use of Qiskit's API trying to ensure the circuit doesn't hard crash the transpiler is correct here because it's a result in internal inconsistencies in Qiskit. Treating the twice round-tripped UnitaryGate as the original unitary gate is not possible because of the loss of information via serialization, but qiskit should at least not have an internal panic!() because of this usage.

Arguably we could fix this in QPY by adding either a matrix field to the format specification for custom gates, or adding handling for _DefinedGate to qpy. But either will require a new format version which is not eligble for backport to 1.4.x. So this commit fixes this issue by updating the transpiler pass in question to just treat the matrix of a gate named unitary as fallible, and we just ignore the custom gate named unitary that's missing a matrix.

This already been fixed on main and stable/2.0 as a side effect of PR #13765 which made the typing around UnitaryGate in rust more explicit. So the custom deserialized gate from QPY which doesn't have a matrix defined is never treated as a unitary gate and even through the qasm2 round trip followed by a qpy round trip we don't encounter any issues.

Details and comments

Fixes #14103

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 Qiskit#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, if you then qpy serialize
the circuit that contains a custom `_DefinedGate` instance that's named
"unitary", QPY doesn't understand what `_DefinedGate` is and assumes that
it can not fully recreate it because it's outside the standard Qiskit data
model. In these cases qpy serializes the fields from the standard data
model defined in Qiskit which it knows about: name, params, definition, etc
and encodes that as a custom gate. When deserializing that custom gate it
populates the standard fields, but anything defined outside of that is
lost through the serialization because it's defined outside of Qiskit.
This means the custom `__array__` implementation is missing from the
qpy round-tripped, previously qasm2 round-tripped, `UnitaryGate`
instance in a circuit. This was then causing the transpiler to panic
(which is a hard crash) in the Split2QUnitaries pass because when it saw
a gate named "unitary" it rightly assumed it was a `UnitaryGate` and
there is always a matrix available (because a `UnitaryGate` is a gate
defined solely by it's matrix). This is an example of why overloading
gate names is typically not allowed and is considered invalid usage of
Qiskit. However, in this case since the resulting gate came from valid
use of Qiskit's API trying to ensure the circuit doesn't hard crash the
transpiler is correct here because it's a result in internal
inconsistencies in Qiskit. Treating the twice round-tripped UnitaryGate
as the original unitary gate is not possible because of the loss of
information via serialization, but qiskit should at least not have
an internal panic!() because of this usage.

Arguably we could fix this in QPY by adding either a matrix field to the
format specification for custom gates, or adding handling for
`_DefinedGate` to qpy. But either will require a new format version
which is not eligble for backport to 1.4.x. So this commit fixes this
issue by updating the transpiler pass in question to just treat the
matrix of a gate named unitary as fallible, and we just ignore the
custom gate named unitary that's missing a matrix.

This already been fixed on main and stable/2.0 as a side effect of
PR Qiskit#13765 which made the typing around UnitaryGate in rust more
explicit. So the custom deserialized gate from QPY which doesn't have a
matrix defined is never treated as a unitary gate and even through the
qasm2 round trip followed by a qpy round trip we don't encounter any
issues.

Fixes Qiskit#14103
@mtreinish mtreinish added Changelog: Bugfix Include in the "Fixed" section of the changelog mod: qasm2 Relating to OpenQASM 2 import or export mod: qpy Related to QPY serialization Rust This PR or issue is related to Rust code in the repository labels Mar 26, 2025
@mtreinish mtreinish added this to the 1.4.3 milestone Mar 26, 2025
@mtreinish mtreinish requested a review from a team as a code owner March 26, 2025 18:23
@qiskit-bot
Copy link
Collaborator

One or more of the following people are relevant to this code:

  • @Qiskit/terra-core

@mtreinish
Copy link
Member Author

mtreinish commented Mar 26, 2025

This already been fixed on main and stable/2.0 as a side effect of PR #13765 which made the typing around UnitaryGate in rust more explicit. So the custom deserialized gate from QPY which doesn't have a matrix defined is never treated as a unitary gate and even through the qasm2 round trip followed by a qpy round trip we don't encounter any issues.

This isn't 100% accurate, the qasm deserialization trying to add the _DefinedGate named unitary instance to the circuit currently panics here because the gate named unitary is not a UnitaryGate. We shouldn't panic there but raise a CircuitError saying the gate name is unitary which is reserved for UnitaryGate instances. I'll push up that up in a separate PR. I'll include a release note for 2.0 documenting this as an API change since it's pretty deep in the data model that this is straight up not expressible anymore.

EDIT: actually we just shouldn't fail there and treat it as a custom gate, that's the simpler fix anyway.

Copy link
Contributor

@raynelfss raynelfss left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Though I wonder if there are any other places within the transpiler in which this may have happened before #13765... Luckily we won't have to worry about this in future versions.

@mtreinish
Copy link
Member Author

LGTM! Though I wonder if there are any other places within the transpiler in which this may have happened before #13765... Luckily we won't have to worry about this in future versions.

I checked and the only place we do this is in UnitarySynthesis and there it's just checking whether we can skip synthesis or not, it shouldn't matter if it's really a unitary gate or not in that case.

@mtreinish mtreinish added this pull request to the merge queue Apr 1, 2025
Merged via the queue into Qiskit:stable/1.4 with commit 5d78070 Apr 1, 2025
17 checks passed
@mtreinish mtreinish deleted the fix-qpy-round-tripped-qasm2-roundtripped-unitary-gate-handling-in-split-2q-unitaries branch April 24, 2025 15:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Changelog: Bugfix Include in the "Fixed" section of the changelog mod: qasm2 Relating to OpenQASM 2 import or export mod: qpy Related to QPY serialization Rust This PR or issue is related to Rust code in the repository
Projects
None yet
Development

Successfully merging this pull request may close these issues.

"pyo3_runtime.PanicException: 'unitary' gates should always have a matrix form" can't be caught
3 participants