Skip to main content

cqlib_core/compile/transform/layout/
vf2.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//! VF2++ perfect initial layout.
14//!
15//! VF2 layout searches for a topology-perfect initial mapping: every required
16//! logical interaction must land on an adjacent physical edge. The search is
17//! non-induced, so the physical topology may contain extra edges that are not
18//! present in the logical interaction graph. This matches the compiler use
19//! case: extra device connectivity is harmless and can help later routing.
20
21use super::vf2_engine::{Vf2Graph, Vf2SearchConfig, find_non_induced_mappings};
22use super::{
23    CircuitLayoutAnalysis, Interaction, LayoutDiagnostics, LayoutObjective, LayoutResult,
24    LayoutScore, PhysicalLayoutGraph, analyze_circuit_for_layout,
25};
26use crate::circuit::Circuit;
27use crate::compile::CompilerError;
28use crate::device::{Device, Layout, LogicalQubit, PhysicalQubit};
29use rustworkx_core::petgraph::graph::NodeIndex;
30use std::collections::{BTreeMap, BTreeSet};
31
32/// Selects which logical interaction edges must be matched by VF2.
33///
34/// This controls only the hard topology constraint. Coupling direction and
35/// calibration quality are still handled by [`LayoutObjective`] after complete
36/// topology candidates are found.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum Vf2EdgeRequirement {
39    /// Require only interactions with positive accumulated weight.
40    PositiveInteractions,
41    /// Require every interaction stored in the interaction graph.
42    AllInteractions,
43}
44
45impl Vf2EdgeRequirement {
46    /// Returns whether `interaction` is part of the VF2 hard topology graph.
47    fn requires(self, interaction: &Interaction) -> bool {
48        match self {
49            Self::PositiveInteractions => interaction.weight > 0.0,
50            Self::AllInteractions => true,
51        }
52    }
53}
54
55/// Configuration for [`vf2_perfect_layout`].
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct Vf2LayoutConfig {
58    /// Maximum number of complete perfect candidates to score.
59    ///
60    /// Larger values can improve calibration-aware ranking but increase search
61    /// and scoring cost. A value of zero is invalid.
62    pub candidate_limit: usize,
63    /// Maximum number of partial mapping extensions attempted by the search.
64    ///
65    /// `None` means no explicit extension limit. When the limit is reached,
66    /// diagnostics record that the search was truncated.
67    pub call_limit: Option<usize>,
68    /// Selects which logical interaction edges are hard topology constraints.
69    pub edge_requirement: Vf2EdgeRequirement,
70}
71
72impl Default for Vf2LayoutConfig {
73    fn default() -> Self {
74        Self {
75            candidate_limit: 10,
76            call_limit: None,
77            edge_requirement: Vf2EdgeRequirement::PositiveInteractions,
78        }
79    }
80}
81
82/// Outcome of an opportunistic prepared VF2 search.
83///
84/// SABRE layout treats VF2 as a bounded candidate generator.  A search that
85/// proves there is no perfect mapping and a search that merely exhausts its
86/// call budget are deliberately distinct outcomes.
87pub(crate) enum Vf2PreparedOutcome {
88    Found(LayoutResult),
89    NoCandidate,
90    BudgetExhausted,
91}
92
93/// Searches for a perfect initial layout using non-induced VF2++ matching.
94///
95/// A perfect layout maps every required logical interaction onto adjacent
96/// physical qubits. Physical extra edges are allowed because the logical
97/// interaction graph is matched as a non-induced subgraph. Coupling direction
98/// is not a hard feasibility constraint; direction mismatch is scored by
99/// [`LayoutObjective`].
100/// Logical qubits that do not participate in required interactions are filled
101/// deterministically after the VF2 match is found.
102///
103/// # Errors
104///
105/// Returns [`CompilerError::InvalidInput`] if physical capacity is insufficient,
106/// `candidate_limit` is zero, no perfect mapping is found, or scoring rejects
107/// all complete candidates.
108///
109/// # Examples
110///
111/// ```rust
112/// use cqlib_core::circuit::{Circuit, Qubit};
113/// use cqlib_core::compile::transform::{
114///     LayoutObjective, Vf2LayoutConfig, vf2_perfect_layout,
115/// };
116/// use cqlib_core::device::Device;
117///
118/// let mut circuit = Circuit::new(3);
119/// circuit.cx(Qubit::new(0), Qubit::new(1)).unwrap();
120/// circuit.cx(Qubit::new(1), Qubit::new(2)).unwrap();
121/// let device = Device::line("line-3", 3).unwrap();
122///
123/// let result = vf2_perfect_layout(
124///     &circuit,
125///     &device,
126///     &LayoutObjective::topology_only(),
127///     &Vf2LayoutConfig::default(),
128/// )
129/// .unwrap();
130/// assert!(result.diagnostics.is_perfect);
131/// ```
132pub fn vf2_perfect_layout(
133    circuit: &Circuit,
134    device: &Device,
135    objective: &LayoutObjective,
136    config: &Vf2LayoutConfig,
137) -> Result<LayoutResult, CompilerError> {
138    let analysis = analyze_circuit_for_layout(circuit)?;
139    let physical = PhysicalLayoutGraph::from_device(device)?;
140    vf2_perfect_layout_prepared(&analysis, &physical, objective, config)
141}
142
143/// Searches for a perfect initial layout on an already-built physical graph.
144///
145/// This lower-level entry point is useful when a workflow has already prepared
146/// circuit analysis and physical graph data for one or more layout algorithms.
147pub fn vf2_perfect_layout_prepared(
148    analysis: &CircuitLayoutAnalysis,
149    physical: &PhysicalLayoutGraph,
150    objective: &LayoutObjective,
151    config: &Vf2LayoutConfig,
152) -> Result<LayoutResult, CompilerError> {
153    match try_vf2_perfect_layout_prepared(analysis, physical, objective, config)? {
154        Vf2PreparedOutcome::Found(result) => Ok(result),
155        Vf2PreparedOutcome::NoCandidate => Err(CompilerError::InvalidInput(
156            "vf2 perfect layout could not find a perfect mapping".to_string(),
157        )),
158        Vf2PreparedOutcome::BudgetExhausted => Err(CompilerError::InvalidInput(
159            "vf2 perfect layout could not find a perfect mapping before the call limit was reached"
160                .to_string(),
161        )),
162    }
163}
164
165/// Runs the prepared VF2 search while preserving no-solution versus budget
166/// exhaustion for callers that use VF2 as an optional prepass.
167pub(crate) fn try_vf2_perfect_layout_prepared(
168    analysis: &CircuitLayoutAnalysis,
169    physical: &PhysicalLayoutGraph,
170    objective: &LayoutObjective,
171    config: &Vf2LayoutConfig,
172) -> Result<Vf2PreparedOutcome, CompilerError> {
173    if config.candidate_limit == 0 {
174        return Err(CompilerError::InvalidInput(
175            "vf2 perfect layout candidate_limit must be greater than zero".to_string(),
176        ));
177    }
178    if analysis.logical_qubits.len() > physical.physical_qubits().len() {
179        return Err(CompilerError::InvalidInput(format!(
180            "vf2 perfect layout requires at least as many usable physical qubits as logical qubits; got {} logical qubits and {} usable physical qubits",
181            analysis.logical_qubits.len(),
182            physical.physical_qubits().len()
183        )));
184    }
185
186    let activity = analysis.interactions.logical_activity();
187    let required_interactions = analysis
188        .interactions
189        .interactions()
190        .iter()
191        .filter(|interaction| config.edge_requirement.requires(interaction))
192        .collect::<Vec<_>>();
193
194    if required_interactions.is_empty() {
195        // Interaction-free circuits are trivially topology-perfect. We still
196        // complete and score the mapping so fidelity-aware objectives can rank
197        // physical qubits by readout quality.
198        let mapping = complete_mapping(BTreeMap::new(), analysis, physical, objective, &activity);
199        let layout = layout_from_mapping(analysis, physical, mapping, "vf2 perfect layout")?;
200        let score = objective.score_layout(analysis, physical, &layout)?;
201        return Ok(Vf2PreparedOutcome::Found(LayoutResult {
202            layout,
203            diagnostics: LayoutDiagnostics {
204                is_perfect: true,
205                candidates_evaluated: 1,
206                used_fidelity: score.used_fidelity,
207                notes: Vec::new(),
208            },
209            score: Some(score),
210        }));
211    }
212
213    // Build a compact graph containing only logical qubits that participate in
214    // hard VF2 constraints. Idle qubits are handled by complete_mapping.
215    let mut logical_index = BTreeMap::new();
216    for interaction in &required_interactions {
217        if !logical_index.contains_key(&interaction.left) {
218            logical_index.insert(interaction.left, logical_index.len());
219        }
220        if !logical_index.contains_key(&interaction.right) {
221            logical_index.insert(interaction.right, logical_index.len());
222        }
223    }
224    let mut logical_nodes = vec![None; logical_index.len()];
225    for (logical, node_index) in &logical_index {
226        logical_nodes[*node_index] = Some(*logical);
227    }
228    let logical_nodes = logical_nodes
229        .into_iter()
230        .map(|logical| logical.expect("logical node indices are contiguous"))
231        .collect::<Vec<_>>();
232
233    let mut logical_graph =
234        Vf2Graph::with_capacity(logical_index.len(), required_interactions.len());
235    for _ in 0..logical_index.len() {
236        logical_graph.add_node(());
237    }
238    for interaction in &required_interactions {
239        let left = NodeIndex::new(logical_index[&interaction.left]);
240        let right = NodeIndex::new(logical_index[&interaction.right]);
241        if left != right && logical_graph.find_edge(left, right).is_none() {
242            logical_graph.add_edge(left, right, ());
243        }
244    }
245
246    // The matching graph is undirected: direction is not a feasibility
247    // condition for perfect layout, only a scoring term.
248    let mut physical_graph = Vf2Graph::with_capacity(physical.physical_qubits().len(), 0);
249    for _ in physical.physical_qubits() {
250        physical_graph.add_node(());
251    }
252    for left in 0..physical.physical_qubits().len() {
253        for right in (left + 1)..physical.physical_qubits().len() {
254            if physical.is_adjacent_undirected(
255                physical.physical_qubits()[left],
256                physical.physical_qubits()[right],
257            ) {
258                physical_graph.add_edge(NodeIndex::new(left), NodeIndex::new(right), ());
259            }
260        }
261    }
262
263    let (mappings, search_stats) = find_non_induced_mappings(
264        &logical_graph,
265        &physical_graph,
266        Vf2SearchConfig {
267            candidate_limit: config.candidate_limit,
268            call_limit: config.call_limit,
269        },
270    );
271
272    let mut best: Option<(Layout, LayoutScore)> = None;
273    let mut scored = 0usize;
274    for mapping in mappings {
275        let mut active_mapping = BTreeMap::new();
276        for (logical_index, physical_index) in mapping.into_iter().enumerate() {
277            active_mapping.insert(
278                logical_nodes[logical_index],
279                physical.physical_qubits()[physical_index],
280            );
281        }
282        let mapping = complete_mapping(active_mapping, analysis, physical, objective, &activity);
283        let layout = layout_from_mapping(analysis, physical, mapping, "vf2 perfect layout")?;
284        let score = objective.score_layout(analysis, physical, &layout)?;
285        scored += 1;
286
287        if best.as_ref().is_none_or(|(best_layout, best_score)| {
288            let layout_signature = analysis
289                .logical_qubits
290                .iter()
291                .map(|logical| {
292                    layout
293                        .get_physical(*logical)
294                        .expect("layout results map every logical qubit")
295                        .id()
296                })
297                .collect::<Vec<_>>();
298            let best_signature = analysis
299                .logical_qubits
300                .iter()
301                .map(|logical| {
302                    best_layout
303                        .get_physical(*logical)
304                        .expect("layout results map every logical qubit")
305                        .id()
306                })
307                .collect::<Vec<_>>();
308
309            score
310                .total
311                .total_cmp(&best_score.total)
312                .then_with(|| layout_signature.cmp(&best_signature))
313                .is_lt()
314        }) {
315            best = Some((layout, score));
316        }
317    }
318
319    let Some((layout, score)) = best else {
320        return Ok(if search_stats.stopped_by_call_limit {
321            Vf2PreparedOutcome::BudgetExhausted
322        } else {
323            Vf2PreparedOutcome::NoCandidate
324        });
325    };
326
327    let mut notes = Vec::new();
328    if search_stats.stopped_by_call_limit {
329        notes.push("vf2 search stopped after reaching call_limit".to_string());
330    }
331
332    Ok(Vf2PreparedOutcome::Found(LayoutResult {
333        layout,
334        diagnostics: LayoutDiagnostics {
335            is_perfect: true,
336            candidates_evaluated: scored,
337            used_fidelity: score.used_fidelity,
338            notes,
339        },
340        score: Some(score),
341    }))
342}
343
344/// Completes a partial VF2 mapping with deterministic assignments.
345///
346/// VF2 only maps logical qubits that participate in hard interaction
347/// constraints. This function assigns the remaining logical qubits to unused
348/// physical qubits, preferring high logical activity and low readout error when
349/// the objective asks for readout-aware scoring.
350fn complete_mapping(
351    mut mapping: BTreeMap<LogicalQubit, PhysicalQubit>,
352    analysis: &CircuitLayoutAnalysis,
353    physical: &PhysicalLayoutGraph,
354    objective: &LayoutObjective,
355    activity: &BTreeMap<LogicalQubit, f64>,
356) -> BTreeMap<LogicalQubit, PhysicalQubit> {
357    let occupied = mapping.values().copied().collect::<BTreeSet<_>>();
358    let mut free_physical = physical
359        .physical_qubits()
360        .iter()
361        .copied()
362        .filter(|qubit| !occupied.contains(qubit))
363        .collect::<Vec<_>>();
364    // Remaining physical qubits are deterministic. If readout fidelity is part
365    // of the objective, prefer lower readout error before qubit ID.
366    free_physical.sort_by(|left, right| {
367        if objective.readout_error_weight != 0.0 {
368            physical
369                .readout_error(*left)
370                .unwrap_or(f64::INFINITY)
371                .total_cmp(&physical.readout_error(*right).unwrap_or(f64::INFINITY))
372                .then_with(|| left.cmp(right))
373        } else {
374            left.cmp(right)
375        }
376    });
377
378    // Busier logical qubits get first choice among the remaining physical
379    // qubits. Interaction-free qubits all get activity 1.0 and fall back to ID.
380    let mut unmapped_logical = analysis
381        .logical_qubits
382        .iter()
383        .copied()
384        .filter(|logical| !mapping.contains_key(logical))
385        .collect::<Vec<_>>();
386    unmapped_logical.sort_by(|left, right| {
387        activity
388            .get(right)
389            .copied()
390            .unwrap_or(1.0)
391            .total_cmp(&activity.get(left).copied().unwrap_or(1.0))
392            .then_with(|| left.cmp(right))
393    });
394
395    for (logical, physical) in unmapped_logical.into_iter().zip(free_physical) {
396        mapping.insert(logical, physical);
397    }
398    mapping
399}
400
401/// Converts a complete logical-to-physical map into a validated [`Layout`].
402///
403/// The `algorithm` label is included in invariant-violation messages so caller
404/// diagnostics identify which layout implementation produced invalid internal
405/// state.
406fn layout_from_mapping(
407    analysis: &CircuitLayoutAnalysis,
408    physical: &PhysicalLayoutGraph,
409    mapping: BTreeMap<LogicalQubit, PhysicalQubit>,
410    algorithm: &str,
411) -> Result<Layout, CompilerError> {
412    Layout::new(
413        analysis.logical_qubits.clone(),
414        physical.physical_qubits().to_vec(),
415        Some(mapping),
416    )
417    .map_err(|error| {
418        CompilerError::InvariantViolation(format!(
419            "{algorithm} failed to construct a valid layout: {error}"
420        ))
421    })
422}