Skip to main content

cqlib_core/device/
layout.rs

1// This code is part of Cqlib.
2//
3// (C) Copyright China Telecom Quantum Group 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// Modified from the original work.
14
15//! Logical-to-physical qubit layout management for circuit routing.
16//!
17//! [`Layout`] tracks where circuit logical qubits are placed on a quantum
18//! device. Physical qubits that do not carry a logical qubit remain vacant and
19//! may be used by routing or by a later compiler step that explicitly activates
20//! an auxiliary logical qubit.
21//!
22//! Layout does not allocate auxiliary qubits. Algorithm auxiliary qubits are
23//! logical circuit resources and must be managed by the compiler resource
24//! manager before they are bound to physical qubits.
25//!
26//! # Example
27//!
28//! ```
29//! use cqlib_core::device::{Layout, LogicalQubit, PhysicalQubit};
30//!
31//! let logical = vec![LogicalQubit::new(0), LogicalQubit::new(1)];
32//! let physical = vec![
33//!     PhysicalQubit::new(100),
34//!     PhysicalQubit::new(101),
35//!     PhysicalQubit::new(102),
36//! ];
37//!
38//! let mut layout = Layout::new(logical, physical, None).unwrap();
39//! assert_eq!(layout.num_vacant_physical(), 1);
40//!
41//! layout
42//!     .bind(LogicalQubit::new(2), PhysicalQubit::new(102))
43//!     .unwrap();
44//! assert_eq!(layout.num_vacant_physical(), 0);
45//! ```
46
47use crate::device::error::LayoutError;
48use crate::device::{LogicalQubit, PhysicalQubit};
49use std::collections::{BTreeMap, BTreeSet};
50
51/// Maps circuit logical qubits to physical qubits on a quantum device.
52///
53/// A layout owns the set of physical qubits available to a placement or
54/// routing step. Every logical qubit present in the layout has exactly one
55/// physical mapping. A physical qubit may be vacant.
56///
57/// Auxiliary qubit ownership and lifetime are intentionally outside this type.
58/// From the layout perspective, an auxiliary logical qubit behaves like any
59/// other logical qubit after it is explicitly [`Layout::bind`]ed.
60#[derive(Debug, Clone, Default, PartialEq, Eq)]
61pub struct Layout {
62    /// Physical qubits available to this layout.
63    physical_qubits: BTreeSet<PhysicalQubit>,
64    /// Mapping from circuit logical qubits to device physical qubits.
65    l2p: BTreeMap<LogicalQubit, PhysicalQubit>,
66    /// Reverse mapping from device physical qubits to circuit logical qubits.
67    p2l: BTreeMap<PhysicalQubit, LogicalQubit>,
68}
69
70impl Layout {
71    /// Creates a layout and maps each supplied logical qubit to a physical qubit.
72    ///
73    /// Entries in `init_map` are applied first. Remaining logical qubits are
74    /// mapped to remaining physical qubits in the order supplied by `logical`
75    /// and `physical`. Extra physical qubits remain vacant.
76    ///
77    /// # Errors
78    ///
79    /// Returns [`LayoutError`] if:
80    /// - there are more logical qubits than physical qubits;
81    /// - the logical or physical qubit list contains duplicate entries;
82    /// - `init_map` references an undeclared logical or physical qubit;
83    /// - `init_map` maps multiple logical qubits to one physical qubit.
84    ///
85    /// # Example
86    ///
87    /// ```
88    /// use cqlib_core::device::{Layout, LogicalQubit, PhysicalQubit};
89    /// use std::collections::BTreeMap;
90    ///
91    /// let logical = vec![LogicalQubit::new(0), LogicalQubit::new(1)];
92    /// let physical = vec![PhysicalQubit::new(100), PhysicalQubit::new(101)];
93    ///
94    /// let mut init = BTreeMap::new();
95    /// init.insert(LogicalQubit::new(0), PhysicalQubit::new(101));
96    ///
97    /// let layout = Layout::new(logical, physical, Some(init)).unwrap();
98    /// assert_eq!(
99    ///     layout.get_physical(LogicalQubit::new(0)),
100    ///     Some(PhysicalQubit::new(101)),
101    /// );
102    /// ```
103    pub fn new(
104        logical: Vec<LogicalQubit>,
105        physical: Vec<PhysicalQubit>,
106        init_map: Option<BTreeMap<LogicalQubit, PhysicalQubit>>,
107    ) -> Result<Self, LayoutError> {
108        if logical.len() > physical.len() {
109            return Err(LayoutError::TooManyLogicalQubits {
110                logical: logical.len(),
111                physical: physical.len(),
112            });
113        }
114
115        let mut logical_qubits = BTreeSet::new();
116        for logical in logical.iter().copied() {
117            if !logical_qubits.insert(logical) {
118                return Err(LayoutError::DuplicateLogicalQubit(logical));
119            }
120        }
121
122        let mut physical_qubits = BTreeSet::new();
123        for physical in physical.iter().copied() {
124            if !physical_qubits.insert(physical) {
125                return Err(LayoutError::DuplicatePhysicalQubit(physical));
126            }
127        }
128
129        let mut layout = Self {
130            physical_qubits,
131            l2p: BTreeMap::new(),
132            p2l: BTreeMap::new(),
133        };
134
135        if let Some(init_map) = init_map {
136            for (logical, physical) in init_map {
137                if !logical_qubits.contains(&logical) {
138                    return Err(LayoutError::InvalidLogicalQubit(logical));
139                }
140                if !layout.physical_qubits.contains(&physical) {
141                    return Err(LayoutError::InvalidPhysicalQubit(physical));
142                }
143                if layout.p2l.contains_key(&physical) {
144                    return Err(LayoutError::PhysicalQubitAlreadyOccupied(physical));
145                }
146
147                layout.l2p.insert(logical, physical);
148                layout.p2l.insert(physical, logical);
149            }
150        }
151
152        let vacant_physical: Vec<_> = physical
153            .into_iter()
154            .filter(|physical| !layout.p2l.contains_key(physical))
155            .collect();
156        let unmapped_logical: Vec<_> = logical
157            .into_iter()
158            .filter(|logical| !layout.l2p.contains_key(logical))
159            .collect();
160
161        for (logical, physical) in unmapped_logical.into_iter().zip(vacant_physical) {
162            debug_assert!(
163                !layout.p2l.contains_key(&physical),
164                "vacant physical qubit became occupied before initial mapping completed"
165            );
166            layout.l2p.insert(logical, physical);
167            layout.p2l.insert(physical, logical);
168        }
169
170        debug_assert_eq!(
171            layout.l2p.len(),
172            logical_qubits.len(),
173            "logical qubit count was validated against physical capacity"
174        );
175        debug_assert_eq!(
176            layout.l2p.len(),
177            layout.p2l.len(),
178            "each logical mapping must have one reverse mapping"
179        );
180        Ok(layout)
181    }
182
183    /// Creates a layout from `(logical, physical)` qubit ID pairs.
184    ///
185    /// Logical qubits are exactly the logical IDs that appear in
186    /// `logical_physical`. Physical qubits are `0..physical_count`; any
187    /// physical qubit not referenced by a pair remains vacant.
188    ///
189    /// # Errors
190    ///
191    /// Returns [`LayoutError`] if a logical ID appears more than once, a
192    /// physical ID appears more than once, or any physical ID is outside
193    /// `0..physical_count`.
194    ///
195    /// # Example
196    ///
197    /// ```
198    /// use cqlib_core::device::{Layout, LogicalQubit, PhysicalQubit};
199    ///
200    /// let layout = Layout::from_pairs(&[(0, 2), (1, 0)], 4).unwrap();
201    ///
202    /// assert_eq!(
203    ///     layout.get_physical(LogicalQubit::new(0)),
204    ///     Some(PhysicalQubit::new(2)),
205    /// );
206    /// assert_eq!(layout.num_vacant_physical(), 2);
207    /// ```
208    pub fn from_pairs(
209        logical_physical: &[(u32, u32)],
210        physical_count: u32,
211    ) -> Result<Self, LayoutError> {
212        let logical = logical_physical
213            .iter()
214            .map(|&(logical, _)| LogicalQubit::new(logical))
215            .collect::<Vec<_>>();
216        let physical = (0..physical_count)
217            .map(PhysicalQubit::new)
218            .collect::<Vec<_>>();
219        let init_map = logical_physical
220            .iter()
221            .map(|&(logical, physical)| (LogicalQubit::new(logical), PhysicalQubit::new(physical)))
222            .collect::<BTreeMap<_, _>>();
223        Self::new(logical, physical, Some(init_map))
224    }
225
226    /// Returns the number of mapped logical qubits.
227    pub fn num_logical(&self) -> usize {
228        self.l2p.len()
229    }
230
231    /// Returns the number of physical qubits available to the layout.
232    pub fn num_physical(&self) -> usize {
233        self.physical_qubits.len()
234    }
235
236    /// Returns the number of physical qubits that do not carry a logical qubit.
237    pub fn num_vacant_physical(&self) -> usize {
238        self.physical_qubits.len() - self.p2l.len()
239    }
240
241    /// Returns the physical qubit carrying `logical`, if it is bound.
242    pub fn get_physical(&self, logical: LogicalQubit) -> Option<PhysicalQubit> {
243        self.l2p.get(&logical).copied()
244    }
245
246    /// Returns the logical qubit carried by `physical`, if the position is occupied.
247    pub fn get_logical(&self, physical: PhysicalQubit) -> Option<LogicalQubit> {
248        self.p2l.get(&physical).copied()
249    }
250
251    /// Returns an iterator over mapped logical qubits.
252    pub fn logical_qubits(&self) -> impl Iterator<Item = LogicalQubit> + '_ {
253        self.l2p.keys().copied()
254    }
255
256    /// Returns an iterator over physical qubits available to the layout.
257    pub fn physical_qubits(&self) -> impl Iterator<Item = PhysicalQubit> + '_ {
258        self.physical_qubits.iter().copied()
259    }
260
261    /// Returns an iterator over vacant physical qubits.
262    pub fn vacant_physical_qubits(&self) -> impl Iterator<Item = PhysicalQubit> + '_ {
263        self.physical_qubits
264            .iter()
265            .copied()
266            .filter(|physical| !self.p2l.contains_key(physical))
267    }
268
269    /// Returns whether `physical` belongs to the layout and is vacant.
270    pub fn is_physical_vacant(&self, physical: PhysicalQubit) -> bool {
271        self.physical_qubits.contains(&physical) && !self.p2l.contains_key(&physical)
272    }
273
274    /// Returns the logical-to-physical qubit mapping.
275    pub fn l2p_map(&self) -> &BTreeMap<LogicalQubit, PhysicalQubit> {
276        &self.l2p
277    }
278
279    /// Returns the physical-to-logical qubit mapping.
280    pub fn p2l_map(&self) -> &BTreeMap<PhysicalQubit, LogicalQubit> {
281        &self.p2l
282    }
283
284    /// Binds an unmapped logical qubit to a vacant physical qubit.
285    ///
286    /// This operation may introduce a new logical qubit to the layout. The
287    /// caller is responsible for ensuring that the logical qubit exists in the
288    /// circuit and is registered with the compiler resource manager when
289    /// required.
290    ///
291    /// # Errors
292    ///
293    /// Returns [`LayoutError::InvalidPhysicalQubit`] if `physical` does not
294    /// belong to the layout. Returns [`LayoutError::LogicalQubitAlreadyBound`]
295    /// or [`LayoutError::PhysicalQubitAlreadyOccupied`] if either qubit already
296    /// participates in a mapping.
297    pub fn bind(
298        &mut self,
299        logical: LogicalQubit,
300        physical: PhysicalQubit,
301    ) -> Result<(), LayoutError> {
302        if !self.physical_qubits.contains(&physical) {
303            return Err(LayoutError::InvalidPhysicalQubit(physical));
304        }
305        if self.l2p.contains_key(&logical) {
306            return Err(LayoutError::LogicalQubitAlreadyBound(logical));
307        }
308        if self.p2l.contains_key(&physical) {
309            return Err(LayoutError::PhysicalQubitAlreadyOccupied(physical));
310        }
311
312        self.l2p.insert(logical, physical);
313        self.p2l.insert(physical, logical);
314        Ok(())
315    }
316
317    /// Removes the mapping for `logical` and returns the released physical qubit.
318    pub fn unbind(&mut self, logical: LogicalQubit) -> Result<PhysicalQubit, LayoutError> {
319        let physical = self
320            .l2p
321            .remove(&logical)
322            .ok_or(LayoutError::LogicalQubitNotBound(logical))?;
323        self.p2l.remove(&physical);
324        Ok(physical)
325    }
326
327    /// Swaps the logical qubits carried by two physical qubits.
328    ///
329    /// Either physical qubit may be vacant. Swapping an occupied qubit with a
330    /// vacant qubit moves the logical qubit to the vacant position.
331    ///
332    /// # Errors
333    ///
334    /// Returns [`LayoutError::InvalidPhysicalQubit`] if either physical qubit
335    /// does not belong to the layout.
336    pub fn swap_physical(
337        &mut self,
338        phys_a: PhysicalQubit,
339        phys_b: PhysicalQubit,
340    ) -> Result<(), LayoutError> {
341        if !self.physical_qubits.contains(&phys_a) {
342            return Err(LayoutError::InvalidPhysicalQubit(phys_a));
343        }
344        if !self.physical_qubits.contains(&phys_b) {
345            return Err(LayoutError::InvalidPhysicalQubit(phys_b));
346        }
347        if phys_a == phys_b {
348            return Ok(());
349        }
350
351        let logical_a = self.p2l.remove(&phys_a);
352        let logical_b = self.p2l.remove(&phys_b);
353
354        if let Some(logical) = logical_a {
355            self.l2p.insert(logical, phys_b);
356            self.p2l.insert(phys_b, logical);
357        }
358        if let Some(logical) = logical_b {
359            self.l2p.insert(logical, phys_a);
360            self.p2l.insert(phys_a, logical);
361        }
362
363        Ok(())
364    }
365}
366
367#[cfg(test)]
368#[path = "./layout_test.rs"]
369mod layout_test;