Movement
PhysicalPlacementStrategy
dataclass
PhysicalPlacementStrategy(
arch_spec: ArchSpec = get_physical_arch_spec(),
traversal: RustPlacementTraversal = (
lambda: RustPlacementTraversal(strategy="entropy")
)(),
target_generator: (
TargetGeneratorABC | TargetGeneratorCallable | None
) = None,
)
Bases: MoveToPlacementStrategyABC
flowchart TD
bloqade.lanes.heuristics.physical.movement.PhysicalPlacementStrategy[PhysicalPlacementStrategy]
bloqade.lanes.analysis.placement.strategy.MoveToPlacementStrategyABC[MoveToPlacementStrategyABC]
bloqade.lanes.analysis.placement.strategy.PlacementStrategyABC[PlacementStrategyABC]
bloqade.lanes.analysis.placement.strategy.MoveToPlacementStrategyABC --> bloqade.lanes.heuristics.physical.movement.PhysicalPlacementStrategy
bloqade.lanes.analysis.placement.strategy.PlacementStrategyABC --> bloqade.lanes.analysis.placement.strategy.MoveToPlacementStrategyABC
click bloqade.lanes.heuristics.physical.movement.PhysicalPlacementStrategy href "" "bloqade.lanes.heuristics.physical.movement.PhysicalPlacementStrategy"
click bloqade.lanes.analysis.placement.strategy.MoveToPlacementStrategyABC href "" "bloqade.lanes.analysis.placement.strategy.MoveToPlacementStrategyABC"
click bloqade.lanes.analysis.placement.strategy.PlacementStrategyABC href "" "bloqade.lanes.analysis.placement.strategy.PlacementStrategyABC"
Physical placement strategy backed by the Rust TargetSolver.
rust_bound_stats_total
property
rust_bound_stats_total: dict[str, float]
Branch-and-bound pruning statistics summed over every solve.
Counters and depth sums add; the per-instance root_lower_bound /
incumbent_cost / gap do not aggregate meaningfully across solves, so
the widest observed gap is kept as max_optimality_gap instead.
Empty — not zeroed — before the first solve, and after any number of
solves in which no bound ran: an unbounded run measured nothing, and
reporting zeros would be indistinguishable from a bounded run that
pruned nothing. A bound runs when
:pyattr:RustPlacementTraversal.completion_bound is set, or when a
cascade strategy's refinement is gated
(:pyattr:RustPlacementTraversal.cascade_bound), which needs no
completion_bound. Key-check rather than expecting the keys to exist.
rust_entropy_fallback_count
property
rust_entropy_fallback_count: int
Number of solved Rust entropy stages that used the budget-exhaustion fallback (Push and Rotate, with the greedy sequential router as the out-of-regime tertiary).
rust_nodes_expanded_total
property
rust_nodes_expanded_total: int
Total Rust solver node expansions for this strategy instance.
rust_proven_total
property
rust_proven_total: int
Solves whose plan the bound proved optimal.
Counts Proof.OPTIMAL only; a Proof.NO_PLAN is a different
claim. Read against the solve count rather than alone: a solve without
a proof is unproven, not suboptimal.
traced_blocked_locations
property
traced_blocked_locations: tuple[LocationAddress, ...]
Spectator atom positions for the traced CZ layer (atoms not in the active placement).
traced_target
property
traced_target: dict[int, LocationAddress]
First candidate target for the traced CZ layer (used by visualizers).
compute_moves
compute_moves(
state_before: ConcreteState, state_after: ConcreteState
) -> tuple[tuple[LaneAddress, ...], ...]
Route atoms between two fixed layouts for user-directed MoveTo.
Delegates to the shared compute_move_layers primitive, passing this
strategy's own traversal and cached :class:SearchEngine so MoveTo
routing uses the same search configuration and per-arch lane index as
cz_placements (the docstring on compute_move_layers notes the two
callsites are meant to share a traversal).
The import is deferred to call time: heuristics.move_synthesis
imports this module, so a top-level import would be a cycle.
Source code in .venv/lib/python3.14/site-packages/bloqade/lanes/heuristics/physical/movement.py
526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 | |
RustPlacementTraversal
dataclass
RustPlacementTraversal(
strategy: SearchStrategyName = "entropy",
max_movesets_per_group: int = 3,
max_goal_candidates: int = 3,
max_expansions: int | None = 300,
restarts: int = 1,
lookahead: bool = False,
collect_entropy_trace: bool = False,
seed: int = 0,
completion_bound: str | None = None,
block_spectators: bool = True,
fallback_push_rotate: bool = False,
cascade_bound: bool = True,
)
Config for the Rust search engine (TargetSolver).
restarts and lookahead are now exposed and threaded into
SolveOptions; per-strategy entropy knobs (max_movesets_per_group,
max_goal_candidates, collect_entropy_trace) feed EntropyOptions
via :func:_move_search_from_traversal.
Not yet exposed (Rust defaults used): weight, deadlock_policy,
w_t. These will be threaded through once validated via Rust-only
benchmarking.
block_spectators
class-attribute
instance-attribute
block_spectators: bool = True
Scope each CZ solve to the participating qubits.
When True (default), qubits not in the CZ's controls/targets are
treated as blocked obstacles for that solve rather than free atoms to route.
This keeps the search effort independent of how many unrelated qubits share
the same merged StaticPlacement block (under always_merge every CZ
would otherwise search over all block qubits, inflating nodes_expanded).
Set False to let each CZ route over all block qubits. Note: blocking
spectators removes routing freedom, so for some strategies it can change
(sometimes increase) move counts (e.g. DFS may relocate a spectator to
shorten a participant's path); the search-effort reduction is not always
move-count-free.
cascade_bound
class-attribute
instance-attribute
cascade_bound: bool = True
Gate a cascade strategy's A* refinement with the completion bound.
On by default; False restores the ungated refinement. The refinement looks for a plan strictly cheaper than the
inner strategy's; with the gate, a child whose g + h already reaches
that cost is dropped before it takes a node, which saves memory without
losing a cheaper plan (a tie may resolve to a different plan of the same
cost). Only the cascade-* strategies read it.
completion_bound
class-attribute
instance-attribute
completion_bound: str | None = None
Admissible completion bound for branch-and-bound pruning.
None (default) prunes a branch only once its accumulated cost alone
reaches the incumbent's. "weighted_distance" additionally prunes when
g + h reaches it, where h is the max objective-weighted distance
over unresolved atoms. Affects which subtrees are explored, never candidate
generation, and never returns a worse plan.
Only consulted by the entropy strategy, and ignored for loose-goal (CZ pair) solves where the bound is not admissible.
fallback_push_rotate
class-attribute
instance-attribute
fallback_push_rotate: bool = False
Route with Push and Rotate when the search fails.
Off by default. When True, a solve the search does not finish is handed
to Push and Rotate, a complete rule-based router: it either returns a
schedule or proves that none exists. Only the failure path pays for it.
Push and Rotate starts from the furthest configuration the search reached,
so the schedule is the search's prefix plus its own layers; if that cannot
finish, it reruns from the original placement. Push and Rotate does not
honour an AOD capacity, so on an architecture that sets one, a schedule
with a shot over it is discarded and the search's own result stands.
convert_move_layers
convert_move_layers(
raw_layers,
) -> tuple[tuple[LaneAddress, ...], ...]
Wrap Rust solver move_layers into Python LaneAddress tuples.
Source code in .venv/lib/python3.14/site-packages/bloqade/lanes/heuristics/physical/movement.py
56 57 58 59 60 61 62 | |
make_physical_placement_strategy
make_physical_placement_strategy(
*,
move_solutions_per_layer: int = 3,
search_budget: int | None = 300,
strategy: SearchStrategyName = "entropy",
arch_spec: ArchSpec | None = None,
return_moves: bool = True,
mover_selection: MoverSelection | None = None
) -> PlacementStrategyABC
Build a physical placement strategy from user-facing search knobs.
Uses :class:~bloqade.lanes.heuristics.physical.nohome.NoHomePlacementStrategy
backed by the Rust NoHomeCzPlacement solver.
move_solutions_per_layer maps to k_candidates (candidate home
sites per qubit in the Hungarian assignment). search_budget maps to
max_expansions. mover_selection picks which qubit of each CZ pair
moves (see :class:~bloqade.lanes.bytecode.MoverSelection); None keeps
NoHome's default, RANKED. lambda_lookahead is fixed at 0 because
palindrome return always moves atoms back to their original home position,
so future-layer proximity penalties carry no signal.
return_moves also selects mirrored solving: it wraps the strategy in
:class:~bloqade.lanes.analysis.placement.PalindromePlacementStrategy
and turns on backwards_search, since palindrome is what makes
mirroring sound (see below).
.. warning::
On the palindrome path (return_moves=True, the default) with a home
starting layout — atoms on their even/home words —
move_solutions_per_layer has no effect. It maps to
k_candidates, which feeds the no-home return assignment, and that
phase never runs under palindrome: solve_nohome skips it whenever
every atom already sits on a home site. Confirmed empirically —
sweeping move_solutions_per_layer over 1, 3, 8 and 20 produces
byte-identical move layers at 4, 6 and 8 atoms. The knob still applies
when return_moves=False, or when atoms start off their home words.
The wider consequence is that palindrome reduces
:class:~bloqade.lanes.heuristics.physical.nohome.NoHomePlacementStrategy
to its CZ phase, so gamma, k_candidates,
top_bus_signatures and bus_reward_rho are all inert on that
path. mover_selection is not: the CZ phase's choice of which qubit
of each pair moves is exactly what it controls.
Source code in .venv/lib/python3.14/site-packages/bloqade/lanes/heuristics/physical/movement.py
553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 | |