Skip to content

Commit

Permalink
Improve the ergonomics of the TranspileLayout class (Qiskit#10835)
Browse files Browse the repository at this point in the history
* Improve the ergonomics of the TranspileLayout class

This commit updates the TranspileLayout class to make it easier to work
with. The TranspileLayout class was orginally just an a container class
that took the information returned from the transpiler needed to reverse
the transpiler layout permutation (in the absence of ancillas) so that
`QuantumCircuit.from_circuit()` could compare Operator instances across
a `transpile()`. However, the internal data structures used by the
transpiler are hard to work with, and don't map well to the normal
reasoning around how the transpiler transforms the circuit. To improve
the usability of the class this commit adds 4 new methods to the class:

 - initial_layout_list() and final_layout_list() which compute a list
   form of the initial_layout and final_layout respectively
 - full_layout() which generates a list that maps the input circuits
   qubit positions in the input circuit to the final position at
   the end of the circuit (which is what most people think of when they
   hear "final layout")
 - permute_sparse_pauli_op() which takes in a SparsePauliOp object and
   permutes it based on the full layout. This is especially useful when
   combined with the Estimator primitive

To implement these functions a few extra pieces of information are
needed to fully implement them. The first is we need to know the number
of original circuit qubits. This is used to account for any ancillas
that are added in the circuit, once we have the input circuit count we
can use the original_qubit_indices attribute to discern which qubits in
the initial layout are from the original circuit and which are ancillas.
The second piece of information needed is the list of qubits in the
output circuit. as this is needed to look up the position of the virtual
qubit in the final_layout. These are both added as optional private
attributes to the TranspileLayout class and there are some small changes
to the pass manager and QPY to accomodate them.

Similarly the change in the TranspileLayout class requires a new QPY
version to include the missing details that were not being serialized
in QPY and weren't representable in the previous payload format.

Fixes Qiskit#10826
Fixes Qiskit#10818

* Handle None for the private TranspileLayout fields in QPY

* Add test for full_layout with ancillas

* Rework interface to be more consistent

This commit reworks the names of the new methods added in this API to be
more self consistent. It uses the format '*_index_layout' to replace the
`*_layout_list` name. Similarly new methods `*_virtual_layout` are added
for methods that return layout objects.

* Apply suggestions from code review

Co-authored-by: Jake Lishman <[email protected]>

* Remove permute_sparse_pauli_op()

This commit remove the TranspileLayout.permute_sparse_pauli_op() method.
This was a bit out of place on the TranspileLayout class, and it will
instead be replaced in a follow-up PR by a apply_layout() method on the
SparePauliOp class that takes in a TranspileLayout object.

* Update docs

* Fix docs build post-rebase

* Fix docs issues

---------

Co-authored-by: Jake Lishman <[email protected]>
  • Loading branch information
mtreinish and jakelishman authored Oct 3, 2023
1 parent 81e28dc commit 19854d1
Show file tree
Hide file tree
Showing 9 changed files with 688 additions and 13 deletions.
9 changes: 6 additions & 3 deletions qiskit/passmanager/passrunner.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,14 @@ def _to_passmanager_ir(self, in_program):
pass

@abstractmethod
def _to_target(self, passmanager_ir):
def _to_target(self, passmanager_ir, in_program):
"""Convert pass manager IR into output program.
Args:
passmanager_ir: Pass manager IR after optimization.
in_program: The input program, this can be used if you need
any metadata about the original input for the output. It
should not be mutated.
Returns:
Output program.
Expand Down Expand Up @@ -229,15 +232,15 @@ def run(
self.metadata = metadata

passmanager_ir = self._to_passmanager_ir(in_program)
del in_program

for controller in self.working_list:
passmanager_ir = self._run_pass_generic(
pass_sequence=controller,
passmanager_ir=passmanager_ir,
options=self.passmanager_options,
)
out_program = self._to_target(passmanager_ir)
out_program = self._to_target(passmanager_ir, in_program)
del in_program

if not isinstance(out_program, self.OUT_PROGRAM_TYPE):
raise TypeError(
Expand Down
24 changes: 23 additions & 1 deletion qiskit/qpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,9 @@
==========
Version 10 adds support for symengine-native serialization for objects of type
:class:`~.ParameterExpression` as well as symbolic expressions in Pulse schedule blocks.
:class:`~.ParameterExpression` as well as symbolic expressions in Pulse schedule blocks. Version
10 also adds support for new fields in the :class:`~.TranspileLayout` class added in the Qiskit
0.45.0 release.
The symbolic_encoding field is added to the file header, and a new encoding type char
is introduced, mapped to each symbolic library as follows: ``p`` refers to sympy
Expand All @@ -171,6 +173,26 @@
char symbolic_encoding;
}
LAYOUT
------
The ``LAYOUT`` struct is updated to have an additional ``input_qubit_count`` field.
With version 10 the ``LAYOUT`` struct is now:
.. code-block:: c
struct {
char exists;
int32_t initial_layout_size;
int32_t input_mapping_size;
int32_t final_layout_size;
uint32_t extra_registers;
int32_t input_qubit_count;
}
The rest of the layout data after the ``LAYOUT`` struct is represented as in previous versions. If
``input qubit_count`` is < 0 that indicates that both ``_input_qubit_count``
and ``_output_qubit_list`` in the :class:`~.TranspileLayout` object are ``None``.
.. _qpy_version_9:
Expand Down
29 changes: 26 additions & 3 deletions qiskit/qpy/binary_io/circuits.py
Original file line number Diff line number Diff line change
Expand Up @@ -831,7 +831,7 @@ def _write_registers(file_obj, in_circ_regs, full_bits):
def _write_layout(file_obj, circuit):
if circuit.layout is None:
# Write a null header if there is no layout present
file_obj.write(struct.pack(formats.LAYOUT_PACK, False, -1, -1, -1, 0))
file_obj.write(struct.pack(formats.LAYOUT_V2_PACK, False, -1, -1, -1, 0, 0))
return
initial_size = -1
input_qubit_mapping = {}
Expand Down Expand Up @@ -874,14 +874,18 @@ def _write_layout(file_obj, circuit):
virtual_bit = final_layout_physical[i]
final_layout_array.append(circuit.find_bit(virtual_bit).index)

input_qubit_count = circuit._layout._input_qubit_count
if input_qubit_count is None:
input_qubit_count = -1
file_obj.write(
struct.pack(
formats.LAYOUT_PACK,
formats.LAYOUT_V2_PACK,
True,
initial_size,
input_qubit_size,
final_layout_size,
len(extra_registers),
input_qubit_count,
)
)
_write_registers(
Expand Down Expand Up @@ -910,6 +914,10 @@ def _read_layout(file_obj, circuit):
)
if not header.exists:
return
_read_common_layout(file_obj, header, circuit)


def _read_common_layout(file_obj, header, circuit):
registers = {
name: QuantumRegister(len(v[1]), name)
for name, v in _read_registers_v4(file_obj, header.extra_registers)["q"].items()
Expand Down Expand Up @@ -958,6 +966,18 @@ def _read_layout(file_obj, circuit):
circuit._layout = TranspileLayout(initial_layout, input_qubit_mapping, final_layout)


def _read_layout_v2(file_obj, circuit):
header = formats.LAYOUT_V2._make(
struct.unpack(formats.LAYOUT_V2_PACK, file_obj.read(formats.LAYOUT_V2_SIZE))
)
if not header.exists:
return
_read_common_layout(file_obj, header, circuit)
if header.input_qubit_count >= 0:
circuit._layout._input_qubit_count = header.input_qubit_count
circuit._layout._output_qubit_list = circuit.qubits


def write_circuit(file_obj, circuit, metadata_serializer=None, use_symengine=False):
"""Write a single QuantumCircuit object in the file like object.
Expand Down Expand Up @@ -1161,5 +1181,8 @@ def read_circuit(file_obj, version, metadata_deserializer=None, use_symengine=Fa
UserWarning,
)
if version >= 8:
_read_layout(file_obj, circ)
if version >= 10:
_read_layout_v2(file_obj, circ)
else:
_read_layout(file_obj, circ)
return circ
15 changes: 15 additions & 0 deletions qiskit/qpy/formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,21 @@
MAP_ITEM_PACK = "!H1cH"
MAP_ITEM_SIZE = struct.calcsize(MAP_ITEM_PACK)

LAYOUT_V2 = namedtuple(
"LAYOUT",
[
"exists",
"initial_layout_size",
"input_mapping_size",
"final_layout_size",
"extra_registers",
"input_qubit_count",
],
)
LAYOUT_V2_PACK = "!?iiiIi"
LAYOUT_V2_SIZE = struct.calcsize(LAYOUT_V2_PACK)


LAYOUT = namedtuple(
"LAYOUT",
["exists", "initial_layout_size", "input_mapping_size", "final_layout_size", "extra_registers"],
Expand Down
2 changes: 2 additions & 0 deletions qiskit/transpiler/basepasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ def __call__(self, circuit, property_set=None):
initial_layout=self.property_set["layout"],
input_qubit_mapping=self.property_set["original_qubit_indices"],
final_layout=self.property_set["final_layout"],
_input_qubit_count=len(circuit.qubits),
_output_qubit_list=result_circuit.qubits,
)
if self.property_set["clbit_write_latency"] is not None:
result_circuit._clbit_write_latency = self.property_set["clbit_write_latency"]
Expand Down
Loading

0 comments on commit 19854d1

Please sign in to comment.