Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add final layout to output quantum circuit from transpiler #8597

Merged
merged 19 commits into from
Jan 18, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion qiskit/quantum_info/operators/operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ def from_label(cls, label):
return op

@classmethod
def from_circuit(cls, circuit, ignore_set_layout=False, layout=None):
def from_circuit(cls, circuit, ignore_set_layout=False, layout=None, final_layout=None):
"""Create a new Operator object from a :class:`.QuantumCircuit`

While a :class:`~.QuantumCircuit` object can passed directly as ``data``
Expand All @@ -224,8 +224,14 @@ def from_circuit(cls, circuit, ignore_set_layout=False, layout=None):
of a layout contained in the ``circuit`` input. If specified
the virtual bits in the :class:`~.Layout` must be present in the
``circuit`` input.
final_layout (Layout): If specified this kwarg can be used to represent the
output permutation caused by swap insertions during the routing stage
of the transpiler.
Returns:
Operator: An operator representing the input circuit

Raises:
KeyError: If a circuit has an empty embedded.
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
"""
dimension = 2**circuit.num_qubits
op = cls(np.eye(dimension))
Expand All @@ -239,6 +245,10 @@ def from_circuit(cls, circuit, ignore_set_layout=False, layout=None):
initial_layout=layout,
input_qubit_mapping={qubit: index for index, qubit in enumerate(circuit.qubits)},
)
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
Expand All @@ -252,6 +262,16 @@ def from_circuit(cls, circuit, ignore_set_layout=False, layout=None):
# 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
if final_layout is not None:
# TODO: Do this without the intermediate Permutation object by just
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you plan to do this TODO in this PR? or another PR?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think we can save it for a follow up, unless you have an easy way to do it now. I don't have the bandwidth to investigate this further anymore.

# operating directly on the array directly
from qiskit.circuit.library import Permutation # pylint: disable=cyclic-import

final_physical_to_virtual = final_layout.get_physical_bits()
perm_pattern = [final_layout._v2p[v] for v in circuit.qubits]
perm_op = Operator(Permutation(len(final_physical_to_virtual), perm_pattern))
op &= perm_op
return op

def is_unitary(self, atol=None, rtol=None):
Expand Down
1 change: 1 addition & 0 deletions qiskit/transpiler/basepasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ def __call__(self, circuit, property_set=None):
result_circuit._layout = TranspileLayout(
initial_layout=self.property_set["layout"],
input_qubit_mapping=self.property_set["original_qubit_indices"],
final_layout=self.property_set["final_layout"],
)
if self.property_set["clbit_write_latency"] is not None:
result_circuit._clbit_write_latency = self.property_set["clbit_write_latency"]
Expand Down
3 changes: 2 additions & 1 deletion qiskit/transpiler/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"""

from dataclasses import dataclass
from typing import Dict
from typing import Dict, Optional

from qiskit.circuit.quantumregister import Qubit, QuantumRegister
from qiskit.transpiler.exceptions import LayoutError
Expand Down Expand Up @@ -374,3 +374,4 @@ class TranspileLayout:

initial_layout: Layout
input_qubit_mapping: Dict[Qubit, int]
final_layout: Optional[Layout] = None
Copy link
Member

Choose a reason for hiding this comment

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

What does it mean for final_layout to be optional? Is it that transpiler passes could swap qubits and not report it, or that they can leave this unset if they are not performing any swaps?

Copy link
Member Author

Choose a reason for hiding this comment

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

It primarily means the latter, if final_layout is None then there wasn't a permutation being caused by the routing stage in the compiler. Basically, the routing pass was skipped. There is also a backwards compatibility component to making this optional as well, but that's less likely to come up in practice outside of the routing pass not run.

1 change: 1 addition & 0 deletions qiskit/transpiler/passes/routing/basic_swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ def run(self, dag):
order = current_layout.reorder_bits(new_dag.qubits)
new_dag.compose(subdag, qubits=order)

self.property_set["final_layout"] = current_layout
return new_dag

def _fake_run(self, dag):
Expand Down
2 changes: 1 addition & 1 deletion qiskit/transpiler/passes/routing/lookahead_swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,8 @@ def run(self, dag):

mapped_gates.extend(gates_mapped)

self.property_set["final_layout"] = current_state.layout
if self.fake_run:
self.property_set["final_layout"] = current_state.layout
return dag

# Preserve input DAG's name, regs, wire_map, etc. but replace the graph.
Expand Down
1 change: 1 addition & 0 deletions qiskit/transpiler/runningpassmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ def run(self, circuit, output_name=None, callback=None):
circuit._layout = TranspileLayout(
initial_layout=self.property_set["layout"],
input_qubit_mapping=self.property_set["original_qubit_indices"],
final_layout=self.property_set["final_layout"],
)
circuit._clbit_write_latency = self.property_set["clbit_write_latency"]
circuit._conditional_latency = self.property_set["conditional_latency"]
Expand Down
14 changes: 14 additions & 0 deletions releasenotes/notes/final_layout-8178327a57b8b96a.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
features:
- |
The :meth:`.Operator.from_circuit` constructor method now will reverse
the output permutation caused by the routing/swap mapping stage of the
transpiler. By default if a transpiled circuit had Swap gates inserted
the output matrix will have that permutation reversed so the returned
matrix will be equivalent to the original un-transpiled circuit. If you'd
like to disable this default behavior the ``ignore_set_layout`` keyword
argument can be set to ``True`` to do this (in addition to previous behavior
of ignoring the initial layout from transpilation). If you'd like to
manually set a final layout you can use the new ``final_layout`` keyword
argument to pass in a :class:`~.Layout` object to use for the output
permutation.
71 changes: 71 additions & 0 deletions test/python/quantum_info/operators/test_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,77 @@ def test_from_circuit_constructor_reverse_embedded_layout(self):
global_phase_equivalent = matrix_equal(op.data, target, ignore_phase=True)
self.assertTrue(global_phase_equivalent)

def test_from_circuit_constructor_reverse_embedded_layout_from_transpile(self):
"""Test initialization from a circuit with an embedded final layout."""
# Test tensor product of 1-qubit gates
circuit = QuantumCircuit(3)
circuit.h(0)
circuit.x(1)
circuit.ry(np.pi / 2, 2)
output = transpile(circuit, initial_layout=[2, 1, 0])
op = Operator.from_circuit(output)
y90 = (1 / np.sqrt(2)) * np.array([[1, -1], [1, 1]])
target = np.kron(y90, np.kron(self.UX, self.UH))
global_phase_equivalent = matrix_equal(op.data, target, ignore_phase=True)
self.assertTrue(global_phase_equivalent)

def test_from_circuit_constructor_reverse_embedded_layout_from_transpile_with_registers(self):
"""Test initialization from a circuit with an embedded final layout."""
# Test tensor product of 1-qubit gates
qr = QuantumRegister(3, name="test_reg")
circuit = QuantumCircuit(qr)
circuit.h(0)
circuit.x(1)
circuit.ry(np.pi / 2, 2)
output = transpile(circuit, initial_layout=[2, 1, 0])
op = Operator.from_circuit(output)
y90 = (1 / np.sqrt(2)) * np.array([[1, -1], [1, 1]])
target = np.kron(y90, np.kron(self.UX, self.UH))
global_phase_equivalent = matrix_equal(op.data, target, ignore_phase=True)
self.assertTrue(global_phase_equivalent)

def test_from_circuit_constructor_reverse_embedded_layout_and_final_layout(self):
"""Test initialization from a circuit with an embedded final layout."""
# Test tensor product of 1-qubit gates
qr = QuantumRegister(3, name="test_reg")
circuit = QuantumCircuit(qr)
circuit.h(2)
circuit.x(1)
circuit.ry(np.pi / 2, 0)
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}),
)
circuit.swap(0, 1)
circuit.swap(1, 2)
op = Operator.from_circuit(circuit)
y90 = (1 / np.sqrt(2)) * np.array([[1, -1], [1, 1]])
target = np.kron(y90, np.kron(self.UX, self.UH))
global_phase_equivalent = matrix_equal(op.data, target, ignore_phase=True)
self.assertTrue(global_phase_equivalent)

def test_from_circuit_constructor_reverse_embedded_layout_and_manual_final_layout(self):
"""Test initialization from a circuit with an embedded final layout."""
# Test tensor product of 1-qubit gates
qr = QuantumRegister(3, name="test_reg")
circuit = QuantumCircuit(qr)
circuit.h(2)
circuit.x(1)
circuit.ry(np.pi / 2, 0)
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)},
)
final_layout = Layout({circuit.qubits[0]: 1, circuit.qubits[1]: 2, circuit.qubits[2]: 0})
circuit.swap(0, 1)
circuit.swap(1, 2)
op = Operator.from_circuit(circuit, final_layout=final_layout)
y90 = (1 / np.sqrt(2)) * np.array([[1, -1], [1, 1]])
target = np.kron(y90, np.kron(self.UX, self.UH))
global_phase_equivalent = matrix_equal(op.data, target, ignore_phase=True)
self.assertTrue(global_phase_equivalent)

def test_from_circuit_constructor_reverse_embedded_layout_ignore_set_layout(self):
"""Test initialization from a circuit with an ignored embedded reverse layout."""
# Test tensor product of 1-qubit gates
Expand Down