Skip to content

Commit

Permalink
Fixing Operator.from_circuit for circuits with final layouts and a no…
Browse files Browse the repository at this point in the history
…n-trivial initial layout (#12057)

* reno, format, test changes

* fix Operator.from_circuit and add failing test case

* Update test_operator.py

* Update operator.py

* Update releasenotes/notes/operator-from-circuit-bugfix-5dab5993526a2b0a.yaml

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>

* selective merge of 11399

* reimplementing from_circuit

---------

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
Co-authored-by: AlexanderIvrii <alexi@il.ibm.com>
  • Loading branch information
3 people authored May 1, 2024
1 parent a4f272f commit a65c9e6
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 21 deletions.
49 changes: 31 additions & 18 deletions qiskit/quantum_info/operators/operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ def __init__(
a Numpy array of shape (2**N, 2**N) qubit systems will be used. If
the input operator is not an N-qubit operator, it will assign a
single subsystem with dimension specified by the shape of the input.
Note that two operators initialized via this method are only considered equivalent if they
match up to their canonical qubit order (or: permutation). See :meth:`.Operator.from_circuit`
to specify a different qubit permutation.
"""
op_shape = None
if isinstance(data, (list, np.ndarray)):
Expand Down Expand Up @@ -391,8 +394,7 @@ def from_circuit(
Returns:
Operator: An operator representing the input circuit
"""
dimension = 2**circuit.num_qubits
op = cls(np.eye(dimension))

if layout is None:
if not ignore_set_layout:
layout = getattr(circuit, "_layout", None)
Expand All @@ -403,27 +405,38 @@ def from_circuit(
initial_layout=layout,
input_qubit_mapping={qubit: index for index, qubit in enumerate(circuit.qubits)},
)

initial_layout = layout.initial_layout if layout is not None else None

if final_layout is None:
if not ignore_set_layout and layout is not None:
final_layout = getattr(layout, "final_layout", None)

qargs = None
# If there was a layout specified (either from the circuit
# or via user input) use that to set qargs to permute qubits
# based on that layout
if layout is not None:
physical_to_virtual = layout.initial_layout.get_physical_bits()
qargs = [
layout.input_qubit_mapping[physical_to_virtual[physical_bit]]
for physical_bit in range(len(physical_to_virtual))
]
# Convert circuit to an instruction
instruction = circuit.to_instruction()
op._append_instruction(instruction, qargs=qargs)
# If final layout is set permute output indices based on layout
from qiskit.synthesis.permutation.permutation_utils import _inverse_pattern

if initial_layout is not None:
input_qubits = [None] * len(layout.input_qubit_mapping)
for q, p in layout.input_qubit_mapping.items():
input_qubits[p] = q

initial_permutation = initial_layout.to_permutation(input_qubits)
initial_permutation_inverse = _inverse_pattern(initial_permutation)

if final_layout is not None:
perm_pattern = [final_layout._v2p[v] for v in circuit.qubits]
op = op.apply_permutation(perm_pattern, front=False)
final_permutation = final_layout.to_permutation(circuit.qubits)
final_permutation_inverse = _inverse_pattern(final_permutation)

op = Operator(circuit)

if initial_layout:
op = op.apply_permutation(initial_permutation, True)

if final_layout:
op = op.apply_permutation(final_permutation_inverse, False)

if initial_layout:
op = op.apply_permutation(initial_permutation_inverse, False)

return op

def is_unitary(self, atol=None, rtol=None):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
fixes:
- |
Fixed an issue with the :meth:`.Operator.from_circuit` constructor method where it would incorrectly
interpret the final layout permutation resulting in an invalid `Operator` being constructed.
Previously, the final layout was processed without regards for the initial layout, i.e. the
initialization was incorrect for all quantum circuits that have a non-trivial initial layout.
51 changes: 48 additions & 3 deletions test/python/quantum_info/operators/test_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import unittest
import logging
import copy

from test import combine
import numpy as np
from ddt import ddt
Expand All @@ -26,6 +27,7 @@
from qiskit import QiskitError
from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit
from qiskit.circuit.library import HGate, CHGate, CXGate, QFT
from qiskit.transpiler import CouplingMap
from qiskit.transpiler.layout import Layout, TranspileLayout
from qiskit.quantum_info.operators import Operator, ScalarOp
from qiskit.quantum_info.operators.predicates import matrix_equal
Expand Down Expand Up @@ -735,6 +737,28 @@ def test_from_circuit_constructor_no_layout(self):
global_phase_equivalent = matrix_equal(op.data, target, ignore_phase=True)
self.assertTrue(global_phase_equivalent)

def test_from_circuit_initial_layout_final_layout(self):
"""Test initialization from a circuit with a non-trivial initial_layout and final_layout as given
by a transpiled circuit."""
qc = QuantumCircuit(5)
qc.h(0)
qc.cx(2, 1)
qc.cx(1, 2)
qc.cx(1, 0)
qc.cx(1, 3)
qc.cx(1, 4)
qc.h(2)

qc_transpiled = transpile(
qc,
coupling_map=CouplingMap.from_line(5),
initial_layout=[2, 3, 4, 0, 1],
optimization_level=1,
seed_transpiler=17,
)

self.assertTrue(Operator.from_circuit(qc_transpiled).equiv(qc))

def test_from_circuit_constructor_reverse_embedded_layout(self):
"""Test initialization from a circuit with an embedded reverse layout."""
# Test tensor product of 1-qubit gates
Expand Down Expand Up @@ -817,7 +841,7 @@ def test_from_circuit_constructor_reverse_embedded_layout_and_final_layout(self)
circuit._layout = TranspileLayout(
Layout({circuit.qubits[2]: 0, circuit.qubits[1]: 1, circuit.qubits[0]: 2}),
{qubit: index for index, qubit in enumerate(circuit.qubits)},
Layout({circuit.qubits[0]: 1, circuit.qubits[1]: 2, circuit.qubits[2]: 0}),
Layout({circuit.qubits[0]: 2, circuit.qubits[1]: 0, circuit.qubits[2]: 1}),
)
circuit.swap(0, 1)
circuit.swap(1, 2)
Expand All @@ -839,7 +863,7 @@ def test_from_circuit_constructor_reverse_embedded_layout_and_manual_final_layou
Layout({circuit.qubits[2]: 0, circuit.qubits[1]: 1, circuit.qubits[0]: 2}),
{qubit: index for index, qubit in enumerate(circuit.qubits)},
)
final_layout = Layout({circuit.qubits[0]: 1, circuit.qubits[1]: 2, circuit.qubits[2]: 0})
final_layout = Layout({circuit.qubits[0]: 2, circuit.qubits[1]: 0, circuit.qubits[2]: 1})
circuit.swap(0, 1)
circuit.swap(1, 2)
op = Operator.from_circuit(circuit, final_layout=final_layout)
Expand Down Expand Up @@ -966,7 +990,7 @@ def test_from_circuit_constructor_empty_layout(self):
circuit.h(0)
circuit.cx(0, 1)
layout = Layout()
with self.assertRaises(IndexError):
with self.assertRaises(KeyError):
Operator.from_circuit(circuit, layout=layout)

def test_compose_scalar(self):
Expand Down Expand Up @@ -1078,6 +1102,27 @@ def test_from_circuit_mixed_reg_loose_bits_transpiled(self):
result = Operator.from_circuit(tqc)
self.assertTrue(Operator(circuit).equiv(result))

def test_from_circuit_into_larger_map(self):
"""Test from_circuit method when the number of physical
qubits is larger than the number of original virtual qubits."""

# original circuit on 3 qubits
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.cx(1, 2)

# transpile into 5-qubits
tqc = transpile(qc, coupling_map=CouplingMap.from_line(5), initial_layout=[0, 2, 4])

# qc expanded with ancilla qubits
expected = QuantumCircuit(5)
expected.h(0)
expected.cx(0, 1)
expected.cx(1, 2)

self.assertEqual(Operator.from_circuit(tqc), Operator(expected))

def test_apply_permutation_back(self):
"""Test applying permutation to the operator,
where the operator is applied first and the permutation second."""
Expand Down

0 comments on commit a65c9e6

Please sign in to comment.