Linear Compiler¶
The Linear compiler turns a quantum circuit into a timed sequence of gates and ion movements for a one-dimensional array of sites. Processing zones mark the sites where gates may run; the remaining sites can be used to store ions while the compiler brings the next operands together.
The compiler is intended for Python workflows. Supply a hardware layout and a
Qiskit circuit, QASM text, or a QASM file, then inspect the returned
CompilationResult.
Compile a circuit¶
The following example uses five sites and one two-site processing zone. The compiler chooses the necessary shuttles, swaps, and gate start times.
1from qiskit import QuantumCircuit
2
3from mqt.ionshuttler.linear import Architecture, CompilationStatus, LinearCompiler
4
5architecture = Architecture(
6 num_sites=5,
7 processing_zones={"gate_zone": [2, 3]},
8)
9
10circuit = QuantumCircuit(2)
11circuit.rx(0.25, 0)
12circuit.ry(0.5, 1)
13circuit.rzz(0.75, 0, 1)
14
15result = LinearCompiler(architecture).compile(circuit)
16
17if result.status is CompilationStatus.SUCCESS:
18 print(f"Compiled in {result.schedule.num_timesteps} timesteps")
19 for action in result.schedule.path:
20 print(action)
21else:
22 print(f"Compilation stopped with {result.status.value}")
Compiled in 5 timesteps
Shuttle(ion=1, src=2, dst=3, duration=1)
Shuttle(ion=0, src=1, dst=2, duration=1)
AdvanceTime()
Rx(ion=0, virtual=False, theta=0.25, duration=1)
AdvanceTime()
Ry(ion=1, virtual=False, theta=0.5, duration=1)
AdvanceTime()
Rzz(ion_a=0, ion_b=1, theta=0.75, duration=2)
AdvanceTime()
AdvanceTime()
By default, ions are distributed across the available sites. Pass one starting site per circuit qubit when a particular loading is required:
1result = LinearCompiler(architecture).compile(
2 circuit,
3 initial_positions=[0, 4],
4)
The positions must be distinct, lie within the architecture, and match the number of circuit qubits. See also the trapped-ion hardware model for further details on the hardware abstraction.
Compile option |
User-visible effect |
|---|---|
|
Selects one distinct starting site per circuit qubit. When omitted, the compiler distributes the ions across the available sites. |
|
On layouts with multiple processing zones, computes a fine-grained gate-to-zone preference once and uses it to guide search. It defaults to |
For example, enable the partition-guided heuristic for a multi-zone layout at the compilation call:
1multi_zone_architecture = Architecture(
2 num_sites=7,
3 processing_zones={"left": [1, 2], "right": [4, 5]},
4)
5partitioned_result = LinearCompiler(multi_zone_architecture).compile(
6 circuit,
7 pre_partition=True,
8)
Pre-partitioning can substantially reduce compilation time when a circuit lacks
spatial locality matching the zone layout. It is not a strict improvement:
circuits whose interactions already match the layout may see little benefit or
an occasional small slowdown. Advanced users can tune the partitioner through
SearchConfig.pre_partition_config using a
FineGrainedTabuConfig.
Circuit inputs¶
compile() accepts:
a Qiskit
qiskit.circuit.QuantumCircuit;a string containing OpenQASM 2 or the supported subset of OpenQASM 3; or
a
pathlib.Pathto a UTF-8 QASM file.
Strings are always treated as QASM text. Use Path("circuit.qasm") for a file.
The default gate set is rx, ry, rz, and rzz, a common trapped-ion gate
set. Additional implemented gates may be enabled through the compiler’s
action_types. Rotation parameters must be numeric when compilation begins.
Barriers and final measurements are accepted but do not become scheduled
hardware operations. Unsupported or malformed circuits raise an exception before
schedule search starts.
Describe the architecture¶
Architectures can be created in Python, as above, or loaded from JSON:
1from mqt.ionshuttler.linear import Architecture
2
3architecture = Architecture.from_json(
4 """{
5 "num_sites": 7,
6 "processing_zones": {
7 "left_zone": [1, 2],
8 "right_zone": [5, 6]
9 }
10 }"""
11)
Use Architecture.load("architecture.json") to read the same JSON
representation from a file.
Site numbers start at zero. Each processing zone must contain one or more
contiguous sites, and processing zones may not overlap. A one-qubit gate needs
its ion in a processing zone. A two-qubit gate needs both ions in the same zone;
they do not need to occupy adjacent sites within that zone. If
processing_zones is omitted, all sites form one processing zone.
An architecture may also contain a site-dependent field_profile. The Linear
compiler carries this information forward for later analysis; it does not use
the profile to choose the base schedule and does not normalize its values.
Hardware timing¶
The timing configuration tells the compiler how long the hardware operations take. Defaults are measured in compiler timesteps:
Operation |
Default duration |
|---|---|
Shuttle to an adjacent empty site |
1 |
Swap adjacent ions |
3 |
|
1 |
|
0, virtual |
|
2 |
Optional |
2 |
For example:
1from mqt.ionshuttler.linear import (
2 GateTiming,
3 HardwareTiming,
4 LinearCompiler,
5 LinearCompilerConfig,
6 TransportTiming,
7)
8
9timing = HardwareTiming(
10 transport=TransportTiming(shuttle=2, swap=5),
11 gates=GateTiming(rx=2, ry=2, rz=0, rxx=4, ryy=4, rzz=4),
12)
13config = LinearCompilerConfig(hardware_timing=timing)
14compiler = LinearCompiler(architecture, config=config)
rx, ry, and rz may be selected as virtual single-qubit gates through
GateTiming.virtual_single_qubit_gates. Virtual rotations must have zero
duration and remain in the logical schedule without reserving hardware time.
rz is virtual by default, reflecting a common trapped-ion implementation. When
reproducing a schedule created with different timing assumptions, pass those
durations explicitly.
Choose a search profile¶
The compiler uses A-star search: it compares the time already used with an estimate of the work still needed and explores promising schedules first [1]. This estimate is called a heuristic. An admissible heuristic never overestimates the remaining cost; with the usual unrestricted A-star conditions, that property is what supports an optimality guarantee.
The Linear heuristic combines estimated ion movement with remaining gate depth. Because movements and gates may overlap, this estimate is not guaranteed to be admissible. The compiler should therefore be viewed as a schedule-quality search, not as an exact optimizer. A successful result is a valid complete schedule, but it is not a proof that no shorter schedule exists.
The defaults favor practical compilation time:
1from mqt.ionshuttler.linear import LinearCompilerConfig, SearchConfig
2
3config = LinearCompilerConfig(
4 search=SearchConfig(
5 horizon=3,
6 committed_gates=2,
7 iterative_diving_search=True,
8 informed_action_prioritization=False,
9 num_solutions=1,
10 max_frontier_size=1000,
11 max_compile_time=1800.0,
12 use_dependencies=True,
13 )
14)
Setting |
User-visible effect |
|---|---|
|
Plans this many upcoming gates at a time. Small horizons are usually faster and use less memory, but make more local decisions. |
|
Number of gates fixed before a rolling-horizon replan. A smaller value preserves more flexibility but replans more often. It must not exceed |
|
Immediately follows a promising branch while retaining alternatives. This often reaches a schedule sooner, but changes search order and tends to yield lower-quality results than standard branching. It independently breaks admissibility and optimality guarantees. |
|
Tries a narrower set of promising movements before falling back to the full set. It may reduce work, especially under a time limit, but can change which schedule is found first. |
|
Continues until this many distinct completed states have been found, then returns the best of them. Larger values can improve the returned schedule at additional cost, without proving global optimality. |
|
Limits stored alternatives. Smaller bounds reduce memory, but discarded candidates can contain the best or only solution. Use |
|
Shared wall-clock budget in seconds. A finite value prevents unexpectedly long runs; |
|
|
|
Remaining-cost estimate guiding the search. |
|
Optional fine-grained tabu settings used when |
A useful, more thorough profile for small circuits is:
1search = SearchConfig(
2 horizon=None,
3 committed_gates=1,
4 iterative_diving_search=False,
5 num_solutions=10,
6 max_frontier_size=None,
7 max_compile_time=None,
8)
This removes the rolling horizon, frontier bound, and timeout, and asks for several solutions. Runtime and memory can grow rapidly. Because the heuristic is not guaranteed admissible and the search stops after the requested number of solutions, this remains a quality-oriented search rather than an exact optimality mode.
For small instances where a minimum-makespan result matters more than search speed, all quality-oriented shortcuts can be disabled:
1from mqt.ionshuttler.linear import zero_heuristic
2
3exact_search = SearchConfig(
4 horizon=None,
5 committed_gates=1,
6 iterative_diving_search=False,
7 informed_action_prioritization=False,
8 num_solutions=1,
9 max_frontier_size=None,
10 max_compile_time=None,
11 use_dependencies=True,
12 heuristic=zero_heuristic,
13)
This turns the search into an unrestricted uniform-cost search. The first complete schedule therefore has the minimum makespan among the schedules represented by the configured hardware and compiler action model. Runtime and memory can grow very quickly, so this profile is mainly useful for small circuits, reference results, and comparisons. A timeout, frontier bound, rolling horizon, iterative dive, or narrowed action generation removes that optimality guarantee.
Supplying a custom heuristic¶
SearchConfig.heuristic selects the remaining-cost estimate. Besides the
default (None) and :func:~mqt.ionshuttler.linear.cost.zero_heuristic, any
callable matching :class:~mqt.ionshuttler.linear.cost.HeuristicFn may be
supplied. It receives the current state, the architecture, the gates to
schedule, and the dependency map, and returns a nonnegative estimate of the
remaining schedule time. gate_order and gates also include gates that are
already completed or running, so you might want to filter them with
state.completed_gates and state.in_progress_gates.
Because a larger estimate marks a state as further from the goal, adding a penalty steers the search away from the states it describes. This heuristic adds one unit of estimated work for every pair of ions sitting on neighboring sites, biasing the search toward schedules that keep ions spread out:
1from itertools import combinations
2
3from mqt.ionshuttler.linear import SearchConfig
4from mqt.ionshuttler.linear.cost import heuristic as default_heuristic
5
6
7def prefer_spread_out_ions(state, architecture, gate_order, gates, predecessors=None):
8 """Penalize neighboring ions on top of the default estimate."""
9 base = default_heuristic(state, architecture, gate_order, gates, predecessors)
10 sites = [site for _, site in state.positions]
11 crowding = sum(1 for left, right in combinations(sites, 2) if abs(left - right) <= 1)
12 return base + crowding
13
14
15custom_search = SearchConfig(heuristic=prefer_spread_out_ions)
The estimate steers the search only; it never changes which schedules are legal. An estimate that overshoots the true remaining time gives up admissibility, exactly as the default already does.
Understand the result¶
The result status describes why compilation stopped:
Status |
Meaning |
|---|---|
|
Every circuit gate was scheduled. |
|
The time budget expired; the result contains the best partial schedule found. |
|
Compilation was interrupted; the best available partial schedule is returned. |
|
The search ran out of candidates before completing the circuit. |
result.schedule is the immutable
ActionSchedule that downstream DD and
simulation stages consume. Its path contains the ordered gate, movement, and
time-advance actions, and its num_timesteps is the makespan. It also contains
the machine-only initial state and a stable identifier for every action. The
architecture is stored alongside it as result.architecture, allowing later
passes to supply a compatible architecture with additional capabilities.
Compiler search progress and control-pass provenance are deliberately absent
from this execution boundary.
The containing result owns compiler-only diagnostics: result.wall_clock_s,
result.score, result.final_state, and result.explored_nodes. Always check
status before treating its schedule as a complete circuit schedule.
Invalid input and configuration errors raise exceptions instead of returning
FAILED. Use result.save(...) to write beneath outputs/results/json in the
current working directory by default, or pass directory explicitly. Use
load() for explicit JSON
output; compilation does not create caches or result files on its own.
See also¶
Trapped-Ion Hardware Model — sites, processing zones, timing, and physical assumptions
Dynamical decoupling for Linear schedules — dynamical-decoupling methods and comparison metrics
Linear Compiler Architecture — package architecture and custom extension points
mqt.ionshuttler.linear — complete Linear Python API reference