Skip to main content

ket/
execution.rs

1// SPDX-FileCopyrightText: 2026 Evandro Chagas Ribeiro da Rosa <evandro@quantuloop.com>
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Execution backend abstractions for the Libket quantum computing library.
6//!
7//! This module defines the traits and data types that decouple the compiler
8//! front-end from the concrete quantum hardware or simulator backend:
9//!
10//! - [`LiveExecution`]: a backend that receives gates one at a time and
11//!   returns measurement results immediately (mid-circuit feedback). Use this
12//!   mode when the circuit outcome influences subsequent gate choices
13//!   (e.g., adaptive circuits, mid-circuit resets).
14//! - [`BatchExecution`]: a backend that receives a fully-compiled native gate
15//!   sequence and returns aggregate results (histograms, expectation values).
16//!   Use this mode for variational algorithms, sampling, and expectation-value
17//!   estimation where the full circuit is known upfront.
18//! - [`NativeGateSet`]: an optional translation layer that maps the Libket
19//!   gate IR to a hardware-specific gate vocabulary. If omitted, the
20//!   built-in `RzRyCX` translation is used, which emits
21//!   `rz`, `ry`, and `cnot` native gates.
22//! - [`QuantumExecution`]: the union of both execution modes, stored inside a
23//!   [`crate::process::Process`].
24//!
25//! ## Error handling
26//!
27//! All trait methods return `Result<_, KetError>`. Backends should return
28//! [`KetError::ExecutionFailed`] for unrecoverable hardware faults and
29//! [`KetError::ShotCountInvalid`] when a requested shot count is out of range.
30
31use num::complex::ComplexFloat;
32use serde::{Deserialize, Serialize};
33
34use crate::{
35    error::KetError,
36    ir::{gate::GateInstruction, hamiltonian::Hamiltonian},
37    matrix::Matrix,
38};
39
40/// A bit-string measurement outcome encoded as a vector of 64-bit words.
41///
42/// Each element in the outer `Vec` is one measurement shot; within a shot the
43/// qubit outcomes are packed into 64-bit words (LSB = qubit 0).
44pub type BitString = Vec<u64>;
45
46/// The quantum-state snapshot returned by a `dump` operation.
47///
48/// The `i`-th basis state has amplitude `amplitudes_real[i] + i·amplitudes_imag[i]`.
49#[derive(Debug, Clone, Default, Deserialize, Serialize)]
50pub struct DumpData {
51    /// Basis states present in the superposition (as bit-strings).
52    pub basis_states: Vec<BitString>,
53    /// Real parts of the corresponding probability amplitudes.
54    pub amplitudes_real: Vec<f64>,
55    /// Imaginary parts of the corresponding probability amplitudes.
56    pub amplitudes_imag: Vec<f64>,
57}
58
59/// The result of a `sample` operation: `(bit_strings, counts)`.
60///
61/// Each element `bit_strings[i]` is a measured bit-string and `counts[i]` is
62/// the number of times it was observed across all shots.
63pub type SampleData = (Vec<BitString>, Vec<usize>);
64
65/// A backend that executes gates as they arrive and can return mid-circuit
66/// measurement results immediately.
67pub trait LiveExecution {
68    /// Dispatches a single logical gate instruction to the backend.
69    ///
70    /// # Errors
71    ///
72    /// Implementations may return a [`KetError`] if gate dispatch fails.
73    fn compute_gate(&mut self, gate: &GateInstruction) -> Result<(), KetError>;
74
75    /// Dispatches a sequence of already-translated native gate instructions to
76    /// the backend.
77    ///
78    /// Called by the live-execution path when a [`NativeGateSet`] translation
79    /// layer is configured. The `gates` slice contains hardware-specific
80    /// instructions produced by [`NativeGateSet::translate`] or
81    /// [`NativeGateSet::cnot`].
82    ///
83    /// # Errors
84    ///
85    /// Implementations may return a [`KetError`] if native gate dispatch fails.
86    fn compute_native_gates(&mut self, gates: &[NativeGate]) -> Result<(), KetError>;
87
88    /// Collapses and reads out the specified `qubits`, returning the result as
89    /// a packed integer (bit `i` corresponds to `qubits[i]`).
90    ///
91    /// # Errors
92    ///
93    /// Returns a [`KetError`] if measurement fails on the backend.
94    fn measure(&mut self, qubits: &[usize]) -> Result<u64, KetError>;
95
96    /// Returns a full state-vector snapshot restricted to `qubits`.
97    ///
98    /// # Errors
99    ///
100    /// Returns a [`KetError`] if state dumping fails on the backend.
101    fn dump(&mut self, qubits: &[usize]) -> Result<DumpData, KetError>;
102
103    /// Samples the measurement distribution of `qubits` over `shots` repetitions.
104    ///
105    /// # Errors
106    ///
107    /// Returns a [`KetError`] if sampling fails on the backend.
108    fn sample(&mut self, qubits: &[usize], shots: usize) -> Result<SampleData, KetError>;
109
110    /// Computes the expectation value of `hamiltonian` with respect to the
111    /// current quantum state.
112    ///
113    /// # Errors
114    ///
115    /// Returns a [`KetError`] if expectation value estimation fails on the backend.
116    fn exp_value(&mut self, hamiltonian: Hamiltonian) -> Result<f64, KetError>;
117}
118
119impl std::fmt::Debug for dyn LiveExecution {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        f.write_str("LiveExecution")
122    }
123}
124
125/// A hardware-native gate: `(name, angles, qubit_indices)`.
126///
127/// The `name` string identifies the gate in the target backend's vocabulary
128/// (e.g. `"cnot"`, `"rx"`, `"h"`). `angles` holds rotation parameters (may be empty).
129/// `qubit_indices` lists the qubits the gate acts on.
130pub type NativeGate = (String, Vec<f64>, Vec<usize>);
131
132/// A backend that receives a fully-compiled gate sequence and returns
133/// aggregate results without mid-circuit feedback.
134///
135/// There are two families of methods: **IR-level** (`sample`, `exp_value`,
136/// `gradient`) which receive [`GateInstruction`] slices directly from the
137/// Libket IR, and **native-level** (`sample_native`, `exp_value_native`) which
138/// receive sequences of [`NativeGate`] tuples already translated through a
139/// [`NativeGateSet`].
140///
141/// All methods have a default implementation that returns an error, so
142/// implementors only need to override the methods they support.
143pub trait BatchExecution {
144    /// Executes `gates` and samples the state of `qubits_to_sample` over
145    /// `shots` shots, receiving gates in the Libket IR format.
146    ///
147    /// Implement this method when the backend can accept [`GateInstruction`]
148    /// slices directly (i.e., no [`NativeGateSet`] translation is configured).
149    ///
150    /// # Errors
151    ///
152    /// Returns [`KetError::GateUnsupported`] by default. Implementations should
153    /// return [`KetError::ExecutionFailed`] for backend faults or
154    /// [`KetError::ShotCountInvalid`] for an out-of-range shot count.
155    fn sample(
156        &self,
157        _gates: &[GateInstruction],
158        _qubits_to_sample: &[usize],
159        _shots: usize,
160    ) -> Result<SampleData, KetError> {
161        Err(KetError::GateUnsupported)
162    }
163
164    /// Executes `gates` and computes the expectation value of each Hamiltonian
165    /// in `hamiltonian_list`, receiving gates in the Libket IR format.
166    ///
167    /// Implement this method when the backend can accept [`GateInstruction`]
168    /// slices directly (i.e., no [`NativeGateSet`] translation is configured).
169    ///
170    /// # Errors
171    ///
172    /// Returns [`KetError::GateUnsupported`] by default. Implementations should
173    /// return [`KetError::ExecutionFailed`] for backend faults.
174    fn exp_value(
175        &self,
176        _gates: &[GateInstruction],
177        _hamiltonian_list: &[Hamiltonian],
178    ) -> Result<Vec<f64>, KetError> {
179        Err(KetError::GateUnsupported)
180    }
181
182    /// Executes `gates` and samples the state of `qubits_to_sample` over
183    /// `shots` shots, receiving pre-translated native gates.
184    ///
185    /// Called when a [`NativeGateSet`] translation layer is configured. The
186    /// `gates` slice contains hardware-specific instructions produced by
187    /// [`NativeGateSet::translate`] or [`NativeGateSet::cnot`].
188    ///
189    /// # Errors
190    ///
191    /// Returns [`KetError::GateUnsupported`] by default. Implementations should
192    /// return [`KetError::ExecutionFailed`] for backend faults or
193    /// [`KetError::ShotCountInvalid`] for an out-of-range shot count.
194    fn sample_native(
195        &self,
196        _gates: &[NativeGate],
197        _qubits_to_sample: &[usize],
198        _shots: usize,
199    ) -> Result<SampleData, KetError> {
200        Err(KetError::GateUnsupported)
201    }
202
203    /// Executes `gates` and computes the expectation value of each Hamiltonian
204    /// in `hamiltonian_list`, receiving pre-translated native gates.
205    ///
206    /// Called when a [`NativeGateSet`] translation layer is configured. The
207    /// `gates` slice contains hardware-specific instructions produced by
208    /// [`NativeGateSet::translate`] or [`NativeGateSet::cnot`].
209    ///
210    /// # Errors
211    ///
212    /// Returns [`KetError::NativeGateUnsupported`] by default. Implementations
213    /// should return [`KetError::ExecutionFailed`] for backend faults.
214    fn exp_value_native(
215        &self,
216        _gates: &[NativeGate],
217        _hamiltonian_list: &[Hamiltonian],
218    ) -> Result<Vec<f64>, KetError> {
219        Err(KetError::NativeGateUnsupported)
220    }
221
222    /// Computes the expectation value and its gradient with respect to all
223    /// circuit parameters in a single backend call.
224    ///
225    /// Returns `(expectation_value, gradient_vector)` where
226    /// `gradient_vector[i]` is `∂⟨H⟩/∂θᵢ` for parameter `i`.
227    ///
228    /// This method is used when [`GradientStrategy::Native`] is selected. If
229    /// the backend cannot compute gradients natively, implement
230    /// [`GradientStrategy::ParameterShiftRule`] instead.
231    ///
232    /// # Errors
233    ///
234    /// Returns [`KetError::NativeGradientUnsuported`] by default. Implementations
235    /// should return [`KetError::ExecutionFailed`] for backend faults.
236    fn gradient(
237        &self,
238        _gates: &[GateInstruction],
239        _hamiltonian: &Hamiltonian,
240    ) -> Result<(f64, Vec<f64>), KetError> {
241        Err(KetError::NativeGradientUnsuported)
242    }
243}
244
245impl std::fmt::Debug for dyn BatchExecution {
246    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247        f.write_str("BatchExecution")
248    }
249}
250
251/// Strategy for computing expectation values in batch mode.
252#[derive(Debug, Clone, Copy, Deserialize)]
253pub enum ExpValueStrategy {
254    /// Delegate directly to the backend's native expectation-value primitive.
255    Native,
256    /// Estimate expectation values using qubit-wise commutation grouping.
257    QubitWiseCommutation(usize),
258    /// Estimate expectation values using the classical shadows protocol.
259    ClassicalShadows {
260        /// Weights for selecting the random measurement basis (X, Y, Z).
261        bias: (u8, u8, u8),
262        /// Number of measurement rounds.
263        samples: usize,
264        /// Number of shots per measurement round.
265        shots: usize,
266    },
267}
268
269/// Strategy for computing parameter-shift gradients.
270#[derive(Debug)]
271pub enum GradientStrategy {
272    /// Gradient computation is disabled.
273    None,
274    /// Use the parameter-shift rule to estimate gradients analytically.
275    ParameterShiftRule,
276    /// Use the backend's native gradient computation.
277    Native,
278}
279
280/// The execution target attached to a [`crate::process::Process`].
281#[derive(Debug)]
282pub enum QuantumExecution {
283    /// Mid-circuit (live) execution: gates are dispatched to the QPU
284    /// immediately as they are appended. Measurement results are available
285    /// inline, enabling adaptive circuits.
286    Live {
287        /// The live-mode QPU backend.
288        qpu: Box<dyn LiveExecution>,
289        /// When `true`, multi-qubit gate instructions are decomposed into
290        /// primitive CNOT + single-qubit gates before dispatch.
291        decompose: bool,
292        /// An optional translation layer for hardware-specific gate vocabularies.
293        native_gate_set: Option<Box<dyn NativeGateSet>>,
294    },
295    /// Deferred (batch) execution: the full compiled circuit is sent at once
296    /// after [`Process::execute`](crate::process::Process::execute) is called.
297    Batch {
298        /// The batch execution backend.
299        qpu: Box<dyn BatchExecution>,
300        /// An optional translation layer for hardware-specific gate vocabularies.
301        /// When `None`, the built-in [`RzRyCX`] identity set is used.
302        native_gate_set: Option<Box<dyn NativeGateSet>>,
303        /// The gradient computation strategy for variational circuits.
304        gradient: GradientStrategy,
305        /// The expectation-value estimation strategy.
306        exp_value: ExpValueStrategy,
307        /// Optional hardware coupling graph expressed as a list of undirected
308        /// edges `(i, j)`. When present, the qubit-mapping pass inserts SWAP
309        /// gates to satisfy connectivity constraints.
310        coupling_graph: Option<Vec<(usize, usize)>>,
311        /// When `true`, multi-qubit gate instructions are decomposed into
312        /// primitive CNOT + single-qubit gates before the circuit is sent
313        /// to the backend.
314        decompose: bool,
315    },
316}
317
318/// Translates Libket IR gates into hardware-specific native gate sequences.
319///
320/// Implement this trait to map the compiler's intermediate representation into
321/// the physical instruction set of a specific QPU or simulator. The default implementation
322/// ([`RzRyCX`]) uses a ZYZ Euler decomposition and emits `rz`, `ry`, and `cnot` instructions.
323pub trait NativeGateSet {
324    /// Translates the 2×2 complex unitary `matrix` acting on `target` into
325    /// zero or more native single-qubit gate instructions.
326    ///
327    /// The matrix is represented as `[[Cf64; 2]; 2]` (row-major). Backends
328    /// may recognise special patterns (H, Rz, etc.) and emit optimised
329    /// instructions accordingly.
330    ///
331    /// # Errors
332    ///
333    /// Returns [`KetError::NativeGateUnsupported`] if the matrix cannot be
334    /// translated into the backend's instruction set.
335    fn translate(&self, matrix: &Matrix, target: usize) -> Result<Vec<NativeGate>, KetError>;
336
337    /// Translates a CNOT gate into native gate instructions.
338    ///
339    /// # Errors
340    ///
341    /// Returns [`KetError::NativeGateUnsupported`] if CNOT is not supported.
342    fn cnot(&self, control: usize, target: usize) -> Result<Vec<NativeGate>, KetError>;
343}
344
345impl std::fmt::Debug for dyn NativeGateSet {
346    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
347        f.write_str("NativeGateSet")
348    }
349}
350
351/// The {RZ, RY, CNOT} native gateset.
352///
353/// For single-qubit gates the 2×2 unitary is decomposed via the ZYZ
354/// Euler decomposition, emitting `rz` and `ry` native gates
355/// (near-zero rotations are dropped). For CNOT
356/// it emits a single `cnot` gate. For SWAP it emits three `cnot` gates.
357///
358/// Used automatically when no [`NativeGateSet`] is configured in
359/// [`QuantumExecution::Batch`].
360pub type RzRyCX = ();
361
362impl NativeGateSet for RzRyCX {
363    fn translate(&self, matrix: &Matrix, target: usize) -> Result<Vec<NativeGate>, KetError> {
364        let [[a, b], [c, d]] = matrix;
365        let det = (*a * *d - *b * c).arg();
366
367        let theta = 2.0 * c.abs().atan2(a.abs());
368
369        let ang1 = d.arg();
370        let ang2 = c.arg();
371
372        let phi = ang1 + ang2 - det;
373        let lam = ang1 - ang2;
374
375        let mut gates = Vec::new();
376
377        const EPS: f64 = 1e-10;
378
379        if lam.abs() > EPS {
380            gates.push(("rz".to_owned(), vec![lam], vec![target]));
381        }
382        if theta.abs() > EPS {
383            gates.push(("ry".to_owned(), vec![theta], vec![target]));
384        }
385        if phi.abs() > EPS {
386            gates.push(("rz".to_owned(), vec![phi], vec![target]));
387        }
388
389        Ok(gates)
390    }
391
392    fn cnot(&self, c: usize, t: usize) -> Result<Vec<NativeGate>, KetError> {
393        Ok(vec![("cnot".to_owned(), vec![], vec![c, t])])
394    }
395}