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

[QHC-833] Improving digital Transpilation #862

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a0f70db
Improving `optimization` word use in the transpilation
GuillermoAbadLopez Dec 23, 2024
0288e5a
Refactor/improve digital Transpiler
GuillermoAbadLopez Dec 28, 2024
5313770
Merge branch 'main' into solving-transpilation
GuillermoAbadLopez Dec 28, 2024
354f58e
make optimize default to False
GuillermoAbadLopez Dec 28, 2024
ab6b082
Correcting docstrings and adding transpilation parameters as kwargs i…
GuillermoAbadLopez Dec 28, 2024
48a3e9e
make transpilation_config shorter
GuillermoAbadLopez Dec 28, 2024
7b8dd49
Make empty dict default
GuillermoAbadLopez Dec 28, 2024
90bb445
Update docstrings
GuillermoAbadLopez Dec 28, 2024
217740a
Solving docstring problems
GuillermoAbadLopez Dec 28, 2024
8f14eae
Solving problems documentation
GuillermoAbadLopez Dec 28, 2024
c3703d6
Improving documentation
GuillermoAbadLopez Dec 28, 2024
8989484
improving documentation
GuillermoAbadLopez Dec 28, 2024
b6770ec
Update test_circuit_transpiler.py
GuillermoAbadLopez Dec 28, 2024
7506b2d
Improve documentation transpiler
GuillermoAbadLopez Dec 28, 2024
27e4874
Improving documentation transpiler
GuillermoAbadLopez Dec 28, 2024
def668f
Solving documentation transpilation
GuillermoAbadLopez Dec 28, 2024
cc56d71
First try of Transpilation summary
GuillermoAbadLopez Dec 28, 2024
72c9c5d
Update circuit_transpiler.py
GuillermoAbadLopez Dec 30, 2024
c22ac09
Update test_circuit_transpiler.py
GuillermoAbadLopez Dec 30, 2024
7d0f697
Improving documentation for transpilation
GuillermoAbadLopez Dec 30, 2024
0764d9b
Improving linking between transpilaiton documentation
GuillermoAbadLopez Dec 30, 2024
44cc7ec
Merge branch 'main' into solving-transpilation
GuillermoAbadLopez Dec 30, 2024
51fab0c
Update platform.py
GuillermoAbadLopez Dec 30, 2024
872c2fe
Improve documentation further
GuillermoAbadLopez Dec 30, 2024
9c17cbd
Adding comments for development
GuillermoAbadLopez Dec 30, 2024
bddc5a8
Update circuit_transpiler.py
GuillermoAbadLopez Dec 31, 2024
7fb7339
Update test_platform.py
GuillermoAbadLopez Dec 31, 2024
8b23588
Update circuit_transpiler.py
GuillermoAbadLopez Jan 8, 2025
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
101 changes: 101 additions & 0 deletions docs/fundamentals/transpilation.rst
Original file line number Diff line number Diff line change
@@ -1,2 +1,103 @@
.. _transpilation:

Transpilation
=============

GuillermoAbadLopez marked this conversation as resolved.
Show resolved Hide resolved
The transpilation is done automatically, for :meth:`ql.execute()` and :meth:`platform.execute()` methods.

But it can also be done manually, with more control, directly using the :class:`.CircuitTranspiler` class.

The process involves the following steps:

1. \*)Routing and Placement: Routes and places the circuit's logical qubits onto the chip's physical qubits. The final qubit layout is returned and logged. This step uses the ``placer``, ``router``, and ``routing_iterations`` parameters from ``transpile_config`` if provided; otherwise, default values are applied. Refer to the :meth:`.CircuitTranspiler.route_circuit()` method for more information.

2. \*\*)Canceling adjacent pairs of Hermitian gates (H, X, Y, Z, CNOT, CZ, and SWAPs). Refer to the :meth:`.CircuitTranspiler.optimize_gates()` method for more information.

3. Native Gate Translation: Translates the circuit into the chip's native gate set (CZ, RZ, Drag, Wait, and M (Measurement)). Refer to the :meth:`.CircuitTranspiler.gates_to_native()` method for more information.

4. Adding phases to our Drag gates, due to commuting RZ gates until the end of the circuit to discard them as virtual Z gates, and due to the phase corrections from CZ. Refer to the :meth:`.CircuitTranspiler.add_phases_from_RZs_and_CZs_to_drags()` method for more information.

5. \*\*)Optimizing the resulting Drag gates, by combining multiple pulses into a single one. Refer to the :meth:`.CircuitTranspiler.optimize_transpiled_gates()` method for more information.

6. Pulse Schedule Conversion: Converts the native gates into a pulse schedule using calibrated settings from the runcard. Refer to the :meth:`.CircuitTranspiler.gates_to_pulses()` method for more information.

.. note::

\*) If ``routing=False`` in ``transpile_config`` (default behavior), step 1. is skipped.

\*\*) If ``optimize=False`` in ``transpile_config`` (default behavior), steps 2. and 5. are skipped.

The rest of steps are always done.

**Examples:**

For example, the most basic use, would be to automatically transpile during an execute, like:

.. code-block:: python

from qibo import gates, Circuit
from qibo.transpiler import ReverseTraversal, Sabre
import qililab as ql

# Create circuit:
c = Circuit(5)
c.add(gates.CNOT(1, 0))

# Create transpilation config:
transpilation = {routing: True, optimize: False, router: Sabre, placer: ReverseTraversal}

# Create transpiler:
result = ql.execute(c, runcard="<path_to_runcard>", transpile_config=transpilation)

Or from a ``platform.execute()`` instead, like:

.. code-block:: python

from qibo import gates, Circuit
from qibo.transpiler import ReverseTraversal, Sabre
from qililab import build_platform

# Create circuit:
c = Circuit(5)
c.add(gates.CNOT(1, 0))

# Create platform:
platform = build_platform(runcard="<path_to_runcard>")
transpilation = {routing: True, optimize: False, router: Sabre, placer: ReverseTraversal}

# Create transpiler:
result = platform.execute(c, num_avg=1000, repetition_duration=200_000, transpile_config=transpilation)

Now, if we want more manual control instead, we can instantiate the ``CircuitTranspiler`` object like:

.. code-block:: python

from qibo import gates
from qibo.models import Circuit
from qibo.transpiler.placer import ReverseTraversal, Trivial
from qibo.transpiler.router import Sabre
from qililab import build_platform
from qililab.circuit_transpiler import CircuitTranspiler

# Create circuit:
c = Circuit(5)
c.add(gates.CNOT(1, 0))

# Create platform:
platform = build_platform(runcard="<path_to_runcard>")

# Create transpiler:
transpiler = CircuitTranspiler(platform.digital_compilation_settings)

And now, transpile manually, like in the following examples:

.. code-block:: python

# Default Transpilation (with ReverseTraversal, Sabre, platform's connectivity and optimize = True):
transpiled_circuit, final_layouts = transpiler.transpile_circuit(c)

# Or another case, not doing optimization for some reason, and with Non-Default placer:
transpiled_circuit, final_layout = transpiler.transpile_circuit(c, placer=Trivial, optimize=False)

# Or also specifying the `router` with kwargs:
transpiled_circuit, final_layouts = transpiler.transpile_circuit(c, router=(Sabre, {"lookahead": 2}))
107 changes: 78 additions & 29 deletions src/qililab/digital/circuit_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

from copy import deepcopy

from qibo import Circuit, gates
from qibo import gates

from qililab import digital
from qililab.settings.digital.digital_compilation_settings import DigitalCompilationSettings
Expand All @@ -37,31 +37,35 @@ def __init__(self, settings: DigitalCompilationSettings):
"""Object containing the digital compilations settings and the info on chip's physical qubits."""

@classmethod
def run_gate_cancellations(cls, circuit: Circuit) -> Circuit:
"""Main method to run the gate cancellations. Currently only consists of cancelling pairs of hermitian gates.
def optimize_gates(cls, gate_list: list[gates.Gate]) -> list[gates.Gate]:
# Docstring related to the public method: :meth:`.CircuitTranspiler.optimize_gates()`. Change it there too.
"""Main method to run the gate optimizations. Currently only consists of cancelling pairs of hermitian gates.

Can/Might be extended in the future to include more complex gate cancellations.
Can/Might be extended in the future to include more complex gate optimization.

Args:
circuit (Circuit): circuit to optimize.
gate_list (list[gates.Gate]): list of gates of the Qibo circuit to cancel gates.

Returns:
Circuit: optimized circuit.
list[gates.Gate]: list of the gates of the Qibo circuit, optimized.
"""
return cls.cancel_pairs_of_hermitian_gates(circuit)
# Add more future methods of optimizing gates here, like 2local optimizations, cliffords ...:
gate_list = cls.cancel_pairs_of_hermitian_gates(gate_list)
# gate_list = cls.2local_gate_optimization(gate_list) TODO:
return gate_list

@classmethod
def cancel_pairs_of_hermitian_gates(cls, circuit: Circuit) -> Circuit:
def cancel_pairs_of_hermitian_gates(cls, gate_list: list[gates.Gate]) -> list[gates.Gate]:
"""Optimizes circuit by cancelling adjacent hermitian gates.

Args:
circuit (Circuit): circuit to optimize.
gate_list (list[gates.Gate]): list of gates of the Qibo circuit to cancel pairs of hermitians.

Returns:
Circuit: optimized circuit.
list[gates.Gate]: list of the gates of the Qibo circuit, optimized.
"""
# Initial and final circuit gates lists, from which to, one by one, after checks, pass non-cancelled gates:
circ_list: list[tuple] = cls._get_circuit_gates(circuit)
circ_list: list[tuple] = cls._get_circuit_gates(gate_list)

# We want to do the sweep circuit cancelling gates least once always:
previous_circ_list = deepcopy(circ_list)
Expand All @@ -73,10 +77,14 @@ def cancel_pairs_of_hermitian_gates(cls, circuit: Circuit) -> Circuit:
output_circ_list = cls._sweep_circuit_cancelling_pairs_of_hermitian_gates(output_circ_list)

# Create optimized circuit, from the obtained non-cancelled list:
return cls._create_circuit(output_circ_list, circuit.nqubits)
return cls._create_circuit_gate_list(output_circ_list)

def optimize_transpilation(self, circuit: Circuit) -> list[gates.Gate]:
"""Optimizes transpiled circuit by applying virtual Z gates.
def add_phases_from_RZs_and_CZs_to_drags(self, gate_list: list[gates.Gate], nqubits: int) -> list[gates.Gate]:
# Docstring related to the public method: :meth:`.CircuitTranspiler.add_phases_from_RZs_and_CZs_to_drags()`. Change it there too.
"""This method adds the phases from RZs and CZs gates of the circuit to the next Drag gates.

- The CZs added phases on the Drags, come from a correction from their calibration, stored on the setting of the CZs.
- The RZs added phases on the Drags, come from commuting all the RZs all the way to the end of the circuit, so they can be deleted as "virtual Z gates".

This is done by moving all RZ to the left of all operators as a single RZ. The corresponding cumulative rotation
from each RZ is carried on as phase in all drag pulses left of the RZ operator.
Expand All @@ -97,18 +105,16 @@ def optimize_transpilation(self, circuit: Circuit) -> list[gates.Gate]:
For more information on virtual Z gates, see https://arxiv.org/abs/1612.00858

Args:
circuit (Circuit): circuit with native gates, to optimize.
gate_list (list[gates.Gate]): list of native gates of the circuit, to pass phases to the Drag gates.
nqubits (int): Number of qubits of the circuit.

Returns:
list[gates.Gate] : list of re-ordered gates
list[gates.Gate]: list of re-ordered gates
"""
nqubits: int = circuit.nqubits
ngates: list[gates.Gate] = circuit.queue

supported_gates = ["rz", "drag", "cz", "wait", "measure"]
new_gates = []
shift = dict.fromkeys(range(nqubits), 0)
for gate in ngates:
for gate in gate_list:
if gate.name not in supported_gates:
raise NotImplementedError(f"{gate.name} not part of native supported gates {supported_gates}")
if isinstance(gate, gates.RZ):
Expand Down Expand Up @@ -143,17 +149,60 @@ def optimize_transpilation(self, circuit: Circuit) -> list[gates.Gate]:

return new_gates

@classmethod
def optimize_transpiled_gates(cls, gate_list: list[gates.Gate]) -> list[gates.Gate]:
# Docstring related to the public method: :meth:`.CircuitTranspiler.optimize_transpiled_gates()`. Change it there too.
"""Bunches consecutive Drag gates together into a single one.

Args:
gate_list (list[gates.Gate]): list of gates of the transpiled circuit, to optimize.

Returns:
list[gates.Gate]: list of gates of the transpiled circuit, optimized.
"""
# Add more optimizations of the transpiled circuit here:
gate_list = cls.bunch_drag_gates(gate_list)
gate_list = cls.delete_gates_with_no_amplitude(gate_list)
return gate_list

@staticmethod
def bunch_drag_gates(gate_list: list[gates.Gate]) -> list[gates.Gate]:
"""Bunches consecutive Drag gates together into a single one.

Args:
gate_list (list[gates.Gate]): list of gates of the transpiled circuit, to bunch drag gates.

Returns:
list[gates.Gate]: list of gates of the transpiled circuit, with drag gates bunched."""

# Add bunching of Drag gates here:
# gate_list =
return gate_list

@staticmethod
def delete_gates_with_no_amplitude(gate_list: list[gates.Gate]) -> list[gates.Gate]:
"""Bunches consecutive Drag gates together into a single one.

Args:
gate_list (list[gates.Gate]): list of gates of the transpiled circuit, to delete gates without amplitude.

Returns:
list[gates.Gate]: list of gates of the transpiled circuit, with gates without amplitude deleted."""
# Add deletion of Drag gates without amplitude here:
# gate_list =
return gate_list

@staticmethod
def _get_circuit_gates(circuit: Circuit) -> list[tuple]:
def _get_circuit_gates(gate_list: list[gates.Gate]) -> list[tuple]:
"""Get the gates of the circuit.

Args:
circuit (qibo.models.Circuit): Circuit to get the gates from.
gate_list (list[gates.Gate]): list of native gates of the Qibo circuit.

Returns:
list[tuple]: List of gates in the circuit. Where each gate is a tuple of ('name', 'init_args', 'initi_kwargs').
list[tuple]: List of gates in the circuit. Where each gate is a tuple of ('name', 'init_args', 'init_kwargs').
"""
return [(type(gate).__name__, gate.init_args, gate.init_kwargs) for gate in circuit.queue]
return [(type(gate).__name__, gate.init_args, gate.init_kwargs) for gate in gate_list]

@staticmethod
def _create_gate(gate_class: str, gate_args: list | int, gate_kwargs: dict) -> gates.Gate:
Expand All @@ -177,23 +226,23 @@ def _create_gate(gate_class: str, gate_args: list | int, gate_kwargs: dict) -> g
)

@classmethod
def _create_circuit(cls, gates_list: list[tuple], nqubits: int) -> Circuit:
def _create_circuit_gate_list(cls, gates_list: list[tuple]) -> list[gates.Gate]:
"""Converts a list of gates (name, qubits) into a qibo Circuit object.

Args:
gates_list (list[tuple]): List of gates in the circuit. Where each gate is a tuple of ('name', 'init_args', 'initi_kwargs')
nqubits (int): Number of qubits in the circuit.

Returns:
Circuit: The qibo Circuit object.
list[gates.Gate]: Gate list of the qibo Circuit.
"""
# Create optimized circuit, from the obtained non-cancelled list:
output_circuit = Circuit(nqubits)
output_gates_list = []
for gate, gate_args, gate_kwargs in gates_list:
qibo_gate = cls._create_gate(gate, gate_args, gate_kwargs)
output_circuit.add(qibo_gate)
output_gates_list.append(qibo_gate)

return output_circuit
return output_gates_list

@classmethod
def _sweep_circuit_cancelling_pairs_of_hermitian_gates(cls, circ_list: list[tuple]) -> list[tuple]:
Expand Down
1 change: 1 addition & 0 deletions src/qililab/digital/circuit_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def __init__(
# 3) Layout stage, where the initial_layout will be created.

def route(self, circuit: Circuit, iterations: int = 10) -> tuple[Circuit, dict[str, int]]:
# Docstring related to the public method: :meth:`.CircuitTranspiler.route_circuit()`. Change it there too.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this comment needed here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, good point... I actually have a problem here, which I wanted to ask about:

If you see, Inside the Transpiler we have basically methods duplicated from the Optimizer..., since we removed the handling of multiple circuits at the same time.. One option would be to remove such duplication, but at the same time that duplications serves as a kind of interface to add more functionality, and shows the user which functions can be called from the transpiler directly, so its not super bad...

Of course this makes that the docstrings are duplicated, and that is more problematic, since we wouldn't; want our devs, only updating the Optimizer docstrings, but not the Transpiler's which are more public. So the options we have are:

  • Delete the duplication in some way (I think this will complicate how we show public method to the user...)
  • Add this silly comment for devs to notice, that they need to change the public documentation always
  • Maybe just delete the docstring from the not public methods? So there is no mistake by the devs? And they know to go and search elsewhere, which docstring to change 🤔...

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'll think more about it the next days, and ask the group with a clearer head...

But such problem make me notice there is a structural code problem from base with having this duplication, which maybe should be addressed more deeply... But 😬 ...

"""Routes the virtual/logical qubits of a circuit to the physical qubits of a chip. Returns and logs the final qubit layout.

**Examples:**
Expand Down
8 changes: 4 additions & 4 deletions src/qililab/digital/circuit_to_pulses.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

import numpy as np
from qibo import gates
from qibo.models import Circuit

from qililab.constants import RUNCARD
from qililab.pulse.pulse import Pulse
Expand All @@ -44,7 +43,8 @@ def __init__(self, settings: DigitalCompilationSettings):
self.settings: DigitalCompilationSettings = settings
"""Object containing the digital compilations settings and the info on chip's physical qubits."""

def run(self, circuit: Circuit) -> PulseSchedule:
def run(self, gate_list: list[gates.Gate]) -> PulseSchedule:
# Docstring related to the public method: :meth:`.CircuitTranspiler.gates_to_pulses()`. Change it there too.
GuillermoAbadLopez marked this conversation as resolved.
Show resolved Hide resolved
"""Translates a circuit into a pulse sequences.

For each circuit gate we look up for its corresponding gates settings in the runcard (the name of the class of the circuit
Expand All @@ -63,15 +63,15 @@ def run(self, circuit: Circuit) -> PulseSchedule:
time is 4 and a pulse applied to qubit k lasts 17ns, the next pulse at qubit k will be at t=20ns

Args:
circuits (List[Circuit]): List of Qibo Circuit classes.
gate_list (list[gates.Gate]): list of native gates of the qibo circuit.

Returns:
list[PulseSequences]: List of :class:`PulseSequences` classes.
"""

pulse_schedule: PulseSchedule = PulseSchedule()
time: dict[int, int] = {} # init/restart time
for gate in circuit.queue:
for gate in gate_list:
# handle wait gates
if isinstance(gate, Wait):
self._update_time(time=time, qubit=gate.qubits[0], gate_time=gate.parameters[0])
Expand Down
Loading
Loading