cqlib_core/compile/transform/layout/
vf2.rs1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum Vf2EdgeRequirement {
39 PositiveInteractions,
41 AllInteractions,
43}
44
45impl Vf2EdgeRequirement {
46 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#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct Vf2LayoutConfig {
58 pub candidate_limit: usize,
63 pub call_limit: Option<usize>,
68 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
82pub(crate) enum Vf2PreparedOutcome {
88 Found(LayoutResult),
89 NoCandidate,
90 BudgetExhausted,
91}
92
93pub 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
143pub 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
165pub(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 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 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 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
344fn 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 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 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
401fn 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}