Skip to content

Commit

Permalink
Detect duplicates in QuantumCircuit.compose (#11451)
Browse files Browse the repository at this point in the history
* Detect duplicates in `QuantumCircuit.compose`

Previously, `QuantumCircuit.compose` would silently attempt to compose
multiple qubits onto the same state, which would fail in weird ways if
any multi-qubit instructions were acting on these collapsed qubits.

* Ensure error-checking happens before any modification

After the move of `QuantumCircuit.data` to Rust-space, some of the state
modification had started happening before the error checking, which
could potentially leave an in-place modification in a partially applied
state if an exception triggered during processing.

This also ensures that the qubits and clbits arguments are checked, even
if the actual data being composed is empty.

* Delay mutation until the last possible time
  • Loading branch information
jakelishman authored Jan 8, 2024
1 parent 3263355 commit d48ba00
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 17 deletions.
42 changes: 25 additions & 17 deletions qiskit/circuit/quantumcircuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -953,9 +953,10 @@ def compose(
"Cannot emit a new composed circuit while a control-flow context is active."
)

# Avoid mutating `dest` until as much of the error checking as possible is complete, to
# avoid an in-place composition getting `self` in a partially mutated state for a simple
# error that the user might want to correct in an interactive session.
dest = self if inplace else self.copy()
dest.duration = None
dest.unit = "dt"

# As a special case, allow composing some clbits onto no clbits - normally the destination
# has to be strictly larger. This allows composing final measurements onto unitary circuits.
Expand Down Expand Up @@ -997,23 +998,9 @@ def compose(
"Trying to compose with another QuantumCircuit which has more 'in' edges."
)

for gate, cals in other.calibrations.items():
dest._calibrations[gate].update(cals)

dest.global_phase += other.global_phase

if not other.data:
# Nothing left to do. Plus, accessing 'data' here is necessary
# to trigger any lazy building since we now access '_data'
# directly.
return None if inplace else dest

# The 'qubits' and 'clbits' used for 'dest'.
# Maps bits in 'other' to bits in 'dest'.
mapped_qubits: list[Qubit]
mapped_clbits: list[Clbit]

# Maps bits in 'other' to bits in 'dest'. Used only for
# adjusting bits in variables (e.g. condition and target).
edge_map: dict[Qubit | Clbit, Qubit | Clbit] = {}
if qubits is None:
mapped_qubits = dest.qubits
Expand All @@ -1025,6 +1012,10 @@ def compose(
f"Number of items in qubits parameter ({len(mapped_qubits)}) does not"
f" match number of qubits in the circuit ({len(other.qubits)})."
)
if len(set(mapped_qubits)) != len(mapped_qubits):
raise CircuitError(
f"Duplicate qubits referenced in 'qubits' parameter: '{mapped_qubits}'"
)
edge_map.update(zip(other.qubits, mapped_qubits))

if clbits is None:
Expand All @@ -1037,8 +1028,25 @@ def compose(
f"Number of items in clbits parameter ({len(mapped_clbits)}) does not"
f" match number of clbits in the circuit ({len(other.clbits)})."
)
if len(set(mapped_clbits)) != len(mapped_clbits):
raise CircuitError(
f"Duplicate clbits referenced in 'clbits' parameter: '{mapped_clbits}'"
)
edge_map.update(zip(other.clbits, dest.cbit_argument_conversion(clbits)))

for gate, cals in other.calibrations.items():
dest._calibrations[gate].update(cals)

dest.duration = None
dest.unit = "dt"
dest.global_phase += other.global_phase

if not other.data:
# Nothing left to do. Plus, accessing 'data' here is necessary
# to trigger any lazy building since we now access '_data'
# directly.
return None if inplace else dest

variable_mapper = _classical_resource_map.VariableMapper(
dest.cregs, edge_map, dest.add_register
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
fixes:
- |
:meth:`.QuantumCircuit.compose` will now correctly raise a :exc:`.CircuitError` when there are
duplicates in the ``qubits`` or ``clbits`` arguments.
11 changes: 11 additions & 0 deletions test/python/circuit/test_compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
Instruction,
CASE_DEFAULT,
SwitchCaseOp,
CircuitError,
)
from qiskit.circuit.library import HGate, RZGate, CXGate, CCXGate, TwoLocal
from qiskit.circuit.classical import expr
Expand Down Expand Up @@ -880,6 +881,16 @@ def test_expr_target_is_mapped(self):

self.assertEqual(dest, expected)

def test_rejects_duplicate_bits(self):
"""Test that compose rejects duplicates in either qubits or clbits."""
base = QuantumCircuit(5, 5)

attempt = QuantumCircuit(2, 2)
with self.assertRaisesRegex(CircuitError, "Duplicate qubits"):
base.compose(attempt, [1, 1], [0, 1])
with self.assertRaisesRegex(CircuitError, "Duplicate clbits"):
base.compose(attempt, [0, 1], [1, 1])


if __name__ == "__main__":
unittest.main()

0 comments on commit d48ba00

Please sign in to comment.