Skip to main content

cqlib_core/compile/resource/
manager.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.
6// You may obtain a copy of this license in the LICENSE.txt file in
7// the root directory of this source tree or at
8// http://www.apache.org/licenses/LICENSE-2.0.
9//
10// Any modifications or derivative works of this code must retain this
11// copyright notice, and modified files need to carry a notice indicating
12// that they have been altered from the originals.
13
14use super::model::QubitOrigin;
15use super::{
16    AncillaRequirement, ResourceError, ResourceLease, ResourceLimits, ResourcePlan, ResourcePolicy,
17    ResourceRequest,
18};
19use crate::circuit::{Circuit, Qubit};
20use std::collections::{BTreeMap, BTreeSet};
21use std::sync::atomic::{AtomicU64, Ordering};
22
23static NEXT_MANAGER_ID: AtomicU64 = AtomicU64::new(0);
24
25/// Resource-allocation phase relative to logical-to-physical layout.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub(crate) enum ResourcePhase {
28    /// Logical ancillary qubits may still be created under policy limits.
29    PreLayout,
30    /// Only already-mapped ancillary resources may be reused.
31    PostLayout,
32}
33
34impl ResourcePhase {
35    const fn diagnostic_name(self) -> &'static str {
36        match self {
37            Self::PreLayout => "pre-layout",
38            Self::PostLayout => "post-layout",
39        }
40    }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44struct LeaseRecord {
45    requirement: AncillaRequirement,
46    qubits: Vec<Qubit>,
47}
48
49/// Tracks compiler-visible logical qubits and temporary ancillary-resource leases.
50///
51/// A manager is created from one circuit and must remain synchronized with that
52/// circuit. It separates resource selection from mutation: [`Self::preview`]
53/// returns a side-effect-free candidate, while [`Self::commit`] reserves the
54/// selected qubits and adds any planned pre-layout logical ancillas to the
55/// circuit. The caller must satisfy the lease's restoration contract before
56/// calling [`Self::release`].
57///
58/// The manager records contracts and resource ownership only. It does not
59/// analyze quantum state or prove that a consuming transform restored leased
60/// qubits correctly.
61#[derive(Debug)]
62pub struct ResourceManager {
63    id: u64,
64    phase: ResourcePhase,
65    resources: BTreeMap<Qubit, QubitOrigin>,
66    leased_qubits: BTreeSet<Qubit>,
67    active_leases: BTreeMap<u64, LeaseRecord>,
68    revision: u64,
69    next_lease_id: u64,
70    policy: ResourcePolicy,
71    limits: ResourceLimits,
72}
73
74impl ResourceManager {
75    /// Creates a pre-layout resource manager for the circuit's current qubits.
76    ///
77    /// Existing circuit qubits are registered as input resources. They are not
78    /// assumed to be clean at an arbitrary compiler boundary, but may later be
79    /// borrowed for a dirty request when permitted by [`ResourcePolicy`].
80    ///
81    /// The returned manager must be used with this circuit as both evolve.
82    ///
83    /// # Errors
84    ///
85    /// Returns [`ResourceError::CapacityExceeded`] when the input circuit
86    /// already exceeds `limits`, or [`ResourceError::CounterOverflow`] if an
87    /// internal manager identifier cannot be allocated.
88    pub fn from_circuit(
89        circuit: &Circuit,
90        policy: ResourcePolicy,
91        limits: ResourceLimits,
92    ) -> Result<Self, ResourceError> {
93        if let Some(limit) = limits.max_total_qubits
94            && circuit.num_qubits() > limit
95        {
96            return Err(ResourceError::CapacityExceeded {
97                limit,
98                requested_total: circuit.num_qubits(),
99            });
100        }
101
102        let resources = circuit
103            .qubits()
104            .into_iter()
105            .map(|qubit| (qubit, QubitOrigin::Input))
106            .collect();
107
108        Ok(Self {
109            id: next_manager_id()?,
110            phase: ResourcePhase::PreLayout,
111            resources,
112            leased_qubits: BTreeSet::new(),
113            active_leases: BTreeMap::new(),
114            revision: 0,
115            next_lease_id: 0,
116            policy,
117            limits,
118        })
119    }
120
121    /// Returns a side-effect-free resource preview for an algorithm candidate.
122    ///
123    /// Preview and commit are separate so a planner can compare feasible
124    /// algorithms without mutating the circuit or reserving resources. A clean
125    /// request may plan fresh logical ancillas before layout. A dirty request
126    /// only reuses registered resources and never plans fresh qubits.
127    ///
128    /// Qubits listed in [`ResourceRequest::excluded`] are never selected. The
129    /// returned plan is tied to this manager's current revision and becomes
130    /// stale after a successful commit, release, or phase transition.
131    ///
132    /// # Errors
133    ///
134    /// Returns [`ResourceError::InvalidRequest`] for malformed requests,
135    /// [`ResourceError::InsufficientResources`] when the active policy cannot
136    /// satisfy the request, [`ResourceError::CapacityExceeded`] when new clean
137    /// qubits would exceed the configured total, or
138    /// [`ResourceError::QubitIdOverflow`] when no new logical identifier can
139    /// be represented.
140    pub fn preview(&self, request: &ResourceRequest) -> Result<ResourcePlan, ResourceError> {
141        self.validate_request(request)?;
142
143        match request.requirement {
144            AncillaRequirement::CleanZero => self.preview_clean(request),
145            AncillaRequirement::Dirty => self.preview_dirty(request),
146        }
147    }
148
149    /// Commits a previously previewed resource selection.
150    ///
151    /// The plan must come from this manager's current revision. Successful
152    /// commits add any planned clean logical qubits to `circuit` atomically,
153    /// reserve all selected qubits, and return a lease that must later be passed
154    /// to [`Self::release`]. While the lease is active, its qubits cannot be
155    /// selected by another plan.
156    ///
157    /// # Errors
158    ///
159    /// Returns [`ResourceError::ForeignPlan`] for a plan created by another
160    /// manager, [`ResourceError::StalePlan`] for an outdated preview, or a
161    /// consistency, phase, capacity, or counter error when the commit cannot
162    /// be applied atomically.
163    pub fn commit(
164        &mut self,
165        circuit: &mut Circuit,
166        plan: ResourcePlan,
167    ) -> Result<ResourceLease, ResourceError> {
168        if plan.manager_id() != self.id {
169            return Err(ResourceError::ForeignPlan);
170        }
171        // A revision change means another candidate committed or the resource
172        // pool changed after preview, so the old selection must not be reused.
173        if plan.revision() != self.revision {
174            return Err(ResourceError::StalePlan {
175                plan_revision: plan.revision(),
176                current_revision: self.revision,
177            });
178        }
179
180        self.verify_consistency(circuit)?;
181        self.validate_plan(&plan)?;
182
183        let next_revision = self.next_revision()?;
184        let lease_id = self.next_lease_id;
185        let next_lease_id =
186            self.next_lease_id
187                .checked_add(1)
188                .ok_or(ResourceError::CounterOverflow {
189                    counter: "lease identifier",
190                })?;
191
192        if !plan.new_qubits().is_empty() {
193            circuit
194                .add_qubits(plan.new_qubits().to_vec())
195                .map_err(|error| ResourceError::CircuitResourceMismatch {
196                    reason: format!("circuit rejected validated ancillary qubits: {error}"),
197                })?;
198        }
199
200        for &qubit in plan.new_qubits() {
201            let previous = self.resources.insert(qubit, QubitOrigin::CompilerAllocated);
202            debug_assert!(previous.is_none());
203        }
204        for &qubit in plan.qubits() {
205            let inserted = self.leased_qubits.insert(qubit);
206            debug_assert!(inserted);
207        }
208
209        let lease = ResourceLease::new(
210            self.id,
211            lease_id,
212            plan.requirement(),
213            plan.qubits().to_vec(),
214        );
215        let previous = self.active_leases.insert(
216            lease_id,
217            LeaseRecord {
218                requirement: lease.requirement(),
219                qubits: lease.qubits().to_vec(),
220            },
221        );
222        debug_assert!(previous.is_none());
223
224        self.revision = next_revision;
225        self.next_lease_id = next_lease_id;
226        Ok(lease)
227    }
228
229    /// Releases a completed ancillary-resource lease.
230    ///
231    /// The consuming algorithm must satisfy its restoration contract before
232    /// calling this method. The manager records that contract but cannot prove
233    /// the algorithm's quantum semantics. Released qubits remain registered and
234    /// may satisfy later requests; releasing a lease does not remove qubits from
235    /// the circuit.
236    ///
237    /// # Errors
238    ///
239    /// Returns [`ResourceError::UnknownLease`] when the lease belongs to
240    /// another manager, has already been released, or does not match the
241    /// active record. Returns a consistency or counter error if internal
242    /// indexes cannot be updated safely.
243    pub fn release(&mut self, lease: &ResourceLease) -> Result<(), ResourceError> {
244        if lease.manager_id() != self.id {
245            return Err(ResourceError::UnknownLease {
246                lease_id: lease.id(),
247            });
248        }
249        let Some(record) = self.active_leases.get(&lease.id()) else {
250            return Err(ResourceError::UnknownLease {
251                lease_id: lease.id(),
252            });
253        };
254        if record.requirement != lease.requirement() || record.qubits.as_slice() != lease.qubits() {
255            return Err(ResourceError::UnknownLease {
256                lease_id: lease.id(),
257            });
258        }
259        if record
260            .qubits
261            .iter()
262            .any(|qubit| !self.leased_qubits.contains(qubit))
263        {
264            return Err(ResourceError::CircuitResourceMismatch {
265                reason: format!("lease {} is missing a leased-qubit record", lease.id()),
266            });
267        }
268
269        let next_revision = self.next_revision()?;
270        let record = self
271            .active_leases
272            .remove(&lease.id())
273            .ok_or(ResourceError::UnknownLease {
274                lease_id: lease.id(),
275            })?;
276        for qubit in record.qubits {
277            let removed = self.leased_qubits.remove(&qubit);
278            debug_assert!(removed);
279        }
280        self.revision = next_revision;
281        Ok(())
282    }
283
284    /// Enters the phase where all reusable logical ancillary qubits must
285    /// already be backed by a layout mapping.
286    ///
287    /// The transition is one-way and requires an idle, consistent manager.
288    /// After this call, previews may reuse existing compiler-allocated ancillas
289    /// but may not plan fresh logical qubits.
290    ///
291    /// # Errors
292    ///
293    /// Returns [`ResourceError::InvalidPhase`] unless the manager is currently
294    /// pre-layout, or a consistency or idle error when the circuit is not at a
295    /// valid layout boundary.
296    pub fn enter_post_layout(&mut self, circuit: &Circuit) -> Result<(), ResourceError> {
297        if self.phase != ResourcePhase::PreLayout {
298            return Err(ResourceError::InvalidPhase {
299                operation: "enter_post_layout",
300                expected: ResourcePhase::PreLayout.diagnostic_name(),
301                actual: self.phase.diagnostic_name(),
302            });
303        }
304        self.verify_idle(circuit)?;
305        let next_revision = self.next_revision()?;
306        self.phase = ResourcePhase::PostLayout;
307        self.revision = next_revision;
308        Ok(())
309    }
310
311    /// Checks that the circuit and all resource-manager indexes agree.
312    ///
313    /// This verifies structural bookkeeping only. It does not inspect quantum
314    /// state or establish that active consumers will satisfy their restoration
315    /// contracts.
316    ///
317    /// # Errors
318    ///
319    /// Returns [`ResourceError::CircuitResourceMismatch`] when a circuit
320    /// qubit, resource record, or lease index is inconsistent.
321    pub fn verify_consistency(&self, circuit: &Circuit) -> Result<(), ResourceError> {
322        let circuit_qubits: BTreeSet<_> = circuit.qubits().into_iter().collect();
323        for qubit in &circuit_qubits {
324            if !self.resources.contains_key(qubit) {
325                return Err(ResourceError::CircuitResourceMismatch {
326                    reason: format!("circuit qubit {qubit} has no resource record"),
327                });
328            }
329        }
330        for qubit in self.resources.keys() {
331            if !circuit_qubits.contains(qubit) {
332                return Err(ResourceError::CircuitResourceMismatch {
333                    reason: format!("resource qubit {qubit} is absent from the circuit"),
334                });
335            }
336        }
337        for qubit in &self.leased_qubits {
338            if !self.resources.contains_key(qubit) {
339                return Err(ResourceError::CircuitResourceMismatch {
340                    reason: format!("leased qubit {qubit} has no resource record"),
341                });
342            }
343        }
344
345        let mut indexed_lease_qubits = BTreeSet::new();
346        for (&lease_id, record) in &self.active_leases {
347            for &qubit in &record.qubits {
348                if !self.resources.contains_key(&qubit) {
349                    return Err(ResourceError::CircuitResourceMismatch {
350                        reason: format!("lease {lease_id} contains unknown qubit {qubit}"),
351                    });
352                }
353                if !indexed_lease_qubits.insert(qubit) {
354                    return Err(ResourceError::CircuitResourceMismatch {
355                        reason: format!("qubit {qubit} is present in overlapping leases"),
356                    });
357                }
358            }
359        }
360        if indexed_lease_qubits != self.leased_qubits {
361            return Err(ResourceError::CircuitResourceMismatch {
362                reason: "leased-qubit index does not match active leases".to_string(),
363            });
364        }
365        Ok(())
366    }
367
368    /// Checks resource indexes and requires every temporary lease to be released.
369    ///
370    /// Compiler stages should use this at boundaries where no algorithm may
371    /// retain temporary ancillary resources, including before entering
372    /// post-layout and after resource-consuming transformations complete.
373    ///
374    /// # Errors
375    ///
376    /// Returns the consistency errors documented by
377    /// [`Self::verify_consistency`], or [`ResourceError::ActiveLeaseRemaining`]
378    /// while any lease remains active.
379    pub fn verify_idle(&self, circuit: &Circuit) -> Result<(), ResourceError> {
380        self.verify_consistency(circuit)?;
381        if !self.active_leases.is_empty() {
382            return Err(ResourceError::ActiveLeaseRemaining {
383                count: self.active_leases.len(),
384            });
385        }
386        Ok(())
387    }
388
389    fn preview_clean(&self, request: &ResourceRequest) -> Result<ResourcePlan, ResourceError> {
390        let mut qubits = Vec::with_capacity(request.count);
391        self.select_origin(QubitOrigin::CompilerAllocated, request, &mut qubits);
392
393        let mut new_qubits = Vec::new();
394        if qubits.len() < request.count {
395            if self.phase == ResourcePhase::PostLayout {
396                // A fresh logical qubit created after layout would not have a
397                // physical mapping, so post-layout requests may only reuse.
398                return Err(self.insufficient(request, qubits.len()));
399            }
400
401            let required_new = request.count - qubits.len();
402            let allocated = self.num_compiler_allocated();
403            let available_new = self
404                .policy
405                .max_pre_layout_clean_ancillas
406                .saturating_sub(allocated);
407            if required_new > available_new {
408                return Err(self.insufficient(request, qubits.len() + available_new));
409            }
410            self.ensure_total_capacity(required_new)?;
411
412            // New logical IDs follow the current maximum. Checked arithmetic
413            // makes the Qubit(u32) representation boundary explicit.
414            new_qubits = self.allocate_new_qubits(required_new)?;
415            qubits.extend_from_slice(&new_qubits);
416        }
417
418        Ok(ResourcePlan::new(
419            self.id,
420            request.requirement,
421            qubits,
422            new_qubits,
423            self.revision,
424        ))
425    }
426
427    fn preview_dirty(&self, request: &ResourceRequest) -> Result<ResourcePlan, ResourceError> {
428        let mut qubits = Vec::with_capacity(request.count);
429        self.select_origin(QubitOrigin::CompilerAllocated, request, &mut qubits);
430        if self.policy.allow_dirty_borrowing {
431            self.select_origin(QubitOrigin::Input, request, &mut qubits);
432        }
433        if qubits.len() < request.count {
434            return Err(self.insufficient(request, qubits.len()));
435        }
436
437        Ok(ResourcePlan::new(
438            self.id,
439            request.requirement,
440            qubits,
441            Vec::new(),
442            self.revision,
443        ))
444    }
445
446    fn select_origin(
447        &self,
448        origin: QubitOrigin,
449        request: &ResourceRequest,
450        selected: &mut Vec<Qubit>,
451    ) {
452        for (&qubit, &qubit_origin) in &self.resources {
453            if selected.len() == request.count {
454                break;
455            }
456            if qubit_origin == origin
457                && !self.leased_qubits.contains(&qubit)
458                && !request.excluded.contains(&qubit)
459            {
460                selected.push(qubit);
461            }
462        }
463    }
464
465    fn validate_request(&self, request: &ResourceRequest) -> Result<(), ResourceError> {
466        if request.count == 0 {
467            return Err(ResourceError::InvalidRequest {
468                reason: "ancillary-qubit count must be greater than zero".to_string(),
469            });
470        }
471        for qubit in &request.excluded {
472            if !self.resources.contains_key(qubit) {
473                return Err(ResourceError::InvalidRequest {
474                    reason: format!("excluded qubit {qubit} is not registered"),
475                });
476            }
477        }
478        Ok(())
479    }
480
481    fn validate_plan(&self, plan: &ResourcePlan) -> Result<(), ResourceError> {
482        if !plan.new_qubits().is_empty() && self.phase != ResourcePhase::PreLayout {
483            return Err(ResourceError::InvalidPhase {
484                operation: "commit new logical ancillary qubits",
485                expected: ResourcePhase::PreLayout.diagnostic_name(),
486                actual: self.phase.diagnostic_name(),
487            });
488        }
489
490        let new_qubits: BTreeSet<_> = plan.new_qubits().iter().copied().collect();
491        if new_qubits.len() != plan.new_qubits().len() {
492            return Err(ResourceError::CircuitResourceMismatch {
493                reason: "resource plan contains duplicate new qubits".to_string(),
494            });
495        }
496        let selected_qubits: BTreeSet<_> = plan.qubits().iter().copied().collect();
497        if selected_qubits.len() != plan.qubits().len() {
498            return Err(ResourceError::CircuitResourceMismatch {
499                reason: "resource plan contains duplicate selected qubits".to_string(),
500            });
501        }
502        for qubit in &new_qubits {
503            if self.resources.contains_key(qubit) {
504                return Err(ResourceError::DuplicateQubit { qubit: *qubit });
505            }
506        }
507        for qubit in &selected_qubits {
508            if self.leased_qubits.contains(qubit) {
509                return Err(ResourceError::CircuitResourceMismatch {
510                    reason: format!("resource plan selected already-leased qubit {qubit}"),
511                });
512            }
513            if !self.resources.contains_key(qubit) && !new_qubits.contains(qubit) {
514                return Err(ResourceError::CircuitResourceMismatch {
515                    reason: format!("resource plan selected unknown qubit {qubit}"),
516                });
517            }
518        }
519        self.ensure_total_capacity(new_qubits.len())
520    }
521
522    fn allocate_new_qubits(&self, count: usize) -> Result<Vec<Qubit>, ResourceError> {
523        let mut qubits = Vec::with_capacity(count);
524        let mut next_id = match self.resources.last_key_value() {
525            Some((qubit, _)) => qubit
526                .id()
527                .checked_add(1)
528                .ok_or(ResourceError::QubitIdOverflow)?,
529            None => 0,
530        };
531        for index in 0..count {
532            qubits.push(Qubit::new(next_id));
533            if index + 1 < count {
534                next_id = next_id
535                    .checked_add(1)
536                    .ok_or(ResourceError::QubitIdOverflow)?;
537            }
538        }
539        Ok(qubits)
540    }
541
542    fn ensure_total_capacity(&self, additional: usize) -> Result<(), ResourceError> {
543        let requested_total = self
544            .resources
545            .len()
546            .checked_add(additional)
547            .ok_or_else(|| ResourceError::InvalidRequest {
548                reason: "logical-qubit count overflow".to_string(),
549            })?;
550        if let Some(limit) = self.limits.max_total_qubits
551            && requested_total > limit
552        {
553            return Err(ResourceError::CapacityExceeded {
554                limit,
555                requested_total,
556            });
557        }
558        Ok(())
559    }
560
561    fn num_compiler_allocated(&self) -> usize {
562        self.resources
563            .values()
564            .filter(|&&origin| origin == QubitOrigin::CompilerAllocated)
565            .count()
566    }
567
568    fn next_revision(&self) -> Result<u64, ResourceError> {
569        self.revision
570            .checked_add(1)
571            .ok_or(ResourceError::CounterOverflow {
572                counter: "revision",
573            })
574    }
575
576    fn insufficient(&self, request: &ResourceRequest, available: usize) -> ResourceError {
577        ResourceError::InsufficientResources {
578            requirement: request.requirement.diagnostic_name(),
579            requested: request.count,
580            available,
581        }
582    }
583}
584
585fn next_manager_id() -> Result<u64, ResourceError> {
586    NEXT_MANAGER_ID
587        .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1))
588        .map_err(|_| ResourceError::CounterOverflow {
589            counter: "manager identifier",
590        })
591}