Skip to main content

cqlib_core/device/
result.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//! Quantum measurement results and execution status.
14//!
15//! This module provides types for representing quantum measurement outcomes,
16//! execution status tracking, and result aggregation with histogram data.
17//!
18//! # Core Types
19//!
20//! - [`Outcome`]: Compact bit vector for measurement outcomes
21//! - [`Status`]: Job execution state machine
22//! - [`ExecutionResult`]: Complete measurement results with metadata
23//!
24//! # Example
25//!
26//! ```rust,ignore
27//! use cqlib_core::device::{Outcome, ExecutionResult};
28//! use std::collections::HashMap;
29//!
30//! // Create a new execution result
31//! let mut result = ExecutionResult::new(
32//!     "task-001".to_string(),
33//!     vec![],
34//!     1000,  // shots
35//!     2,     // num_qubits
36//!     Some("simulator".to_string()),
37//!     None,
38//! );
39//!
40//! // Mark as running and complete with counts
41//! result.start(None);
42//! let mut counts = HashMap::new();
43//! counts.insert(Outcome::from_bitstring("00").unwrap(), 520);
44//! counts.insert(Outcome::from_bitstring("11").unwrap(), 480);
45//! result.finish(counts, None);
46//!
47//! // Calculate probabilities
48//! result.calc_probabilities();
49//! ```
50
51use crate::circuit::Qubit;
52use smallvec::SmallVec;
53use std::collections::HashMap;
54use std::fmt;
55use std::fmt::Write;
56use std::str::FromStr;
57use thiserror::Error;
58use time::OffsetDateTime;
59
60/// Number of bits per u64 chunk.
61const BITS_PER_CHUNK: usize = 64;
62
63/// Error type for outcome parsing failures.
64#[derive(Error, Debug, Clone, PartialEq, Eq)]
65pub enum OutcomeError {
66    /// Invalid character in binary string at given index.
67    #[error("Invalid character '{1}' in binary string at index {0}")]
68    InvalidCharacter(usize, char),
69}
70
71/// Measurement outcome as a compact bit vector.
72///
73/// # Bit Layout & Endianness (Little-Endian)
74///
75/// This struct uses **Little-Endian** storage:
76/// - **Low Index holds Low Bits**: `measured_qubits[0]` is stored in `chunks[0]`, bit 0.
77/// - **High Index holds High Bits**: `measured_qubits[64]` is stored in `chunks[1]`, bit 0.
78///
79/// ## Visual Mapping
80///
81/// ```text
82/// Memory Index:    [     0      ]  [     1      ] ...
83/// Data (u64):      [ 63 ... 0   ]  [ 127 ... 64 ]
84///                    ^      ^                 ^
85///                    |      |                 |
86/// Qubit Index:      MSB    LSB               LSB of next chunk
87/// ```
88///
89/// Note: while stored as little-endian chunks, bitstrings are written in standard
90/// binary form (MSB left). The original input width is not stored: parsing `"001"`
91/// and `"1"` produces the same outcome. Callers must provide the desired output
92/// width to [`Outcome::to_bitstring`].
93#[derive(Debug, Clone, PartialEq, Eq, Hash)]
94#[repr(transparent)]
95pub struct Outcome(pub SmallVec<[u64; 4]>);
96
97impl Outcome {
98    /// Creates an outcome from raw chunks.
99    #[inline(always)]
100    pub fn new(chunks: SmallVec<[u64; 4]>) -> Self {
101        Self(chunks)
102    }
103
104    /// Creates an outcome with the given bit indices set to one.
105    ///
106    /// `width` is the number of logical bits in the outcome. Indices greater
107    /// than or equal to `width` are ignored. Bit index 0 is stored as the least
108    /// significant bit, matching [`Outcome`]'s little-endian layout.
109    pub fn from_indices(width: usize, indices: impl IntoIterator<Item = usize>) -> Self {
110        let mut chunks = SmallVec::from_elem(0u64, width.div_ceil(BITS_PER_CHUNK));
111        for index in indices {
112            if index < width {
113                chunks[index / BITS_PER_CHUNK] |= 1u64 << (index % BITS_PER_CHUNK);
114            }
115        }
116        Self(chunks)
117    }
118
119    /// Creates an outcome from bits in little-endian order.
120    ///
121    /// `bits[0]` becomes bit index 0, the least-significant bit.
122    pub(crate) fn from_lsb_bits(bits: &[bool]) -> Self {
123        let mut chunks = SmallVec::from_elem(0u64, bits.len().div_ceil(BITS_PER_CHUNK));
124        for (index, value) in bits.iter().copied().enumerate() {
125            if value {
126                chunks[index / BITS_PER_CHUNK] |= 1u64 << (index % BITS_PER_CHUNK);
127            }
128        }
129        Self(chunks)
130    }
131
132    /// Create an Outcome from a binary string (e.g., "101").
133    ///
134    /// # Convention
135    /// - The string is interpreted as **Big-Endian visual order**.
136    /// - The **last character** (rightmost) corresponds to **Qubit 0** (LSB).
137    /// - The **first character** (leftmost) corresponds to **Qubit N-1** (MSB).
138    ///
139    /// Example: `from_bitstring("100")` -> Qubit 2=1, Qubit 1=0, Qubit 0=0.
140    pub fn from_bitstring(s: &str) -> Result<Self, OutcomeError> {
141        let len = s.len();
142        if len == 0 {
143            return Ok(Self(SmallVec::new()));
144        }
145
146        // Calculate required chunks (ceil(len / 64))
147        let num_chunks = len.div_ceil(BITS_PER_CHUNK);
148
149        // Pre-allocate and initialize to 0
150        let mut chunks = smallvec::smallvec![0u64; num_chunks];
151
152        // Iterate from rightmost character (qubit 0)
153        // enumerate() gives the qubit index directly (0, 1, 2...)
154        for (qubit_index, c) in s.chars().rev().enumerate() {
155            match c {
156                '1' => {
157                    let chunk_idx = qubit_index / BITS_PER_CHUNK;
158                    let bit_idx = qubit_index % BITS_PER_CHUNK;
159
160                    // Safety: chunk size is calculated from len, always valid
161                    chunks[chunk_idx] |= 1u64 << bit_idx;
162                }
163                '0' => {
164                    // Default is 0, no operation needed
165                }
166                _ => {
167                    // Calculate original string index for accurate error message
168                    let original_index = len - 1 - qubit_index;
169                    return Err(OutcomeError::InvalidCharacter(original_index, c));
170                }
171            }
172        }
173
174        Ok(Self(chunks))
175    }
176
177    /// Returns true if the bit at given index is 1.
178    #[inline]
179    pub fn is_one(&self, index: usize) -> bool {
180        let chunk_idx = index / BITS_PER_CHUNK;
181        let bit_idx = index % BITS_PER_CHUNK;
182
183        // Safety: SmallVec indexing is fast, usually bounds check is elided in loops
184        // if the compiler can prove bounds, but explicit check is safe.
185        if let Some(chunk) = self.0.get(chunk_idx) {
186            (chunk >> bit_idx) & 1 == 1
187        } else {
188            false
189        }
190    }
191
192    /// Formats the outcome as a binary string with the requested width.
193    ///
194    /// Output is big-endian (MSB left), padded with leading zeros. If the
195    /// requested width is smaller than the stored value, high bits are truncated.
196    pub fn to_bitstring(&self, num_qubits: usize) -> String {
197        if num_qubits == 0 {
198            return String::new();
199        }
200
201        let mut s = String::with_capacity(num_qubits);
202        let full_chunks = num_qubits / BITS_PER_CHUNK;
203        let remainder_bits = num_qubits % BITS_PER_CHUNK;
204        // Little-endian storage, but usually printed Big-endian (Q_n...Q_0)
205        if remainder_bits > 0 {
206            let chunk = self.0.get(full_chunks).copied().unwrap_or(0);
207            let mask = if remainder_bits == 64 {
208                u64::MAX
209            } else {
210                (1u64 << remainder_bits) - 1
211            };
212            let val = chunk & mask;
213            write!(&mut s, "{:0width$b}", val, width = remainder_bits).unwrap();
214        }
215
216        // Iterate from high index chunk to low index chunk
217        for i in (0..full_chunks).rev() {
218            let chunk = self.0.get(i).copied().unwrap_or(0);
219            // Print full 64 bits
220            // e.g., Chunk 0: bit 63 ... bit 0 prints as "0...01"
221            write!(&mut s, "{:064b}", chunk).unwrap();
222        }
223        s
224    }
225}
226
227impl FromStr for Outcome {
228    type Err = OutcomeError;
229
230    fn from_str(s: &str) -> Result<Self, Self::Err> {
231        Self::from_bitstring(s)
232    }
233}
234
235/// Execution status of a quantum job.
236#[derive(Debug, Clone, PartialEq, Eq, Hash)]
237pub enum Status {
238    /// Task has been submitted and is queued for execution.
239    Queued,
240    /// Task is currently running.
241    Running,
242    /// Task completed successfully.
243    Completed,
244    /// Task failed with an error.
245    Failed { error_msg: String, error_code: i32 },
246    /// Task was cancelled by the user.
247    Cancelled,
248}
249
250impl fmt::Display for Status {
251    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252        match self {
253            Status::Queued => write!(f, "Queued"),
254            Status::Running => write!(f, "Running"),
255            Status::Completed => write!(f, "Completed"),
256            Status::Failed {
257                error_msg,
258                error_code,
259            } => {
260                write!(f, "Failed (Code {}): {}", error_code, error_msg)
261            }
262            Status::Cancelled => write!(f, "Cancelled"),
263        }
264    }
265}
266
267impl Status {
268    /// Returns true if the status is terminal (completed, failed, or cancelled).
269    pub fn is_terminal(&self) -> bool {
270        matches!(
271            self,
272            Status::Completed | Status::Failed { .. } | Status::Cancelled
273        )
274    }
275
276    /// Returns true if the job completed successfully.
277    pub fn is_success(&self) -> bool {
278        matches!(self, Status::Completed)
279    }
280}
281
282/// Quantum measurement results with histogram data and metadata.
283///
284/// # Measurement Mapping & Bit Ordering
285///
286/// There is a strictly **1-to-1 correspondence** between the indices of the
287/// [`qubits`](ExecutionResult::qubits) vector and the bits in the [`Outcome`] keys.
288///
289/// - **Rule**: The qubit at `qubits[i]` corresponds to the **$i$-th bit** (value $2^i$) in the outcome.
290/// - **String Representation**: Since binary strings are printed in Big-Endian (MSB left),
291///   the string order is the **reverse** of the `qubits` vector order.
292///
293/// ## Example Mapping
294///
295/// Suppose we measure two qubits: `qubits = vec![Qubit(2), Qubit(5)]`.
296///
297/// | Index | Qubit ID | Outcome Bit (Storage) | String Position ("01") |
298/// | :---: | :---:    | :---:                 | :---:                  |
299/// | **0** | `Qubit(2)` | Bit 0 (LSB, $2^0$)  | **Last** (Rightmost)   |
300/// | **1** | `Qubit(5)` | Bit 1 (MSB, $2^1$)  | **First** (Leftmost)   |
301///
302/// If the result is `Qubit(2) = 1` and `Qubit(5) = 0`:
303/// - **Storage**: `...001` (Bit 0 is 1)
304/// - **String**: `"01"` (Qubit 5 is '0', Qubit 2 is '1')
305#[derive(Debug, Clone)]
306pub struct ExecutionResult {
307    /// Unique task identifier.
308    task_id: String,
309    /// Number of shots executed.
310    shots: usize,
311    /// Number of qubits measured.
312    num_qubits: usize,
313    /// Measured qubits.
314    qubits: Vec<Qubit>,
315    /// Current execution status.
316    status: Status,
317    /// Task creation timestamp.
318    created_at: OffsetDateTime,
319    /// Execution start timestamp (None if not started).
320    started_at: Option<OffsetDateTime>,
321    /// Execution finish timestamp (None if not finished).
322    finished_at: Option<OffsetDateTime>,
323    /// Backend name where executed.
324    backend: Option<String>,
325    /// Measurement counts per outcome.
326    counts: HashMap<Outcome, usize>,
327    /// Calculated probabilities (computed on demand).
328    probabilities: Option<HashMap<Outcome, f64>>,
329}
330
331impl ExecutionResult {
332    /// Creates a new execution result in Queued status.
333    pub fn new(
334        task_id: String,
335        qubits: Vec<Qubit>,
336        shots: usize,
337        num_qubits: usize,
338        backend: Option<String>,
339        created_at: Option<OffsetDateTime>,
340    ) -> Self {
341        Self {
342            task_id,
343            status: Status::Queued,
344            shots,
345            qubits,
346            num_qubits,
347            created_at: created_at.unwrap_or(OffsetDateTime::now_utc()),
348            started_at: None,
349            finished_at: None,
350            backend,
351            counts: HashMap::new(),
352            probabilities: None,
353        }
354    }
355
356    /// Creates a completed execution result from measurement counts.
357    ///
358    /// This applies the same lifecycle as `new`, `start`, `finish`, and
359    /// `calc_probabilities`, but keeps simulator call sites focused on
360    /// producing counts.
361    pub fn from_counts(
362        task_id: String,
363        qubits: Vec<Qubit>,
364        shots: usize,
365        num_qubits: usize,
366        backend: Option<String>,
367        counts: HashMap<Outcome, usize>,
368    ) -> Self {
369        let mut result = Self::new(task_id, qubits, shots, num_qubits, backend, None);
370        result.start(None).finish(counts, None).calc_probabilities();
371        result
372    }
373
374    /// Marks the job as running.
375    pub fn start(&mut self, t: Option<OffsetDateTime>) -> &mut Self {
376        self.started_at = t.or(Some(OffsetDateTime::now_utc()));
377        self.status = Status::Running;
378        self
379    }
380
381    /// Marks the job as completed with measurement counts.
382    pub fn finish(
383        &mut self,
384        counts: HashMap<Outcome, usize>,
385        t: Option<OffsetDateTime>,
386    ) -> &mut Self {
387        self.counts = counts;
388        self.probabilities = None;
389        self.status = Status::Completed;
390        self.finished_at = t.or(Some(OffsetDateTime::now_utc()));
391        self
392    }
393
394    /// Marks the job as failed with error code and message.
395    pub fn fail(&mut self, msg: String, code: i32) {
396        self.status = Status::Failed {
397            error_msg: msg,
398            error_code: code,
399        };
400    }
401
402    /// Marks the job as cancelled.
403    pub fn cancel(&mut self) {
404        self.status = Status::Cancelled;
405    }
406
407    /// Calculates probabilities from counts.
408    pub fn calc_probabilities(&mut self) -> &mut Self {
409        let total_observed: usize = self.counts.values().sum();
410        if total_observed == 0 {
411            self.probabilities = None;
412        } else {
413            let mut probs = HashMap::with_capacity(self.counts.len());
414            let inv_total = 1.0 / (total_observed as f64);
415            for (outcome, &count) in &self.counts {
416                probs.insert(outcome.clone(), count as f64 * inv_total);
417            }
418            self.probabilities = Some(probs);
419        }
420        self
421    }
422
423    /// Returns the task ID.
424    pub fn task_id(&self) -> &str {
425        self.task_id.as_str()
426    }
427
428    /// Returns the number of shots.
429    pub fn shots(&self) -> usize {
430        self.shots
431    }
432
433    /// Returns the number of qubits.
434    pub fn num_qubits(&self) -> usize {
435        self.num_qubits
436    }
437
438    /// Returns the measured qubits.
439    pub fn qubits(&self) -> &Vec<Qubit> {
440        &self.qubits
441    }
442
443    /// Returns the execution status.
444    pub fn status(&self) -> &Status {
445        &self.status
446    }
447
448    /// Returns the creation timestamp.
449    pub fn created_at(&self) -> &OffsetDateTime {
450        &self.created_at
451    }
452
453    /// Returns the start timestamp (None if not started).
454    pub fn started_at(&self) -> &Option<OffsetDateTime> {
455        &self.started_at
456    }
457
458    /// Returns the finish timestamp (None if not finished).
459    pub fn finished_at(&self) -> &Option<OffsetDateTime> {
460        &self.finished_at
461    }
462
463    /// Returns the backend name.
464    pub fn backend(&self) -> Option<&String> {
465        self.backend.as_ref()
466    }
467
468    /// Returns the measurement counts.
469    pub fn counts(&self) -> &HashMap<Outcome, usize> {
470        &self.counts
471    }
472
473    /// Returns the calculated probabilities (None if not computed).
474    pub fn probabilities(&self) -> &Option<HashMap<Outcome, f64>> {
475        &self.probabilities
476    }
477}
478
479#[cfg(test)]
480mod api_tests {
481    use super::{ExecutionResult, Outcome, Status};
482    use crate::circuit::Qubit;
483    use std::collections::HashMap;
484
485    #[test]
486    fn outcome_from_indices_uses_little_endian_bit_indices() {
487        let outcome = Outcome::from_indices(66, [0, 2, 65, 99]);
488
489        assert!(outcome.is_one(0));
490        assert!(!outcome.is_one(1));
491        assert!(outcome.is_one(2));
492        assert!(outcome.is_one(65));
493        assert!(!outcome.is_one(99));
494    }
495
496    #[test]
497    fn execution_result_from_counts_returns_completed_result_with_probabilities() {
498        let zero = Outcome::from_indices(1, []);
499        let one = Outcome::from_indices(1, [0]);
500        let mut counts = HashMap::new();
501        counts.insert(zero.clone(), 3);
502        counts.insert(one.clone(), 1);
503
504        let result = ExecutionResult::from_counts(
505            "task".to_string(),
506            vec![Qubit::new(0)],
507            4,
508            1,
509            Some("backend".to_string()),
510            counts,
511        );
512
513        assert_eq!(result.status(), &Status::Completed);
514        assert_eq!(result.counts().get(&zero), Some(&3));
515        assert_eq!(result.counts().get(&one), Some(&1));
516        let probabilities = result.probabilities().as_ref().unwrap();
517        assert_eq!(probabilities.get(&zero), Some(&0.75));
518        assert_eq!(probabilities.get(&one), Some(&0.25));
519    }
520}
521
522#[cfg(test)]
523#[path = "./result_test.rs"]
524mod result_test;