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