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 optimisation level 3 #417

Merged
merged 13 commits into from
Nov 20, 2024
1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
## 0.60.0 (Unreleased)

- Revert a change made in release v0.59.0 where users are warned about implicit qubit permutations in {py:func}`tk_to_qiskit`. This avoids spamming the user with unhelpful warnings when using pytket-qiskit backends. These backends handle implicit permutations automatically.
- Add new level 3 optimisation that uses `GreedyPauliSimp`

## 0.59.0 (November 2024)

Expand Down
17 changes: 16 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@ For instance those familiar with qiskit may wish to convert their circuits to py

## Default Compilation

Every {py:class}`~pytket.backends.backend.Backend` in pytket has its own {py:meth}`~pytket.backends.Backend.default_compilation_pass` method. This method applies a sequence of optimisations to a circuit depending on the value of an `optimisation_level` parameter. This default compilation will ensure that the circuit meets all the constraints required to run on the {py:class}`~pytket.backends.backend.Backend`. The passes applied by different levels of optimisation are specified in the table below.
Every {py:class}`~pytket.backends.backend.Backend` in pytket has its own {py:meth}`~pytket.backends.Backend.default_compilation_pass` method. This method applies a sequence of optimisations to a circuit depending on the value of an `optimisation_level` parameter. This default compilation will ensure that the circuit meets all the constraints required to run on the {py:class}`~pytket.backends.backend.Backend`. The passes applied by different levels of optimisation are specified in the table below. Note that optimisation levels 0, 1 and
2 preserve barriers in a circuit, while optimisation level 3 will remove them.

:::{list-table} **Default compilation pass for the IBMQBackend and IBMQEmulatorBackend**
:widths: 25 25 25
Expand All @@ -199,30 +200,44 @@ Every {py:class}`~pytket.backends.backend.Backend` in pytket has its own {py:met
* - optimisation_level = 0
- optimisation_level = 1
- optimisation_level = 2 [1]
- optimisation_level = 3
* - [DecomposeBoxes](inv:#*.passes.DecomposeBoxes)
- [DecomposeBoxes](inv:#*.passes.DecomposeBoxes)
- [DecomposeBoxes](inv:#*.passes.DecomposeBoxes)
- [DecomposeBoxes](inv:#*.passes.DecomposeBoxes)
* - [AutoRebase [2]](inv:#*.AutoRebase)
- [SynthesiseTket](inv:#*.SynthesiseTket)
- [FullPeepholeOptimise](inv:#*.passes.FullPeepholeOptimise)
- [RemoveBarriers](inv:#*.passes.RmoveBarriers)
* - LightSabre [3]
- LightSabre [3]
- LightSabre [3]
- [AutoRebase [2]](inv:#*.AutoRebase)
* - [AutoRebase [2]](inv:#*.AutoRebase)
- [SynthesiseTket](inv:#*.SynthesiseTket)
- [KAKDecomposition(allow_swaps=False)](inv:#*.passes.KAKDecomposition)
- [GreedyPauliSimp](inv:#*.passes.GreedyPauliSimp)
* - [RemoveRedundancies](inv:#*.passes.RemoveRedundancies)
- [AutoRebase [2]](inv:#*.AutoRebase)
- [CliffordSimp(allow_swaps=False)](inv:#*.passes.CliffordSimp)
- [AutoRebase [2]](inv:#*.AutoRebase)
* -
- [RemoveRedundancies](inv:#*.passes.RemoveRedundancies)
- [SynthesiseTket](inv:#*.SynthesiseTket)
- LightSabre [3]
* -
-
- [AutoRebase [2]](inv:#*.AutoRebase)
- [SynthesiseTket](inv:#*.SynthesiseTket)
* -
-
- [RemoveRedundancies](inv:#*.passes.RemoveRedundancies)
- [AutoRebase [2]](inv:#*.AutoRebase)

* -
-
-
- [RemoveRedundancies](inv:#*.passes.RemoveRedundancies)

:::

Expand Down
57 changes: 38 additions & 19 deletions pytket/extensions/qiskit/backends/aer.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
CustomPass,
DecomposeBoxes,
FullPeepholeOptimise,
NaivePlacementPass,
GreedyPauliSimp,
RemoveBarriers,
SequencePass,
SynthesiseTket,
)
Expand Down Expand Up @@ -163,12 +164,12 @@ def _arch_dependent_default_compilation_pass(
self,
arch: Architecture,
optimisation_level: int = 2,
timeout: int = 300,
) -> BasePass:
assert optimisation_level in range(3)
assert optimisation_level in range(4)
arch_specific_passes = [
AutoRebase({OpType.CX, OpType.TK1}),
CustomPass(_gen_lightsabre_transformation(arch, optimisation_level)),
NaivePlacementPass(arch),
CustomPass(_gen_lightsabre_transformation(arch), label="lightsabrepass"),
]
if optimisation_level == 0:
return SequencePass(
Expand All @@ -178,7 +179,6 @@ def _arch_dependent_default_compilation_pass(
*arch_specific_passes,
self.rebase_pass(),
],
False,
)
if optimisation_level == 1:
return SequencePass(
Expand All @@ -188,48 +188,67 @@ def _arch_dependent_default_compilation_pass(
*arch_specific_passes,
SynthesiseTket(),
],
False,
)
if optimisation_level == 2:
return SequencePass(
[
DecomposeBoxes(),
FullPeepholeOptimise(),
*arch_specific_passes,
CliffordSimp(False),
SynthesiseTket(),
],
)
return SequencePass(
[
DecomposeBoxes(),
FullPeepholeOptimise(),
RemoveBarriers(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why are you removing the barriers?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

GreedyPauliSimp doesn't support them - I'll update the documentation to make it clear that this optimisation level will remove them

AutoRebase({OpType.CX, OpType.H, OpType.Rz}),
GreedyPauliSimp(thread_timeout=timeout, only_reduce=True, trials=10),
*arch_specific_passes,
CliffordSimp(False),
SynthesiseTket(),
],
False,
)

def _arch_independent_default_compilation_pass(
self, optimisation_level: int = 2
self,
optimisation_level: int = 2,
timeout: int = 300,
) -> BasePass:
assert optimisation_level in range(3)
assert optimisation_level in range(4)
if optimisation_level == 0:
return SequencePass([DecomposeBoxes(), self.rebase_pass()])
if optimisation_level == 1:
return SequencePass([DecomposeBoxes(), SynthesiseTket()])
return SequencePass([DecomposeBoxes(), FullPeepholeOptimise()])
if optimisation_level == 2:
return SequencePass([DecomposeBoxes(), FullPeepholeOptimise()])
return SequencePass(
[
DecomposeBoxes(),
RemoveBarriers(),
AutoRebase({OpType.CX, OpType.H, OpType.Rz}),
GreedyPauliSimp(thread_timeout=timeout, only_reduce=True, trials=10),
],
)

def default_compilation_pass(
self,
optimisation_level: int = 2,
timeout: int = 300,
) -> BasePass:
"""
See documentation for :py:meth:`IBMQBackend.default_compilation_pass`.
"""
arch = self._backend_info.architecture
if (
self._has_arch
and arch.coupling # type: ignore
and self._backend_info.get_misc("characterisation")
):
if self._has_arch and arch.coupling: # type: ignore
return self._arch_dependent_default_compilation_pass(
arch, # type: ignore
optimisation_level,
timeout,
)

return self._arch_independent_default_compilation_pass(optimisation_level)
return self._arch_independent_default_compilation_pass(
optimisation_level, timeout
)

def process_circuits(
self,
Expand Down
29 changes: 22 additions & 7 deletions pytket/extensions/qiskit/backends/ibm.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@
CustomPass,
DecomposeBoxes,
FullPeepholeOptimise,
GreedyPauliSimp,
KAKDecomposition,
NaivePlacementPass,
RemoveBarriers,
RemoveRedundancies,
SequencePass,
SimplifyInitial,
Expand Down Expand Up @@ -344,6 +345,7 @@ def required_predicates(self) -> list[Predicate]:
def default_compilation_pass(
self,
optimisation_level: int = 2,
timeout: int = 300,
) -> BasePass:
"""
A suggested compilation pass that will will, if possible, produce an equivalent
Expand All @@ -360,33 +362,37 @@ def default_compilation_pass(

:param optimisation_level: The level of optimisation to perform during
compilation.
:param timeout: Parameter for optimisation level 3, given in seconds.

- Level 0 does the minimum required to solves the device constraints,
without any optimisation.
- Level 1 additionally performs some light optimisations.
- Level 2 (the default) adds more computationally intensive optimisations
that should give the best results from execution.
- Level 3 re-synthesises the circuit using the computationally intensive
`GreedyPauliSimp`. This will remove any barriers while optimising.


: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
config, props, optimisation_level, timeout
)

@staticmethod
def default_compilation_pass_offline(
config: PulseBackendConfiguration,
props: Optional[BackendProperties],
optimisation_level: int = 2,
timeout: int = 300,
) -> BasePass:
backend_info = IBMQBackend._get_backend_info(config, props)
primitive_gates = _get_primitive_gates(_tk_gate_set(config))
supports_rz = OpType.Rz in primitive_gates

assert optimisation_level in range(3)
assert optimisation_level in range(4)
passlist = [DecomposeBoxes()]
# If you make changes to the default_compilation_pass,
# then please update this page accordingly
Expand All @@ -402,14 +408,22 @@ def default_compilation_pass_offline(
passlist.append(SynthesiseTket())
elif optimisation_level == 2:
passlist.append(FullPeepholeOptimise())
elif optimisation_level == 3:
passlist.append(RemoveBarriers())
passlist.append(AutoRebase({OpType.CX, OpType.H, OpType.Rz}))
passlist.append(
GreedyPauliSimp(thread_timeout=timeout, only_reduce=True, trials=10)
)
arch = backend_info.architecture
assert arch is not None
if not isinstance(arch, FullyConnected):
passlist.append(AutoRebase(primitive_gates))
passlist.append(
CustomPass(_gen_lightsabre_transformation(arch, optimisation_level))
CustomPass(
_gen_lightsabre_transformation(arch, optimisation_level),
"lightsabre",
)
)
passlist.append(NaivePlacementPass(arch))
if optimisation_level == 1:
passlist.append(SynthesiseTket())
if optimisation_level == 2:
Expand All @@ -420,11 +434,12 @@ def default_compilation_pass_offline(
SynthesiseTket(),
]
)

if optimisation_level == 3:
passlist.append(SynthesiseTket())
passlist.extend(
[IBMQBackend.rebase_pass_offline(primitive_gates), RemoveRedundancies()]
)
return SequencePass(passlist, False)
return SequencePass(passlist)

@property
def _result_id_type(self) -> _ResultIdTuple:
Expand Down
7 changes: 4 additions & 3 deletions pytket/extensions/qiskit/backends/ibm_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,13 @@ def _architecture_to_couplingmap(architecture: Architecture) -> CouplingMap:


def _gen_lightsabre_transformation( # type: ignore
architecture: Architecture, optimization_level: int = 2, seed=0, attempts=20
architecture: Architecture, 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
Expand Down Expand Up @@ -147,7 +146,9 @@ def _gen_lightsabre_transformation( # type: ignore
)

def lightsabre(circuit: Circuit) -> Circuit:
c: Circuit = qiskit_to_tk(sabre_pass.run(tk_to_qiskit(circuit)))
c: Circuit = qiskit_to_tk(
sabre_pass.run(tk_to_qiskit(circuit, replace_implicit_swaps=True))
)
c.remove_blank_wires()
c.rename_units({q: Node(q.index[0]) for q in c.qubits})
RebaseTket().apply(c)
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
packages=find_namespace_packages(include=["pytket.*"]),
include_package_data=True,
install_requires=[
"pytket >= 1.34.0",
"pytket >= 1.35.0",
"qiskit >= 1.2.4",
"qiskit-ibm-runtime >= 0.30.0",
"qiskit-aer >= 0.15.1",
Expand Down
47 changes: 46 additions & 1 deletion tests/backend_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@
FractionalUnitary,
NoisyCircuitBuilder,
)
from pytket.extensions.qiskit.backends.ibm_utils import _gen_lightsabre_transformation
from pytket.mapping import LexiLabellingMethod, LexiRouteRoutingMethod, MappingManager
from pytket.passes import CliffordSimp, FlattenRelabelRegistersPass
from pytket.passes import CliffordSimp, FlattenRelabelRegistersPass, SequencePass
from pytket.pauli import Pauli, QubitPauliString
from pytket.predicates import (
CompilationUnit,
Expand Down Expand Up @@ -1496,3 +1497,47 @@ def test_mc_gate_on_aer() -> None:
c.measure_all()
r = b.run_circuit(c, n_shots=10)
assert r.get_counts() == Counter({(1, 1, 1): 10})


def test_optimisation_level_3_compilation() -> None:
b = AerBackend()
a = Architecture([(0, 1), (0, 2), (0, 3), (3, 4), (4, 5), (4, 6), (4, 7)])
b._has_arch = True
b._backend_info.architecture = a

c = Circuit(6)
for _ in range(6):
for i in range(4):
for j in range(i + 1, 4):
c.CX(i, j)
c.Rz(0.23, j)
c.S(j)
c.H(i)

compiled_2 = b.get_compiled_circuit(c, 2)
compiled_3 = b.get_compiled_circuit(c, 3)

assert compiled_2.n_2qb_gates() == 78
assert compiled_2.n_gates == 205
assert compiled_2.depth() == 147
assert compiled_3.n_2qb_gates() == 61
assert compiled_3.n_gates == 164
assert compiled_3.depth() == 114


def test_optimisation_level_3_serialisation() -> None:
b = AerBackend()
a = Architecture([(0, 1), (0, 2), (0, 3), (3, 4), (4, 5), (4, 6), (4, 7)])
b._has_arch = True
b._backend_info.architecture = a

p_dict = b.default_compilation_pass(3).to_dict()
passlist = SequencePass.from_dict(
p_dict,
{
"lightsabrepass": _gen_lightsabre_transformation(
b._backend_info.architecture
)
},
)
assert p_dict == passlist.to_dict()