Skip to main content

ket/process/
mod.rs

1// SPDX-FileCopyrightText: 2026 Evandro Chagas Ribeiro da Rosa <evandro@quantuloop.com>
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Quantum process: the top-level unit of compilation and execution.
6//!
7//! A [`Process`] holds a [`BasicBlock`] of gate instructions together with
8//! measurement requests and drives them through compilation (qubit mapping,
9//! gate decomposition) and execution on a [`QuantumExecution`] backend.
10//!
11//! ## State machine
12//!
13//! A `Process` advances through the following states:
14//!
15//! ```text
16//! AcceptingGate
17//!   ├── append_block / alloc / param      (stays AcceptingGate)
18//!   ├── exp_value (batch, no gradient)  → AcceptingHamiltonian
19//!   ├── exp_value (batch, with gradient)→ ReadyToExecute
20//!   └── sample (batch)                  → ReadyToExecute
21//! AcceptingHamiltonian
22//!   ├── exp_value (batch, no gradient)     (stays AcceptingHamiltonian)
23//!   └── execute                          → Terminated
24//! ReadyToExecute
25//!   └── execute                          → Terminated
26//! Terminated
27//!   └── (no further mutations allowed)
28//! ```
29//!
30//! In **live** mode ([`QuantumExecution::Live`]) gates are dispatched to the
31//! QPU backend immediately; `execute` is not used. In **batch** mode
32//! ([`QuantumExecution::Batch`]) the full circuit is compiled and sent at once
33//! when [`Process::execute`] is called.
34
35mod classical_shadow;
36mod qwc;
37
38use std::collections::HashMap;
39use std::f64::consts::FRAC_PI_2;
40
41use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
42
43use crate::error::KetError;
44use crate::execution::{
45    BatchExecution, DumpData, ExpValueStrategy, GradientStrategy, NativeGate, NativeGateSet,
46    QuantumExecution, SampleData,
47};
48use crate::ir::gate::GateInstruction;
49use crate::ir::{
50    block::BasicBlock,
51    gate::{DecomposedGate, Param, QuantumGate},
52    hamiltonian::Hamiltonian,
53};
54use crate::mapping;
55use crate::matrix::Matrix;
56use crate::process::classical_shadow::execute_classical_shadows;
57use crate::process::qwc::execute_qwc_exp_value_live;
58
59/// Configuration of the quantum processing unit (QPU) available to this process.
60#[derive(Debug)]
61pub struct QPUConfig {
62    /// Maximum number of logical qubits that may be allocated by this process.
63    /// Attempting to allocate beyond this limit returns
64    /// [`KetError::QubitLimitExceeded`].
65    pub num_qubits: usize,
66    /// The execution backend (live or batch) used to run circuits.
67    /// When `None`, gate sequences can be built but execution will fail.
68    pub quantum_execution: Option<QuantumExecution>,
69}
70
71/// Internal state machine for a [`Process`].
72///
73/// The state advances monotonically; once `Terminated` no further mutations
74/// are possible. Transitions are triggered by [`Process::sample`],
75/// [`Process::exp_value`], and [`Process::execute`].
76#[derive(Debug, Default, Clone, Copy)]
77pub enum ProcessState {
78    /// The process is accepting gate instructions via [`Process::append_block`].
79    /// This is the initial state.
80    #[default]
81    AcceptingGate,
82    /// At least one Hamiltonian expectation-value request has been registered
83    /// via [`Process::exp_value`] (batch mode, no gradient). Additional
84    /// Hamiltonians may still be added.
85    AcceptingHamiltonian,
86    /// All measurements are registered and the process is ready to execute.
87    /// No further gates or Hamiltonian registrations are accepted.
88    ReadyToExecute,
89    /// The process has been executed and is in a terminal state. No further
90    /// instructions can be appended or execution triggered.
91    Terminated,
92}
93
94/// A quantum process: the top-level compilation and execution unit.
95///
96/// Create a [`Process`] with [`Process::new`], allocate qubits with
97/// [`Process::alloc`], append gate sequences with [`Process::append_block`],
98/// and trigger execution with [`Process::execute`] (batch mode) or read
99/// results immediately after each gate/measurement in live mode.
100///
101/// Each process follows a linear [`ProcessState`] machine and can be executed
102/// at most once. Results are cached and retrieved via [`Process::read_sample`],
103/// [`Process::read_exp_value`], and [`Process::read_gradient`].
104#[derive(Debug)]
105pub struct Process {
106    /// The accumulated gate sequence for this process.
107    main_block: BasicBlock,
108    /// QPU configuration (qubit count and execution backend).
109    qpu_config: QPUConfig,
110    /// Number of qubits that have been allocated so far.
111    qubit_counter: usize,
112    /// Logical qubit indices and shot count for a pending `sample` request.
113    qubits_to_sample: Option<(Vec<usize>, usize)>,
114    /// Cached sample result after execution.
115    sample: Option<SampleData>,
116    /// List of Hamiltonians registered for expectation-value computation,
117    /// in registration order.
118    hamiltonian_list: Vec<Hamiltonian>,
119    /// Cached expectation-value results after execution, one per Hamiltonian.
120    exp_value: Option<Vec<f64>>,
121    /// Concrete parameter values for variational circuits, in registration order.
122    parameters: Vec<f64>,
123    /// Cached gradient values after execution (parameter-shift rule),
124    /// one per registered parameter.
125    grad: Option<Vec<f64>>,
126    /// Current state of the process state machine.
127    state: ProcessState,
128    /// Angle threshold (in radians) below which a rotation is treated as the
129    /// identity and dropped during gate simplification.
130    epsilon: f64,
131    /// Index into `main_block.gates` of the next gate to dispatch in live mode.
132    live_pc: usize,
133}
134
135/// A borrowed view of a compiled gate sequence, dispatching to either the
136/// Libket IR path or the pre-translated native-gate path.
137///
138/// This enum is threaded through `execute_sample`, `execute_exp_value`, and
139/// the QWC/classical-shadows helpers so that the same logic works regardless
140/// of whether a [`NativeGateSet`] is configured.
141pub(super) enum GateList<'a> {
142    /// IR-level gates: passed directly to [`BatchExecution::sample`] /
143    /// [`BatchExecution::exp_value`] / [`BatchExecution::gradient`].
144    Ir { gates: &'a [GateInstruction] },
145    /// Pre-translated native gates: passed to [`BatchExecution::sample_native`]
146    /// / [`BatchExecution::exp_value_native`].
147    ///
148    /// The `native_gate_set` reference is kept so that basis-change gates
149    /// appended by QWC measurement circuits can also be translated on the fly.
150    Native {
151        gates: &'a [NativeGate],
152        native_gate_set: &'a dyn NativeGateSet,
153    },
154}
155
156/// An owned version of [`GateList`] produced by measurement-circuit
157/// construction (e.g., QWC basis-change appending).
158pub(super) enum GateListOwned {
159    /// Owned IR-level gate sequence.
160    Ir { gates: Vec<GateInstruction> },
161    /// Owned native gate sequence together with a reference to the gate-set
162    /// translator (needed for further basis-change translation).
163    Native { gates: Vec<NativeGate> },
164}
165
166impl Process {
167    /// Creates a new [`Process`] from the given [`QPUConfig`].
168    ///
169    /// The process starts in the [`ProcessState::AcceptingGate`] state with an
170    /// empty gate sequence, no allocated qubits, and an angle-cancellation
171    /// threshold (`epsilon`) of `1e-10` radians.
172    #[must_use]
173    pub fn new(qpu_config: QPUConfig) -> Self {
174        Self {
175            main_block: BasicBlock::new(),
176            qubit_counter: 0,
177            qpu_config,
178            qubits_to_sample: None,
179            sample: None,
180            hamiltonian_list: Vec::new(),
181            exp_value: None,
182            parameters: Vec::new(),
183            grad: None,
184            state: ProcessState::default(),
185            epsilon: 1e-10,
186            live_pc: 0,
187        }
188    }
189
190    /// Allocates the next available logical qubit and returns its index.
191    ///
192    /// # Errors
193    /// - [`KetError::ProcessTerminated`]: the process has already been executed.
194    /// - [`KetError::QubitLimitExceeded`]: all qubits permitted by the QPU
195    ///   configuration have been allocated.
196    pub const fn alloc(&mut self) -> Result<usize, KetError> {
197        if matches!(self.state, ProcessState::Terminated) {
198            Err(KetError::ProcessTerminated)
199        } else if self.qubit_counter < self.qpu_config.num_qubits {
200            let index = self.qubit_counter;
201            self.qubit_counter += 1;
202            Ok(index)
203        } else {
204            Err(KetError::QubitLimitExceeded)
205        }
206    }
207
208    /// Appends a [`BasicBlock`] of gate instructions to this process.
209    ///
210    /// If required, gate decomposition is performed in parallel before
211    /// the block is merged into the main circuit. In live mode every
212    /// gate is sent to the QPU backend immediately.
213    ///
214    /// # Errors
215    /// - [`KetError::GateAppendForbidden`]: the process is not in the
216    ///   gate-accepting state.
217    /// - [`KetError::QubitIndexOutOfRange`]: the block references a qubit
218    ///   that has not yet been allocated.
219    pub fn append_block(&mut self, mut block: BasicBlock) -> Result<(), KetError> {
220        if !matches!(self.state, ProcessState::AcceptingGate) {
221            return Err(KetError::GateAppendForbidden);
222        }
223
224        if block
225            .max_qubit_index()
226            .is_some_and(|index| index >= self.qubit_counter)
227        {
228            return Err(KetError::QubitIndexOutOfRange);
229        }
230
231        if self.need_to_decompose() {
232            block.gates.iter_mut().for_each(|gate| {
233                gate.decompose((self.qubit_counter, self.qpu_config.num_qubits));
234            });
235        }
236
237        self.main_block.append_block(block, Some(self.epsilon));
238        Ok(())
239    }
240
241    #[must_use]
242    fn need_to_decompose(&self) -> bool {
243        matches!(
244            self.qpu_config.quantum_execution,
245            Some(
246                QuantumExecution::Batch {
247                    coupling_graph: Some(_),
248                    ..
249                } | QuantumExecution::Batch {
250                    native_gate_set: Some(_),
251                    ..
252                } | QuantumExecution::Batch {
253                    decompose: true,
254                    ..
255                } | QuantumExecution::Live {
256                    decompose: true,
257                    ..
258                }
259            )
260        )
261    }
262
263    #[must_use]
264    /// Returns a reference to the accumulated gate sequence of this process.
265    ///
266    /// Useful for introspection or serialization before execution.
267    pub fn main_block(&self) -> &BasicBlock {
268        &self.main_block
269    }
270
271    /// Performs a single-shot mid-circuit measurement of `qubits`.
272    ///
273    /// # Errors
274    /// - [`KetError::MeasurementUnavailableInBatch`]: the process is not
275    ///   running in live execution mode.
276    pub fn measure(&mut self, qubits: &[usize]) -> Result<u64, KetError> {
277        self.live_execute()?;
278
279        if let Some(QuantumExecution::Live { qpu, .. }) = &mut self.qpu_config.quantum_execution {
280            Ok(qpu.measure(qubits)?)
281        } else {
282            Err(KetError::MeasurementUnavailableInBatch)
283        }
284    }
285
286    /// Dumps the quantum state of `qubits` (live mode only).
287    ///
288    /// # Errors
289    /// - [`KetError::DumpUnavailableInBatch`]: the process is not running in
290    ///   live execution mode.
291    pub fn dump(&mut self, qubits: &[usize]) -> Result<DumpData, KetError> {
292        self.live_execute()?;
293
294        if let Some(QuantumExecution::Live { qpu, .. }) = &mut self.qpu_config.quantum_execution {
295            Ok(qpu.dump(qubits)?)
296        } else {
297            Err(KetError::DumpUnavailableInBatch)
298        }
299    }
300
301    /// Drains any newly appended gates to the live backend.
302    ///
303    /// Iterates from `live_pc` (the position of the last dispatched gate) to
304    /// the end of `main_block.gates`, sending each gate to the QPU via
305    /// [`crate::execution::LiveExecution::compute_gate`]. A no-op in batch mode.
306    ///
307    /// # Errors
308    /// Propagates any [`KetError`] returned by the underlying backend.
309    fn live_execute(&mut self) -> Result<(), KetError> {
310        if let Some(QuantumExecution::Live {
311            qpu,
312            native_gate_set,
313            ..
314        }) = &mut self.qpu_config.quantum_execution
315        {
316            while self.live_pc < self.main_block.gates.len() {
317                let gate = &self.main_block.gates[self.live_pc];
318
319                if let Some(native_gate_set) = native_gate_set {
320                    let translated = if let Some(decomposed) = &gate.decomposed {
321                        Self::translate_circuit(
322                            &decomposed.iter().map(Into::into).collect::<Vec<_>>(),
323                            native_gate_set.as_ref(),
324                        )
325                    } else {
326                        Self::translate_circuit(
327                            std::slice::from_ref(gate),
328                            native_gate_set.as_ref(),
329                        )
330                    }?;
331
332                    qpu.compute_native_gates(&translated)?;
333                } else if let Some(decomposed) = &gate.decomposed {
334                    for gate in decomposed {
335                        qpu.compute_gate(&gate.into())?;
336                    }
337                } else {
338                    qpu.compute_gate(gate)?;
339                }
340                self.live_pc += 1;
341            }
342        }
343        Ok(())
344    }
345
346    /// Requests a measurement sample of `qubits` over `shots` repetitions.
347    ///
348    /// **Live mode:** The sample is taken immediately and returned as
349    /// `Ok(Some(SampleData))`.
350    ///
351    /// **Batch mode:** The request is recorded and `Ok(None)` is returned.
352    /// The process transitions to `ProcessState::ReadyToExecute`. Call
353    /// [`Process::execute`] to dispatch the circuit and then retrieve the
354    /// result with [`Process::read_sample`].
355    ///
356    /// # Errors
357    /// - [`KetError::SamplingUnavailable`]: a conflicting measurement has
358    ///   already been registered, or no execution backend is configured.
359    pub fn sample(
360        &mut self,
361        qubits: &[usize],
362        shots: usize,
363    ) -> Result<Option<SampleData>, KetError> {
364        self.live_execute()?;
365
366        match &mut self.qpu_config.quantum_execution {
367            Some(QuantumExecution::Live { qpu, .. }) => Ok(Some(qpu.sample(qubits, shots)?)),
368            Some(QuantumExecution::Batch { .. }) => {
369                if matches!(self.state, ProcessState::AcceptingGate) {
370                    self.state = ProcessState::ReadyToExecute;
371                    self.qubits_to_sample = Some((qubits.to_owned(), shots));
372                    Ok(None)
373                } else {
374                    Err(KetError::SamplingUnavailable)
375                }
376            }
377            None => Err(KetError::SamplingUnavailable),
378        }
379    }
380
381    /// Returns a reference to the cached [`SampleData`] from the most recent
382    /// execution, or `None` if no sample result is available yet.
383    #[must_use]
384    pub fn read_sample(&self) -> Option<&SampleData> {
385        self.sample.as_ref()
386    }
387
388    /// Requests the expectation value `⟨ψ|H|ψ⟩` of `hamiltonian`.
389    ///
390    /// **Live mode:** The expectation value is computed immediately and
391    /// returned as `Ok(Some(f64))`.
392    ///
393    /// **Batch mode (no gradient):** The Hamiltonian is appended to the
394    /// internal list. The process stays in (or transitions to)
395    /// [`ProcessState::AcceptingHamiltonian`], allowing additional
396    /// Hamiltonians to be registered before execution.
397    ///
398    /// **Batch mode (with gradient):** The Hamiltonian is registered and the
399    /// process immediately transitions to [`ProcessState::ReadyToExecute`]
400    /// because the parameter-shift rule requires exactly one observable.
401    ///
402    /// In both batch cases `Ok(None)` is returned; call [`Process::execute`]
403    /// followed by [`Process::read_exp_value`] to retrieve the results.
404    ///
405    /// # Errors
406    /// - [`KetError::ExpectationValueUnavailable`]: the process is in an
407    ///   incompatible state (e.g., already terminated or a sample request has
408    ///   been registered), or no execution backend is configured.
409    pub fn exp_value(&mut self, hamiltonian: Hamiltonian) -> Result<Option<f64>, KetError> {
410        self.live_execute()?;
411
412        match &mut self.qpu_config.quantum_execution {
413            Some(QuantumExecution::Live { qpu, .. }) => Ok(Some(execute_qwc_exp_value_live(
414                qpu.as_mut(),
415                &hamiltonian,
416            )?)),
417            Some(QuantumExecution::Batch { gradient, .. }) => {
418                if matches!(
419                    self.state,
420                    ProcessState::AcceptingGate | ProcessState::AcceptingHamiltonian
421                ) {
422                    self.state = if matches!(gradient, GradientStrategy::None) {
423                        ProcessState::AcceptingHamiltonian
424                    } else {
425                        ProcessState::ReadyToExecute
426                    };
427
428                    self.hamiltonian_list.push(hamiltonian);
429                    Ok(None)
430                } else {
431                    Err(KetError::ExpectationValueUnavailable)
432                }
433            }
434            None => Err(KetError::ExpectationValueUnavailable),
435        }
436    }
437
438    /// Returns a slice of the cached expectation-value results from the most
439    /// recent execution, or `None` if no results are available yet.
440    ///
441    /// The slice contains one value per [`Hamiltonian`] registered via
442    /// [`Process::exp_value`], in registration order.
443    #[must_use]
444    pub fn read_exp_value(&self) -> Option<&[f64]> {
445        self.exp_value.as_deref()
446    }
447
448    /// Returns a slice of the cached gradient values from the most recent
449    /// execution, or `None` if gradient computation was not enabled or has
450    /// not yet been performed.
451    ///
452    /// The slice contains one value per registered parameter (in registration
453    /// order). Each element is the partial derivative of the expectation value
454    /// with respect to that parameter.
455    #[must_use]
456    pub fn read_gradient(&self) -> Option<&[f64]> {
457        self.grad.as_deref()
458    }
459
460    /// Registers a new differentiable parameter with initial value `param`
461    /// and returns a [`Param::Ref`] token for embedding in gate rotation angles.
462    pub fn param(&mut self, param: f64) -> Param {
463        let index = self.parameters.len();
464        self.parameters.push(param);
465
466        Param::Ref {
467            index,
468            multiplier: 1.0,
469            value: param,
470        }
471    }
472
473    /// Compiles and executes the accumulated circuit (batch mode only).
474    ///
475    /// # Errors
476    /// - [`KetError::NoPendingMeasurement`]: no `sample` or `exp_value`
477    ///   request has been registered before calling `execute`.
478    /// - [`KetError::ExplicitExecuteInLiveMode`]: the process is in live
479    ///   execution mode, where gates are dispatched immediately.
480    pub fn execute(&mut self) -> Result<(), KetError> {
481        if matches!(self.state, ProcessState::Terminated) {
482            return Ok(());
483        }
484
485        if !matches!(
486            self.state,
487            ProcessState::AcceptingHamiltonian | ProcessState::ReadyToExecute
488        ) {
489            return Err(KetError::NoPendingMeasurement);
490        }
491
492        let (final_gates, mapped_hamiltonian_list, mapped_qubits_to_sample) =
493            if let Some(QuantumExecution::Batch {
494                coupling_graph: Some(coupling_graph),
495                ..
496            }) = self.qpu_config.quantum_execution.as_ref()
497            {
498                let (gates, hamiltonian, qubits_to_sample) = mapping::map_circuit(
499                    &self.main_block,
500                    &self.hamiltonian_list,
501                    &self.qubits_to_sample.clone().unwrap_or((vec![], 0)).0,
502                    coupling_graph,
503                    self.qpu_config.num_qubits,
504                )?;
505                let qubits_to_sample = self
506                    .qubits_to_sample
507                    .clone()
508                    .map(|(_, shots)| (qubits_to_sample, shots));
509
510                (gates, hamiltonian, qubits_to_sample)
511            } else {
512                (
513                    std::mem::take(&mut self.main_block.gates),
514                    std::mem::take(&mut self.hamiltonian_list),
515                    self.qubits_to_sample.take(),
516                )
517            };
518
519        let mut quantum_execution = self.qpu_config.quantum_execution.take();
520
521        if let Some(QuantumExecution::Batch {
522            qpu,
523            native_gate_set,
524            gradient,
525            exp_value,
526            ..
527        }) = &mut quantum_execution
528        {
529            if let Some((qubits_to_sample, shots)) = &mapped_qubits_to_sample {
530                if let Some(native_gate_set) = native_gate_set {
531                    let gates = GateList::Native {
532                        gates: &Self::translate_circuit(&final_gates, native_gate_set.as_ref())?,
533                        native_gate_set: native_gate_set.as_ref(),
534                    };
535                    self.sample = Some(Self::execute_sample(
536                        qpu.as_ref(),
537                        gates,
538                        qubits_to_sample,
539                        *shots,
540                    )?);
541                } else {
542                    let gates = GateList::Ir {
543                        gates: &final_gates,
544                    };
545                    self.sample = Some(Self::execute_sample(
546                        qpu.as_ref(),
547                        gates,
548                        qubits_to_sample,
549                        *shots,
550                    )?);
551                }
552            } else if !mapped_hamiltonian_list.is_empty() {
553                self.grad = match gradient {
554                    GradientStrategy::None => None,
555                    GradientStrategy::Native => {
556                        let (exp_result, grad) =
557                            qpu.gradient(&final_gates, &mapped_hamiltonian_list[0])?;
558                        self.exp_value = Some(vec![exp_result]);
559                        Some(grad)
560                    }
561                    GradientStrategy::ParameterShiftRule => Some(Self::parameter_shift(
562                        qpu.as_ref(),
563                        native_gate_set.as_ref().map(std::convert::AsRef::as_ref),
564                        self.qpu_config.num_qubits,
565                        &final_gates,
566                        &self.parameters,
567                        &mapped_hamiltonian_list,
568                        exp_value,
569                    )?),
570                };
571
572                if self.exp_value.is_none() {
573                    if let Some(native_gate_set) = native_gate_set {
574                        let gates = GateList::Native {
575                            gates: &Self::translate_circuit(
576                                &final_gates,
577                                native_gate_set.as_ref(),
578                            )?,
579                            native_gate_set: native_gate_set.as_ref(),
580                        };
581
582                        self.exp_value = Some(Self::execute_exp_value(
583                            qpu.as_ref(),
584                            self.qpu_config.num_qubits,
585                            gates,
586                            &mapped_hamiltonian_list,
587                            exp_value,
588                        )?);
589                    } else {
590                        let gates = GateList::Ir {
591                            gates: &final_gates,
592                        };
593                        self.exp_value = Some(Self::execute_exp_value(
594                            qpu.as_ref(),
595                            self.qpu_config.num_qubits,
596                            gates,
597                            &mapped_hamiltonian_list,
598                            exp_value,
599                        )?);
600                    }
601                }
602            }
603        } else {
604            return Err(KetError::ExplicitExecuteInLiveMode);
605        }
606
607        self.qpu_config.quantum_execution = quantum_execution;
608
609        self.state = ProcessState::Terminated;
610
611        Ok(())
612    }
613
614    /// Delegates a sampling request to the batch backend.
615    fn execute_sample(
616        qpu: &(impl BatchExecution + ?Sized),
617        gates: GateList,
618        qubits_to_sample: &[usize],
619        shots: usize,
620    ) -> Result<SampleData, KetError> {
621        match gates {
622            GateList::Ir { gates } => qpu.sample(gates, qubits_to_sample, shots),
623            GateList::Native { gates, .. } => qpu.sample_native(gates, qubits_to_sample, shots),
624        }
625    }
626
627    /// Dispatches an expectation-value computation to the appropriate strategy.
628    ///
629    /// Selects among [`ExpValueStrategy::Native`],
630    /// [`ExpValueStrategy::ClassicalShadows`], and
631    /// [`ExpValueStrategy::QubitWiseCommutation`] based on `exp_value_strategy`.
632    fn execute_exp_value(
633        qpu: &(impl BatchExecution + ?Sized),
634        num_qubits: usize,
635        gates: GateList,
636        hamiltonian_list: &[Hamiltonian],
637        exp_value_strategy: &ExpValueStrategy,
638    ) -> Result<Vec<f64>, KetError> {
639        match exp_value_strategy {
640            ExpValueStrategy::Native => {
641                Self::execute_native_exp_value(qpu, gates, hamiltonian_list)
642            }
643            ExpValueStrategy::ClassicalShadows {
644                bias,
645                samples,
646                shots,
647            } => execute_classical_shadows(
648                qpu,
649                num_qubits,
650                gates,
651                hamiltonian_list,
652                *samples,
653                *shots,
654                *bias,
655            ),
656            ExpValueStrategy::QubitWiseCommutation(shots) => {
657                qwc::execute_qwc(qpu, num_qubits, gates, hamiltonian_list, *shots)
658            }
659        }
660    }
661
662    /// Delegates expectation-value computation directly to the backend's
663    /// native primitive ([`BatchExecution::exp_value`]).
664    fn execute_native_exp_value(
665        qpu: &(impl BatchExecution + ?Sized),
666        gates: GateList,
667        hamiltonian_list: &[Hamiltonian],
668    ) -> Result<Vec<f64>, KetError> {
669        qwc::execute_qwc_exp_value(qpu, gates, hamiltonian_list)
670    }
671
672    fn translate_circuit(
673        gates: &[GateInstruction],
674        native_gate_set: &(impl NativeGateSet + ?Sized),
675    ) -> Result<Vec<NativeGate>, KetError> {
676        let mut physical_circuit = Vec::new();
677        let mut pending_sq = HashMap::<usize, Matrix>::new();
678
679        macro_rules! flush_sq {
680            ($phys:expr) => {{
681                if let Some(m) = pending_sq.remove(&$phys) {
682                    if !crate::matrix::is_identity(&m) {
683                        physical_circuit.append(&mut native_gate_set.translate(&m, $phys)?);
684                    }
685                }
686            }};
687        }
688
689        for gate in gates {
690            if let Some(decomposed) = &gate.decomposed {
691                for gate in decomposed {
692                    match gate {
693                        DecomposedGate::U(gate, target) => {
694                            let m = gate.matrix();
695                            let entry = pending_sq
696                                .entry(*target)
697                                .or_insert_with(crate::matrix::identity);
698                            *entry = crate::matrix::mat_mul(&m, entry);
699                        }
700                        DecomposedGate::CNOT(control, target) => {
701                            flush_sq!(*control);
702                            flush_sq!(*target);
703                            physical_circuit.append(&mut native_gate_set.cnot(*control, *target)?);
704                        }
705                    }
706                }
707            } else {
708                assert!(
709                    gate.control.is_empty(),
710                    "controlled gates should be decomposed at this pont"
711                );
712
713                let m = gate.gate.matrix();
714                let entry = pending_sq
715                    .entry(gate.target)
716                    .or_insert_with(crate::matrix::identity);
717                *entry = crate::matrix::mat_mul(&m, entry);
718            }
719        }
720
721        let remaining_phys: Vec<usize> = pending_sq.keys().copied().collect();
722        for phys in remaining_phys {
723            flush_sq!(phys);
724        }
725
726        Ok(physical_circuit)
727    }
728
729    fn shift_param(
730        gates: &[GateInstruction],
731        shift_inst: usize,
732        shift_amt: f64,
733    ) -> Vec<GateInstruction> {
734        gates
735            .par_iter()
736            .enumerate()
737            .map(|(inst_idx, gate)| {
738                let mut gate = gate.clone();
739
740                if inst_idx == shift_inst {
741                    gate.gate = match &gate.gate {
742                        QuantumGate::RotationX(p) => {
743                            QuantumGate::RotationX(Param::Value(p.value() + shift_amt))
744                        }
745                        QuantumGate::RotationY(p) => {
746                            QuantumGate::RotationY(Param::Value(p.value() + shift_amt))
747                        }
748                        QuantumGate::RotationZ(p) => {
749                            QuantumGate::RotationZ(Param::Value(p.value() + shift_amt))
750                        }
751                        QuantumGate::Phase(p) => {
752                            QuantumGate::Phase(Param::Value(p.value() + shift_amt))
753                        }
754                        other => *other,
755                    };
756                }
757                gate
758            })
759            .collect()
760    }
761
762    #[must_use]
763    /// Returns the current [`ProcessState`] of this process.
764    pub fn status(&self) -> ProcessState {
765        self.state
766    }
767
768    fn parameter_shift(
769        qpu: &(impl BatchExecution + ?Sized),
770        native_gate_set: Option<&dyn NativeGateSet>,
771        num_qubits: usize,
772        gates: &[GateInstruction],
773        parameters: &[f64],
774        hamiltonian: &[Hamiltonian],
775        exp_value_strategy: &ExpValueStrategy,
776    ) -> Result<Vec<f64>, KetError> {
777        (0..parameters.len())
778            .into_iter()
779            .map(|param_idx| -> Result<f64, KetError> {
780                let mut multipliers = Vec::new();
781                for (inst_idx, gate) in gates.iter().enumerate() {
782                    if let Some((idx, mult)) = gate.gate.param_index() {
783                        if idx == param_idx {
784                            multipliers.push((inst_idx, mult));
785                        }
786                    }
787                }
788                if multipliers.is_empty() {
789                    return Ok(0.0);
790                }
791                let mut total_grad = 0.0;
792                for (inst_idx, mult) in multipliers {
793                    let circuit_plus = Self::shift_param(gates, inst_idx, FRAC_PI_2);
794                    let circuit_plus = if let Some(native_gate_set) = native_gate_set {
795                        GateList::Native {
796                            gates: &Self::translate_circuit(&circuit_plus, native_gate_set)?,
797                            native_gate_set,
798                        }
799                    } else {
800                        GateList::Ir {
801                            gates: &circuit_plus,
802                        }
803                    };
804                    let e_plus = Self::execute_exp_value(
805                        qpu,
806                        num_qubits,
807                        circuit_plus,
808                        hamiltonian,
809                        exp_value_strategy,
810                    )?[0];
811
812                    let circuit_minus = Self::shift_param(gates, inst_idx, -FRAC_PI_2);
813                    let circuit_minus = if let Some(native_gate_set) = native_gate_set {
814                        GateList::Native {
815                            gates: &Self::translate_circuit(&circuit_minus, native_gate_set)?,
816                            native_gate_set,
817                        }
818                    } else {
819                        GateList::Ir {
820                            gates: &circuit_minus,
821                        }
822                    };
823                    let e_minus = Self::execute_exp_value(
824                        qpu,
825                        num_qubits,
826                        circuit_minus,
827                        hamiltonian,
828                        exp_value_strategy,
829                    )?[0];
830
831                    total_grad += mult * (e_plus - e_minus) / 2.0;
832                }
833                Ok(total_grad)
834            })
835            .collect()
836    }
837}