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}