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}