Skip to main content

cqlib_core/qis/
sampling.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//! Direct circuit sampling with the existing ideal simulators.
14
15use std::collections::{HashMap, HashSet};
16
17use rand::SeedableRng;
18use rand::distr::{Distribution, weighted::WeightedIndex};
19use rand::rngs::SmallRng;
20
21use crate::circuit::gate::ClassicalDataOp;
22use crate::circuit::{Circuit, Directive, Instruction, Qubit};
23use crate::device::{ExecutionResult, Outcome};
24use crate::qis::{DensityMatrix, QisError, StabilizerState, Statevector};
25
26/// Ideal simulator used by [`sample`].
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
28pub enum Simulator {
29    /// General pure-state simulation; the default.
30    #[default]
31    Statevector,
32    /// Density-matrix simulation, including deterministic reset channels.
33    DensityMatrix,
34    /// Simulation of the Clifford gates supported by [`StabilizerState`].
35    Stabilizer,
36}
37
38/// Samples a circuit from the all-zero initial state without changing it.
39///
40/// An explicit `qubits` list selects the final Z-basis outputs in bit order,
41/// with the first qubit displayed at the right of each result string. Otherwise,
42/// terminal measurement declarations select the outputs in first-occurrence
43/// order, with duplicates removed. Without declarations, all circuit qubits
44/// are sampled in circuit order, including idle qubits.
45///
46/// The circuit must have bound parameters and only terminal measurements.
47/// Classical control flow is unsupported. Barrier and Delay are no-ops; Store
48/// does not affect the returned quantum measurement counts. Reset requires
49/// [`Simulator::DensityMatrix`]. A seed makes counts reproducible for the same
50/// simulator and library version. `shots` and the selected output width must
51/// both be positive.
52///
53/// ```
54/// use cqlib_core::circuit::{Circuit, Qubit};
55/// use cqlib_core::qis::{sample, Simulator};
56///
57/// let mut circuit = Circuit::new(2);
58/// circuit.h(Qubit::new(0)).unwrap();
59/// circuit.cx(Qubit::new(0), Qubit::new(1)).unwrap();
60/// let result = sample(&circuit, 1000, Some(42), Simulator::default(), None).unwrap();
61/// assert_eq!(result.counts().values().sum::<usize>(), 1000);
62/// ```
63pub fn sample(
64    circuit: &Circuit,
65    shots: usize,
66    seed: Option<u64>,
67    simulator: Simulator,
68    qubits: Option<&[Qubit]>,
69) -> Result<ExecutionResult, QisError> {
70    if shots == 0 {
71        return Err(QisError::InvalidParameterValue(
72            "shots must be a positive integer".into(),
73        ));
74    }
75
76    if circuit
77        .operations()
78        .iter()
79        .any(|op| matches!(op.instruction, Instruction::ClassicalControl(_)))
80    {
81        return Err(QisError::UnsupportedOperation(
82            "sample does not support classical control flow".into(),
83        ));
84    }
85    let circuit = circuit.decompose()?;
86    if simulator != Simulator::DensityMatrix
87        && circuit
88            .operations()
89            .iter()
90            .any(|op| matches!(op.instruction, Instruction::Directive(Directive::Reset)))
91    {
92        return Err(QisError::UnsupportedOperation(
93            "sample with reset requires the density_matrix simulator".into(),
94        ));
95    }
96    super::state::validate_terminal_measurements(&circuit)?;
97
98    let circuit_qubits = circuit.qubits();
99    let mut selected = qubits.map(<[Qubit]>::to_vec).unwrap_or_else(|| {
100        let mut seen = HashSet::new();
101        circuit
102            .operations()
103            .iter()
104            .filter(|op| {
105                matches!(
106                    op.instruction,
107                    Instruction::ClassicalData(
108                        ClassicalDataOp::MeasureBit { .. } | ClassicalDataOp::MeasureBits { .. }
109                    )
110                )
111            })
112            .flat_map(|op| op.qubits.iter().copied())
113            .filter(|qubit| seen.insert(*qubit))
114            .collect()
115    });
116    if qubits.is_none() && selected.is_empty() {
117        selected = circuit_qubits.clone();
118    }
119    if selected.is_empty() {
120        return Err(QisError::InvalidParameterValue(
121            "sample requires at least one output qubit".into(),
122        ));
123    }
124    let qubit_positions: HashMap<_, _> = circuit_qubits
125        .iter()
126        .enumerate()
127        .map(|(position, &qubit)| (qubit, position))
128        .collect();
129    let mut seen = HashSet::new();
130    let positions = selected
131        .iter()
132        .map(|qubit| {
133            if !seen.insert(*qubit) {
134                return Err(QisError::InvalidParameterValue(
135                    "qubits must not contain duplicates".into(),
136                ));
137            }
138            qubit_positions.get(qubit).copied().ok_or_else(|| {
139                QisError::InvalidParameterValue(format!(
140                    "output qubit {} is not in the circuit",
141                    qubit.id()
142                ))
143            })
144        })
145        .collect::<Result<Vec<_>, _>>()?;
146
147    let mut counts = HashMap::new();
148    let backend = match simulator {
149        Simulator::Statevector | Simulator::DensityMatrix => {
150            let (mut probabilities, backend) = match simulator {
151                Simulator::Statevector => (
152                    Statevector::from_circuit(&circuit)?.probabilities(),
153                    "statevector",
154                ),
155                _ => (
156                    DensityMatrix::from_circuit(&circuit)?.probabilities(),
157                    "density_matrix",
158                ),
159            };
160            // Density-matrix evolution can leave tiny negative diagonal entries
161            // through roundoff, even for a gate followed by its inverse.
162            for probability in &mut probabilities {
163                if (-1e-12..0.0).contains(probability) {
164                    *probability = 0.0;
165                }
166            }
167            let distribution = WeightedIndex::new(probabilities)
168                .map_err(|error| QisError::InvalidParameterValue(error.to_string()))?;
169            let mut rng = seed.map_or_else(SmallRng::from_os_rng, SmallRng::seed_from_u64);
170            for _ in 0..shots {
171                let basis = distribution.sample(&mut rng);
172                let outcome = project(&positions, |position| basis & (1usize << position) != 0);
173                *counts.entry(outcome).or_insert(0) += 1;
174            }
175            backend
176        }
177        Simulator::Stabilizer => {
178            let mut state = StabilizerState::from_circuit(&circuit)?;
179            if let Some(seed) = seed {
180                state.seed_rng(seed);
181            }
182            for full in state.sample_shots(shots) {
183                let outcome = project(&positions, |position| full.is_one(position));
184                *counts.entry(outcome).or_insert(0) += 1;
185            }
186            "stabilizer"
187        }
188    };
189
190    Ok(ExecutionResult::from_counts(
191        format!("{backend}-sample"),
192        selected,
193        shots,
194        positions.len(),
195        Some(backend.into()),
196        counts,
197    ))
198}
199
200fn project(positions: &[usize], is_one: impl Fn(usize) -> bool) -> Outcome {
201    Outcome::from_indices(
202        positions.len(),
203        positions
204            .iter()
205            .enumerate()
206            .filter_map(|(bit, &position)| is_one(position).then_some(bit)),
207    )
208}