1use super::analysis::GateInteraction;
22use super::scoring::score_adjacent_interaction;
23use super::{
24 CircuitLayoutAnalysis, Interaction, LayoutDiagnostics, LayoutObjective, LayoutResult,
25 PhysicalLayoutGraph, analyze_circuit_for_layout, is_perfect_layout,
26};
27use crate::circuit::Circuit;
28use crate::compile::CompilerError;
29use crate::device::{Device, Layout, PhysicalQubit};
30use std::cmp::Ordering;
31use std::collections::BTreeMap;
32
33pub fn greedy_layout(
72 circuit: &Circuit,
73 device: &Device,
74 objective: &LayoutObjective,
75) -> Result<LayoutResult, CompilerError> {
76 let analysis = analyze_circuit_for_layout(circuit)?;
77 let physical = PhysicalLayoutGraph::from_device(device)?;
78 greedy_layout_prepared(&analysis, &physical, objective)
79}
80
81pub fn greedy_layout_prepared(
86 analysis: &CircuitLayoutAnalysis,
87 physical: &PhysicalLayoutGraph,
88 objective: &LayoutObjective,
89) -> Result<LayoutResult, CompilerError> {
90 match greedy_layout_candidate_prepared(analysis, physical, objective)? {
91 GreedyCandidateOutcome::Found(result) => Ok(result),
92 GreedyCandidateOutcome::Disconnected { left, right } => Err(CompilerError::InvalidInput(
93 format!("physical qubits {left} and {right} are disconnected in the usable topology"),
94 )),
95 }
96}
97
98pub(crate) enum GreedyCandidateOutcome {
100 Found(LayoutResult),
101 Disconnected {
102 left: PhysicalQubit,
103 right: PhysicalQubit,
104 },
105}
106
107pub(crate) fn greedy_layout_candidate_prepared(
108 analysis: &CircuitLayoutAnalysis,
109 physical: &PhysicalLayoutGraph,
110 objective: &LayoutObjective,
111) -> Result<GreedyCandidateOutcome, CompilerError> {
112 if analysis.logical_qubits.len() > physical.physical_qubits().len() {
113 return Err(CompilerError::InvalidInput(format!(
114 "greedy layout requires at least as many usable physical qubits as logical qubits; got {} logical qubits and {} usable physical qubits",
115 analysis.logical_qubits.len(),
116 physical.physical_qubits().len()
117 )));
118 }
119
120 let logical_index = analysis
123 .logical_qubits
124 .iter()
125 .copied()
126 .enumerate()
127 .map(|(index, logical)| (logical, index))
128 .collect::<BTreeMap<_, _>>();
129 let mut activity = vec![None; analysis.logical_qubits.len()];
130 for interaction in analysis.interactions.interactions() {
131 for logical in [interaction.left, interaction.right] {
132 let entry = &mut activity[logical_index[&logical]];
133 *entry = Some(entry.unwrap_or(0.0) + interaction.weight);
134 }
135 }
136 let pair_candidates = sorted_physical_pair_candidates(physical);
137 let mut mapping = vec![None; analysis.logical_qubits.len()];
138 let mut vacant = vec![true; physical.physical_qubits().len()];
139 let mut candidates_evaluated = 0usize;
140
141 let mut interaction_indices = analysis
144 .interactions
145 .interactions()
146 .iter()
147 .enumerate()
148 .filter(|(_, interaction)| interaction.weight > 0.0)
149 .map(|(index, _)| index)
150 .collect::<Vec<_>>();
151 interaction_indices.sort_by(|a, b| {
152 let a = &analysis.interactions.interactions()[*a];
153 let b = &analysis.interactions.interactions()[*b];
154 b.weight
155 .total_cmp(&a.weight)
156 .then_with(|| a.first_seen_order.cmp(&b.first_seen_order))
157 .then_with(|| a.left.cmp(&b.left))
158 .then_with(|| a.right.cmp(&b.right))
159 });
160
161 for interaction_index in interaction_indices {
162 let interaction = &analysis.interactions.interactions()[interaction_index];
163 let scored_interaction = ScoredInteraction {
164 interaction,
165 gate_contributions: analysis.interactions.gate_contributions(interaction_index),
166 };
167 let left_logical = logical_index[&interaction.left];
168 let right_logical = logical_index[&interaction.right];
169 match (mapping[left_logical], mapping[right_logical]) {
170 (Some(_), Some(_)) => {}
171 (Some(left_physical), None) => {
173 let (right_physical, evaluated) = choose_single_candidate(
174 scored_interaction,
175 right_logical,
176 left_physical,
177 true,
178 &vacant,
179 physical,
180 objective,
181 &activity,
182 )?;
183 candidates_evaluated += evaluated;
184 mapping[right_logical] = Some(right_physical);
185 vacant[right_physical] = false;
186 }
187 (None, Some(right_physical)) => {
188 let (left_physical, evaluated) = choose_single_candidate(
189 scored_interaction,
190 left_logical,
191 right_physical,
192 false,
193 &vacant,
194 physical,
195 objective,
196 &activity,
197 )?;
198 candidates_evaluated += evaluated;
199 mapping[left_logical] = Some(left_physical);
200 vacant[left_physical] = false;
201 }
202 (None, None) => {
205 let (left_physical, right_physical, evaluated) = choose_pair_candidate(
206 scored_interaction,
207 left_logical,
208 right_logical,
209 &vacant,
210 physical,
211 objective,
212 &activity,
213 &pair_candidates,
214 )?;
215 candidates_evaluated += evaluated;
216 mapping[left_logical] = Some(left_physical);
217 mapping[right_logical] = Some(right_physical);
218 vacant[left_physical] = false;
219 vacant[right_physical] = false;
220 }
221 }
222 }
223
224 for logical_index in 0..analysis.logical_qubits.len() {
225 if mapping[logical_index].is_some() {
226 continue;
227 }
228 let (physical_qubit, evaluated) = choose_idle_candidate(
231 logical_index,
232 &mapping,
233 &vacant,
234 physical,
235 objective,
236 &activity,
237 )?;
238 candidates_evaluated += evaluated;
239 mapping[logical_index] = Some(physical_qubit);
240 vacant[physical_qubit] = false;
241 }
242
243 let mut layout_mapping = BTreeMap::new();
244 for (logical_index, physical_index) in mapping.iter().copied().enumerate() {
245 let physical_index = physical_index.ok_or_else(|| {
246 CompilerError::InvariantViolation(
247 "greedy layout left a logical qubit unmapped".to_string(),
248 )
249 })?;
250 let physical_qubit = physical.physical_at(physical_index).ok_or_else(|| {
251 CompilerError::InvariantViolation(format!(
252 "greedy layout selected physical index {physical_index} outside target topology"
253 ))
254 })?;
255 layout_mapping.insert(analysis.logical_qubits[logical_index], physical_qubit);
256 }
257 let layout = Layout::new(
258 analysis.logical_qubits.clone(),
259 physical.physical_qubits().to_vec(),
260 Some(layout_mapping),
261 )
262 .map_err(|error| {
263 CompilerError::InvariantViolation(format!(
264 "greedy layout failed to construct a valid layout: {error}"
265 ))
266 })?;
267
268 if let Some((left, right)) = analysis
269 .interactions
270 .interactions()
271 .iter()
272 .filter_map(|interaction| {
273 Some((
274 layout.get_physical(interaction.left)?,
275 layout.get_physical(interaction.right)?,
276 ))
277 })
278 .find(|(left, right)| physical.distance(*left, *right).is_none())
279 {
280 return Ok(GreedyCandidateOutcome::Disconnected { left, right });
281 }
282 let score = objective.score_layout(analysis, physical, &layout)?;
283 let diagnostics = LayoutDiagnostics {
284 is_perfect: is_perfect_layout(analysis, physical, &layout),
285 candidates_evaluated,
286 used_fidelity: score.used_fidelity,
287 notes: Vec::new(),
288 };
289
290 Ok(GreedyCandidateOutcome::Found(LayoutResult {
291 layout,
292 score: Some(score),
293 diagnostics,
294 }))
295}
296
297#[allow(clippy::too_many_arguments)]
303fn choose_pair_candidate(
304 interaction: ScoredInteraction<'_>,
305 left_logical: usize,
306 right_logical: usize,
307 vacant: &[bool],
308 physical: &PhysicalLayoutGraph,
309 objective: &LayoutObjective,
310 activity: &[Option<f64>],
311 pair_candidates: &[(usize, usize)],
312) -> Result<(usize, usize, usize), CompilerError> {
313 let mut best: Option<(usize, usize, CandidateCost)> = None;
314 let mut evaluated = 0usize;
315
316 for (left_physical, right_physical) in pair_candidates.iter().copied() {
321 if !vacant[left_physical] || !vacant[right_physical] {
322 continue;
323 }
324 evaluated += 1;
325 update_best_pair(
326 interaction,
327 left_logical,
328 right_logical,
329 left_physical,
330 right_physical,
331 physical,
332 objective,
333 activity,
334 &mut best,
335 );
336 }
337
338 if best.is_none() {
339 for left_physical in 0..vacant.len() {
340 if !vacant[left_physical] {
341 continue;
342 }
343 for (right_physical, item) in vacant.iter().enumerate() {
344 if left_physical == right_physical || !item {
345 continue;
346 }
347 evaluated += 1;
348 update_best_pair(
349 interaction,
350 left_logical,
351 right_logical,
352 left_physical,
353 right_physical,
354 physical,
355 objective,
356 activity,
357 &mut best,
358 );
359 }
360 }
361 }
362
363 best.map(|(left, right, _)| (left, right, evaluated))
364 .ok_or_else(|| {
365 CompilerError::InvalidInput(
366 "greedy layout could not find two vacant physical qubits for an interaction"
367 .to_string(),
368 )
369 })
370}
371
372#[allow(clippy::too_many_arguments)]
378fn choose_single_candidate(
379 interaction: ScoredInteraction<'_>,
380 candidate_logical: usize,
381 anchored_physical: usize,
382 anchor_is_left: bool,
383 vacant: &[bool],
384 physical: &PhysicalLayoutGraph,
385 objective: &LayoutObjective,
386 activity: &[Option<f64>],
387) -> Result<(usize, usize), CompilerError> {
388 let mut best: Option<(usize, CandidateCost)> = None;
389 let mut evaluated = 0usize;
390
391 for (candidate_physical, item) in vacant.iter().enumerate() {
394 if !item {
395 continue;
396 }
397 evaluated += 1;
398 let (left_physical, right_physical) = if anchor_is_left {
399 (anchored_physical, candidate_physical)
400 } else {
401 (candidate_physical, anchored_physical)
402 };
403 let (left_logical, right_logical) = if anchor_is_left {
404 (usize::MAX, candidate_logical)
405 } else {
406 (candidate_logical, usize::MAX)
407 };
408 let cost = CandidateCost::for_interaction(
409 interaction,
410 left_logical,
411 right_logical,
412 left_physical,
413 right_physical,
414 physical,
415 objective,
416 activity,
417 );
418 let candidate = (candidate_physical, cost);
419 if best.as_ref().is_none_or(|best| {
420 compare_cost(candidate.1, best.1)
421 .then_with(|| {
422 physical.physical_qubits()[candidate.0].cmp(&physical.physical_qubits()[best.0])
423 })
424 .is_lt()
425 }) {
426 best = Some(candidate);
427 }
428 }
429
430 best.map(|(physical, _)| (physical, evaluated))
431 .ok_or_else(|| {
432 CompilerError::InvalidInput(
433 "greedy layout could not find a vacant physical qubit for an interaction"
434 .to_string(),
435 )
436 })
437}
438
439fn choose_idle_candidate(
444 logical: usize,
445 mapping: &[Option<usize>],
446 vacant: &[bool],
447 physical: &PhysicalLayoutGraph,
448 objective: &LayoutObjective,
449 activity: &[Option<f64>],
450) -> Result<(usize, usize), CompilerError> {
451 let active_physical = mapping.iter().flatten().copied().collect::<Vec<_>>();
452 let mut best: Option<(usize, usize, u64, f64)> = None;
453 let mut evaluated = 0usize;
454
455 for (candidate_physical, item) in vacant.iter().enumerate() {
459 if !item {
460 continue;
461 }
462 evaluated += 1;
463 let mut disconnected = 0usize;
464 let mut distance_sum = 0u64;
465 for mapped_physical in &active_physical {
466 match physical.distance_by_index(candidate_physical, *mapped_physical) {
467 Some(distance) => distance_sum += u64::from(distance),
468 None => disconnected += 1,
469 }
470 }
471
472 let readout = readout_cost(logical, candidate_physical, physical, objective, activity);
473 let candidate = (candidate_physical, disconnected, distance_sum, readout);
474 if best.as_ref().is_none_or(|best| {
475 candidate
476 .1
477 .cmp(&best.1)
478 .then_with(|| candidate.2.cmp(&best.2))
479 .then_with(|| candidate.3.total_cmp(&best.3))
480 .then_with(|| {
481 physical.physical_qubits()[candidate.0].cmp(&physical.physical_qubits()[best.0])
482 })
483 .is_lt()
484 }) {
485 best = Some(candidate);
486 }
487 }
488
489 best.map(|(physical, _, _, _)| (physical, evaluated))
490 .ok_or_else(|| {
491 CompilerError::InvalidInput(
492 "greedy layout could not find a vacant physical qubit for an idle logical qubit"
493 .to_string(),
494 )
495 })
496}
497
498#[derive(Debug, Clone, Copy)]
499struct ScoredInteraction<'a> {
500 interaction: &'a Interaction,
501 gate_contributions: &'a [GateInteraction],
502}
503
504#[derive(Debug, Clone, Copy)]
505struct CandidateCost {
506 disconnected: bool,
508 distance: u32,
510 objective_total: f64,
512}
513
514impl CandidateCost {
515 #[allow(clippy::too_many_arguments)]
520 fn for_interaction(
521 interaction: ScoredInteraction<'_>,
522 left_logical: usize,
523 right_logical: usize,
524 left_physical: usize,
525 right_physical: usize,
526 physical: &PhysicalLayoutGraph,
527 objective: &LayoutObjective,
528 activity: &[Option<f64>],
529 ) -> Self {
530 let Some(distance) = physical.distance_by_index(left_physical, right_physical) else {
531 return Self {
532 disconnected: true,
533 distance: u32::MAX,
534 objective_total: f64::INFINITY,
535 };
536 };
537
538 let adjacent = if distance == 1 {
539 score_adjacent_interaction(
540 interaction.gate_contributions,
541 left_physical,
542 right_physical,
543 physical,
544 )
545 } else {
546 Default::default()
547 };
548 let two_qubit_error = if distance == 1 && objective.two_qubit_error_weight != 0.0 {
549 adjacent.effective_two_qubit_error
550 } else {
551 0.0
552 };
553 let readout = readout_cost(left_logical, left_physical, physical, objective, activity)
554 + readout_cost(right_logical, right_physical, physical, objective, activity);
555
556 Self {
557 disconnected: false,
558 distance,
559 objective_total: objective.distance_weight
560 * interaction.interaction.weight
561 * f64::from(distance)
562 + objective.direction_weight * adjacent.direction
563 + objective.two_qubit_error_weight * two_qubit_error
564 + objective.readout_error_weight * readout,
565 }
566 }
567}
568
569fn readout_cost(
575 logical: usize,
576 physical_qubit: usize,
577 physical: &PhysicalLayoutGraph,
578 objective: &LayoutObjective,
579 activity: &[Option<f64>],
580) -> f64 {
581 if objective.readout_error_weight == 0.0 {
582 return 0.0;
583 }
584
585 physical
586 .readout_error_by_index(physical_qubit)
587 .map(|error| activity.get(logical).copied().flatten().unwrap_or(1.0) * error)
588 .unwrap_or(0.0)
589}
590
591fn compare_cost(a: CandidateCost, b: CandidateCost) -> Ordering {
593 a.disconnected
596 .cmp(&b.disconnected)
597 .then_with(|| a.distance.cmp(&b.distance))
598 .then_with(|| a.objective_total.total_cmp(&b.objective_total))
599}
600
601#[allow(clippy::too_many_arguments)]
606fn update_best_pair(
607 interaction: ScoredInteraction<'_>,
608 left_logical: usize,
609 right_logical: usize,
610 left_physical: usize,
611 right_physical: usize,
612 physical: &PhysicalLayoutGraph,
613 objective: &LayoutObjective,
614 activity: &[Option<f64>],
615 best: &mut Option<(usize, usize, CandidateCost)>,
616) {
617 let cost = CandidateCost::for_interaction(
618 interaction,
619 left_logical,
620 right_logical,
621 left_physical,
622 right_physical,
623 physical,
624 objective,
625 activity,
626 );
627 let candidate = (left_physical, right_physical, cost);
628 if best.as_ref().is_none_or(|best| {
629 compare_cost(candidate.2, best.2)
630 .then_with(|| {
631 physical.physical_qubits()[candidate.0].cmp(&physical.physical_qubits()[best.0])
632 })
633 .then_with(|| {
634 physical.physical_qubits()[candidate.1].cmp(&physical.physical_qubits()[best.1])
635 })
636 .is_lt()
637 }) {
638 *best = Some(candidate);
639 }
640}
641
642fn sorted_physical_pair_candidates(physical: &PhysicalLayoutGraph) -> Vec<(usize, usize)> {
644 let mut candidates = Vec::new();
647 for left in 0..physical.physical_qubits().len() {
648 for right in 0..physical.physical_qubits().len() {
649 if left != right && physical.distance_by_index(left, right).is_some() {
650 candidates.push((left, right));
651 }
652 }
653 }
654 candidates.sort_by(|a, b| {
655 physical
656 .distance_by_index(a.0, a.1)
657 .cmp(&physical.distance_by_index(b.0, b.1))
658 .then_with(|| physical.physical_qubits()[a.0].cmp(&physical.physical_qubits()[b.0]))
659 .then_with(|| physical.physical_qubits()[a.1].cmp(&physical.physical_qubits()[b.1]))
660 });
661 candidates
662}