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

[WIP] Adding FinalPermutation attribute to DAGCircuit and QuantumCircuit #12534

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
96 changes: 96 additions & 0 deletions qiskit/circuit/final_permutation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2024.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Reasoning about the implicit permutation of output qubits."""

from qiskit.circuit.exceptions import CircuitError


class FinalPermutation:
r"""
Reasons about the implicit permutation of output qubits.

The notation use here is the same as for the class :class:`~PermutationGate`:
the permutation is stored as a list describing which qubits occupy the
positions 0, 1, 2, etc. after applying the permutation. As an example,
the permutation ``[2, 4, 3, 0, 1]`` means that the qubit ``2`` goes to
position ``0``, qubit ``4`` goes to the position ``1``, and so on.
In particular, a circuit with an implicit permutation :math:`\sigma`
can be replaced by a :class:`~PermutationGate` with the same permutation
pattern :math:`\sigma`.
"""

def __init__(self, permutation=None):
"""Initializer."""
if permutation is None:
permutation = []
self.permutation = permutation

def add_qubit(self):
"""Extends the permutation when a new qubit is added to a DAGCircuit or to
a QuantumCircuit."""
self.permutation.append(len(self.permutation))

def num_qubits(self) -> int:
"""Returns the length of the permutation (i.e. the total number of qubits)."""
return len(self.permutation)

def is_identity(self) -> bool:
"""Returns whether the permutation is the identity permutation."""
return all(from_index == to_index for from_index, to_index in enumerate(self.permutation))

def compose_with_permutation(self, permutation, front) -> "FinalPermutation":
"""Composes FinalPermution with a permutation."""
Comment on lines +51 to +52
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am looking for a better name for this function.

# pylint: disable=cyclic-import
from qiskit.synthesis.permutation.permutation_utils import _compose_permutations

if front:
composed_permutation = _compose_permutations(self.permutation, permutation)
else:
composed_permutation = _compose_permutations(permutation, self.permutation)
return FinalPermutation(composed_permutation)

def __repr__(self):
return str(self.permutation)

def copy(self) -> "FinalPermutation":
"""Creates a copy of the FinalPermutation object."""
return FinalPermutation(self.permutation.copy())

def push_using_mapping(self, forward_map, num_target_qubits=None) -> "FinalPermutation":
Copy link
Contributor Author

Choose a reason for hiding this comment

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

And for a better name for this function.

r"""
Applies a layout mapping (or more generally any mapping) to a
permutation.

More precisely, given a permutation :math:`\sigma: V \rightarrow V`,
and a map :math:`\tau: V \rightarrow P`, returns a permutation
:math:`\tilde{\sigma}: P\rightarrow P`, where
:math:`\tilde{\sigma}` maps :math:`\tau(a)` to :math:`\tau(b)`
whenever :math:`\sigma` maps :math:`a` to :math:`b`, and
:math:`\tilde{\sigma}` is identity on the remaining elements.
"""

if num_target_qubits is None:
num_target_qubits = len(forward_map)

target_permutation = list(range(num_target_qubits))

if isinstance(forward_map, list):
for inp, out in enumerate(forward_map):
target_permutation[out] = forward_map[self.permutation[inp]]
elif isinstance(forward_map, dict):
for inp, out in forward_map.items():
target_permutation[out] = forward_map[self.permutation[inp]]
else:
raise CircuitError("The map should be given either as a list or as a dict.")

return FinalPermutation(target_permutation)
4 changes: 4 additions & 0 deletions qiskit/circuit/quantumcircuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from qiskit.circuit.gate import Gate
from qiskit.circuit.parameter import Parameter
from qiskit.circuit.exceptions import CircuitError
from qiskit.circuit.final_permutation import FinalPermutation
from . import _classical_resource_map
from ._utils import sort_parameters
from .controlflow import ControlFlowOp
Expand Down Expand Up @@ -1115,6 +1116,7 @@ def __init__(
# within that register.
self._qubit_indices: dict[Qubit, BitLocations] = {}
self._clbit_indices: dict[Clbit, BitLocations] = {}
self._final_permutation = FinalPermutation()

# Data contains a list of instructions and their contexts,
# in the order they were applied.
Expand Down Expand Up @@ -2919,6 +2921,7 @@ def add_register(self, *regs: Register | int | Sequence[Bit]) -> None:
self._qubit_indices[bit] = BitLocations(
self._data.num_qubits - 1, [(register, idx)]
)
self._final_permutation.add_qubit()

elif isinstance(register, ClassicalRegister):
self.cregs.append(register)
Expand Down Expand Up @@ -2951,6 +2954,7 @@ def add_bits(self, bits: Iterable[Bit]) -> None:
if isinstance(bit, Qubit):
self._data.add_qubit(bit)
self._qubit_indices[bit] = BitLocations(self._data.num_qubits - 1, [])
self._final_permutation.add_qubit()
elif isinstance(bit, Clbit):
self._data.add_clbit(bit)
self._clbit_indices[bit] = BitLocations(self._data.num_clbits - 1, [])
Expand Down
2 changes: 2 additions & 0 deletions qiskit/converters/circuit_to_dag.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,6 @@ def circuit_to_dag(circuit, copy_operations=True, *, qubit_order=None, clbit_ord

dagcircuit.duration = circuit.duration
dagcircuit.unit = circuit.unit
dagcircuit._final_permutation = circuit._final_permutation.copy()

return dagcircuit
1 change: 1 addition & 0 deletions qiskit/converters/dag_to_circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def dag_to_circuit(dag, copy_operations=True):
circuit.add_uninitialized_var(var)
circuit.metadata = dag.metadata
circuit.calibrations = dag.calibrations
circuit._final_permutation = dag._final_permutation.copy()

for node in dag.topological_op_nodes():
op = node.op
Expand Down
6 changes: 6 additions & 0 deletions qiskit/dagcircuit/dagcircuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
from qiskit.circuit.parameterexpression import ParameterExpression
from qiskit.dagcircuit.exceptions import DAGCircuitError
from qiskit.dagcircuit.dagnode import DAGNode, DAGOpNode, DAGInNode, DAGOutNode
from qiskit.circuit.final_permutation import FinalPermutation
from qiskit.circuit.bit import Bit
from qiskit.pulse import Schedule

Expand Down Expand Up @@ -146,6 +147,9 @@ def __init__(self):
self.duration = None
self.unit = "dt"

# implicit final permutation on the DAG's qubits
self._final_permutation = FinalPermutation()

@property
def wires(self):
"""Return a list of the wires in order."""
Expand Down Expand Up @@ -277,6 +281,7 @@ def add_qubits(self, qubits):
self.qubits.append(qubit)
self._qubit_indices[qubit] = BitLocations(len(self.qubits) - 1, [])
self._add_wire(qubit)
self._final_permutation.add_qubit()

def add_clbits(self, clbits):
"""Add individual clbit wires."""
Expand Down Expand Up @@ -309,6 +314,7 @@ def add_qreg(self, qreg):
len(self.qubits) - 1, registers=[(qreg, j)]
)
self._add_wire(qreg[j])
self._final_permutation.add_qubit()

def add_creg(self, creg):
"""Add all wires in a classical register."""
Expand Down
66 changes: 64 additions & 2 deletions qiskit/quantum_info/operators/operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ def from_circuit(
ignore_set_layout: bool = False,
layout: Layout | None = None,
final_layout: Layout | None = None,
original_qubit_indices: dict | None = None,
) -> Operator:
"""Create a new Operator object from a :class:`.QuantumCircuit`

Expand All @@ -372,7 +373,7 @@ def from_circuit(
you control how the :class:`.Operator` is created so it can be adjusted
for a particular use case.

By default this constructor method will permute the qubits based on a
By default, this constructor method will permute the qubits based on a
configured initial layout (i.e. after it was transpiled). It also
provides an option to manually provide a :class:`.Layout` object
directly.
Expand All @@ -391,6 +392,8 @@ def from_circuit(
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.
original_qubit_indices (dict): The mapping from qubits to positional indices
for the ``layout`` argument.
Comment on lines +395 to +396
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This argument allows creating a callback function that checks the equivalence of the current operator and the original operator after each transpiler pass, something along the following:

    def callback_func(pass_, dag, time, property_set, count):
        circuit = dag_to_circuit(dag)
        current_op = Operator.from_circuit(
            circuit,
            layout=property_set['layout'],
            final_layout=property_set['final_layout'],
            original_qubit_indices=property_set['original_qubit_indices']
        )

        current_op_new = Operator._from_circuit_new(
            circuit,
            layout=property_set['layout'],
            original_qubit_indices=property_set['original_qubit_indices']
        )

        original_op = op if circuit.num_qubits == original_num_qubits else extended_op

        print(f"After-pass: equiv =  {original_op.equiv(current_op)}")
        print(f"After-pass-new: equiv =  {original_op.equiv(current_op_new)}")

I have found this very useful to catch problems, however this is still not fully correct: the SetLayout pass sets the pass manager's "layout" attribute, but it's only the later pass ApplyLayout that effectively applies it to the circuit, so in-between these two passes the equivalence is not preserved. Though a simple way would be to have ApplyLayout set an extra attribute layout_is_applied and use layout when checking equivalence only when layout_is_applied is set.

Returns:
Operator: An operator representing the input circuit
"""
Expand All @@ -401,9 +404,13 @@ def from_circuit(
else:
from qiskit.transpiler.layout import TranspileLayout # pylint: disable=cyclic-import

if original_qubit_indices is not None:
input_qubit_mapping = original_qubit_indices
else:
input_qubit_mapping = {qubit: index for index, qubit in enumerate(circuit.qubits)}
layout = TranspileLayout(
initial_layout=layout,
input_qubit_mapping={qubit: index for index, qubit in enumerate(circuit.qubits)},
input_qubit_mapping=input_qubit_mapping,
)

initial_layout = layout.initial_layout if layout is not None else None
Expand Down Expand Up @@ -439,6 +446,61 @@ def from_circuit(

return op

@classmethod
def _from_circuit_new(
cls,
circuit: QuantumCircuit,
ignore_set_layout: bool = False,
layout: Layout | None = None,
original_qubit_indices: dict | None = None,
) -> Operator:
"""
Implements the same functionality as ``from_circuit`` but obtains the final
permutation from the circuit's attribute ``_final_permutation`` rather than
from the property set.
"""
if layout is None:
if not ignore_set_layout:
layout = getattr(circuit, "_layout", None)
else:
from qiskit.transpiler.layout import TranspileLayout # pylint: disable=cyclic-import

if original_qubit_indices is not None:
input_qubit_mapping = original_qubit_indices
else:
input_qubit_mapping = {qubit: index for index, qubit in enumerate(circuit.qubits)}

layout = TranspileLayout(
initial_layout=layout,
input_qubit_mapping=input_qubit_mapping,
)

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

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)

final_permutation = circuit._final_permutation.permutation

op = Operator(circuit)

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

op = op.apply_permutation(final_permutation, False)

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

return op

def is_unitary(self, atol=None, rtol=None):
"""Return True if operator is a unitary matrix."""
if atol is None:
Expand Down
10 changes: 10 additions & 0 deletions qiskit/synthesis/permutation/permutation_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,13 @@ def _decompose_cycles(cycles):
for i in range(m // 2):
swap_list.append((cycle[i - 1], cycle[m - 2 - i]))
return swap_list


def _compose_permutations(*perms):
Copy link
Member

Choose a reason for hiding this comment

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

note that this file was moved to rust in #12327

"""Compose multiple permutations, with the permutations applied in the
order they appear in the list.
"""
out = range(len(perms[0]))
for perm in perms:
out = [perm[i] for i in out]
return out
11 changes: 11 additions & 0 deletions qiskit/transpiler/passes/layout/apply_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from qiskit.circuit import QuantumRegister
from qiskit.dagcircuit import DAGCircuit
from qiskit.synthesis.permutation.permutation_utils import _inverse_pattern
from qiskit.transpiler.basepasses import TransformationPass
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.transpiler.layout import Layout
Expand Down Expand Up @@ -80,6 +81,12 @@ def run(self, dag):
for node in dag.topological_op_nodes():
qargs = [q[virtual_physical_map[qarg]] for qarg in node.qargs]
new_dag.apply_operation_back(node.op, qargs, node.cargs, check=False)

forward_map_inverse = layout.to_permutation(dag.qubits)
forward_map = _inverse_pattern(forward_map_inverse)
new_dag._final_permutation = dag._final_permutation.push_using_mapping(
forward_map, len(new_dag.qubits)
)
else:
# First build a new layout object going from:
# old virtual -> old physical -> new virtual -> new physical
Expand Down Expand Up @@ -108,6 +115,10 @@ def run(self, dag):
}
out_layout = Layout(final_layout_mapping)
self.property_set["final_layout"] = out_layout
new_dag._final_permutation = dag._final_permutation.push_using_mapping(
phys_map, len(new_dag.qubits)
)

new_dag._global_phase = dag._global_phase

return new_dag
20 changes: 20 additions & 0 deletions qiskit/transpiler/passes/layout/sabre_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,26 @@ def run(self, dag):
component.circuit_to_dag_dict,
)
disjoint_utils.combine_barriers(mapped_dag, retain_uuid=False)

# the permutation of the output qubits created by the SabreLayout pass
# we extend the permutation to be identity on the missing qubits
sabre_final_permutation = {i: i for i in range(len(mapped_dag.qubits))}
for component in components:
for initial, final in enumerate(component.final_permutation):
sabre_final_permutation[component.coupling_map.graph[initial]] = (
component.coupling_map.graph[final]
)

# a possibly partial map from the logical qubits to the physical qubits.
forward_map = {
logic: component.coupling_map.graph[phys]
for component in components
for logic, phys in component.initial_layout.layout_mapping()
if logic < len(component.dag.qubits)
}
mapped_dag._final_permutation = dag._final_permutation.push_using_mapping(
forward_map, num_target_qubits=len(mapped_dag.qubits)
).compose_with_permutation(sabre_final_permutation, front=False)
return mapped_dag

def _inner_run(self, dag, coupling_map, starting_layouts=None):
Expand Down
5 changes: 5 additions & 0 deletions qiskit/transpiler/passes/optimization/elide_permutations.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,9 @@ def _apply_mapping(qargs):
)
else:
self.property_set["virtual_permutation_layout"] = new_layout

new_dag._final_permutation = dag._final_permutation.compose_with_permutation(
qubit_mapping, front=True
)

return new_dag
6 changes: 5 additions & 1 deletion qiskit/transpiler/passes/routing/basic_swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# that they have been altered from the originals.

"""Map (with minimum effort) a DAGCircuit onto a ``coupling_map`` adding swap gates."""

from qiskit.synthesis.permutation.permutation_utils import _inverse_pattern
from qiskit.transpiler.basepasses import TransformationPass
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.dagcircuit import DAGCircuit
Expand Down Expand Up @@ -120,6 +120,10 @@ def run(self, dag):
self.property_set["final_layout"] = current_layout.compose(
self.property_set["final_layout"], dag.qubits
)
layout_permutation = _inverse_pattern(current_layout.to_permutation(new_dag.qubits))
new_dag._final_permutation = dag._final_permutation.compose_with_permutation(
layout_permutation, front=True
)

return new_dag

Expand Down
Loading
Loading