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;