Skip to content

Commit ef46cd4

Browse files
committed
Add type-safe VecMap for slices indexed by newtypes
Sabre uses several objects that are logically maps from an index-like newtype (like `NodeIndex` or `PhysicalQubit`) to some value, and are implemented as fixed-slice `Vec`s for lookup efficiency. The newtype provides type safety while it's in use, but we have to cast it away to index, which makes it easy to index slices with the wrong object. This introduces a `VecMap` object, which provides a (minimal) slice-like interface, but indexes using the relevant newtype. The current implementation of Sabre does not use this _too_ much, but a refactoring of the layer structures will have them store one slice indexed by `PhysicalQubit` and one by `VirtualQubit`, which are trivially easy to get switched (a frequent mistake that is the base reason those new types were introduced in the first place).
1 parent 5ede84f commit ef46cd4

4 files changed

Lines changed: 104 additions & 39 deletions

File tree

crates/transpiler/src/passes/sabre/layer.rs

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ use rustworkx_core::petgraph::prelude::*;
1616

1717
use qiskit_circuit::PhysicalQubit;
1818

19+
use super::vec_map::VecMap;
20+
1921
/// A container for the current non-routable parts of the front layer. This only ever holds
2022
/// two-qubit gates; the only reason a 0q- or 1q operation can be unroutable is because it has an
2123
/// unsatisfied 2q predecessor, which disqualifies it from being in the front layer.
@@ -31,7 +33,7 @@ pub struct FrontLayer {
3133
nodes: IndexMap<NodeIndex, [PhysicalQubit; 2], ::ahash::RandomState>,
3234
/// Map of each qubit to the node that acts on it and the other qubit that node acts on, if this
3335
/// qubit is active (otherwise `None`).
34-
qubits: Vec<Option<(NodeIndex, PhysicalQubit)>>,
36+
qubits: VecMap<PhysicalQubit, Option<(NodeIndex, PhysicalQubit)>>,
3537
}
3638

3739
impl FrontLayer {
@@ -43,7 +45,7 @@ impl FrontLayer {
4345
num_qubits as usize / 2,
4446
::ahash::RandomState::default(),
4547
),
46-
qubits: vec![None; num_qubits as usize],
48+
qubits: vec![None; num_qubits as usize].into(),
4749
}
4850
}
4951

@@ -54,15 +56,15 @@ impl FrontLayer {
5456

5557
/// View onto the mapping between qubits and their `(node, other_qubit)` pair. Index `i`
5658
/// corresponds to physical qubit `i`.
57-
pub fn qubits(&self) -> &[Option<(NodeIndex, PhysicalQubit)>] {
59+
pub fn qubits(&self) -> &VecMap<PhysicalQubit, Option<(NodeIndex, PhysicalQubit)>> {
5860
&self.qubits
5961
}
6062

6163
/// Add a node into the front layer, with the two qubits it operates on.
6264
pub fn insert(&mut self, index: NodeIndex, qubits: [PhysicalQubit; 2]) {
6365
let [a, b] = qubits;
64-
self.qubits[a.index()] = Some((index, b));
65-
self.qubits[b.index()] = Some((index, a));
66+
self.qubits[a] = Some((index, b));
67+
self.qubits[b] = Some((index, a));
6668
self.nodes.insert(index, qubits);
6769
}
6870

@@ -74,14 +76,14 @@ impl FrontLayer {
7476
.nodes
7577
.swap_remove(index)
7678
.expect("Tried removing index that does not exist.");
77-
self.qubits[a.index()] = None;
78-
self.qubits[b.index()] = None;
79+
self.qubits[a] = None;
80+
self.qubits[b] = None;
7981
}
8082

8183
/// Query whether a qubit has an active node.
8284
#[inline]
8385
pub fn is_active(&self, qubit: PhysicalQubit) -> bool {
84-
self.qubits[qubit.index()].is_some()
86+
self.qubits[qubit].is_some()
8587
}
8688

8789
/// Calculate the score _difference_ caused by this swap, compared to not making the swap.
@@ -95,10 +97,10 @@ impl FrontLayer {
9597
// equal anyway, so not affect the score.
9698
let [a, b] = swap;
9799
let mut total = 0.0;
98-
if let Some((_, c)) = self.qubits[a.index()] {
100+
if let Some((_, c)) = self.qubits[a] {
99101
total += dist[[b.index(), c.index()]] - dist[[a.index(), c.index()]]
100102
}
101-
if let Some((_, c)) = self.qubits[b.index()] {
103+
if let Some((_, c)) = self.qubits[b] {
102104
total += dist[[a.index(), c.index()]] - dist[[b.index(), c.index()]]
103105
}
104106
total
@@ -114,25 +116,25 @@ impl FrontLayer {
114116
/// Apply a physical swap to the current layout data structure.
115117
pub fn apply_swap(&mut self, swap: [PhysicalQubit; 2]) {
116118
let [a, b] = swap;
117-
match (self.qubits[a.index()], self.qubits[b.index()]) {
119+
match (self.qubits[a], self.qubits[b]) {
118120
(Some((index1, _)), Some((index2, _))) if index1 == index2 => {
119121
let entry = self.nodes.get_mut(&index1).unwrap();
120122
*entry = [entry[1], entry[0]];
121123
return;
122124
}
123125
_ => {}
124126
}
125-
if let Some((index, c)) = self.qubits[a.index()] {
126-
self.qubits[c.index()] = Some((index, b));
127+
if let Some((index, c)) = self.qubits[a] {
128+
self.qubits[c] = Some((index, b));
127129
let entry = self.nodes.get_mut(&index).unwrap();
128130
*entry = if *entry == [a, c] { [b, c] } else { [c, b] };
129131
}
130-
if let Some((index, c)) = self.qubits[b.index()] {
131-
self.qubits[c.index()] = Some((index, a));
132+
if let Some((index, c)) = self.qubits[b] {
133+
self.qubits[c] = Some((index, a));
132134
let entry = self.nodes.get_mut(&index).unwrap();
133135
*entry = if *entry == [b, c] { [a, c] } else { [c, a] };
134136
}
135-
self.qubits.swap(a.index(), b.index());
137+
self.qubits.swap(a, b);
136138
}
137139

138140
/// True if there are no nodes in the current layer.

crates/transpiler/src/passes/sabre/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub mod heuristic;
1515
mod layer;
1616
mod layout;
1717
pub(crate) mod route;
18+
mod vec_map;
1819

1920
use pyo3::prelude::*;
2021
use pyo3::wrap_pyfunction;

crates/transpiler/src/passes/sabre/route.rs

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ use smallvec::{SmallVec, smallvec};
3737
use super::dag::{InteractionKind, SabreDAG};
3838
use super::heuristic::{BasicHeuristic, DecayHeuristic, Heuristic, LookaheadHeuristic, SetScaling};
3939
use super::layer::{ExtendedSet, FrontLayer};
40+
use super::vec_map::VecMap;
4041
use crate::TranspilerError;
4142
use crate::neighbors::Neighbors;
4243
use crate::target::{Target, TargetCouplingError};
@@ -439,8 +440,8 @@ struct State {
439440
extended_set: ExtendedSet,
440441
/// How many predecessors still need to be satisfied for each node index before it is at the
441442
/// front of the topological iteration through the nodes as they're routed.
442-
required_predecessors: Vec<u32>,
443-
decay: Vec<f64>,
443+
required_predecessors: VecMap<NodeIndex, u32>,
444+
decay: VecMap<PhysicalQubit, f64>,
444445
/// Reusable allocated storage space for accumulating and scoring swaps. This is owned as part
445446
/// of the general state to avoid reallocation costs.
446447
swap_scores: Vec<([PhysicalQubit; 2], f64)>,
@@ -468,7 +469,7 @@ impl State {
468469
problem: RoutingProblem,
469470
qubit: PhysicalQubit,
470471
) -> Option<NodeIndex> {
471-
self.front_layer.qubits()[qubit.index()].and_then(|(node, other)| {
472+
self.front_layer.qubits()[qubit].and_then(|(node, other)| {
472473
problem
473474
.target
474475
.neighbors
@@ -552,11 +553,10 @@ impl State {
552553
.dag
553554
.edges_directed(node_id, Direction::Outgoing)
554555
{
555-
let successor_node = edge.target();
556-
let successor_index = successor_node.index();
557-
self.required_predecessors[successor_index] -= 1;
558-
if self.required_predecessors[successor_index] == 0 {
559-
to_visit.push_back(successor_node);
556+
let successor = edge.target();
557+
self.required_predecessors[successor] -= 1;
558+
if self.required_predecessors[successor] == 0 {
559+
to_visit.push_back(successor);
560560
}
561561
}
562562
}
@@ -614,25 +614,24 @@ impl State {
614614
return;
615615
};
616616
let mut to_visit = self.front_layer.iter_nodes().copied().collect::<Vec<_>>();
617-
let mut decremented: IndexMap<usize, u32, ahash::RandomState> =
617+
let mut decremented: IndexMap<NodeIndex, u32, ahash::RandomState> =
618618
IndexMap::with_hasher(ahash::RandomState::default());
619619
let mut i = 0;
620620
while i < to_visit.len() && self.extended_set.len() < extended_set_size {
621621
let node = to_visit[i];
622622
for edge in problem.sabre.dag.edges_directed(node, Direction::Outgoing) {
623-
let successor_node = edge.target();
624-
let successor_index = successor_node.index();
625-
*decremented.entry(successor_index).or_insert(0) += 1;
626-
self.required_predecessors[successor_index] -= 1;
627-
if self.required_predecessors[successor_index] == 0 {
623+
let successor = edge.target();
624+
*decremented.entry(successor).or_insert(0) += 1;
625+
self.required_predecessors[successor] -= 1;
626+
if self.required_predecessors[successor] == 0 {
628627
// TODO: this looks "through" control-flow ops without seeing them, but we
629628
// actually eagerly route control-flow blocks as soon as they're eligible, so
630629
// they should be reflected in the extended set.
631-
if let InteractionKind::TwoQ([a, b]) = &problem.sabre.dag[successor_node].kind {
630+
if let InteractionKind::TwoQ([a, b]) = &problem.sabre.dag[successor].kind {
632631
self.extended_set
633632
.push([a.to_phys(&self.layout), b.to_phys(&self.layout)]);
634633
}
635-
to_visit.push(successor_node);
634+
to_visit.push(successor);
636635
}
637636
}
638637
i += 1;
@@ -755,8 +754,7 @@ impl State {
755754

756755
if let Some(DecayHeuristic { .. }) = problem.heuristic.decay {
757756
for (swap, score) in self.swap_scores.iter_mut() {
758-
*score = (absolute_score + *score)
759-
* self.decay[swap[0].index()].max(self.decay[swap[1].index()]);
757+
*score = (absolute_score + *score) * self.decay[swap[0]].max(self.decay[swap[1]]);
760758
}
761759
}
762760

@@ -849,14 +847,14 @@ pub fn swap_map_trial<'a>(
849847
let mut order = Order::for_problem(problem);
850848

851849
let num_qubits: u32 = problem.target.num_qubits().try_into().unwrap();
852-
let mut required_predecessors = vec![0; problem.sabre.dag.node_count()];
850+
let mut required_predecessors = VecMap::from(vec![0; problem.sabre.dag.node_count()]);
853851
for edge in problem.sabre.dag.edge_references() {
854-
required_predecessors[edge.target().index()] += 1;
852+
required_predecessors[edge.target()] += 1;
855853
}
856854
let mut state = State {
857855
front_layer: FrontLayer::new(num_qubits),
858856
extended_set: ExtendedSet::new(num_qubits),
859-
decay: vec![1.; num_qubits as usize],
857+
decay: vec![1.; num_qubits as usize].into(),
860858
required_predecessors,
861859
layout: initial_layout.clone(),
862860
swap_scores: Vec::with_capacity(problem.target.neighbors.edge_count() / 2),
@@ -892,8 +890,8 @@ pub fn swap_map_trial<'a>(
892890
state.decay.fill(1.);
893891
num_search_steps = 0;
894892
} else {
895-
state.decay[best_swap[0].index()] += increment;
896-
state.decay[best_swap[1].index()] += increment;
893+
state.decay[best_swap[0]] += increment;
894+
state.decay[best_swap[1]] += increment;
897895
}
898896
}
899897
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// This code is part of Qiskit.
2+
//
3+
// (C) Copyright IBM 2026
4+
//
5+
// This code is licensed under the Apache License, Version 2.0. You may
6+
// obtain a copy of this license in the LICENSE.txt file in the root directory
7+
// of this source tree or at https://www.apache.org/licenses/LICENSE-2.0.
8+
//
9+
// Any modifications or derivative works of this code must retain this
10+
// copyright notice, and modified files need to carry a notice indicating
11+
// that they have been altered from the originals.
12+
13+
use std::marker::PhantomData;
14+
use std::ops;
15+
16+
use rustworkx_core::petgraph::graph::IndexType;
17+
18+
/// Internal helper struct that represents a `Box<[T]>`, but is indexed by a petgraph-like indexer.
19+
///
20+
/// The layer structures involve several flat arrays, each representing a map from an index to a
21+
/// value, but the index types are often different, so it's less error-prone if we _enforce_ that
22+
/// you index using the object, rather than calling its `.index` method and erasing the type.
23+
#[derive(Clone, Debug)]
24+
pub struct VecMap<Idx: IndexType, T> {
25+
phantom: PhantomData<Idx>,
26+
data: Box<[T]>,
27+
}
28+
impl<Idx: IndexType, T> VecMap<Idx, T> {
29+
/// Swap the values of two indices.
30+
#[inline]
31+
pub fn swap(&mut self, a: Idx, b: Idx) {
32+
self.data.swap(a.index(), b.index())
33+
}
34+
35+
/// Fill all entries in the slice with a given value.
36+
#[inline]
37+
pub fn fill(&mut self, val: T)
38+
where
39+
T: Clone,
40+
{
41+
self.data.fill(val)
42+
}
43+
}
44+
45+
impl<Idx: IndexType, T> ops::Index<Idx> for VecMap<Idx, T> {
46+
type Output = <[T] as ops::Index<usize>>::Output;
47+
fn index(&self, index: Idx) -> &Self::Output {
48+
&self.data[index.index()]
49+
}
50+
}
51+
impl<Idx: IndexType, T> ops::IndexMut<Idx> for VecMap<Idx, T> {
52+
fn index_mut(&mut self, index: Idx) -> &mut Self::Output {
53+
&mut self.data[index.index()]
54+
}
55+
}
56+
57+
impl<Idx: IndexType, T> From<Vec<T>> for VecMap<Idx, T> {
58+
fn from(value: Vec<T>) -> Self {
59+
Self {
60+
phantom: PhantomData,
61+
data: value.into_boxed_slice(),
62+
}
63+
}
64+
}

0 commit comments

Comments
 (0)