Skip to main content

cqlib_core/compile/transform/layout/
sabre.rs

1// This code is part of Cqlib.
2//
3// (C) Copyright China Telecom Quantum Group 2025-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 http://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//! SABRE initial-layout adapter.
14//!
15//! Algorithm reference: Gushu Li, Yufei Ding, and Yuan Xie,
16//! [Tackling the Qubit Mapping Problem for NISQ-Era Quantum Devices](https://arxiv.org/abs/1809.02573).
17//!
18//! This module owns layout-candidate generation, objective scoring, and
19//! [`LayoutResult`] construction. The SABRE core remains in
20//! [`crate::compile::sabre`] and is used here only through crate-internal
21//! routing primitives.
22//!
23//! The dependency direction is intentional: layout may call SABRE to refine and
24//! score candidate initial layouts, but the standalone SABRE routing module must
25//! not depend on layout algorithms or layout result types.
26
27use super::interaction_seed::{interaction_aware_layouts, interaction_layout_cost};
28use super::{
29    CircuitLayoutAnalysis, GreedyCandidateOutcome, LayoutDiagnostics, LayoutObjective,
30    LayoutResult, LayoutScore, PhysicalLayoutGraph, Vf2EdgeRequirement, Vf2LayoutConfig,
31    Vf2PreparedOutcome, analyze_circuit_for_layout, greedy_layout_candidate_prepared,
32    is_perfect_layout, try_vf2_perfect_layout_prepared,
33};
34use crate::circuit::Circuit;
35use crate::compile::device_planning::DevicePlanningSession;
36use crate::compile::parallelism::{ParallelismThresholds, parallel_chunk_size};
37use crate::compile::sabre::{
38    ComponentAssignmentSearch, InteractionReachability, PreparedRouteMetadata, RankedTrial,
39    RequirementReachabilityFailure, RoutingTarget, SabreConfig, SabreDag, TrialResult,
40    compare_ranked_trials, interaction_reachability_for_target_with_metadata,
41    movement_component_assignment, normalize_initial_layout_for_target,
42    refine_layout_with_metadata, route_ranked_trial_with_metadata,
43};
44use crate::compile::{CompilerError, SabreRoutingFailure};
45use crate::device::{Device, Layout, LogicalQubit, PhysicalQubit};
46use rand::SeedableRng;
47use rand::rngs::StdRng;
48use rand::seq::SliceRandom;
49use rayon::prelude::*;
50use std::collections::{BTreeMap, BTreeSet};
51use std::sync::Arc;
52
53/// Approximate routed-DAG visits needed before candidate-level scheduling
54/// amortizes Rayon dispatch. Higher tiers require proportionally more work so
55/// short searches do not spread a few candidates across an oversized pool.
56const LAYOUT_PARALLELISM_THRESHOLDS: ParallelismThresholds =
57    ParallelismThresholds::new(2_048, 8_192, 32_768);
58const PARETO_ROUTE_FRONTIER_CAPACITY: usize = 64;
59const PARETO_EQUAL_QUALITY_CAPACITY: usize = 2;
60const PARETO_ROUTE_OUTCOME_BATCH_CAPACITY: usize = 64;
61
62/// Circuit-side data prepared once for repeated SABRE layout selection.
63///
64/// The fields are intentionally private so the interaction analysis and the
65/// dependency DAGs cannot be supplied from different circuits. A prepared
66/// value can be reused with different physical targets, objectives, and SABRE
67/// configurations.
68#[derive(Debug, Clone)]
69pub struct PreparedSabreCircuit {
70    analysis: CircuitLayoutAnalysis,
71    routing_dag: SabreDag,
72    refinement_dag: SabreDag,
73    backward_refinement_dag: SabreDag,
74}
75
76/// Device-side SABRE data prepared for one circuit's interaction signatures.
77///
78/// The physical graph remains the layout/scoring view. The routing target owns
79/// the connectivity, feasibility model, and costs chosen by the preparation
80/// function. Route metadata for the original and bidirectional refinement DAGs
81/// is stored alongside it so fused candidate refinement and routing reuse the
82/// same preparation.
83#[derive(Debug, Clone)]
84pub struct PreparedSabreTarget {
85    physical: Arc<PhysicalLayoutGraph>,
86    routing: Arc<RoutingTarget>,
87    routing_metadata: PreparedRouteMetadata,
88    refinement_metadata: PreparedRouteMetadata,
89    backward_refinement_metadata: PreparedRouteMetadata,
90}
91
92impl PreparedSabreTarget {
93    /// Returns the physical graph used by layout objective scoring.
94    pub fn physical(&self) -> &PhysicalLayoutGraph {
95        &self.physical
96    }
97
98    pub(crate) fn routing_target(&self) -> &RoutingTarget {
99        &self.routing
100    }
101}
102
103impl PreparedSabreCircuit {
104    /// Returns the reusable circuit layout analysis.
105    pub fn analysis(&self) -> &CircuitLayoutAnalysis {
106        &self.analysis
107    }
108
109    /// Returns logical qubits in source-circuit order.
110    pub fn logical_qubits(&self) -> &[LogicalQubit] {
111        &self.analysis.logical_qubits
112    }
113}
114
115/// Prepares the circuit-side analysis and dependency models used by SABRE.
116///
117/// Preparing once and calling [`sabre_layout_prepared`] repeatedly avoids
118/// rebuilding circuit analysis and dependency DAGs for every physical target
119/// or SABRE configuration.
120///
121/// # Errors
122///
123/// Returns [`CompilerError::InvalidInput`] when the circuit contains an
124/// operation that must be decomposed before SABRE, and propagates circuit
125/// analysis or DAG-construction failures.
126pub fn prepare_sabre_circuit(circuit: &Circuit) -> Result<PreparedSabreCircuit, CompilerError> {
127    let analysis = analyze_circuit_for_layout(circuit)?;
128    let routing_dag = SabreDag::from_operations(circuit.operations())?;
129    let refinement_dag = SabreDag::refinement_workload(circuit.operations())?;
130    let backward_refinement_dag = refinement_dag.reverse_interactions();
131    Ok(PreparedSabreCircuit {
132        analysis,
133        routing_dag,
134        refinement_dag,
135        backward_refinement_dag,
136    })
137}
138
139/// Prepares topology-only SABRE data for a circuit.
140///
141/// This path considers usable physical qubits and undirected connectivity. It
142/// does not construct a device-planning session or native implementation
143/// catalog.
144pub fn prepare_sabre_topology_target(
145    prepared: &PreparedSabreCircuit,
146    device: &Device,
147) -> Result<PreparedSabreTarget, CompilerError> {
148    let physical = Arc::new(PhysicalLayoutGraph::from_device(device)?);
149    let routing = Arc::new(RoutingTarget::from_physical(&physical)?);
150    prepare_sabre_topology_target_with_prepared(prepared, physical, routing)
151}
152
153pub(crate) fn prepare_sabre_topology_target_with_prepared(
154    prepared: &PreparedSabreCircuit,
155    physical: Arc<PhysicalLayoutGraph>,
156    routing: Arc<RoutingTarget>,
157) -> Result<PreparedSabreTarget, CompilerError> {
158    finish_sabre_target_preparation(prepared, physical, routing)
159}
160
161/// Prepares exact device-native SABRE data for a circuit.
162///
163/// This path includes native operation feasibility, direction, and
164/// implementation costs. Use it when the result will be compiled against the
165/// exact device contract rather than a topology-and-basis target.
166pub fn prepare_sabre_device_target(
167    prepared: &PreparedSabreCircuit,
168    device: &Device,
169) -> Result<PreparedSabreTarget, CompilerError> {
170    let planning_session = DevicePlanningSession::new(device);
171    prepare_sabre_device_target_with_session(prepared, device, &planning_session)
172}
173
174pub(crate) fn prepare_sabre_device_target_with_session(
175    prepared: &PreparedSabreCircuit,
176    device: &Device,
177    planning_session: &DevicePlanningSession,
178) -> Result<PreparedSabreTarget, CompilerError> {
179    let physical = Arc::new(PhysicalLayoutGraph::from_device(device)?);
180    prepare_sabre_device_target_with_session_and_physical(
181        prepared,
182        device,
183        physical,
184        planning_session,
185    )
186}
187
188pub(crate) fn prepare_sabre_device_target_with_session_and_physical(
189    prepared: &PreparedSabreCircuit,
190    device: &Device,
191    physical: Arc<PhysicalLayoutGraph>,
192    planning_session: &DevicePlanningSession,
193) -> Result<PreparedSabreTarget, CompilerError> {
194    let routing = Arc::new(RoutingTarget::from_device_with_session(
195        device,
196        &physical,
197        &prepared.routing_dag,
198        planning_session,
199    )?);
200    finish_sabre_target_preparation(prepared, physical, routing)
201}
202
203fn finish_sabre_target_preparation(
204    prepared: &PreparedSabreCircuit,
205    physical: Arc<PhysicalLayoutGraph>,
206    routing: Arc<RoutingTarget>,
207) -> Result<PreparedSabreTarget, CompilerError> {
208    let routing_metadata = PreparedRouteMetadata::new(&prepared.routing_dag, &routing)?;
209    let refinement_metadata = PreparedRouteMetadata::new(&prepared.refinement_dag, &routing)?;
210    let backward_refinement_metadata =
211        PreparedRouteMetadata::new(&prepared.backward_refinement_dag, &routing)?;
212    Ok(PreparedSabreTarget {
213        physical,
214        routing,
215        routing_metadata,
216        refinement_metadata,
217        backward_refinement_metadata,
218    })
219}
220
221/// Selects an initial layout with fused SABRE refinement and routing search.
222///
223/// The search completes every configured refinement iteration before routing
224/// the resulting layout. Complete routes are never created for intermediate
225/// refinement states.
226/// This layout-only API returns the winning route's initial layout;
227/// [`crate::compile::transform::route_sabre`] consumes the same fused result
228/// directly and does not route the winner a second time.
229///
230/// Candidate layouts include deterministic component-feasible anchors,
231/// reachable greedy/VF2 and interaction-aware layouts, plus the full randomized
232/// budget controlled by [`SabreConfig::layout_trials`]. Each candidate is
233/// refined through SABRE forward/backward passes; complete trials are ranked by
234/// predicted native two-qubit count, native two-qubit depth, native total
235/// depth, and stable candidate/trial order. [`LayoutObjective`] contributes
236/// candidate generation and diagnostics, but is not the final selection key.
237///
238/// # Errors
239///
240/// Returns [`CompilerError::InvalidInput`] for invalid SABRE layout
241/// configuration, insufficient usable physical qubits, unreachable
242/// interactions in the usable topology, or unsupported circuit operations.
243///
244/// # Examples
245///
246/// ```rust
247/// use cqlib_core::circuit::{Circuit, Qubit};
248/// use cqlib_core::compile::sabre::SabreConfig;
249/// use cqlib_core::compile::transform::{LayoutObjective, sabre_layout};
250/// use cqlib_core::device::Device;
251///
252/// let mut circuit = Circuit::new(3);
253/// circuit.cx(Qubit::new(0), Qubit::new(2)).unwrap();
254/// let device = Device::line("line-3", 3).unwrap();
255///
256/// let result = sabre_layout(
257///     &circuit,
258///     &device,
259///     &LayoutObjective::topology_only(),
260///     &SabreConfig::default(),
261/// )
262/// .unwrap();
263/// assert!(result.score.is_some());
264/// ```
265pub fn sabre_layout(
266    circuit: &Circuit,
267    device: &Device,
268    objective: &LayoutObjective,
269    config: &SabreConfig,
270) -> Result<LayoutResult, CompilerError> {
271    let prepared = prepare_sabre_circuit(circuit)?;
272    let target = prepare_sabre_topology_target(&prepared, device)?;
273    sabre_layout_prepared(&prepared, &target, objective, config)
274}
275
276/// Selects a SABRE initial layout from prepared circuit-side data.
277///
278/// Reuse `prepared` across targets or configurations to avoid repeating
279/// circuit analysis and dependency-DAG construction.
280///
281/// This API intentionally replaces the former
282/// `sabre_layout_prepared(circuit, analysis, physical, objective, config)`
283/// signature. Keeping circuit-derived data in [`PreparedSabreCircuit`] prevents
284/// callers from combining a circuit with analysis produced from another one.
285///
286/// # Errors
287///
288/// Returns [`CompilerError::InvalidInput`] for invalid SABRE configuration,
289/// insufficient or component-incompatible physical capacity, and unreachable
290/// interactions. Objective-scoring and routing failures are propagated.
291///
292/// # Examples
293///
294/// ```rust
295/// use cqlib_core::circuit::{Circuit, Qubit};
296/// use cqlib_core::compile::sabre::SabreConfig;
297/// use cqlib_core::compile::transform::{
298///     LayoutObjective, prepare_sabre_circuit, prepare_sabre_topology_target,
299///     sabre_layout_prepared,
300/// };
301/// use cqlib_core::device::Device;
302///
303/// let mut circuit = Circuit::new(3);
304/// circuit.cx(Qubit::new(0), Qubit::new(2))?;
305/// let prepared = prepare_sabre_circuit(&circuit)?;
306/// let target = prepare_sabre_topology_target(&prepared, &Device::line("line-3", 3)?)?;
307///
308/// let result = sabre_layout_prepared(
309///     &prepared,
310///     &target,
311///     &LayoutObjective::topology_only(),
312///     &SabreConfig::deterministic_seeded(42),
313/// )?;
314/// assert_eq!(result.layout.logical_qubits().count(), 3);
315/// # Ok::<(), Box<dyn std::error::Error>>(())
316/// ```
317pub fn sabre_layout_prepared(
318    prepared: &PreparedSabreCircuit,
319    prepared_target: &PreparedSabreTarget,
320    objective: &LayoutObjective,
321    config: &SabreConfig,
322) -> Result<LayoutResult, CompilerError> {
323    let selection =
324        sabre_route_selection_prepared(prepared, prepared_target, objective, config, false)?;
325    Ok(LayoutResult {
326        layout: selection.initial_layout,
327        score: Some(selection.score),
328        diagnostics: selection.diagnostics,
329    })
330}
331
332pub(crate) struct PreparedSabreRouteSelection {
333    pub(crate) initial_layout: Layout,
334    pub(crate) trial: TrialResult,
335    pub(crate) selected_trial_index: usize,
336    pub(crate) trials_evaluated: usize,
337    pub(crate) score: LayoutScore,
338    pub(crate) diagnostics: LayoutDiagnostics,
339}
340
341/// Stable identity of one route in the exploratory SABRE profile.
342#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
343pub(crate) struct SabreParetoCandidateId {
344    pub(crate) layout_index: usize,
345    pub(crate) route_index: usize,
346}
347
348#[derive(Debug, Clone, Copy, PartialEq, Eq)]
349struct SabreRouteQuality {
350    native_two_qubit_ops: usize,
351    native_two_qubit_depth: usize,
352    native_total_depth: usize,
353    native_total_ops: usize,
354}
355
356impl SabreRouteQuality {
357    fn dominates(self, other: Self) -> bool {
358        self.native_two_qubit_ops <= other.native_two_qubit_ops
359            && self.native_two_qubit_depth <= other.native_two_qubit_depth
360            && self.native_total_depth <= other.native_total_depth
361            && self.native_total_ops <= other.native_total_ops
362            && self != other
363    }
364
365    fn objective(self, index: usize) -> usize {
366        match index {
367            0 => self.native_two_qubit_ops,
368            1 => self.native_two_qubit_depth,
369            2 => self.native_total_depth,
370            3 => self.native_total_ops,
371            _ => unreachable!("SABRE route quality has four objectives"),
372        }
373    }
374
375    fn stable_route_key(self) -> (usize, usize, usize, usize) {
376        (
377            self.native_two_qubit_ops,
378            self.native_two_qubit_depth,
379            self.native_total_depth,
380            self.native_total_ops,
381        )
382    }
383}
384
385pub(crate) struct PreparedSabreParetoRouteSelection {
386    pub(crate) id: SabreParetoCandidateId,
387    pub(crate) selection: PreparedSabreRouteSelection,
388}
389
390#[derive(Clone)]
391struct ParetoRefinedCandidate {
392    index: usize,
393    layout: Layout,
394    score: LayoutScore,
395}
396
397#[derive(Clone)]
398struct ParetoRouteCandidate {
399    id: SabreParetoCandidateId,
400    layout: Layout,
401    score: LayoutScore,
402    trial: RankedTrial,
403    quality: SabreRouteQuality,
404    structure_fingerprint: u64,
405}
406
407/// Incremental exploratory search used by the Enhanced strict-device
408/// workflow. Refinement is prepared once, route indices are extended without
409/// replaying earlier trials, and only a compact non-dominated frontier is
410/// retained between tiers.
411pub(crate) struct PreparedSabreParetoSearch {
412    config: SabreConfig,
413    base_seed: u64,
414    refined: Vec<ParetoRefinedCandidate>,
415    frontier: Vec<ParetoRouteCandidate>,
416    emitted: BTreeSet<SabreParetoCandidateId>,
417    next_route_index: usize,
418    trials_evaluated: usize,
419    candidates_evaluated: usize,
420    candidate_notes: Vec<String>,
421}
422
423impl PreparedSabreParetoSearch {
424    /// Builds the multi-start four-refinement search profile. A supplied
425    /// layout is normalized and kept fixed; generated layouts deliberately do
426    /// not use the dense-interaction refinement shortcut employed by candidate
427    /// zero.
428    pub(crate) fn new(
429        prepared: &PreparedSabreCircuit,
430        prepared_target: &PreparedSabreTarget,
431        objective: &LayoutObjective,
432        config: &SabreConfig,
433        supplied_layout: Option<&Layout>,
434    ) -> Result<Self, CompilerError> {
435        validate_layout_config(config)?;
436        let target = prepared_target.routing_target();
437        if prepared.logical_qubits().len() > target.physical_qubits.len() {
438            return Err(CompilerError::InvalidInput(format!(
439                "sabre layout requires at least as many usable physical qubits as logical qubits; got {} logical qubits and {} usable physical qubits",
440                prepared.logical_qubits().len(),
441                target.physical_qubits.len()
442            )));
443        }
444
445        // Keep the exploratory streams disjoint from candidate zero even when
446        // both profiles share the workflow seed.
447        let base_seed =
448            splitmix64(config.seed.unwrap_or_else(rand::random) ^ 0x7061_7265_746f_5f62);
449        let (layouts, mut candidate_notes) = if let Some(layout) = supplied_layout {
450            (
451                vec![normalize_initial_layout_for_target(
452                    prepared.logical_qubits(),
453                    target,
454                    layout,
455                )?],
456                vec!["kept the caller-supplied initial layout fixed".to_string()],
457            )
458        } else {
459            let mut rng = StdRng::seed_from_u64(base_seed);
460            let generated =
461                initial_layout_candidates(prepared, prepared_target, objective, config, &mut rng)?;
462            (generated.layouts, generated.notes)
463        };
464        let candidates_evaluated = layouts.len();
465        if supplied_layout.is_none()
466            && dense_interaction_path_skips_refinement(
467                prepared.analysis(),
468                prepared_target.physical(),
469            )
470        {
471            candidate_notes.push(
472                "forced four-refinement exploratory profile for dense interactions".to_string(),
473            );
474        }
475
476        let refinement_iterations = if supplied_layout.is_none() {
477            config.refinement_iterations
478        } else {
479            0
480        };
481        let mut refined = Vec::with_capacity(layouts.len());
482        let mut missing_terminal = 0;
483        let mut movement_unreachable = 0;
484        let mut unsupported_native = 0;
485        for (index, layout) in layouts.into_iter().enumerate() {
486            match prepare_pareto_refined_candidate(
487                prepared,
488                prepared_target,
489                objective,
490                config,
491                refinement_iterations,
492                CandidateTrial {
493                    index,
494                    layout,
495                    base_seed,
496                },
497            )? {
498                CandidateOutcome::Success(candidate) => refined.push(candidate),
499                CandidateOutcome::Infeasible(reason, _) => match reason {
500                    CandidateInfeasibleReason::MissingTerminal => missing_terminal += 1,
501                    CandidateInfeasibleReason::MovementUnreachable => movement_unreachable += 1,
502                    CandidateInfeasibleReason::UnsupportedNative => unsupported_native += 1,
503                },
504            }
505        }
506        if refined.is_empty() {
507            return Err(CompilerError::SabreRoutingFailed(
508                SabreRoutingFailure::NoFeasibleLayoutCandidate {
509                    evaluated: candidates_evaluated,
510                    missing_terminal,
511                    movement_unreachable,
512                    unsupported_native,
513                },
514            ));
515        }
516
517        Ok(Self {
518            config: config.clone(),
519            base_seed,
520            refined,
521            frontier: Vec::new(),
522            emitted: BTreeSet::new(),
523            next_route_index: 0,
524            trials_evaluated: 0,
525            candidates_evaluated,
526            candidate_notes,
527        })
528    }
529
530    /// Extends routing through `route_limit` and materializes at most
531    /// `candidate_limit` previously unseen, diverse frontier candidates.
532    pub(crate) fn extend_to(
533        &mut self,
534        prepared: &PreparedSabreCircuit,
535        prepared_target: &PreparedSabreTarget,
536        route_limit: usize,
537        candidate_limit: usize,
538    ) -> Result<Vec<PreparedSabreParetoRouteSelection>, CompilerError> {
539        if route_limit <= self.next_route_index || route_limit > self.config.routing_trials {
540            return Err(CompilerError::InvalidInput(format!(
541                "incremental SABRE route limit must be in {}..={}, got {route_limit}",
542                self.next_route_index.saturating_add(1),
543                self.config.routing_trials
544            )));
545        }
546        let start = self.next_route_index;
547        let tasks = self
548            .refined
549            .iter()
550            .enumerate()
551            .flat_map(|(refined_index, _)| {
552                (start..route_limit).map(move |route_index| (refined_index, route_index))
553            })
554            .collect::<Vec<_>>();
555        for task_batch in tasks.chunks(PARETO_ROUTE_OUTCOME_BATCH_CAPACITY) {
556            let outcomes = task_batch
557                .into_par_iter()
558                .map(|&(refined_index, route_index)| {
559                    route_pareto_candidate(
560                        prepared,
561                        prepared_target,
562                        &self.config,
563                        self.base_seed,
564                        &self.refined[refined_index],
565                        route_index,
566                    )
567                })
568                .collect::<Result<Vec<_>, CompilerError>>()?;
569            self.trials_evaluated = self.trials_evaluated.saturating_add(outcomes.len());
570
571            let mut new_candidates = Vec::new();
572            for outcome in outcomes {
573                if let CandidateOutcome::Success(candidate) = outcome {
574                    new_candidates.push(candidate);
575                }
576            }
577            self.merge_frontier(new_candidates);
578        }
579        self.next_route_index = route_limit;
580        let selected_ids = self.select_diverse_unseen(candidate_limit);
581        let target = prepared_target.routing_target();
582        selected_ids
583            .into_iter()
584            .map(|id| {
585                let candidate = self
586                    .frontier
587                    .iter()
588                    .find(|candidate| candidate.id == id)
589                    .expect("selected Pareto candidate remains on the frontier")
590                    .clone();
591                let is_perfect = is_perfect_layout(
592                    prepared.analysis(),
593                    prepared_target.physical(),
594                    &candidate.layout,
595                );
596                let mut notes = self.candidate_notes.clone();
597                notes.push(format!(
598                    "selected exploratory Pareto route layout={} trial={} from 0..{route_limit}",
599                    id.layout_index, id.route_index
600                ));
601                Ok(PreparedSabreParetoRouteSelection {
602                    id,
603                    selection: PreparedSabreRouteSelection {
604                        initial_layout: candidate.layout,
605                        trial: candidate.trial.finish(target)?,
606                        selected_trial_index: id.route_index,
607                        trials_evaluated: self.trials_evaluated,
608                        score: candidate.score.clone(),
609                        diagnostics: LayoutDiagnostics {
610                            is_perfect,
611                            candidates_evaluated: self.candidates_evaluated,
612                            used_fidelity: candidate.score.used_fidelity,
613                            notes,
614                        },
615                    },
616                })
617            })
618            .collect()
619    }
620
621    fn merge_frontier(&mut self, new_candidates: Vec<ParetoRouteCandidate>) {
622        self.frontier.extend(new_candidates);
623        self.frontier.sort_by_key(|candidate| candidate.id);
624        let mut frontier = Vec::<ParetoRouteCandidate>::new();
625        for candidate in self.frontier.drain(..) {
626            if frontier.iter().any(|incumbent| {
627                incumbent.quality.dominates(candidate.quality)
628                    || incumbent.quality == candidate.quality
629                        && (incumbent.structure_fingerprint == candidate.structure_fingerprint
630                            || frontier
631                                .iter()
632                                .filter(|peer| peer.quality == candidate.quality)
633                                .count()
634                                >= PARETO_EQUAL_QUALITY_CAPACITY)
635            }) {
636                continue;
637            }
638            frontier.retain(|incumbent| !candidate.quality.dominates(incumbent.quality));
639            frontier.push(candidate);
640        }
641        if frontier.len() > PARETO_ROUTE_FRONTIER_CAPACITY {
642            let retained = select_diverse_candidate_ids(
643                frontier.iter().collect(),
644                PARETO_ROUTE_FRONTIER_CAPACITY,
645            )
646            .into_iter()
647            .collect::<BTreeSet<_>>();
648            frontier.retain(|candidate| retained.contains(&candidate.id));
649        }
650        self.frontier = frontier;
651    }
652
653    fn select_diverse_unseen(&mut self, limit: usize) -> Vec<SabreParetoCandidateId> {
654        if limit == 0 {
655            return Vec::new();
656        }
657        let unseen = self
658            .frontier
659            .iter()
660            .filter(|candidate| !self.emitted.contains(&candidate.id))
661            .collect::<Vec<_>>();
662        let selected = select_diverse_candidate_ids(unseen, limit);
663        self.emitted.extend(selected.iter().copied());
664        selected
665    }
666}
667
668fn select_diverse_candidate_ids(
669    mut candidates: Vec<&ParetoRouteCandidate>,
670    limit: usize,
671) -> Vec<SabreParetoCandidateId> {
672    let mut selected = Vec::new();
673    for objective in 0..4 {
674        let best = candidates
675            .iter()
676            .copied()
677            .filter(|candidate| !selected.contains(&candidate.id))
678            .min_by_key(|candidate| {
679                (
680                    candidate.quality.objective(objective),
681                    candidate.quality.stable_route_key(),
682                    candidate.structure_fingerprint,
683                    candidate.id,
684                )
685            });
686        if let Some(best) = best {
687            selected.push(best.id);
688        }
689        if selected.len() == limit {
690            return selected;
691        }
692    }
693    candidates.sort_by_key(|candidate| {
694        (
695            candidate.quality.stable_route_key(),
696            candidate.structure_fingerprint,
697            candidate.id,
698        )
699    });
700    for candidate in candidates {
701        if !selected.contains(&candidate.id) {
702            selected.push(candidate.id);
703        }
704        if selected.len() == limit {
705            break;
706        }
707    }
708    selected
709}
710
711#[allow(clippy::too_many_arguments)]
712fn prepare_pareto_refined_candidate(
713    prepared: &PreparedSabreCircuit,
714    prepared_target: &PreparedSabreTarget,
715    objective: &LayoutObjective,
716    config: &SabreConfig,
717    refinement_iterations: usize,
718    trial: CandidateTrial,
719) -> Result<CandidateOutcome<ParetoRefinedCandidate>, CompilerError> {
720    let target = prepared_target.routing_target();
721    match interaction_reachability_for_target_with_metadata(
722        &prepared.routing_dag,
723        target,
724        &prepared_target.routing_metadata,
725        &trial.layout,
726    )? {
727        InteractionReachability::Reachable => {}
728        InteractionReachability::UnreachableUnary {
729            cause: RequirementReachabilityFailure::NoExecutableTerminal,
730            ..
731        }
732        | InteractionReachability::UnreachablePair {
733            cause: RequirementReachabilityFailure::NoExecutableTerminal,
734            ..
735        } => {
736            return Ok(CandidateOutcome::Infeasible(
737                CandidateInfeasibleReason::MissingTerminal,
738                0,
739            ));
740        }
741        InteractionReachability::UnreachableUnary {
742            cause: RequirementReachabilityFailure::MovementDisconnected,
743            ..
744        }
745        | InteractionReachability::UnreachablePair {
746            cause: RequirementReachabilityFailure::MovementDisconnected,
747            ..
748        } => {
749            return Ok(CandidateOutcome::Infeasible(
750                CandidateInfeasibleReason::MovementUnreachable,
751                0,
752            ));
753        }
754    }
755
756    let mut refined = trial.layout;
757    let initial_signature = layout_mapping_signature(&refined, prepared.logical_qubits())?;
758    let mut seen_signatures = BTreeSet::from([initial_signature]);
759    for iteration in 0..refinement_iterations {
760        let forward_seed = derive_semantic_seed(
761            trial.base_seed,
762            11,
763            trial.index,
764            iteration,
765            layout_signature(&refined),
766        );
767        let forward = match refine_layout_with_metadata(
768            &prepared.refinement_dag,
769            target,
770            &prepared_target.refinement_metadata,
771            &refined,
772            &config.heuristic,
773            forward_seed,
774        ) {
775            Ok(layout) => layout,
776            Err(error) => return classify_candidate_error(error, 0),
777        };
778        let backward_seed = derive_semantic_seed(
779            trial.base_seed,
780            12,
781            trial.index,
782            iteration,
783            layout_signature(&forward),
784        );
785        let next = match refine_layout_with_metadata(
786            &prepared.backward_refinement_dag,
787            target,
788            &prepared_target.backward_refinement_metadata,
789            &forward,
790            &config.heuristic,
791            backward_seed,
792        ) {
793            Ok(layout) => layout,
794            Err(error) => return classify_candidate_error(error, 0),
795        };
796        let signature = layout_mapping_signature(&next, prepared.logical_qubits())?;
797        if !seen_signatures.insert(signature) {
798            break;
799        }
800        refined = next;
801    }
802    let score =
803        objective.score_layout(prepared.analysis(), prepared_target.physical(), &refined)?;
804    Ok(CandidateOutcome::Success(ParetoRefinedCandidate {
805        index: trial.index,
806        layout: refined,
807        score,
808    }))
809}
810
811fn route_pareto_candidate(
812    prepared: &PreparedSabreCircuit,
813    prepared_target: &PreparedSabreTarget,
814    config: &SabreConfig,
815    base_seed: u64,
816    refined: &ParetoRefinedCandidate,
817    route_index: usize,
818) -> Result<CandidateOutcome<ParetoRouteCandidate>, CompilerError> {
819    let target = prepared_target.routing_target();
820    let seed = derive_semantic_seed(
821        base_seed,
822        13,
823        refined.index,
824        route_index,
825        layout_signature(&refined.layout),
826    );
827    let mut trial = match route_ranked_trial_with_metadata(
828        &prepared.routing_dag,
829        target,
830        &prepared_target.routing_metadata,
831        &refined.layout,
832        &config.heuristic,
833        seed,
834    ) {
835        Ok(trial) => trial,
836        Err(error) => return classify_candidate_error(error, 1),
837    };
838    let quality = match trial.quality(target) {
839        Ok(quality) => quality,
840        Err(error) => return classify_candidate_error(error, 1),
841    };
842    let structure_fingerprint = trial.structure_fingerprint();
843    Ok(CandidateOutcome::Success(ParetoRouteCandidate {
844        id: SabreParetoCandidateId {
845            layout_index: refined.index,
846            route_index,
847        },
848        layout: refined.layout.clone(),
849        score: refined.score.clone(),
850        trial,
851        structure_fingerprint,
852        quality: SabreRouteQuality {
853            native_two_qubit_ops: quality.native_two_qubit_ops,
854            native_two_qubit_depth: quality.native_two_qubit_depth,
855            native_total_depth: quality.native_total_depth,
856            native_total_ops: quality.native_total_ops,
857        },
858    }))
859}
860
861pub(crate) fn sabre_route_selection_prepared(
862    prepared: &PreparedSabreCircuit,
863    prepared_target: &PreparedSabreTarget,
864    objective: &LayoutObjective,
865    config: &SabreConfig,
866    retain_provenance: bool,
867) -> Result<PreparedSabreRouteSelection, CompilerError> {
868    validate_layout_config(config)?;
869    let physical = &prepared_target.physical;
870    let target = &prepared_target.routing;
871    let analysis = &prepared.analysis;
872    let sabre = &prepared.routing_dag;
873    let forwards = &prepared.refinement_dag;
874    let backwards = &prepared.backward_refinement_dag;
875    let logical_qubits = analysis.logical_qubits.clone();
876
877    if logical_qubits.len() > target.physical_qubits.len() {
878        return Err(CompilerError::InvalidInput(format!(
879            "sabre layout requires at least as many usable physical qubits as logical qubits; got {} logical qubits and {} usable physical qubits",
880            logical_qubits.len(),
881            target.physical_qubits.len()
882        )));
883    }
884
885    let base_seed = config.seed.unwrap_or_else(rand::random);
886    let mut rng = StdRng::seed_from_u64(base_seed);
887    let initial_candidates =
888        initial_layout_candidates(prepared, prepared_target, objective, config, &mut rng)?;
889    let candidates = initial_candidates.layouts;
890    let mut candidate_notes = initial_candidates.notes;
891    let generated_candidates = candidates.len();
892    let refinement_iterations = if dense_interaction_path_skips_refinement(analysis, physical) {
893        candidate_notes.push(
894            "skipped bidirectional refinement because dense logical interactions already span a path topology"
895                .to_string(),
896        );
897        0
898    } else {
899        config.refinement_iterations
900    };
901    let perfect_candidate = (!target.uses_native_costs())
902        .then(|| {
903            candidates
904                .iter()
905                .position(|layout| is_perfect_layout(analysis, physical, layout))
906        })
907        .flatten();
908    let early_search = if let Some(index) = perfect_candidate {
909        let mut optimal_config = config.clone();
910        optimal_config.refinement_iterations = 0;
911        optimal_config.routing_trials = 1;
912        match evaluate_candidate(
913            sabre,
914            forwards,
915            backwards,
916            target,
917            prepared_target,
918            analysis,
919            physical,
920            objective,
921            &optimal_config,
922            0,
923            CandidateTrial {
924                index,
925                layout: candidates[index].clone(),
926                base_seed,
927            },
928            1,
929        )? {
930            CandidateOutcome::Success(evaluation) if evaluation.trial.swap_count() == 0 => {
931                candidate_notes.push(format!(
932                    "selected a zero-SWAP topology-optimal candidate without evaluating the remaining {} candidates",
933                    generated_candidates.saturating_sub(1)
934                ));
935                Some(CandidateSearch::from_outcome(CandidateOutcome::Success(
936                    evaluation,
937                )))
938            }
939            CandidateOutcome::Success(_) | CandidateOutcome::Infeasible(_, _) => None,
940        }
941    } else {
942        None
943    };
944
945    let (search, candidates_evaluated) = if let Some(search) = early_search {
946        (search, 1)
947    } else {
948        let trials = candidates
949            .into_iter()
950            .enumerate()
951            .map(|(index, layout)| CandidateTrial {
952                index,
953                layout,
954                base_seed,
955            })
956            .collect::<Vec<_>>();
957        let work_units = trials
958            .len()
959            .saturating_mul(
960                refinement_iterations
961                    .saturating_mul(2)
962                    .saturating_add(config.routing_trials),
963            )
964            .saturating_mul(sabre.graph.node_count());
965        let evaluate = |search: CandidateSearch, trial: CandidateTrial| {
966            let outcome = evaluate_candidate(
967                sabre,
968                forwards,
969                backwards,
970                target,
971                prepared_target,
972                analysis,
973                physical,
974                objective,
975                config,
976                refinement_iterations,
977                trial,
978                config.routing_trials,
979            )?;
980            search.merge(CandidateSearch::from_outcome(outcome), target)
981        };
982        let search = if let Some(chunk_size) =
983            parallel_chunk_size(trials.len(), work_units, LAYOUT_PARALLELISM_THRESHOLDS)
984        {
985            trials
986                .into_par_iter()
987                .chunks(chunk_size)
988                .map(|chunk| {
989                    chunk
990                        .into_iter()
991                        .try_fold(CandidateSearch::default(), &evaluate)
992                })
993                .try_reduce(CandidateSearch::default, |left, right| {
994                    left.merge(right, target)
995                })?
996        } else {
997            trials
998                .into_iter()
999                .try_fold(CandidateSearch::default(), evaluate)?
1000        };
1001        (search, generated_candidates)
1002    };
1003
1004    let best = search.best.ok_or_else(|| {
1005        CompilerError::SabreRoutingFailed(SabreRoutingFailure::NoFeasibleLayoutCandidate {
1006            evaluated: candidates_evaluated,
1007            missing_terminal: search.missing_terminal,
1008            movement_unreachable: search.movement_unreachable,
1009            unsupported_native: search.unsupported_native,
1010        })
1011    })?;
1012    let swap_count = best.trial.swap_count();
1013    let is_perfect = is_perfect_layout(analysis, physical, &best.layout);
1014
1015    let mut notes = candidate_notes;
1016    notes.push(format!(
1017        "selected SABRE refined layout with {swap_count} final-route swaps"
1018    ));
1019    notes.push(
1020        "winner selected by predicted native 2Q count/depth and total depth; layout score is diagnostic"
1021            .to_string(),
1022    );
1023    let trial = if retain_provenance {
1024        best.trial.finish(target)?
1025    } else {
1026        best.trial.finish_without_provenance(target)?
1027    };
1028    Ok(PreparedSabreRouteSelection {
1029        initial_layout: best.layout,
1030        trial,
1031        selected_trial_index: best.route_index,
1032        trials_evaluated: search.trials_evaluated,
1033        score: best.score.clone(),
1034        diagnostics: LayoutDiagnostics {
1035            is_perfect,
1036            candidates_evaluated,
1037            used_fidelity: best.score.used_fidelity,
1038            notes,
1039        },
1040    })
1041}
1042
1043#[allow(clippy::too_many_arguments)]
1044fn evaluate_candidate(
1045    sabre: &SabreDag,
1046    forwards: &SabreDag,
1047    backwards: &SabreDag,
1048    target: &RoutingTarget,
1049    prepared_target: &PreparedSabreTarget,
1050    analysis: &CircuitLayoutAnalysis,
1051    physical: &PhysicalLayoutGraph,
1052    objective: &LayoutObjective,
1053    config: &SabreConfig,
1054    refinement_iterations: usize,
1055    trial: CandidateTrial,
1056    route_trials: usize,
1057) -> Result<CandidateOutcome<CandidateEvaluation>, CompilerError> {
1058    match interaction_reachability_for_target_with_metadata(
1059        sabre,
1060        target,
1061        &prepared_target.routing_metadata,
1062        &trial.layout,
1063    )? {
1064        InteractionReachability::Reachable => {}
1065        InteractionReachability::UnreachableUnary {
1066            cause: RequirementReachabilityFailure::NoExecutableTerminal,
1067            ..
1068        }
1069        | InteractionReachability::UnreachablePair {
1070            cause: RequirementReachabilityFailure::NoExecutableTerminal,
1071            ..
1072        } => {
1073            return Ok(CandidateOutcome::Infeasible(
1074                CandidateInfeasibleReason::MissingTerminal,
1075                0,
1076            ));
1077        }
1078        InteractionReachability::UnreachableUnary {
1079            cause: RequirementReachabilityFailure::MovementDisconnected,
1080            ..
1081        }
1082        | InteractionReachability::UnreachablePair {
1083            cause: RequirementReachabilityFailure::MovementDisconnected,
1084            ..
1085        } => {
1086            return Ok(CandidateOutcome::Infeasible(
1087                CandidateInfeasibleReason::MovementUnreachable,
1088                0,
1089            ));
1090        }
1091    }
1092    let initial_signature = layout_mapping_signature(&trial.layout, &analysis.logical_qubits)?;
1093    let mut seen_signatures = BTreeSet::from([initial_signature]);
1094    let mut refined = trial.layout;
1095    for iteration in 0..refinement_iterations {
1096        let forward_seed = derive_semantic_seed(
1097            trial.base_seed,
1098            1,
1099            trial.index,
1100            iteration,
1101            layout_signature(&refined),
1102        );
1103        let forward = match refine_layout_with_metadata(
1104            forwards,
1105            target,
1106            &prepared_target.refinement_metadata,
1107            &refined,
1108            &config.heuristic,
1109            forward_seed,
1110        ) {
1111            Ok(layout) => layout,
1112            Err(error) => return classify_candidate_error(error, 0),
1113        };
1114        let backward_seed = derive_semantic_seed(
1115            trial.base_seed,
1116            2,
1117            trial.index,
1118            iteration,
1119            layout_signature(&forward),
1120        );
1121        let next_refined = match refine_layout_with_metadata(
1122            backwards,
1123            target,
1124            &prepared_target.backward_refinement_metadata,
1125            &forward,
1126            &config.heuristic,
1127            backward_seed,
1128        ) {
1129            Ok(layout) => layout,
1130            Err(error) => return classify_candidate_error(error, 0),
1131        };
1132        let mapping_signature = layout_mapping_signature(&next_refined, &analysis.logical_qubits)?;
1133        if !seen_signatures.insert(mapping_signature) {
1134            break;
1135        }
1136        refined = next_refined;
1137    }
1138
1139    let mut best = None::<(usize, RankedTrial)>;
1140    for route_index in 0..route_trials {
1141        let seed = derive_semantic_seed(
1142            trial.base_seed,
1143            3,
1144            trial.index,
1145            route_index,
1146            layout_signature(&refined),
1147        );
1148        let mut routed = match route_ranked_trial_with_metadata(
1149            sabre,
1150            target,
1151            &prepared_target.routing_metadata,
1152            &refined,
1153            &config.heuristic,
1154            seed,
1155        ) {
1156            Ok(routed) => routed,
1157            Err(error) => return classify_candidate_error(error, route_index + 1),
1158        };
1159        let replace = if let Some((best_index, current)) = best.as_mut() {
1160            compare_ranked_trials(
1161                &mut routed,
1162                (trial.index, route_index),
1163                current,
1164                (trial.index, *best_index),
1165                target,
1166            )?
1167            .is_lt()
1168        } else {
1169            true
1170        };
1171        if replace {
1172            best = Some((route_index, routed));
1173        }
1174    }
1175    let (route_index, routed) = best.expect("route_trials is greater than zero");
1176    let score = objective.score_layout(analysis, physical, &refined)?;
1177    Ok(CandidateOutcome::Success(CandidateEvaluation {
1178        index: trial.index,
1179        route_index,
1180        trial: routed,
1181        layout: refined,
1182        score,
1183        trials_evaluated: route_trials,
1184    }))
1185}
1186
1187pub(crate) fn dense_interaction_path_skips_refinement(
1188    analysis: &CircuitLayoutAnalysis,
1189    physical: &PhysicalLayoutGraph,
1190) -> bool {
1191    let logical_count = analysis.logical_qubits.len();
1192    if logical_count < 4 {
1193        return false;
1194    }
1195    let possible_interactions = logical_count.saturating_mul(logical_count.saturating_sub(1)) / 2;
1196    if analysis.interactions.len() != possible_interactions {
1197        return false;
1198    }
1199    let (min_weight, max_weight) = analysis
1200        .interactions
1201        .interactions()
1202        .iter()
1203        .fold((f64::INFINITY, 0.0_f64), |(min, max), interaction| {
1204            (min.min(interaction.weight), max.max(interaction.weight))
1205        });
1206    if !min_weight.is_finite() || min_weight <= 0.0 || max_weight > min_weight * 4.0 {
1207        return false;
1208    }
1209
1210    let physical_count = physical.physical_qubits().len();
1211    if physical_count < 2 {
1212        return false;
1213    }
1214    let mut degree = vec![0usize; physical_count];
1215    let mut edge_count = 0usize;
1216    for (left, right) in physical.undirected_edges_by_index() {
1217        degree[left] += 1;
1218        degree[right] += 1;
1219        edge_count += 1;
1220    }
1221    let is_connected =
1222        (1..physical_count).all(|index| physical.distance_by_index(0, index).is_some());
1223    is_connected
1224        && edge_count + 1 == physical_count
1225        && degree.iter().filter(|value| **value == 1).count() == 2
1226        && degree.iter().all(|value| matches!(*value, 1 | 2))
1227}
1228
1229#[derive(Default)]
1230struct CandidateSearch {
1231    best: Option<CandidateEvaluation>,
1232    missing_terminal: usize,
1233    movement_unreachable: usize,
1234    unsupported_native: usize,
1235    trials_evaluated: usize,
1236}
1237
1238impl CandidateSearch {
1239    fn from_outcome(outcome: CandidateOutcome<CandidateEvaluation>) -> Self {
1240        let mut search = Self::default();
1241        match outcome {
1242            CandidateOutcome::Success(evaluation) => {
1243                search.trials_evaluated = evaluation.trials_evaluated();
1244                search.best = Some(evaluation);
1245            }
1246            CandidateOutcome::Infeasible(
1247                CandidateInfeasibleReason::MissingTerminal,
1248                trials_evaluated,
1249            ) => {
1250                search.trials_evaluated = trials_evaluated;
1251                search.missing_terminal = 1;
1252            }
1253            CandidateOutcome::Infeasible(
1254                CandidateInfeasibleReason::MovementUnreachable,
1255                trials_evaluated,
1256            ) => {
1257                search.trials_evaluated = trials_evaluated;
1258                search.movement_unreachable = 1;
1259            }
1260            CandidateOutcome::Infeasible(
1261                CandidateInfeasibleReason::UnsupportedNative,
1262                trials_evaluated,
1263            ) => {
1264                search.trials_evaluated = trials_evaluated;
1265                search.unsupported_native = 1;
1266            }
1267        }
1268        search
1269    }
1270
1271    fn merge(mut self, mut other: Self, target: &RoutingTarget) -> Result<Self, CompilerError> {
1272        self.missing_terminal = self.missing_terminal.saturating_add(other.missing_terminal);
1273        self.movement_unreachable = self
1274            .movement_unreachable
1275            .saturating_add(other.movement_unreachable);
1276        self.unsupported_native = self
1277            .unsupported_native
1278            .saturating_add(other.unsupported_native);
1279        self.trials_evaluated = self.trials_evaluated.saturating_add(other.trials_evaluated);
1280        if let Some(mut candidate) = other.best.take() {
1281            let replace = if let Some(current) = self.best.as_mut() {
1282                candidate.compare(current, target)?.is_lt()
1283            } else {
1284                true
1285            };
1286            if replace {
1287                self.best = Some(candidate);
1288            }
1289        }
1290        Ok(self)
1291    }
1292}
1293
1294impl CandidateEvaluation {
1295    fn compare(
1296        &mut self,
1297        other: &mut Self,
1298        target: &RoutingTarget,
1299    ) -> Result<std::cmp::Ordering, CompilerError> {
1300        compare_ranked_trials(
1301            &mut self.trial,
1302            (self.index, self.route_index),
1303            &mut other.trial,
1304            (other.index, other.route_index),
1305            target,
1306        )
1307    }
1308
1309    fn trials_evaluated(&self) -> usize {
1310        self.trials_evaluated
1311    }
1312}
1313
1314struct CandidateTrial {
1315    /// Stable candidate order used as the final deterministic tie-breaker.
1316    index: usize,
1317    /// Candidate layout before forward/backward refinement.
1318    layout: Layout,
1319    /// Base seed used for semantic refinement and final-route seed derivation.
1320    base_seed: u64,
1321}
1322
1323fn layout_mapping_signature(
1324    layout: &Layout,
1325    logical_qubits: &[LogicalQubit],
1326) -> Result<Vec<u32>, CompilerError> {
1327    logical_qubits
1328        .iter()
1329        .map(|logical| {
1330            layout
1331                .get_physical(*logical)
1332                .map(PhysicalQubit::id)
1333                .ok_or_else(|| {
1334                    CompilerError::InvariantViolation(format!(
1335                        "sabre layout does not map logical qubit {logical}"
1336                    ))
1337                })
1338        })
1339        .collect()
1340}
1341
1342fn layout_signature(layout: &Layout) -> u64 {
1343    layout
1344        .l2p_map()
1345        .iter()
1346        .fold(0x6a09_e667_f3bc_c909, |hash, (logical, physical)| {
1347            splitmix64(hash ^ (u64::from(logical.id()) << 32) ^ u64::from(physical.id()))
1348        })
1349}
1350
1351fn derive_semantic_seed(
1352    base: u64,
1353    domain: u64,
1354    candidate_index: usize,
1355    step_index: usize,
1356    layout_signature: u64,
1357) -> u64 {
1358    splitmix64(
1359        base ^ domain.wrapping_mul(0x9e37_79b9_7f4a_7c15)
1360            ^ (candidate_index as u64).wrapping_mul(0xbf58_476d_1ce4_e5b9)
1361            ^ (step_index as u64).wrapping_mul(0x94d0_49bb_1331_11eb)
1362            ^ layout_signature,
1363    )
1364}
1365
1366fn splitmix64(mut value: u64) -> u64 {
1367    value = value.wrapping_add(0x9e37_79b9_7f4a_7c15);
1368    value = (value ^ (value >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
1369    value = (value ^ (value >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
1370    value ^ (value >> 31)
1371}
1372
1373enum CandidateOutcome<T> {
1374    Success(T),
1375    Infeasible(CandidateInfeasibleReason, usize),
1376}
1377
1378enum CandidateInfeasibleReason {
1379    MissingTerminal,
1380    MovementUnreachable,
1381    UnsupportedNative,
1382}
1383
1384fn classify_candidate_error<T>(
1385    error: CompilerError,
1386    trials_evaluated: usize,
1387) -> Result<CandidateOutcome<T>, CompilerError> {
1388    match error {
1389        CompilerError::SabreRoutingFailed(
1390            SabreRoutingFailure::NoExecutableUnaryTerminal { .. }
1391            | SabreRoutingFailure::NoExecutablePairTerminal { .. },
1392        ) => Ok(CandidateOutcome::Infeasible(
1393            CandidateInfeasibleReason::MissingTerminal,
1394            trials_evaluated,
1395        )),
1396        CompilerError::SabreRoutingFailed(
1397            SabreRoutingFailure::UnreachableUnaryPlacement { .. }
1398            | SabreRoutingFailure::UnreachablePairPlacement { .. },
1399        ) => Ok(CandidateOutcome::Infeasible(
1400            CandidateInfeasibleReason::MovementUnreachable,
1401            trials_evaluated,
1402        )),
1403        CompilerError::DeviceLoweringFailed(_) => Ok(CandidateOutcome::Infeasible(
1404            CandidateInfeasibleReason::UnsupportedNative,
1405            trials_evaluated,
1406        )),
1407        fatal => Err(fatal),
1408    }
1409}
1410
1411struct CandidateEvaluation {
1412    /// Original candidate index, retained after parallel evaluation.
1413    index: usize,
1414    /// Stable route-trial index within this layout candidate.
1415    route_index: usize,
1416    /// Best complete final route observed for the refined layout.
1417    trial: RankedTrial,
1418    /// Refined initial layout.
1419    layout: Layout,
1420    /// Objective score for the refined initial layout.
1421    score: LayoutScore,
1422    /// Number of complete original-DAG routes evaluated for this candidate.
1423    trials_evaluated: usize,
1424}
1425
1426/// Validates SABRE settings that are specific to layout selection.
1427///
1428/// The routing core owns generic SABRE validation. This adapter adds checks for
1429/// layout-only knobs that must be non-zero before candidate generation or
1430/// final-route scoring starts.
1431fn validate_layout_config(config: &SabreConfig) -> Result<(), CompilerError> {
1432    if config.layout_trials == 0 {
1433        return Err(CompilerError::InvalidInput(
1434            "sabre layout_trials must be greater than zero".to_string(),
1435        ));
1436    }
1437    if config.layout_assignment_budget == 0 {
1438        return Err(CompilerError::InvalidInput(
1439            "sabre layout_assignment_budget must be greater than zero".to_string(),
1440        ));
1441    }
1442    if let Some(vf2) = config.vf2_prepass {
1443        if vf2.candidate_limit == 0 {
1444            return Err(CompilerError::InvalidInput(
1445                "sabre vf2_prepass candidate_limit must be greater than zero".to_string(),
1446            ));
1447        }
1448        if vf2.call_limit == 0 {
1449            return Err(CompilerError::InvalidInput(
1450                "sabre vf2_prepass call_limit must be greater than zero".to_string(),
1451            ));
1452        }
1453    }
1454    config.validate()
1455}
1456
1457struct InitialLayoutCandidates {
1458    layouts: Vec<Layout>,
1459    notes: Vec<String>,
1460}
1461
1462/// Generates the candidate set refined by SABRE layout.
1463///
1464/// Candidates include deterministic movement anchors, interaction-graph
1465/// embeddings, opportunistic greedy/VF2 results, and seeded random physical
1466/// orders. Cheap graph bounds skip VF2 when exact embedding is oversized or
1467/// impossible. The result is deduplicated in logical-qubit order so duplicate
1468/// layouts from different sources are evaluated once.
1469fn initial_layout_candidates(
1470    prepared: &PreparedSabreCircuit,
1471    prepared_target: &PreparedSabreTarget,
1472    objective: &LayoutObjective,
1473    config: &SabreConfig,
1474    rng: &mut StdRng,
1475) -> Result<InitialLayoutCandidates, CompilerError> {
1476    let analysis = &prepared.analysis;
1477    let sabre = &prepared.routing_dag;
1478    let physical = &prepared_target.physical;
1479    let target = &prepared_target.routing;
1480    let mut candidates = Vec::new();
1481    let mut notes = Vec::new();
1482    let logical_qubits = &analysis.logical_qubits;
1483    let assignment = match movement_component_assignment(
1484        sabre,
1485        target,
1486        logical_qubits,
1487        config.layout_assignment_budget,
1488    )? {
1489        ComponentAssignmentSearch::Found(assignment) => assignment,
1490        ComponentAssignmentSearch::ProvenInfeasible => {
1491            return Err(CompilerError::SabreRoutingFailed(
1492                SabreRoutingFailure::MovementAssignmentInfeasible,
1493            ));
1494        }
1495        ComponentAssignmentSearch::BudgetExhausted { expansions } => {
1496            return Err(CompilerError::SabreRoutingFailed(
1497                SabreRoutingFailure::MovementAssignmentBudgetExhausted {
1498                    budget: config.layout_assignment_budget,
1499                    expansions,
1500                },
1501            ));
1502        }
1503    };
1504
1505    // Deterministic anchors guarantee that exact movement reachability does not
1506    // depend on a random seed, including cross-component terminal pairs.
1507    candidates.push(movement_component_layout(
1508        logical_qubits,
1509        &assignment.components,
1510        &assignment.logical_components,
1511        target,
1512        false,
1513        None,
1514    )?);
1515    candidates.push(movement_component_layout(
1516        logical_qubits,
1517        &assignment.components,
1518        &assignment.logical_components,
1519        target,
1520        true,
1521        None,
1522    )?);
1523
1524    let structured_budget = config.layout_trials.min(6);
1525    let structured = interaction_aware_layouts(analysis, physical, structured_budget)?;
1526    for candidate in structured {
1527        if interaction_reachability_for_target_with_metadata(
1528            sabre,
1529            target,
1530            &prepared_target.routing_metadata,
1531            &candidate,
1532        )? == InteractionReachability::Reachable
1533        {
1534            candidates.push(candidate);
1535        }
1536    }
1537
1538    match greedy_layout_candidate_prepared(analysis, physical, objective)? {
1539        GreedyCandidateOutcome::Found(greedy) => {
1540            let greedy =
1541                normalize_initial_layout_for_target(logical_qubits, target, &greedy.layout)?;
1542            if interaction_reachability_for_target_with_metadata(
1543                sabre,
1544                target,
1545                &prepared_target.routing_metadata,
1546                &greedy,
1547            )?
1548                == InteractionReachability::Reachable
1549            {
1550                candidates.push(greedy);
1551            }
1552        }
1553        GreedyCandidateOutcome::Disconnected { left, right } => notes.push(format!(
1554            "SABRE greedy prepass candidate mapped an interaction across disconnected physical qubits {left} and {right}"
1555        )),
1556    }
1557
1558    if let Some(vf2_prepass) = config.vf2_prepass
1559        && vf2_is_promising(analysis, physical)
1560    {
1561        let vf2_config = Vf2LayoutConfig {
1562            candidate_limit: vf2_prepass.candidate_limit,
1563            call_limit: Some(vf2_prepass.call_limit),
1564            edge_requirement: Vf2EdgeRequirement::PositiveInteractions,
1565        };
1566        match try_vf2_perfect_layout_prepared(analysis, physical, objective, &vf2_config)? {
1567            Vf2PreparedOutcome::Found(vf2) => {
1568                let vf2 = normalize_initial_layout_for_target(logical_qubits, target, &vf2.layout)?;
1569                if interaction_reachability_for_target_with_metadata(
1570                    sabre,
1571                    target,
1572                    &prepared_target.routing_metadata,
1573                    &vf2,
1574                )? == InteractionReachability::Reachable
1575                {
1576                    candidates.push(vf2);
1577                }
1578            }
1579            Vf2PreparedOutcome::NoCandidate => {
1580                notes.push("SABRE VF2 prepass found no perfect layout candidate".to_string());
1581            }
1582            Vf2PreparedOutcome::BudgetExhausted => notes.push(format!(
1583                "SABRE VF2 prepass exhausted its call budget of {}",
1584                vf2_prepass.call_limit
1585            )),
1586        }
1587    } else if config.vf2_prepass.is_some() {
1588        notes.push(
1589            "SABRE skipped VF2 because cheap graph bounds indicate an oversized or impossible exact embedding"
1590                .to_string(),
1591        );
1592    }
1593
1594    // Randomized candidates complete the pool after deterministic and
1595    // interaction-aware seeds have supplied stable anchors.
1596    for _ in 0..config.layout_trials {
1597        candidates.push(movement_component_layout(
1598            logical_qubits,
1599            &assignment.components,
1600            &assignment.logical_components,
1601            target,
1602            false,
1603            Some(rng),
1604        )?);
1605    }
1606
1607    let candidates = deduplicate_layouts(candidates, logical_qubits)?;
1608    Ok(InitialLayoutCandidates {
1609        layouts: prune_layout_candidates(
1610            candidates,
1611            analysis,
1612            physical,
1613            logical_qubits,
1614            config.layout_trials,
1615        )?,
1616        notes,
1617    })
1618}
1619
1620fn vf2_is_promising(analysis: &CircuitLayoutAnalysis, physical: &PhysicalLayoutGraph) -> bool {
1621    let interactions = analysis
1622        .interactions
1623        .interactions()
1624        .iter()
1625        .filter(|interaction| interaction.weight > 0.0)
1626        .collect::<Vec<_>>();
1627    if interactions.is_empty() {
1628        return false;
1629    }
1630    let mut logical_degree = BTreeMap::<LogicalQubit, usize>::new();
1631    for interaction in &interactions {
1632        *logical_degree.entry(interaction.left).or_default() += 1;
1633        *logical_degree.entry(interaction.right).or_default() += 1;
1634    }
1635    let mut physical_degree = vec![0usize; physical.physical_qubits().len()];
1636    let physical_edges = physical
1637        .undirected_edges_by_index()
1638        .inspect(|(left, right)| {
1639            physical_degree[*left] += 1;
1640            physical_degree[*right] += 1;
1641        })
1642        .count();
1643    logical_degree.len() <= 40
1644        && interactions.len() <= physical_edges
1645        && logical_degree.values().copied().max().unwrap_or(0)
1646            <= physical_degree.into_iter().max().unwrap_or(0)
1647}
1648
1649fn movement_component_layout(
1650    logical_qubits: &[LogicalQubit],
1651    physical_components: &[Vec<PhysicalQubit>],
1652    logical_components: &[usize],
1653    target: &RoutingTarget,
1654    reverse: bool,
1655    mut rng: Option<&mut StdRng>,
1656) -> Result<Layout, CompilerError> {
1657    let mut mapping = BTreeMap::new();
1658    let mut used = BTreeSet::new();
1659    for (physical_index, physical_component) in physical_components.iter().enumerate() {
1660        let mut logical = logical_qubits
1661            .iter()
1662            .enumerate()
1663            .filter(|(index, _)| logical_components[*index] == physical_index)
1664            .map(|(_, logical)| *logical)
1665            .collect::<Vec<_>>();
1666        let mut physical = physical_component.clone();
1667        if let Some(rng) = rng.as_deref_mut() {
1668            logical.shuffle(rng);
1669            physical.shuffle(rng);
1670        } else if reverse {
1671            physical.reverse();
1672        }
1673        for (logical, physical) in logical.into_iter().zip(physical) {
1674            mapping.insert(logical, physical);
1675            used.insert(physical);
1676        }
1677    }
1678
1679    let mut idle = logical_qubits
1680        .iter()
1681        .copied()
1682        .filter(|logical| !mapping.contains_key(logical))
1683        .collect::<Vec<_>>();
1684    let mut remaining = target
1685        .physical_qubits
1686        .iter()
1687        .copied()
1688        .filter(|physical| !used.contains(physical))
1689        .collect::<Vec<_>>();
1690    if let Some(rng) = rng {
1691        idle.shuffle(rng);
1692        remaining.shuffle(rng);
1693    } else if reverse {
1694        remaining.reverse();
1695    }
1696    mapping.extend(idle.into_iter().zip(remaining));
1697
1698    Layout::new(
1699        logical_qubits.to_vec(),
1700        target.physical_qubits.clone(),
1701        Some(mapping),
1702    )
1703    .map_err(|error| {
1704        CompilerError::InvariantViolation(format!(
1705            "sabre layout failed to construct an initial candidate: {error}"
1706        ))
1707    })
1708}
1709
1710/// Removes duplicate candidate layouts while preserving first occurrence.
1711///
1712/// Equality is based on physical-qubit IDs in logical-qubit order, not on
1713/// object identity or candidate source.
1714fn deduplicate_layouts(
1715    candidates: Vec<Layout>,
1716    logical_qubits: &[LogicalQubit],
1717) -> Result<Vec<Layout>, CompilerError> {
1718    let mut seen = BTreeSet::new();
1719    let mut unique = Vec::new();
1720    for layout in candidates {
1721        // Signatures use physical IDs in logical-qubit order so layouts built
1722        // from different candidate sources deduplicate consistently.
1723        let signature = logical_qubits
1724            .iter()
1725            .map(|logical| {
1726                layout
1727                    .get_physical(*logical)
1728                    .map(PhysicalQubit::id)
1729                    .ok_or_else(|| {
1730                        CompilerError::InvariantViolation(format!(
1731                            "sabre layout candidate does not map logical qubit {logical}"
1732                        ))
1733                    })
1734            })
1735            .collect::<Result<Vec<_>, CompilerError>>()?;
1736        if seen.insert(signature) {
1737            unique.push(layout);
1738        }
1739    }
1740    Ok(unique)
1741}
1742
1743fn prune_layout_candidates(
1744    candidates: Vec<Layout>,
1745    analysis: &CircuitLayoutAnalysis,
1746    physical: &PhysicalLayoutGraph,
1747    logical_qubits: &[LogicalQubit],
1748    limit: usize,
1749) -> Result<Vec<Layout>, CompilerError> {
1750    if candidates.len() <= limit {
1751        return Ok(candidates);
1752    }
1753    let mut scored = candidates
1754        .into_iter()
1755        .map(|layout| {
1756            let cost = interaction_layout_cost(analysis, physical, &layout)?;
1757            let mapping = logical_qubits
1758                .iter()
1759                .map(|logical| {
1760                    layout
1761                        .get_physical(*logical)
1762                        .map(PhysicalQubit::id)
1763                        .ok_or_else(|| {
1764                            CompilerError::InvariantViolation(format!(
1765                                "sabre layout candidate does not map logical qubit {logical}"
1766                            ))
1767                        })
1768                })
1769                .collect::<Result<Vec<_>, _>>()?;
1770            let mut subset = mapping.clone();
1771            subset.sort_unstable();
1772            Ok((cost, subset, mapping, layout))
1773        })
1774        .collect::<Result<Vec<_>, CompilerError>>()?;
1775    scored.sort_by(|left, right| {
1776        left.0
1777            .total_cmp(&right.0)
1778            .then_with(|| left.1.cmp(&right.1))
1779            .then_with(|| left.2.cmp(&right.2))
1780    });
1781
1782    let mut subset_counts = BTreeMap::<Vec<u32>, usize>::new();
1783    let mut selected = Vec::with_capacity(limit);
1784    let mut deferred = Vec::new();
1785    for candidate in scored {
1786        let count = subset_counts.entry(candidate.1.clone()).or_default();
1787        if *count < 3 && selected.len() < limit {
1788            *count += 1;
1789            selected.push(candidate.3);
1790        } else {
1791            deferred.push(candidate.3);
1792        }
1793    }
1794    selected.extend(
1795        deferred
1796            .into_iter()
1797            .take(limit.saturating_sub(selected.len())),
1798    );
1799    Ok(selected)
1800}