Skip to content

Commit

Permalink
Add LightSABRE to default_compilation_pass (#400)
Browse files Browse the repository at this point in the history
* add lightsabre custom pass

* reformat, add descriptions

* Update qiskit_convert.py

* Update ibm_utils.py

* Update ibm_utils.py

* Update ibm.py

* redirect CX gates

* Update aer.py

* Update aer.py

* move Directed rewrite into `gen_lightsabre_transformation`

* Update ibm_utils.py

* Update ibm_utils.py

* Update ibm_utils.py

* Update ibm_utils.py

* rebase to CX gates to redirect ECR gates

* rebase to CX for noise model routing

* Update pytket-docs-theming

* Update aer.py

* Update ibm.py

* Update ibmq_emulator.py

* Update ibm_utils.py

* fix imports, reformat

* Update ibm_utils.py

* Update aer.py

* Update changelog.md

* Update ibm_utils.py

* Update ibm_utils.py

* Update ibm_utils.py

* Update backend_test.py

* Update backend_test.py

* reformat, fix mypy

* reformat

* fix mypy issues

* fix mypy

* Update index.md

* Update ibm_utils.py

* Update backend_test.py

* Update backend_test.py

* Update backend_test.py
  • Loading branch information
sjdilkes authored Nov 8, 2024
1 parent ffbf31a commit 1e8bd80
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 109 deletions.
4 changes: 3 additions & 1 deletion docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

## Unreleased

- Fix handling of non-default registers when selecting bits in results.
- Fix handling of non-default registers when selecting bits in results.
- Update default compilation to use `Qiskit` `SabreLayoutPassManager` as a `CustomPass`.
- Fix handling of non-default registers when selecting bits in results.
- The {py:func}`tk_to_qiskit` converter gives a warning if the input {py:class}`~pytket.circuit.Circuit` contains [implicit qubit permutations](https://docs.quantinuum.com/tket/user-guide/manual/manual_circuit.html#implicit-qubit-permutations).

## 0.58.0 (October 2024)
Expand Down
11 changes: 4 additions & 7 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,12 +205,9 @@ Every {py:class}`~pytket.backends.backend.Backend` in pytket has its own {py:met
* - [AutoRebase [2]](inv:#*.AutoRebase)
- [SynthesiseTket](inv:#*.SynthesiseTket)
- [FullPeepholeOptimise](inv:#*.passes.FullPeepholeOptimise)
* - [CXMappingPass [3]](inv:#*.passes.CXMappingPass)
- [CXMappingPass [3]](inv:#*.passes.CXMappingPass)
- [CXMappingPass [3]](inv:#*.passes.CXMappingPass)
* - [NaivePlacementPass](inv:#*.passes.NaivePlacementPass)
- [NaivePlacementPass](inv:#*.passes.NaivePlacementPass)
- [NaivePlacementPass](inv:#*.passes.NaivePlacementPass)
* - LightSabre [3]
- LightSabre [3]
- LightSabre [3]
* - [AutoRebase [2]](inv:#*.AutoRebase)
- [SynthesiseTket](inv:#*.SynthesiseTket)
- [KAKDecomposition(allow_swaps=False)](inv:#*.passes.KAKDecomposition)
Expand All @@ -231,7 +228,7 @@ Every {py:class}`~pytket.backends.backend.Backend` in pytket has its own {py:met

- \[1\] If no value is specified then `optimisation_level` defaults to a value of 2.
- \[2\] {py:class}`~pytket._tket.passes.AutoRebase` is a conversion to the gateset supported by the backend. For IBM quantum devices and emulators the supported gate set is either $\{X, SX, Rz, CX\}$, $\{X, SX, Rz, ECR\}$, or $\{X, SX, Rz, CZ\}$. The more idealised Aer simulators have a much broader range of supported gates.
- \[3\] Here [CXMappingPass](inv:#*.passes.CXMappingPass) maps program qubits to the architecture using a [NoiseAwarePlacement](inv:#*.NoiseAwarePlacement)
- \[3\] This is imported from qiskit and corresponds to the method in "LightSABRE: A Lightweight and Enhanced SABRE Algorithm", Henry Zou, Matthew Treinish, Kevin Hartman, Alexander Ivrii, Jake Lishman, arXiv:2409.08368.

**Note:** The {py:meth}`~AerBackend.default_compilation_pass` for {py:class}`AerBackend` is the same as above.

Expand Down
2 changes: 1 addition & 1 deletion docs/pytket-docs-theming
43 changes: 12 additions & 31 deletions pytket/extensions/qiskit/backends/aer.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,14 @@
AutoRebase,
BasePass,
CliffordSimp,
CXMappingPass,
CustomPass,
DecomposeBoxes,
FullPeepholeOptimise,
NaivePlacementPass,
SequencePass,
SynthesiseTket,
)
from pytket.pauli import Pauli, QubitPauliString
from pytket.placement import NoiseAwarePlacement
from pytket.predicates import (
ConnectivityPredicate,
DefaultRegisterPredicate,
Expand Down Expand Up @@ -73,7 +72,7 @@
CrosstalkParams,
NoisyCircuitBuilder,
)
from .ibm_utils import _STATUS_MAP, _batch_circuits
from .ibm_utils import _STATUS_MAP, _batch_circuits, _gen_lightsabre_transformation

if TYPE_CHECKING:
from qiskit_aer import AerJob
Expand Down Expand Up @@ -164,32 +163,11 @@ def _arch_dependent_default_compilation_pass(
self,
arch: Architecture,
optimisation_level: int = 2,
placement_options: Optional[dict[str, Any]] = None,
) -> BasePass:
assert optimisation_level in range(3)
if placement_options is not None:
noise_aware_placement = NoiseAwarePlacement(
arch,
self._backend_info.averaged_node_gate_errors, # type: ignore
self._backend_info.averaged_edge_gate_errors, # type: ignore
self._backend_info.averaged_readout_errors, # type: ignore
**placement_options,
)
else:
noise_aware_placement = NoiseAwarePlacement(
arch,
self._backend_info.averaged_node_gate_errors, # type: ignore
self._backend_info.averaged_edge_gate_errors, # type: ignore
self._backend_info.averaged_readout_errors, # type: ignore
)

arch_specific_passes = [
CXMappingPass(
arch,
noise_aware_placement,
directed_cx=True,
delay_measures=False,
),
AutoRebase({OpType.CX, OpType.TK1}),
CustomPass(_gen_lightsabre_transformation(arch, optimisation_level)),
NaivePlacementPass(arch),
]
if optimisation_level == 0:
Expand All @@ -199,7 +177,8 @@ def _arch_dependent_default_compilation_pass(
self.rebase_pass(),
*arch_specific_passes,
self.rebase_pass(),
]
],
False,
)
if optimisation_level == 1:
return SequencePass(
Expand All @@ -208,7 +187,8 @@ def _arch_dependent_default_compilation_pass(
SynthesiseTket(),
*arch_specific_passes,
SynthesiseTket(),
]
],
False,
)
return SequencePass(
[
Expand All @@ -217,7 +197,8 @@ def _arch_dependent_default_compilation_pass(
*arch_specific_passes,
CliffordSimp(False),
SynthesiseTket(),
]
],
False,
)

def _arch_independent_default_compilation_pass(
Expand All @@ -233,7 +214,6 @@ def _arch_independent_default_compilation_pass(
def default_compilation_pass(
self,
optimisation_level: int = 2,
placement_options: Optional[dict[str, Any]] = None,
) -> BasePass:
"""
See documentation for :py:meth:`IBMQBackend.default_compilation_pass`.
Expand All @@ -245,7 +225,8 @@ def default_compilation_pass(
and self._backend_info.get_misc("characterisation")
):
return self._arch_dependent_default_compilation_pass(
arch, optimisation_level, placement_options=placement_options # type: ignore
arch, # type: ignore
optimisation_level,
)

return self._arch_independent_default_compilation_pass(optimisation_level)
Expand Down
47 changes: 9 additions & 38 deletions pytket/extensions/qiskit/backends/ibm.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
AutoRebase,
BasePass,
CliffordSimp,
CXMappingPass,
CustomPass,
DecomposeBoxes,
FullPeepholeOptimise,
KAKDecomposition,
Expand All @@ -61,7 +61,6 @@
SimplifyInitial,
SynthesiseTket,
)
from pytket.placement import NoiseAwarePlacement
from pytket.predicates import (
DirectednessPredicate,
GateSetPredicate,
Expand Down Expand Up @@ -94,7 +93,7 @@
tk_to_qiskit,
)
from .config import QiskitConfig
from .ibm_utils import _STATUS_MAP, _batch_circuits
from .ibm_utils import _STATUS_MAP, _batch_circuits, _gen_lightsabre_transformation

if TYPE_CHECKING:
from qiskit_ibm_runtime.ibm_backend import IBMBackend # type: ignore
Expand Down Expand Up @@ -345,7 +344,6 @@ def required_predicates(self) -> list[Predicate]:
def default_compilation_pass(
self,
optimisation_level: int = 2,
placement_options: Optional[dict[str, Any]] = None,
) -> BasePass:
"""
A suggested compilation pass that will will, if possible, produce an equivalent
Expand All @@ -359,8 +357,6 @@ def default_compilation_pass(
is tailored to the backend's requirements.
The default compilation passes for the :py:class:`IBMQBackend` and the
Aer simulators support an optional ``placement_options`` dictionary containing
arguments to override the default settings in :py:class:`NoiseAwarePlacement`.
:param optimisation_level: The level of optimisation to perform during
compilation.
Expand All @@ -372,22 +368,19 @@ def default_compilation_pass(
that should give the best results from execution.
:param placement_options: Optional argument allowing the user to override
the default settings in :py:class:`NoiseAwarePlacement`.
:return: Compilation pass guaranteeing required predicates.
"""
config: PulseBackendConfiguration = self._backend.configuration()
props: Optional[BackendProperties] = self._backend.properties()
return IBMQBackend.default_compilation_pass_offline(
config, props, optimisation_level, placement_options
config, props, optimisation_level
)

@staticmethod
def default_compilation_pass_offline(
config: PulseBackendConfiguration,
props: Optional[BackendProperties],
optimisation_level: int = 2,
placement_options: Optional[dict[str, Any]] = None,
) -> BasePass:
backend_info = IBMQBackend._get_backend_info(config, props)
primitive_gates = _get_primitive_gates(_tk_gate_set(config))
Expand All @@ -409,33 +402,12 @@ def default_compilation_pass_offline(
passlist.append(SynthesiseTket())
elif optimisation_level == 2:
passlist.append(FullPeepholeOptimise())
mid_measure = backend_info.supports_midcircuit_measurement
arch = backend_info.architecture
assert arch is not None
if not isinstance(arch, FullyConnected):
if placement_options is not None:
noise_aware_placement = NoiseAwarePlacement(
arch,
backend_info.averaged_node_gate_errors, # type: ignore
backend_info.averaged_edge_gate_errors, # type: ignore
backend_info.averaged_readout_errors, # type: ignore
**placement_options,
)
else:
noise_aware_placement = NoiseAwarePlacement(
arch,
backend_info.averaged_node_gate_errors, # type: ignore
backend_info.averaged_edge_gate_errors, # type: ignore
backend_info.averaged_readout_errors, # type: ignore
)

passlist.append(AutoRebase(primitive_gates))
passlist.append(
CXMappingPass(
arch,
noise_aware_placement,
directed_cx=True,
delay_measures=(not mid_measure),
)
CustomPass(_gen_lightsabre_transformation(arch, optimisation_level))
)
passlist.append(NaivePlacementPass(arch))
if optimisation_level == 1:
Expand All @@ -449,11 +421,10 @@ def default_compilation_pass_offline(
]
)

if supports_rz:
passlist.extend(
[IBMQBackend.rebase_pass_offline(primitive_gates), RemoveRedundancies()]
)
return SequencePass(passlist)
passlist.extend(
[IBMQBackend.rebase_pass_offline(primitive_gates), RemoveRedundancies()]
)
return SequencePass(passlist, False)

@property
def _result_id_type(self) -> _ResultIdTuple:
Expand Down
95 changes: 90 additions & 5 deletions pytket/extensions/qiskit/backends/ibm_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,27 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""Shared utility methods for ibm backends.
"""
"""Shared utility methods for ibm backends."""

import itertools
from collections.abc import Collection, Sequence
from typing import TYPE_CHECKING, Optional
from typing import Callable, Optional

import numpy as np

from pytket.architecture import Architecture
from pytket.backends.status import StatusEnum
from pytket.circuit import Circuit, Node
from pytket.passes import RebaseTket
from pytket.transform import Transform
from qiskit.passmanager.flow_controllers import ConditionalController # type: ignore
from qiskit.providers import JobStatus # type: ignore
from qiskit.transpiler import CouplingMap, PassManager # type: ignore
from qiskit.transpiler.passes import SabreLayout, SetLayout # type: ignore
from qiskit.transpiler.passmanager_config import PassManagerConfig # type: ignore
from qiskit.transpiler.preset_passmanagers import common # type: ignore

if TYPE_CHECKING:
from pytket.circuit import Circuit
from ..qiskit_convert import qiskit_to_tk, tk_to_qiskit

_STATUS_MAP = {
JobStatus.CANCELLED: StatusEnum.CANCELLED,
Expand Down Expand Up @@ -70,3 +77,81 @@ def _batch_circuits(
for n, indices in itertools.groupby(order, key=lambda i: n_shots[i])
]
return batches, batch_order


def _architecture_to_couplingmap(architecture: Architecture) -> CouplingMap:
"""
Converts a pytket Architecture object to a Qiskit CouplingMap object.
:param architecture: Architecture to be converted
:return: A Qiskit CouplingMap object corresponding to the same connectivity
"""
# we can make some assumptions from how the Architecture object is
# originally constructed from the Qiskit CouplingMap:
# 1) All nodes are single indexed
# 2) All nodes are default register
# 3) Node with index "i" corresponds to integer "i" in the original coupling map
# We confirm assumption 1) and 2) while producing the coupling map
coupling_map: list[tuple[int, int]] = []
for edge in architecture.coupling:
assert len(edge[0].index) == 1
assert len(edge[1].index) == 1
assert edge[0].reg_name == "node"
assert edge[1].reg_name == "node"
coupling_map.append((edge[0].index[0], edge[1].index[0]))
return CouplingMap(coupling_map)


def _gen_lightsabre_transformation( # type: ignore
architecture: Architecture, optimization_level: int = 2, seed=0, attempts=20
) -> Callable[[Circuit], Circuit]:
"""
Generates a function that can be passed to CustomPass for running
LightSABRE routing.
:param architecture: Architecture LightSABRE routes circuits to match
:param optimization_level: Corresponds to qiskit optmization levels
:param seed: LightSABRE routing is stochastic, with this parameter setting the seed
:param attempts: Number of generated random solutions to pick from.
:return: A function that accepts a pytket Circuit and returns a new Circuit that
has been routed to the architecture using LightSABRE
"""
config: PassManagerConfig = PassManagerConfig(
coupling_map=_architecture_to_couplingmap(architecture),
routing_method="sabre",
seed_transpiler=seed,
)
sabre_pass: PassManager = PassManager(
[
SetLayout(config.initial_layout),
ConditionalController(
[
SabreLayout(
config.coupling_map,
max_iterations=2,
seed=config.seed_transpiler,
swap_trials=attempts,
layout_trials=attempts,
skip_routing=False,
)
],
condition=lambda property_set: not property_set["layout"],
),
ConditionalController(
common.generate_embed_passmanager(
config.coupling_map
).to_flow_controller(),
condition=lambda property_set: property_set["final_layout"] is None,
),
]
)

def lightsabre(circuit: Circuit) -> Circuit:
c: Circuit = qiskit_to_tk(sabre_pass.run(tk_to_qiskit(circuit)))
c.remove_blank_wires()
c.rename_units({q: Node(q.index[0]) for q in c.qubits})
RebaseTket().apply(c)
Transform.DecomposeCXDirected(architecture).apply(c)
return c

return lightsabre
Loading

0 comments on commit 1e8bd80

Please sign in to comment.