1pub mod braket;
8pub mod compiled;
9mod decomposed;
10mod dispatch;
11pub mod gradient;
12pub mod homological;
13mod metadata;
14pub mod noise;
15mod observable;
16mod probability;
17pub(crate) mod shots;
18pub mod stabilizer_rank;
19mod terminal_sampling;
20mod trajectory;
21pub mod unified_pauli;
22
23pub use braket::ResultValue;
24pub(crate) use decomposed::merge_probabilities;
25use decomposed::{
26 MIN_DECOMPOSITION_QUBITS, run_decomposed, run_decomposed_prefused, should_decompose,
27};
28pub use dispatch::BackendKind;
29use dispatch::{
30 AUTO_SPD_MAX_TERMS, BackendPlan, ExecutionPlan, Family, MAX_AUTO_T_COUNT_EXACT,
31 MAX_AUTO_T_COUNT_SHOTS, MAX_STABILIZER_RANK_QUBITS, MIN_BLOCK_FOR_FACTORED_STAB,
32 MIN_FACTORED_STABILIZER_QUBITS, MIN_QUBITS_FOR_SPD_AUTO, accel_for, approximate_route_name,
33 auto_selects_cpu_statevector, build_statevector, has_temporal_clifford_opportunity,
34 initial_state_plan, plan_for_family, plan_temporal_clifford, resolve, resolve_backend,
35 run_temporal_clifford, stabilizer_rank_budget, validate_explicit_backend,
36};
37pub use metadata::{Engine, Exactness, ExpectationResult, Placement, ResolvedBackend, RunMetadata};
38#[cfg(feature = "distributed")]
39pub(crate) use observable::pauli_sandwich;
40pub use observable::{ObservableExpectation, PauliObservable};
41pub(crate) use observable::{
42 finish_expectations, i_pow, pauli_expectation_from_masks, pauli_expectations_from_masks,
43 pauli_masks, pauli_sandwiches_from_masks, validate_observable,
44};
45pub use probability::{FactoredBlock, Probabilities, ProbabilitiesIter};
46pub use shots::{ShotsResult, bitstring};
47
48use std::collections::HashMap;
49
50use num_complex::Complex64;
51
52use crate::backend::sparse::MAX_SPARSE_INDEX_QUBITS;
53use crate::backend::statevector::StatevectorBackend;
54use crate::backend::{Backend, max_statevector_qubits};
55use crate::circuit::{Circuit, Instruction};
56use crate::error::{PrismError, Result};
57use crate::sim::noise::NoiseModel;
58use shots::{packed_shots_to_classical_bits, sample_shots, shots_from_basis_samples};
59use terminal_sampling::{
60 sample_counts_from_probs, sample_counts_from_state, sample_shots_from_probs,
61 sample_shots_from_state,
62};
63use unified_pauli::PauliTerm;
64
65type TerminalStatevector = (StatevectorBackend, Vec<(usize, usize)>);
66
67#[derive(Debug, Clone, Copy)]
68pub(crate) struct SimOptions {
69 pub(crate) probabilities: bool,
70}
71
72impl Default for SimOptions {
73 fn default() -> Self {
74 Self {
75 probabilities: true,
76 }
77 }
78}
79
80impl SimOptions {
81 pub(crate) fn classical_only() -> Self {
82 Self {
83 probabilities: false,
84 }
85 }
86}
87
88#[derive(Debug, Clone)]
90pub struct RunOutcome {
91 pub classical_bits: Vec<bool>,
94 pub probabilities: Option<Probabilities>,
100 pub metadata: RunMetadata,
102}
103
104#[derive(Debug, Clone)]
106pub struct CountsResult {
107 pub counts: HashMap<Vec<u64>, u64>,
110 pub num_classical_bits: usize,
111 pub metadata: RunMetadata,
112}
113
114impl CountsResult {
115 pub fn into_counts(self) -> HashMap<Vec<u64>, u64> {
116 self.counts
117 }
118}
119
120#[derive(Debug, Clone)]
122pub struct MarginalsResult {
123 pub marginals: Vec<(f64, f64)>,
125 pub metadata: RunMetadata,
126}
127
128impl MarginalsResult {
129 pub fn into_vec(self) -> Vec<(f64, f64)> {
130 self.marginals
131 }
132}
133
134#[derive(Debug, Clone)]
137pub struct ReducedDensityMatrix {
138 pub qubits: Vec<usize>,
140 pub data: Vec<Complex64>,
145 pub metadata: RunMetadata,
146}
147
148impl ReducedDensityMatrix {
149 pub fn purity(&self) -> f64 {
151 self.data.iter().map(|entry| entry.norm_sqr()).sum()
152 }
153}
154
155#[derive(Debug, Clone)]
158pub struct ObservableVariance {
159 pub variance: f64,
161 pub mean: f64,
163 pub metadata: RunMetadata,
164}
165
166#[derive(Debug, Clone)]
169pub struct EntropyResult {
170 pub subsystem: Vec<usize>,
173 pub entropy: f64,
175 pub schmidt_values: Option<Vec<f64>>,
180 pub metadata: RunMetadata,
181}
182
183#[derive(Debug, Clone)]
186pub struct OverlapResult {
187 pub fidelity: f64,
192 pub left: RunMetadata,
195 pub right: RunMetadata,
197}
198
199#[derive(Debug, Clone, Copy)]
201pub struct Unseeded;
202
203#[derive(Debug, Clone, Copy)]
205pub struct Seeded {
206 seed: u64,
207}
208
209pub struct Simulate<'c, SeedState> {
211 circuit: &'c Circuit,
212 kind: BackendKind,
213 seed: SeedState,
214 noise_model: Option<&'c noise::NoiseModel>,
215 initial_state: Option<&'c [Complex64]>,
216 require_exact: bool,
217}
218
219impl<'c, SeedState> Simulate<'c, SeedState> {
220 #[inline]
222 pub fn backend(mut self, kind: BackendKind) -> Self {
223 self.kind = kind;
224 self
225 }
226
227 #[inline]
240 pub fn require_exact(mut self) -> Self {
241 self.require_exact = true;
242 self
243 }
244
245 #[inline]
259 pub fn noise(mut self, model: &'c noise::NoiseModel) -> Self {
260 self.noise_model = Some(model);
261 self
262 }
263
264 #[inline]
279 pub fn initial_state(mut self, amplitudes: &'c [Complex64]) -> Self {
280 self.initial_state = Some(amplitudes);
281 self
282 }
283
284 #[cfg(feature = "gpu")]
286 #[inline]
287 pub fn gpu(self, context: std::sync::Arc<crate::gpu::GpuContext>) -> Self {
288 self.backend(BackendKind::StatevectorGpu { context })
289 }
290
291 #[cfg(feature = "gpu")]
297 #[inline]
298 pub fn gpu_auto(self, context: std::sync::Arc<crate::gpu::GpuContext>) -> Self {
299 self.backend(BackendKind::AutoGpu { context })
300 }
301
302 #[cfg(feature = "distributed")]
307 pub fn distributed(
308 self,
309 context: std::sync::Arc<crate::distributed::DistributedContext>,
310 ) -> Self {
311 self.backend(BackendKind::StatevectorDistributed { context })
312 }
313}
314
315impl<'c> Simulate<'c, Unseeded> {
316 #[inline]
318 pub fn seed(self, seed: u64) -> Simulate<'c, Seeded> {
319 Simulate {
320 circuit: self.circuit,
321 kind: self.kind,
322 seed: Seeded { seed },
323 noise_model: self.noise_model,
324 initial_state: self.initial_state,
325 require_exact: self.require_exact,
326 }
327 }
328}
329
330impl<'c> Simulate<'c, Seeded> {
331 #[inline]
332 fn seed_value(&self) -> u64 {
333 self.seed.seed
334 }
335
336 fn require_no_initial_state_under_noise(&self, terminal: &str) -> Result<()> {
339 if self.initial_state.is_some() {
340 return Err(reject_initial_state(
341 &self.kind,
342 terminal,
343 "noisy trajectory replay starts every shot from |0...0>; read the exact mixture \
344 with `run`, `marginals`, or `expectation_values` on the density-matrix backend",
345 ));
346 }
347 Ok(())
348 }
349
350 #[inline]
359 pub fn run(self) -> Result<RunOutcome> {
360 let seed = self.seed_value();
361 if self.require_exact {
362 reject_approximate_route(&self.kind, self.circuit)?;
363 }
364 if let Some(noise_model) = self.noise_model {
365 require_exact_mixture(&self.kind, "a single run")?;
366 reject_readout_at(self.circuit, noise_model, "a single run")?;
367 let probabilities = exact_noisy_probabilities(
368 &self.kind,
369 self.circuit,
370 noise_model,
371 self.initial_state,
372 seed,
373 )?;
374 let classical_bits =
375 sample_exact_noisy_shots(&probabilities, self.circuit, noise_model, 1, seed)
376 .swap_remove(0);
377 return Ok(RunOutcome {
378 classical_bits,
379 probabilities: Some(probabilities),
380 metadata: exact_mixture_metadata(&self.kind),
381 });
382 }
383 if let Some(state) = self.initial_state {
384 return run_from_initial_state(
385 &self.kind,
386 self.circuit,
387 state,
388 seed,
389 &SimOptions::default(),
390 );
391 }
392 let outcome = run_with_internal(self.kind, self.circuit, seed, SimOptions::default())?;
393 ensure_exact_result(self.require_exact, &outcome.metadata)?;
394 Ok(outcome)
395 }
396
397 #[inline]
400 pub fn shots(self, num_shots: usize) -> Result<ShotsResult> {
401 let seed = self.seed_value();
402 let require_exact = self.require_exact;
403 if require_exact {
404 reject_approximate_route(&self.kind, self.circuit)?;
405 }
406 let result = if let Some(noise_model) = self.noise_model {
407 self.require_no_initial_state_under_noise("shot sampling")?;
408 run_shots_with_noise(self.kind, self.circuit, noise_model, num_shots, seed)?
409 } else if let Some(state) = self.initial_state {
410 shots_from_initial_state(&self.kind, self.circuit, state, num_shots, seed)?
411 } else {
412 run_shots_with(self.kind, self.circuit, num_shots, seed)?
413 };
414 ensure_exact_result(require_exact, &result.metadata)?;
415 Ok(result)
416 }
417
418 #[inline]
425 pub fn sample_counts(self, num_shots: usize) -> Result<CountsResult> {
426 let seed = self.seed_value();
427 if self.require_exact {
428 reject_approximate_route(&self.kind, self.circuit)?;
429 }
430 let (counts, metadata) = if let Some(noise_model) = self.noise_model {
431 self.require_no_initial_state_under_noise("count sampling")?;
432 let shots =
433 run_shots_with_noise(self.kind, self.circuit, noise_model, num_shots, seed)?;
434 (shots.counts(), shots.metadata)
435 } else if let Some(state) = self.initial_state {
436 let shots = shots_from_initial_state(&self.kind, self.circuit, state, num_shots, seed)?;
437 (shots.counts(), shots.metadata)
438 } else {
439 run_counts_with(self.kind, self.circuit, num_shots, seed)?
440 };
441 ensure_exact_result(self.require_exact, &metadata)?;
442 Ok(CountsResult {
443 counts,
444 num_classical_bits: self.circuit.num_classical_bits,
445 metadata,
446 })
447 }
448
449 #[inline]
455 pub fn marginals(self) -> Result<MarginalsResult> {
456 let seed = self.seed_value();
457 if self.require_exact {
458 reject_approximate_route(&self.kind, self.circuit)?;
459 }
460 if let Some(noise_model) = self.noise_model {
461 require_exact_mixture(&self.kind, "marginals")?;
462 reject_readout_at(self.circuit, noise_model, "marginals")?;
463 let probs = exact_noisy_probabilities(
464 &self.kind,
465 self.circuit,
466 noise_model,
467 self.initial_state,
468 seed,
469 )?;
470 return Ok(MarginalsResult {
471 marginals: probs.marginals(),
472 metadata: exact_mixture_metadata(&self.kind),
473 });
474 }
475 let result = if let Some(state) = self.initial_state {
476 marginals_from_initial_state(&self.kind, self.circuit, state, seed)?
477 } else {
478 run_marginals_result_with(self.kind, self.circuit, seed)?
479 };
480 ensure_exact_result(self.require_exact, &result.metadata)?;
481 Ok(result)
482 }
483
484 #[inline]
500 pub fn expectation_values(self, observables: &[Vec<PauliTerm>]) -> Result<Vec<f64>> {
501 self.expectation_values_reported(observables)
502 .map(ExpectationResult::into_values)
503 }
504
505 pub fn expectation_values_reported(
508 self,
509 observables: &[Vec<PauliTerm>],
510 ) -> Result<ExpectationResult> {
511 let seed = self.seed_value();
512 if self.require_exact {
513 reject_approximate_route(&self.kind, self.circuit)?;
514 }
515 if let Some(noise_model) = self.noise_model {
516 reject_readout_at(self.circuit, noise_model, "expectation values")?;
517 }
518 if let BackendKind::PauliPath { epsilon, max_terms } = self.kind {
519 reject_pauli_path_initial_state(self.initial_state)?;
520 return pauli_path_expectations(
521 self.circuit,
522 self.noise_model,
523 observables,
524 epsilon,
525 max_terms,
526 );
527 }
528 if let Some(noise_model) = self.noise_model {
529 require_exact_mixture(&self.kind, "expectation values")?;
530 require_unitary_circuit(&self.kind, self.circuit, "expectation values require")?;
531 let values = noise::dm_expectation_values(
532 &self.kind,
533 self.circuit,
534 observables,
535 Some(noise_model),
536 self.initial_state,
537 seed,
538 )?;
539 return Ok(analytic_expectations(
540 values,
541 exact_mixture_metadata(&self.kind),
542 ));
543 }
544 if let Some(state) = self.initial_state {
545 require_unitary_circuit(&self.kind, self.circuit, "expectation values require")?;
546 return expectation_values_from_initial_state(
547 &self.kind,
548 self.circuit,
549 state,
550 observables,
551 seed,
552 );
553 }
554 let result = run_expectation_values_reported(self.kind, self.circuit, observables, seed)?;
555 ensure_exact_result(self.require_exact, &result.metadata)?;
556 Ok(result)
557 }
558
559 pub fn observable_expectation(
570 self,
571 observable: &PauliObservable,
572 ) -> Result<ObservableExpectation> {
573 self.observable_expectation_ref(observable)
574 }
575
576 fn observable_expectation_ref(
580 &self,
581 observable: &PauliObservable,
582 ) -> Result<ObservableExpectation> {
583 let seed = self.seed_value();
584 if self.require_exact {
585 reject_approximate_route(&self.kind, self.circuit)?;
586 }
587 if let Some(noise_model) = self.noise_model {
588 reject_readout_at(self.circuit, noise_model, "observable expectation")?;
589 }
590 if let BackendKind::PauliPath { epsilon, max_terms } = self.kind {
591 reject_pauli_path_initial_state(self.initial_state)?;
592 let result = pauli_path_expectations(
593 self.circuit,
594 self.noise_model,
595 &observable_vecs(observable),
596 epsilon,
597 max_terms,
598 )?;
599 let metadata = result.metadata;
600 return Ok(weighted_observable_result(
601 observable,
602 &result.values,
603 None,
604 metadata,
605 ));
606 }
607 if let Some(noise_model) = self.noise_model {
608 require_exact_mixture(&self.kind, "expectation values")?;
609 require_unitary_circuit(&self.kind, self.circuit, "expectation values require")?;
610 let values = noise::dm_expectation_values(
611 &self.kind,
612 self.circuit,
613 &observable_vecs(observable),
614 Some(noise_model),
615 self.initial_state,
616 seed,
617 )?;
618 return Ok(weighted_observable_result(
619 observable,
620 &values,
621 None,
622 exact_mixture_metadata(&self.kind),
623 ));
624 }
625 if let Some(state) = self.initial_state {
626 require_unitary_circuit(&self.kind, self.circuit, "expectation values require")?;
627 let result = expectation_values_from_initial_state(
628 &self.kind,
629 self.circuit,
630 state,
631 &observable_vecs(observable),
632 seed,
633 )?;
634 return Ok(weighted_observable_result(
635 observable,
636 &result.values,
637 result.std_errors.as_deref(),
638 result.metadata,
639 ));
640 }
641 let result =
642 run_observable_expectation_reported(self.kind.clone(), self.circuit, observable, seed)?;
643 ensure_exact_result(self.require_exact, &result.metadata)?;
644 Ok(result)
645 }
646
647 pub fn observable_variance(self, observable: &PauliObservable) -> Result<ObservableVariance> {
662 let (offset, traceless) = observable.split_identity();
663 let mean = self.observable_expectation_ref(observable)?;
664 let second = self.observable_expectation_ref(&traceless.square())?;
665 let centered = mean.mean - offset;
666 Ok(ObservableVariance {
667 variance: second.mean - centered * centered,
668 mean: mean.mean,
669 metadata: mean.metadata,
670 })
671 }
672
673 pub fn probabilities_of(self, qubits: &[usize]) -> Result<Vec<f64>> {
686 crate::backend::schmidt::validate_qubit_set(qubits, self.circuit.num_qubits)?;
687 let kind = format!("{:?}", self.kind);
688 let outcome = self.run()?;
689 let probabilities = outcome
690 .probabilities
691 .ok_or(PrismError::BackendUnsupported {
692 backend: kind,
693 operation: "a probability distribution to marginalize".into(),
694 })?;
695 Ok(probabilities.subset_marginal(qubits))
696 }
697
698 pub fn state_vector(self) -> Result<Vec<Complex64>> {
713 let seed = self.seed_value();
714 let diagnostic = Diagnostic::StateVector;
715 require_unitary_circuit(&self.kind, self.circuit, "a statevector requires")?;
716 if self.require_exact {
717 reject_approximate_route(&self.kind, self.circuit)?;
718 }
719 if self.noise_model.is_some() {
720 return Err(PrismError::IncompatibleBackend {
721 backend: format!("{:?}", self.kind),
722 reason: format!(
723 "{} is a pure state; a noise model evolves a mixture, which \
724 `reduced_density_matrix` over the whole register reports",
725 diagnostic.terminal()
726 ),
727 });
728 }
729 let backend = diagnostic_backend(
730 &self.kind,
731 self.circuit,
732 self.initial_state,
733 seed,
734 diagnostic,
735 self.circuit.num_qubits,
736 )?;
737 ensure_exact_result(self.require_exact, &backend_metadata(&*backend))?;
738 backend.export_statevector()
739 }
740
741 pub fn reduced_density_matrix(self, qubits: &[usize]) -> Result<ReducedDensityMatrix> {
756 let seed = self.seed_value();
757 let diagnostic = Diagnostic::ReducedDensityMatrix;
758 let terminal = diagnostic.terminal();
759 crate::backend::schmidt::validate_qubit_set(qubits, self.circuit.num_qubits)?;
760 require_unitary_circuit(
761 &self.kind,
762 self.circuit,
763 "a reduced density matrix requires",
764 )?;
765 if self.require_exact {
766 reject_approximate_route(&self.kind, self.circuit)?;
767 }
768 if let Some(noise_model) = self.noise_model {
769 reject_readout_at(self.circuit, noise_model, terminal)?;
770 require_exact_mixture(&self.kind, terminal)?;
771 let mut mixture = noise::evolve_density_matrix(
772 &self.kind,
773 self.circuit,
774 Some(noise_model),
775 self.initial_state,
776 seed,
777 )?;
778 return Ok(ReducedDensityMatrix {
779 qubits: qubits.to_vec(),
780 data: mixture.reduced_density_matrix(qubits)?,
781 metadata: exact_mixture_metadata(&self.kind),
782 });
783 }
784 let mut backend = diagnostic_backend(
785 &self.kind,
786 self.circuit,
787 self.initial_state,
788 seed,
789 diagnostic,
790 qubits.len(),
791 )?;
792 let metadata = backend_metadata(&*backend);
793 ensure_exact_result(self.require_exact, &metadata)?;
794 Ok(ReducedDensityMatrix {
795 qubits: qubits.to_vec(),
796 data: backend.reduced_density_matrix(qubits)?,
797 metadata,
798 })
799 }
800
801 pub fn entanglement_entropy(self, subsystem: &[usize]) -> Result<EntropyResult> {
818 let seed = self.seed_value();
819 let diagnostic = Diagnostic::Entropy;
820 let terminal = diagnostic.terminal();
821 crate::backend::schmidt::validate_subsystem(subsystem, self.circuit.num_qubits)?;
822 require_unitary_circuit(&self.kind, self.circuit, "entanglement entropy requires")?;
823 if self.require_exact {
824 reject_approximate_route(&self.kind, self.circuit)?;
825 }
826 if let Some(noise_model) = self.noise_model {
827 reject_readout_at(self.circuit, noise_model, terminal)?;
828 require_exact_mixture(&self.kind, terminal)?;
829 let mut mixture = noise::evolve_density_matrix(
830 &self.kind,
831 self.circuit,
832 Some(noise_model),
833 self.initial_state,
834 seed,
835 )?;
836 return Ok(EntropyResult {
837 subsystem: subsystem.to_vec(),
838 entropy: mixture.entanglement_entropy(subsystem)?,
839 schmidt_values: None,
840 metadata: exact_mixture_metadata(&self.kind),
841 });
842 }
843 let mut backend = diagnostic_backend(
844 &self.kind,
845 self.circuit,
846 self.initial_state,
847 seed,
848 diagnostic,
849 subsystem.len(),
850 )?;
851 let metadata = backend_metadata(&*backend);
852 ensure_exact_result(self.require_exact, &metadata)?;
853 let (entropy, schmidt_values) = match backend.schmidt_values(subsystem) {
858 Ok(values) => (
859 crate::backend::schmidt::entropy_of_schmidt_values(&values),
860 Some(values),
861 ),
862 Err(declined) => match backend.entanglement_entropy(subsystem) {
863 Ok(entropy) => (entropy, None),
864 Err(_) => return Err(declined),
865 },
866 };
867 Ok(EntropyResult {
868 subsystem: subsystem.to_vec(),
869 entropy,
870 schmidt_values,
871 metadata,
872 })
873 }
874
875 pub fn overlap(self, other: Simulate<'_, Seeded>) -> Result<OverlapResult> {
891 let diagnostic = Diagnostic::Overlap;
892 if self.circuit.num_qubits != other.circuit.num_qubits {
893 return Err(PrismError::InvalidParameter {
894 message: format!(
895 "{} needs two circuits of the same width; got {} and {} qubits",
896 diagnostic.terminal(),
897 self.circuit.num_qubits,
898 other.circuit.num_qubits
899 ),
900 });
901 }
902 let (left_backend, left) = self.overlap_side(diagnostic)?;
903 let (right_backend, right) = other.overlap_side(diagnostic)?;
904 Ok(OverlapResult {
905 fidelity: left_backend.overlap_sq(&*right_backend)?,
906 left,
907 right,
908 })
909 }
910
911 fn overlap_side(self, diagnostic: Diagnostic) -> Result<(Box<dyn Backend>, RunMetadata)> {
915 let seed = self.seed_value();
916 let terminal = diagnostic.terminal();
917 require_unitary_circuit(&self.kind, self.circuit, "a state overlap requires")?;
918 if self.require_exact {
919 reject_approximate_route(&self.kind, self.circuit)?;
920 }
921 if self.noise_model.is_some() {
922 return Err(PrismError::IncompatibleBackend {
923 backend: format!("{:?}", self.kind),
924 reason: format!(
925 "{terminal} is an inner product of two pure states, and a noise model \
926 evolves a mixture, whose fidelity is a different computation; drop \
927 the model, or compare the mixtures through `reduced_density_matrix`"
928 ),
929 });
930 }
931 let backend = diagnostic_backend(
932 &self.kind,
933 self.circuit,
934 self.initial_state,
935 seed,
936 diagnostic,
937 0,
938 )?;
939 let metadata = backend_metadata(&*backend);
940 ensure_exact_result(self.require_exact, &metadata)?;
941 Ok((backend, metadata))
942 }
943
944 #[inline]
953 pub fn expectation_gradient(
954 self,
955 hamiltonian: &[(f64, Vec<PauliTerm>)],
956 params: &crate::circuit::Parameters,
957 ) -> Result<gradient::ExpectationGradient> {
958 let seed = self.seed_value();
959 if self.require_exact {
960 reject_approximate_route(&self.kind, self.circuit)?;
961 }
962 if self.noise_model.is_some() {
963 return Err(PrismError::IncompatibleBackend {
964 backend: format!("{:?}", self.kind),
965 reason: "the adjoint method backpropagates through a pure state, so no backend \
966 has a noisy adjoint path; drop the noise model, or take \
967 `expectation_gradient_shift` on the density-matrix backend"
968 .into(),
969 });
970 }
971 if self.initial_state.is_some() {
972 return Err(reject_initial_state(
973 &self.kind,
974 "the adjoint gradient",
975 "the backward pass reconstructs the input register by inverting the circuit from \
976 |0...0>, so a start state would have to be inverted with it",
977 ));
978 }
979 if !(self.kind.is_auto() || matches!(self.kind, BackendKind::Statevector)) {
980 return Err(PrismError::IncompatibleBackend {
981 backend: format!("{:?}", self.kind),
982 reason:
983 "adjoint gradients run on the statevector backend; select Auto or Statevector"
984 .into(),
985 });
986 }
987 gradient::run_expectation_gradient(self.circuit, hamiltonian, params, seed)
988 }
989
990 #[inline]
1005 pub fn expectation_gradient_shift(
1006 self,
1007 hamiltonian: &[(f64, Vec<PauliTerm>)],
1008 params: &crate::circuit::Parameters,
1009 ) -> Result<gradient::ExpectationGradient> {
1010 let seed = self.seed_value();
1011 if self.require_exact {
1012 reject_approximate_route(&self.kind, self.circuit)?;
1013 }
1014 if self.noise_model.is_some()
1015 && !self.kind.is_density_matrix()
1016 && !matches!(self.kind, BackendKind::PauliPath { .. })
1017 {
1018 return Err(PrismError::IncompatibleBackend {
1019 backend: format!("{:?}", self.kind),
1020 reason: "a parameter-shift gradient under a noise model evaluates the exact \
1021 mixed state, which only the density-matrix backend holds; select \
1022 `BackendKind::DensityMatrix` or its device sibling, or drop the noise \
1023 model"
1024 .into(),
1025 });
1026 }
1027 gradient::shift_gradient(
1028 &self.kind,
1029 self.circuit,
1030 hamiltonian,
1031 params,
1032 self.noise_model,
1033 self.initial_state,
1034 seed,
1035 )
1036 }
1037}
1038
1039#[inline]
1061pub fn simulate(circuit: &Circuit) -> Simulate<'_, Unseeded> {
1062 Simulate {
1063 circuit,
1064 kind: BackendKind::Auto,
1065 seed: Unseeded,
1066 noise_model: None,
1067 initial_state: None,
1068 require_exact: false,
1069 }
1070}
1071
1072fn reject_pauli_path(terminal: &str) -> PrismError {
1076 PrismError::IncompatibleBackend {
1077 backend: "PauliPath".into(),
1078 reason: format!(
1079 "{terminal} is not served by Pauli path propagation, which answers \
1080 `expectation_values` and `observable_expectation` only"
1081 ),
1082 }
1083}
1084
1085fn reject_pauli_path_initial_state(state: Option<&[Complex64]>) -> Result<()> {
1086 match state {
1087 Some(_) => Err(reject_pauli_path("a start state")),
1088 None => Ok(()),
1089 }
1090}
1091
1092fn pauli_path_expectations(
1099 circuit: &Circuit,
1100 noise: Option<&NoiseModel>,
1101 observables: &[Vec<PauliTerm>],
1102 epsilon: f64,
1103 max_terms: usize,
1104) -> Result<ExpectationResult> {
1105 let empty;
1106 let noise = match noise {
1107 Some(model) => model,
1108 None => {
1109 empty = NoiseModel {
1110 after_gate: vec![Vec::new(); circuit.instructions.len()],
1111 readout: vec![None; circuit.num_classical_bits],
1112 };
1113 &empty
1114 }
1115 };
1116 let mut values = Vec::with_capacity(observables.len());
1117 let mut discarded = 0.0f64;
1118 for obs in observables {
1119 let result =
1120 unified_pauli::run_pauli_path_observable(circuit, noise, obs, epsilon, max_terms)?;
1121 discarded = discarded.max(result.total_discarded);
1122 values.push(result.mean);
1123 }
1124 let metadata = if discarded > 0.0 {
1125 RunMetadata::approximate(ResolvedBackend::PauliPath)
1126 } else {
1127 RunMetadata::exact(ResolvedBackend::PauliPath)
1128 };
1129 Ok(analytic_expectations(values, metadata))
1130}
1131
1132fn require_exact_mixture(kind: &BackendKind, terminal: &str) -> Result<()> {
1133 if kind.is_density_matrix() {
1134 return Ok(());
1135 }
1136 Err(PrismError::IncompatibleBackend {
1137 backend: format!("{kind:?}"),
1138 reason: format!(
1139 "{terminal} under a noise model reads the exact mixed state, which only the \
1140 density-matrix backend holds; select it, or average trajectories through `shots` \
1141 or `sample_counts`"
1142 ),
1143 })
1144}
1145
1146fn reject_readout_at(
1155 circuit: &Circuit,
1156 noise_model: &noise::NoiseModel,
1157 terminal: &str,
1158) -> Result<()> {
1159 let inert = noise_model
1160 .readout
1161 .iter()
1162 .take(circuit.num_classical_bits)
1163 .all(|entry| entry.as_ref().is_none_or(|readout| readout.is_inert()));
1164 if inert {
1165 return Ok(());
1166 }
1167 Err(PrismError::InvalidParameter {
1168 message: format!(
1169 "{terminal} answers from the mixed state, which readout error is not part of: it \
1170 acts on the measurement record and is indexed by classical bit, not qubit. Drop \
1171 it from the model, or use `shots` or `sample_counts`, which apply it"
1172 ),
1173 })
1174}
1175
1176fn reject_initial_state(kind: &BackendKind, terminal: &str, instead: &str) -> PrismError {
1179 PrismError::IncompatibleBackend {
1180 backend: format!("{kind:?}"),
1181 reason: format!("{terminal} does not accept a start state; {instead}"),
1182 }
1183}
1184
1185fn check_initial_state_len(state: &[Complex64], num_qubits: usize) -> Result<()> {
1192 let want = (num_qubits < usize::BITS as usize).then(|| 1usize << num_qubits);
1193 if want == Some(state.len()) {
1194 return Ok(());
1195 }
1196 let needs = match want {
1197 Some(count) => count.to_string(),
1198 None => format!("2^{num_qubits}"),
1199 };
1200 Err(PrismError::InvalidParameter {
1201 message: format!(
1202 "start state has {} amplitudes, but a {num_qubits}-qubit circuit needs {needs}",
1203 state.len()
1204 ),
1205 })
1206}
1207
1208fn backend_from_initial_state(
1210 kind: &BackendKind,
1211 circuit: &Circuit,
1212 state: &[Complex64],
1213 seed: u64,
1214) -> Result<Box<dyn Backend>> {
1215 if !kind.is_auto() {
1216 validate_explicit_backend(kind, circuit)?;
1217 }
1218 check_initial_state_len(state, circuit.num_qubits)?;
1219 let mut backend = initial_state_plan(kind, circuit.num_qubits)?.build(seed);
1220 backend.init_from_amplitudes(state.to_vec(), circuit.num_classical_bits)?;
1221 Ok(backend)
1222}
1223
1224fn expand_for_backend<'c>(
1227 backend: &dyn Backend,
1228 circuit: &'c Circuit,
1229) -> std::borrow::Cow<'c, Circuit> {
1230 use std::borrow::Cow;
1231 let expanded = if backend.supports_qft_block() {
1232 Cow::Borrowed(circuit)
1233 } else {
1234 crate::circuit::expand_qft_blocks(circuit)
1235 };
1236 if backend.supports_pauli_rotation() {
1237 return expanded;
1238 }
1239 match expanded {
1240 Cow::Borrowed(borrowed) => crate::circuit::expand_pauli_rotations(borrowed),
1241 Cow::Owned(owned) => {
1242 let rotations = crate::circuit::expand_pauli_rotations(&owned);
1243 if let Cow::Owned(expanded_rotations) = rotations {
1244 return Cow::Owned(expanded_rotations);
1245 }
1246 Cow::Owned(owned)
1247 }
1248 }
1249}
1250
1251fn fuse_for_backend<'a>(
1254 backend: &dyn Backend,
1255 circuit: &'a Circuit,
1256) -> std::borrow::Cow<'a, Circuit> {
1257 crate::circuit::fusion::fuse_circuit_for_width(
1258 circuit,
1259 backend.supports_fused_gates(),
1260 backend.fusion_state_qubits(circuit.num_qubits),
1261 )
1262}
1263
1264fn apply_fused_circuit(backend: &mut dyn Backend, circuit: &Circuit) -> Result<()> {
1267 let expanded = expand_for_backend(&*backend, circuit);
1268 let fused = fuse_for_backend(&*backend, &expanded);
1269 backend.apply_instructions(&fused.instructions)
1270}
1271
1272fn run_from_initial_state(
1273 kind: &BackendKind,
1274 circuit: &Circuit,
1275 state: &[Complex64],
1276 seed: u64,
1277 opts: &SimOptions,
1278) -> Result<RunOutcome> {
1279 let mut backend = backend_from_initial_state(kind, circuit, state, seed)?;
1280 apply_fused_circuit(&mut *backend, circuit)?;
1281
1282 let probabilities = if opts.probabilities {
1283 try_backend_probabilities(&*backend)?
1284 } else {
1285 None
1286 };
1287 Ok(RunOutcome {
1288 classical_bits: backend.classical_results().to_vec(),
1289 probabilities,
1290 metadata: backend_metadata(&*backend),
1291 })
1292}
1293
1294fn shots_from_initial_state(
1298 kind: &BackendKind,
1299 circuit: &Circuit,
1300 state: &[Complex64],
1301 num_shots: usize,
1302 seed: u64,
1303) -> Result<ShotsResult> {
1304 let bits = circuit.num_classical_bits;
1305 if circuit.has_terminal_measurements_only() {
1306 let stripped = circuit.without_measurements();
1307 let outcome = run_from_initial_state(kind, &stripped, state, seed, &SimOptions::default())?;
1308 if let Some(probs) = outcome.probabilities {
1309 let meas_map = circuit.measurement_map();
1310 return Ok(ShotsResult::from_shots(
1311 sample_shots(&probs, &meas_map, bits, num_shots, seed),
1312 bits,
1313 )
1314 .with_metadata(outcome.metadata));
1315 }
1316 }
1317
1318 if !kind.is_auto() {
1321 validate_explicit_backend(kind, circuit)?;
1322 }
1323 check_initial_state_len(state, circuit.num_qubits)?;
1324 let plan = initial_state_plan(kind, circuit.num_qubits)?;
1325 let probe = plan.build(seed);
1326 let expanded = expand_for_backend(&*probe, circuit);
1327 let fused = fuse_for_backend(&*probe, &expanded);
1328
1329 collect_shots(circuit, num_shots, seed, plan.resolved(), |shot_seed| {
1330 let mut backend = plan.build(shot_seed);
1331 backend.init_from_amplitudes(state.to_vec(), circuit.num_classical_bits)?;
1332 backend.apply_instructions(&fused.instructions)?;
1333 Ok((
1334 backend.classical_results().to_vec(),
1335 backend_metadata(&*backend),
1336 ))
1337 })
1338}
1339
1340fn marginals_from_initial_state(
1341 kind: &BackendKind,
1342 circuit: &Circuit,
1343 state: &[Complex64],
1344 seed: u64,
1345) -> Result<MarginalsResult> {
1346 let mut backend = backend_from_initial_state(kind, circuit, state, seed)?;
1347 apply_fused_circuit(&mut *backend, circuit)?;
1348 if backend.supports_pauli_expectation() {
1349 return marginals_from_pauli_expectations(&*backend, circuit.num_qubits);
1350 }
1351 Ok(MarginalsResult {
1352 marginals: Probabilities::Dense(backend.probabilities()?).marginals(),
1353 metadata: backend_metadata(&*backend),
1354 })
1355}
1356
1357fn expectation_values_from_initial_state(
1358 kind: &BackendKind,
1359 circuit: &Circuit,
1360 state: &[Complex64],
1361 observables: &[Vec<PauliTerm>],
1362 seed: u64,
1363) -> Result<ExpectationResult> {
1364 let masks = observables
1366 .iter()
1367 .map(|obs| pauli_masks(obs, circuit.num_qubits))
1368 .collect::<Result<Vec<_>>>()?;
1369
1370 let mut backend = backend_from_initial_state(kind, circuit, state, seed)?;
1371 apply_fused_circuit(&mut *backend, circuit)?;
1372 let metadata = backend_metadata(&*backend);
1373 if backend.supports_pauli_expectation() {
1374 let values = backend.pauli_expectations(observables)?;
1375 return Ok(analytic_expectations(values, metadata));
1376 }
1377
1378 let evolved = backend.export_statevector()?;
1379 let norm = crate::backend::state_norm_sqr(&evolved);
1380 let values = pauli_expectations_from_masks(&evolved, &masks, norm);
1381 Ok(analytic_expectations(values, metadata))
1382}
1383
1384fn ensure_exact_result(require_exact: bool, metadata: &RunMetadata) -> Result<()> {
1391 if require_exact && !metadata.is_exact() {
1392 return Err(PrismError::IncompatibleBackend {
1393 backend: format!("{:?}", metadata.backend),
1394 reason: "require_exact rejects an approximate result; this engine discarded state weight while running, which the route could not predict"
1395 .into(),
1396 });
1397 }
1398 Ok(())
1399}
1400
1401fn reject_approximate_route(kind: &BackendKind, circuit: &Circuit) -> Result<()> {
1405 match approximate_route_name(kind, circuit) {
1406 Some(engine) => Err(PrismError::IncompatibleBackend {
1407 backend: engine.into(),
1408 reason: "require_exact rejects a route that can discard state weight; drop the \
1409 requirement to accept the approximation, which the result reports, or \
1410 select a backend that represents this circuit exactly"
1411 .into(),
1412 }),
1413 None => Ok(()),
1414 }
1415}
1416
1417fn require_unitary_circuit(kind: &BackendKind, circuit: &Circuit, subject: &str) -> Result<()> {
1421 if has_nonunitary_or_classical_ops(circuit) {
1422 return Err(PrismError::IncompatibleBackend {
1423 backend: format!("{kind:?}"),
1424 reason: format!(
1425 "{subject} a unitary circuit without measurements, resets, or conditionals"
1426 ),
1427 });
1428 }
1429 Ok(())
1430}
1431
1432fn exact_mixture_metadata(kind: &BackendKind) -> RunMetadata {
1435 let metadata = RunMetadata::exact(ResolvedBackend::DensityMatrix);
1436 #[cfg(feature = "gpu")]
1437 if matches!(kind, BackendKind::DensityMatrixGpu { .. }) {
1438 let mut on_device = metadata;
1439 on_device.placement = Placement::Device;
1440 return on_device;
1441 }
1442 #[cfg(not(feature = "gpu"))]
1443 let _ = kind;
1444 metadata
1445}
1446
1447fn exact_noisy_probabilities(
1451 kind: &BackendKind,
1452 circuit: &Circuit,
1453 noise_model: &noise::NoiseModel,
1454 initial_state: Option<&[Complex64]>,
1455 seed: u64,
1456) -> Result<Probabilities> {
1457 Ok(Probabilities::Dense(noise::density_matrix_probabilities(
1458 kind,
1459 circuit,
1460 noise_model,
1461 initial_state,
1462 seed,
1463 )?))
1464}
1465
1466fn sample_exact_noisy_shots(
1471 probs: &Probabilities,
1472 circuit: &Circuit,
1473 noise_model: &noise::NoiseModel,
1474 num_shots: usize,
1475 seed: u64,
1476) -> Vec<Vec<bool>> {
1477 let bits = circuit.num_classical_bits;
1478 let mut shots = sample_shots(probs, &circuit.measurement_map(), bits, num_shots, seed);
1479 if noise_model.readout.iter().any(Option::is_some) {
1480 let readout = trajectory::written_readout(circuit, &noise_model.readout);
1481 let mut rng = trajectory::noise_rng(seed);
1482 for shot in &mut shots {
1483 trajectory::apply_readout_errors(shot, &readout, &mut rng);
1484 }
1485 }
1486 shots
1487}
1488
1489#[inline]
1490fn probs_only_result(probs: Vec<f64>, metadata: RunMetadata) -> RunOutcome {
1491 RunOutcome {
1492 probabilities: Some(Probabilities::Dense(probs)),
1493 classical_bits: vec![],
1494 metadata,
1495 }
1496}
1497
1498fn try_backend_probabilities(backend: &dyn Backend) -> Result<Option<Probabilities>> {
1499 if let Some(factored) = backend.block_probabilities() {
1500 return Ok(Some(factored));
1501 }
1502 match backend.probabilities() {
1503 Ok(probs) => Ok(Some(Probabilities::Dense(probs))),
1504 Err(PrismError::BackendUnsupported { .. }) => Ok(None),
1505 Err(err) => Err(err),
1506 }
1507}
1508
1509fn execute(backend: &mut dyn Backend, circuit: &Circuit, opts: &SimOptions) -> Result<RunOutcome> {
1511 let expanded = expand_for_backend(&*backend, circuit);
1512 let fused = fuse_for_backend(&*backend, &expanded);
1513 execute_circuit(backend, &fused, opts)
1514}
1515
1516pub(crate) struct PreparedRoute {
1524 plan: BackendPlan,
1525 supports_fused: bool,
1526 held: Option<(u64, Box<dyn Backend + Send>)>,
1530}
1531
1532impl PreparedRoute {
1533 pub(crate) fn supports_fused(&self) -> bool {
1536 self.supports_fused
1537 }
1538
1539 pub(crate) fn run(&mut self, circuit: &Circuit, seed: u64) -> Result<RunOutcome> {
1541 if !matches!(&self.held, Some((s, _)) if *s == seed) {
1542 self.held = Some((seed, self.plan.build(seed)));
1543 }
1544 let (_, backend) = self.held.as_mut().expect("just built");
1545 execute_circuit(&mut **backend, circuit, &SimOptions::default())
1546 }
1547}
1548
1549pub(crate) fn prepared_route(kind: &BackendKind, template: &Circuit) -> Option<PreparedRoute> {
1553 if !kind.is_auto() && validate_explicit_backend(kind, template).is_err() {
1554 return None;
1555 }
1556 let ProbabilityRoute::Direct {
1557 has_partial_independence,
1558 } = plan_probability_route(kind, template)
1559 else {
1560 return None;
1561 };
1562 let ExecutionPlan::Backend(plan) = resolve(kind, template, has_partial_independence) else {
1563 return None;
1564 };
1565 let probe = plan.build(0);
1566 let has_qft_block = crate::circuit::any_gate(&template.instructions, &mut |gate| {
1567 matches!(gate, crate::gates::Gate::QftBlock { .. })
1568 });
1569 if has_qft_block && !probe.supports_qft_block() {
1570 return None;
1571 }
1572 let has_pauli_rot = crate::circuit::any_gate(&template.instructions, &mut |gate| {
1573 matches!(gate, crate::gates::Gate::PauliRot(_))
1574 });
1575 if has_pauli_rot && !probe.supports_pauli_rotation() {
1576 return None;
1577 }
1578 Some(PreparedRoute {
1579 supports_fused: probe.supports_fused_gates(),
1580 plan,
1581 held: None,
1582 })
1583}
1584
1585fn execute_circuit(
1587 backend: &mut dyn Backend,
1588 circuit: &Circuit,
1589 opts: &SimOptions,
1590) -> Result<RunOutcome> {
1591 backend.init(circuit.num_qubits, circuit.num_classical_bits)?;
1592 backend.apply_instructions(&circuit.instructions)?;
1593
1594 let probabilities = if opts.probabilities {
1595 try_backend_probabilities(backend)?
1596 } else {
1597 None
1598 };
1599
1600 Ok(RunOutcome {
1601 classical_bits: backend.classical_results().to_vec(),
1602 probabilities,
1603 metadata: backend_metadata(backend),
1604 })
1605}
1606
1607pub(crate) fn backend_metadata(backend: &dyn Backend) -> RunMetadata {
1612 RunMetadata::new(backend.resolved(), backend.exactness(), backend.placement())
1613}
1614
1615#[cfg(test)]
1616fn run(circuit: &Circuit, seed: u64) -> Result<RunOutcome> {
1617 run_with(BackendKind::Auto, circuit, seed)
1618}
1619
1620pub(crate) fn run_with(kind: BackendKind, circuit: &Circuit, seed: u64) -> Result<RunOutcome> {
1623 run_with_internal(kind, circuit, seed, SimOptions::default())
1624}
1625
1626fn run_with_internal(
1627 kind: BackendKind,
1628 circuit: &Circuit,
1629 seed: u64,
1630 opts: SimOptions,
1631) -> Result<RunOutcome> {
1632 if !kind.is_auto() {
1633 validate_explicit_backend(&kind, circuit)?;
1634 }
1635 #[cfg(feature = "distributed")]
1640 if matches!(kind, BackendKind::StatevectorDistributed { .. }) {
1641 let mut backend = resolve_backend(&kind, circuit, false).build(seed);
1642 if opts.probabilities {
1643 crate::backend::dense_probability_len(backend.name(), circuit.num_qubits)?;
1648 }
1649 return execute(&mut *backend, circuit, &opts);
1650 }
1651 let route = plan_probability_route(&kind, circuit);
1652 run_route(&kind, circuit, seed, opts, &route)
1653}
1654
1655fn run_route(
1658 kind: &BackendKind,
1659 circuit: &Circuit,
1660 seed: u64,
1661 opts: SimOptions,
1662 route: &ProbabilityRoute,
1663) -> Result<RunOutcome> {
1664 match route {
1665 ProbabilityRoute::FactoredStabilizer => {
1666 let mut backend =
1667 crate::backend::factored_stabilizer::FactoredStabilizerBackend::new(seed);
1668 let fs_opts = if circuit.num_qubits > 64 {
1669 SimOptions {
1670 probabilities: false,
1671 }
1672 } else {
1673 opts
1674 };
1675 execute(&mut backend, circuit, &fs_opts)
1676 }
1677 ProbabilityRoute::Decomposed(components) => {
1678 run_decomposed(kind, components, circuit, seed, &opts)
1679 }
1680 ProbabilityRoute::StabilizerRank => {
1681 let sr = stabilizer_rank::run_stabilizer_rank(circuit, seed)?;
1682 let metadata = RunMetadata::exact(ResolvedBackend::StabilizerRank);
1683 Ok(probs_only_result(sr.probabilities, metadata))
1684 }
1685 ProbabilityRoute::TemporalClifford {
1686 has_partial_independence,
1687 } => match plan_temporal_clifford(kind, circuit) {
1688 Some(tc) => run_temporal_clifford(&tc, seed, opts.probabilities),
1689 None => run_direct(kind, circuit, seed, opts, *has_partial_independence),
1690 },
1691 ProbabilityRoute::Direct {
1692 has_partial_independence,
1693 } => run_direct(kind, circuit, seed, opts, *has_partial_independence),
1694 }
1695}
1696
1697fn run_direct(
1698 kind: &BackendKind,
1699 circuit: &Circuit,
1700 seed: u64,
1701 opts: SimOptions,
1702 has_partial_independence: bool,
1703) -> Result<RunOutcome> {
1704 match resolve(kind, circuit, has_partial_independence) {
1705 ExecutionPlan::Backend(plan) => {
1706 let mut backend = plan.build(seed);
1707 execute(&mut *backend, circuit, &opts)
1708 }
1709 ExecutionPlan::StabilizerRank => {
1710 let sr = stabilizer_rank::run_stabilizer_rank(circuit, seed)?;
1711 Ok(probs_only_result(
1712 sr.probabilities,
1713 RunMetadata::exact(ResolvedBackend::StabilizerRank),
1714 ))
1715 }
1716 ExecutionPlan::StochasticPauli { num_samples } => {
1717 Err(crate::error::PrismError::IncompatibleBackend {
1718 backend: format!(
1719 "{:?}",
1720 BackendKind::StochasticPauli { num_samples }
1721 ),
1722 reason: "StochasticPauli produces marginal estimates only; use `simulate(...).marginals()`".into(),
1723 })
1724 }
1725 ExecutionPlan::DeterministicPauli { epsilon, max_terms } => {
1726 Err(crate::error::PrismError::IncompatibleBackend {
1727 backend: format!(
1728 "{:?}",
1729 BackendKind::DeterministicPauli { epsilon, max_terms }
1730 ),
1731 reason: "DeterministicPauli produces marginals only; use `simulate(...).marginals()`".into(),
1732 })
1733 }
1734 ExecutionPlan::PauliPath => Err(reject_pauli_path("a single run")),
1735 }
1736}
1737
1738pub fn run_on(backend: &mut dyn Backend, circuit: &Circuit) -> Result<RunOutcome> {
1741 execute(backend, circuit, &SimOptions::default())
1742}
1743
1744pub fn run_on_state(
1750 backend: &mut dyn Backend,
1751 circuit: &Circuit,
1752 initial_state: &[Complex64],
1753) -> Result<RunOutcome> {
1754 check_initial_state_len(initial_state, circuit.num_qubits)?;
1755 backend.init_from_amplitudes(initial_state.to_vec(), circuit.num_classical_bits)?;
1756 apply_fused_circuit(backend, circuit)?;
1757 Ok(RunOutcome {
1758 classical_bits: backend.classical_results().to_vec(),
1759 probabilities: try_backend_probabilities(backend)?,
1760 metadata: backend_metadata(backend),
1761 })
1762}
1763
1764pub fn run_qasm(qasm: &str, seed: u64) -> Result<RunOutcome> {
1766 let circuit = crate::circuit::openqasm::parse(qasm)?;
1767 simulate(&circuit).seed(seed).run()
1768}
1769
1770#[cfg(test)]
1771fn run_shots(circuit: &Circuit, num_shots: usize, seed: u64) -> Result<ShotsResult> {
1772 run_shots_with(BackendKind::Auto, circuit, num_shots, seed)
1773}
1774
1775pub(crate) fn supports_compiled_measurement_sampling(circuit: &Circuit) -> bool {
1776 circuit.is_clifford_only()
1777 && !circuit.has_resets()
1778 && circuit.has_terminal_measurements_only()
1779 && circuit
1780 .instructions
1781 .iter()
1782 .any(|inst| matches!(inst, Instruction::Measure { .. }))
1783}
1784
1785fn supports_deferred_measurement_sampling(circuit: &Circuit) -> bool {
1786 circuit.is_clifford_only()
1787 && (circuit.has_resets() || !circuit.has_terminal_measurements_only())
1788 && circuit
1789 .instructions
1790 .iter()
1791 .any(|inst| matches!(inst, Instruction::Measure { .. }))
1792 && !circuit.instructions.iter().any(|inst| {
1793 matches!(
1794 inst,
1795 Instruction::Conditional { .. } | Instruction::Region(_)
1796 )
1797 })
1798}
1799
1800fn is_clifford_sampler_kind(kind: &BackendKind) -> bool {
1801 if kind.is_auto() {
1802 return true;
1803 }
1804 match kind {
1805 BackendKind::Stabilizer | BackendKind::FactoredStabilizer => true,
1806 #[cfg(feature = "gpu")]
1807 BackendKind::StabilizerGpu { .. } => true,
1808 _ => false,
1809 }
1810}
1811
1812fn should_use_compiled_clifford_sampling(
1813 kind: &BackendKind,
1814 circuit: &Circuit,
1815 num_shots: usize,
1816) -> bool {
1817 num_shots >= 2
1818 && supports_compiled_measurement_sampling(circuit)
1819 && is_clifford_sampler_kind(kind)
1820}
1821
1822fn should_use_deferred_clifford_sampling(
1823 kind: &BackendKind,
1824 circuit: &Circuit,
1825 num_shots: usize,
1826) -> bool {
1827 num_shots >= 2
1828 && supports_deferred_measurement_sampling(circuit)
1829 && is_clifford_sampler_kind(kind)
1830}
1831
1832fn compile_measurements_for_kind(
1833 kind: &BackendKind,
1834 circuit: &Circuit,
1835 seed: u64,
1836) -> Result<compiled::CompiledSampler> {
1837 #[cfg(not(feature = "gpu"))]
1838 let _ = kind;
1839
1840 let sampler = compiled::compile_measurements(circuit, seed)?;
1841
1842 #[cfg(feature = "gpu")]
1843 if let BackendKind::StabilizerGpu { context } = kind {
1844 return Ok(sampler.with_gpu(context.clone()));
1845 }
1846
1847 Ok(sampler)
1848}
1849
1850fn analyze_independence(circuit: &Circuit) -> (Option<Vec<Vec<usize>>>, bool) {
1855 if circuit.num_qubits >= MIN_DECOMPOSITION_QUBITS {
1856 let components = circuit.independent_subsystems();
1857 if components.len() > 1 {
1858 if should_decompose(&components, circuit.num_qubits) {
1859 return (Some(components), false);
1860 }
1861 return (None, true);
1862 }
1863 }
1864 (None, false)
1865}
1866
1867fn auto_clifford_t_budget(circuit: &Circuit) -> Option<(usize, usize)> {
1870 (circuit.is_clifford_plus_t() && circuit.has_t_gates()).then(|| {
1871 (
1872 circuit.t_count(),
1873 stabilizer_rank_budget(circuit.num_qubits),
1874 )
1875 })
1876}
1877
1878pub(super) fn auto_stabilizer_rank_t_count(circuit: &Circuit, max_t: usize) -> Option<usize> {
1882 let (t, sr_budget) = auto_clifford_t_budget(circuit)?;
1883 (t <= max_t && t <= sr_budget).then_some(t)
1884}
1885
1886enum ProbabilityRoute {
1893 FactoredStabilizer,
1894 Decomposed(Vec<Vec<usize>>),
1895 StabilizerRank,
1896 TemporalClifford {
1899 has_partial_independence: bool,
1900 },
1901 Direct {
1902 has_partial_independence: bool,
1903 },
1904}
1905
1906fn plan_probability_route(kind: &BackendKind, circuit: &Circuit) -> ProbabilityRoute {
1907 let (decompose, has_partial_independence) = analyze_independence(circuit);
1908 if let Some(components) = decompose {
1909 let max_block = components.iter().map(|c| c.len()).max().unwrap_or(0);
1910 if kind.is_auto()
1911 && circuit.is_clifford_only()
1912 && circuit.num_qubits >= MIN_FACTORED_STABILIZER_QUBITS
1913 && max_block >= MIN_BLOCK_FOR_FACTORED_STAB
1914 {
1915 return ProbabilityRoute::FactoredStabilizer;
1916 }
1917 return ProbabilityRoute::Decomposed(components);
1918 }
1919 if kind.is_auto()
1920 && circuit.num_qubits <= MAX_STABILIZER_RANK_QUBITS
1921 && !has_nonunitary_or_classical_ops(circuit)
1922 && auto_stabilizer_rank_t_count(circuit, MAX_AUTO_T_COUNT_EXACT).is_some()
1923 {
1924 return ProbabilityRoute::StabilizerRank;
1925 }
1926 if has_temporal_clifford_opportunity(kind, circuit) {
1927 return ProbabilityRoute::TemporalClifford {
1928 has_partial_independence,
1929 };
1930 }
1931 ProbabilityRoute::Direct {
1932 has_partial_independence,
1933 }
1934}
1935
1936fn auto_terminal_statevector_candidate(circuit: &Circuit) -> bool {
1939 match plan_probability_route(&BackendKind::Auto, circuit) {
1940 ProbabilityRoute::Direct {
1941 has_partial_independence,
1942 } => auto_selects_cpu_statevector(circuit, has_partial_independence),
1943 _ => false,
1944 }
1945}
1946
1947fn terminal_statevector_candidate(kind: &BackendKind, circuit: &Circuit) -> bool {
1948 if kind.is_auto() {
1949 return auto_terminal_statevector_candidate(circuit);
1950 }
1951 match kind {
1952 BackendKind::Statevector => true,
1953 #[cfg(feature = "gpu")]
1954 BackendKind::StatevectorGpu { .. } => true,
1955 _ => false,
1956 }
1957}
1958
1959fn try_terminal_statevector_backend(
1960 kind: &BackendKind,
1961 circuit: &Circuit,
1962 seed: u64,
1963) -> Result<Option<TerminalStatevector>> {
1964 if !circuit.has_terminal_measurements_only() {
1965 return Ok(None);
1966 }
1967
1968 let meas_map = circuit.measurement_map();
1969 if meas_map.is_empty() {
1970 return Ok(None);
1971 }
1972
1973 let stripped = circuit.without_measurements();
1974 if !terminal_statevector_candidate(kind, &stripped) {
1975 return Ok(None);
1976 }
1977
1978 let accel = accel_for(kind, Family::Statevector, stripped.num_qubits);
1979 let mut backend = build_statevector(&accel, seed);
1980 let expanded = expand_for_backend(&backend, &stripped);
1981 let fused = fuse_for_backend(&backend, &expanded);
1982 backend.init(fused.num_qubits, fused.num_classical_bits)?;
1983 backend.apply_instructions(&fused.instructions)?;
1984
1985 Ok(Some((backend, meas_map)))
1986}
1987
1988fn try_native_terminal_backend(
2003 kind: &BackendKind,
2004 stripped: &Circuit,
2005 seed: u64,
2006) -> Result<Option<Box<dyn Backend>>> {
2007 if !kind.is_auto() {
2008 validate_explicit_backend(kind, stripped)?;
2009 }
2010 let (decomposed, has_partial_independence) = match plan_probability_route(kind, stripped) {
2011 ProbabilityRoute::Direct {
2012 has_partial_independence,
2013 } => (false, has_partial_independence),
2014 ProbabilityRoute::Decomposed(_) => (true, false),
2015 _ => return Ok(None),
2016 };
2017 let ExecutionPlan::Backend(plan) = resolve(kind, stripped, has_partial_independence) else {
2018 return Ok(None);
2019 };
2020 if decomposed && !matches!(plan, BackendPlan::ProductState) {
2021 return Ok(None);
2022 }
2023 let mut backend = plan.build(seed);
2024 if !backend.supports_native_sampling() {
2025 return Ok(None);
2026 }
2027 execute(&mut *backend, stripped, &SimOptions::classical_only())?;
2028 Ok(Some(backend))
2029}
2030
2031fn try_native_marginal_backend(
2045 kind: &BackendKind,
2046 circuit: &Circuit,
2047 seed: u64,
2048) -> Result<Option<Box<dyn Backend>>> {
2049 if !kind.is_auto() {
2050 validate_explicit_backend(kind, circuit)?;
2051 }
2052 let (decomposed, has_partial_independence) = match plan_probability_route(kind, circuit) {
2053 ProbabilityRoute::Direct {
2054 has_partial_independence,
2055 } => (false, has_partial_independence),
2056 ProbabilityRoute::Decomposed(_) => (true, false),
2057 _ => return Ok(None),
2058 };
2059 let ExecutionPlan::Backend(plan) = resolve(kind, circuit, has_partial_independence) else {
2060 return Ok(None);
2061 };
2062 if decomposed && !matches!(plan, BackendPlan::ProductState) {
2063 return Ok(None);
2064 }
2065 let mut backend = plan.build(seed);
2066 if !backend.supports_pauli_expectation() {
2067 return Ok(None);
2068 }
2069 execute(&mut *backend, circuit, &SimOptions::classical_only())?;
2070 Ok(Some(backend))
2071}
2072
2073enum ShotSource {
2077 Compiled {
2080 sampler: Box<compiled::CompiledSampler>,
2081 meas_map: Vec<(usize, usize)>,
2082 deferred: bool,
2083 },
2084 TerminalStatevector {
2085 backend: Box<StatevectorBackend>,
2086 meas_map: Vec<(usize, usize)>,
2087 },
2088 Native {
2090 backend: Box<dyn Backend>,
2091 meas_map: Vec<(usize, usize)>,
2092 },
2093 TerminalProbabilities {
2096 probs: Probabilities,
2097 meas_map: Vec<(usize, usize)>,
2098 metadata: RunMetadata,
2099 },
2100 StabilizerRank,
2101 PerShot,
2102}
2103
2104impl ShotSource {
2105 fn metadata(&self) -> Option<RunMetadata> {
2109 match self {
2110 ShotSource::Compiled { .. } => Some(
2111 RunMetadata::exact(ResolvedBackend::CompiledStabilizer)
2112 .with_engine(Engine::CompiledSampler),
2113 ),
2114 ShotSource::TerminalStatevector { backend, .. } => Some(backend_metadata(&**backend)),
2115 ShotSource::Native { backend, .. } => Some(backend_metadata(&**backend)),
2116 ShotSource::TerminalProbabilities { metadata, .. } => Some(metadata.clone()),
2117 ShotSource::StabilizerRank => Some(RunMetadata::exact(ResolvedBackend::StabilizerRank)),
2118 ShotSource::PerShot => None,
2119 }
2120 }
2121}
2122
2123fn prepare_shot_source(
2129 kind: &BackendKind,
2130 circuit: &Circuit,
2131 num_shots: usize,
2132 seed: u64,
2133) -> Result<ShotSource> {
2134 if should_use_compiled_clifford_sampling(kind, circuit, num_shots) {
2135 return Ok(ShotSource::Compiled {
2136 sampler: Box::new(compile_measurements_for_kind(kind, circuit, seed)?),
2137 meas_map: circuit.measurement_map(),
2138 deferred: false,
2139 });
2140 }
2141
2142 if should_use_deferred_clifford_sampling(kind, circuit, num_shots) {
2143 if let Ok(deferred) = compiled::defer_measure_reset_circuit(circuit) {
2144 return Ok(ShotSource::Compiled {
2145 sampler: Box::new(compile_measurements_for_kind(kind, &deferred, seed)?),
2146 meas_map: deferred.measurement_map(),
2147 deferred: true,
2148 });
2149 }
2150 }
2151
2152 if let Some((backend, meas_map)) = try_terminal_statevector_backend(kind, circuit, seed)? {
2153 return Ok(ShotSource::TerminalStatevector {
2154 backend: Box::new(backend),
2155 meas_map,
2156 });
2157 }
2158
2159 if matches!(kind, BackendKind::StabilizerRank) && circuit.has_t_gates() {
2160 return Ok(ShotSource::StabilizerRank);
2161 }
2162 if kind.is_auto()
2163 && circuit.has_terminal_measurements_only()
2164 && circuit.num_qubits > MAX_STABILIZER_RANK_QUBITS
2165 && auto_stabilizer_rank_t_count(circuit, MAX_AUTO_T_COUNT_SHOTS).is_some()
2166 {
2167 return Ok(ShotSource::StabilizerRank);
2168 }
2169
2170 if circuit.has_terminal_measurements_only() {
2171 let stripped = circuit.without_measurements();
2172 if let Some(backend) = try_native_terminal_backend(kind, &stripped, seed)? {
2173 return Ok(ShotSource::Native {
2174 backend,
2175 meas_map: circuit.measurement_map(),
2176 });
2177 }
2178 let result = run_with_internal(kind.clone(), &stripped, seed, SimOptions::default())?;
2179 if let Some(probs) = result.probabilities {
2180 return Ok(ShotSource::TerminalProbabilities {
2181 probs,
2182 meas_map: circuit.measurement_map(),
2183 metadata: result.metadata,
2184 });
2185 }
2186 }
2187
2188 Ok(ShotSource::PerShot)
2189}
2190
2191#[cfg(test)]
2192fn run_counts(circuit: &Circuit, num_shots: usize, seed: u64) -> Result<HashMap<Vec<u64>, u64>> {
2193 run_counts_with(BackendKind::Auto, circuit, num_shots, seed).map(|(counts, _)| counts)
2194}
2195
2196pub(crate) fn run_counts_with(
2209 kind: BackendKind,
2210 circuit: &Circuit,
2211 num_shots: usize,
2212 seed: u64,
2213) -> Result<(HashMap<Vec<u64>, u64>, RunMetadata)> {
2214 #[cfg(feature = "distributed")]
2215 if matches!(kind, BackendKind::StatevectorDistributed { .. }) {
2216 let shots = run_shots_with(kind, circuit, num_shots, seed)?;
2217 return Ok((shots.counts(), shots.metadata));
2218 }
2219
2220 let folded = circuit.fold_static_guards();
2221 let circuit = folded.as_ref();
2222
2223 let bits = circuit.num_classical_bits;
2224 let source = prepare_shot_source(&kind, circuit, num_shots, seed)?;
2225 let Some(metadata) = source.metadata() else {
2226 let shots = run_shots_per_shot(kind, circuit, num_shots, seed)?;
2227 return Ok((shots.counts(), shots.metadata));
2228 };
2229 let counts = match source {
2230 ShotSource::Compiled {
2231 mut sampler,
2232 meas_map,
2233 deferred,
2234 } => {
2235 if deferred {
2236 let packed = sampler.try_sample_bulk_packed(num_shots)?;
2237 counts_of(
2238 packed_shots_to_classical_bits(&packed, &meas_map, bits),
2239 bits,
2240 )
2241 } else {
2242 sampler.try_sample_counts(num_shots)?
2243 }
2244 }
2245 ShotSource::TerminalStatevector { backend, meas_map } => {
2246 if backend.is_gpu_resident() {
2247 let probs = backend.probabilities()?;
2248 sample_counts_from_probs(&probs, &meas_map, bits, num_shots, seed)
2249 } else {
2250 sample_counts_from_state(
2251 backend.state_vector(),
2252 backend.probability_scale(),
2253 &meas_map,
2254 bits,
2255 num_shots,
2256 seed,
2257 )
2258 }
2259 }
2260 ShotSource::Native {
2261 mut backend,
2262 meas_map,
2263 } => {
2264 let samples = backend.sample_basis_states(num_shots, seed)?;
2265 counts_of(shots_from_basis_samples(&samples, &meas_map, bits), bits)
2266 }
2267 ShotSource::TerminalProbabilities {
2268 probs, meas_map, ..
2269 } => counts_of(sample_shots(&probs, &meas_map, bits, num_shots, seed), bits),
2270 ShotSource::StabilizerRank => {
2271 stabilizer_rank::run_stabilizer_rank_shots(circuit, num_shots, seed)?.counts()
2272 }
2273 ShotSource::PerShot => unreachable!("handled above"),
2274 };
2275 Ok((counts, metadata.with_shots(num_shots)))
2276}
2277
2278fn counts_of(shots: Vec<Vec<bool>>, num_classical_bits: usize) -> HashMap<Vec<u64>, u64> {
2279 ShotsResult::from_shots(shots, num_classical_bits).counts()
2280}
2281
2282#[cfg(test)]
2283fn run_marginals(circuit: &Circuit, seed: u64) -> Result<Vec<(f64, f64)>> {
2284 run_marginals_result_with(BackendKind::Auto, circuit, seed).map(MarginalsResult::into_vec)
2285}
2286
2287#[cfg(test)]
2288fn run_marginals_with(kind: BackendKind, circuit: &Circuit, seed: u64) -> Result<Vec<(f64, f64)>> {
2289 run_marginals_result_with(kind, circuit, seed).map(MarginalsResult::into_vec)
2290}
2291
2292fn marginals_from_pauli_expectations(
2295 backend: &dyn Backend,
2296 num_qubits: usize,
2297) -> Result<MarginalsResult> {
2298 let observables: Vec<Vec<PauliTerm>> = (0..num_qubits).map(|q| vec![PauliTerm::z(q)]).collect();
2299 let expectations = backend.pauli_expectations(&observables)?;
2300 Ok(MarginalsResult {
2301 marginals: expectations_to_marginals(&expectations),
2302 metadata: backend_metadata(backend),
2303 })
2304}
2305
2306fn spd_metadata(epsilon: f64) -> RunMetadata {
2313 if epsilon > 0.0 {
2314 RunMetadata::approximate(ResolvedBackend::DeterministicPauli)
2315 } else {
2316 RunMetadata::exact(ResolvedBackend::DeterministicPauli)
2317 }
2318}
2319
2320pub(crate) fn expectations_to_marginals(expectations: &[f64]) -> Vec<(f64, f64)> {
2321 expectations
2322 .iter()
2323 .map(|ez| {
2324 let p0 = ((1.0 + ez) / 2.0).clamp(0.0, 1.0);
2325 (p0, 1.0 - p0)
2326 })
2327 .collect()
2328}
2329
2330pub(super) fn has_nonunitary_or_classical_ops(circuit: &Circuit) -> bool {
2331 circuit.instructions.iter().any(|inst| {
2332 matches!(
2333 inst,
2334 Instruction::Measure { .. }
2335 | Instruction::Reset { .. }
2336 | Instruction::Conditional { .. }
2337 | Instruction::Region(_)
2338 )
2339 })
2340}
2341
2342fn supports_pauli_marginal_backend(circuit: &Circuit) -> bool {
2343 circuit.is_clifford_plus_t() && !has_nonunitary_or_classical_ops(circuit)
2344}
2345
2346fn validate_pauli_marginal_backend(kind: &BackendKind, circuit: &Circuit) -> Result<()> {
2349 if has_nonunitary_or_classical_ops(circuit) {
2350 return Err(PrismError::IncompatibleBackend {
2351 backend: format!("{kind:?}"),
2352 reason: "Pauli marginal backends require a unitary circuit without measurements, resets, or conditionals".into(),
2353 });
2354 }
2355 Ok(())
2356}
2357
2358fn run_marginals_result_with(
2359 kind: BackendKind,
2360 circuit: &Circuit,
2361 seed: u64,
2362) -> Result<MarginalsResult> {
2363 let n = circuit.num_qubits;
2364
2365 match &kind {
2366 BackendKind::StochasticPauli { num_samples } => {
2367 validate_pauli_marginal_backend(&kind, circuit)?;
2368 let spp = unified_pauli::run_spp(circuit, *num_samples, seed)?;
2369 return Ok(MarginalsResult {
2370 marginals: expectations_to_marginals(&spp.expectations),
2371 metadata: RunMetadata::approximate(ResolvedBackend::StochasticPauli)
2372 .with_shots(*num_samples),
2373 });
2374 }
2375 BackendKind::DeterministicPauli { epsilon, max_terms } => {
2376 validate_pauli_marginal_backend(&kind, circuit)?;
2377 let spd = unified_pauli::run_spd(circuit, *epsilon, *max_terms)?;
2378 return Ok(MarginalsResult {
2379 marginals: expectations_to_marginals(&spd.expectations),
2380 metadata: spd_metadata(*epsilon),
2381 });
2382 }
2383 _ => {}
2384 }
2385
2386 if kind.is_auto()
2387 && supports_pauli_marginal_backend(circuit)
2388 && circuit.has_t_gates()
2389 && n >= MIN_QUBITS_FOR_SPD_AUTO
2390 {
2391 let spd = unified_pauli::run_spd(circuit, 0.0, AUTO_SPD_MAX_TERMS)?;
2392 return Ok(MarginalsResult {
2393 marginals: expectations_to_marginals(&spd.expectations),
2394 metadata: spd_metadata(0.0),
2395 });
2396 }
2397
2398 #[cfg(feature = "distributed")]
2401 if let BackendKind::StatevectorDistributed { context } = &kind {
2402 let mut backend =
2403 crate::backend::distributed_statevector::DistributedStatevectorBackend::new(
2404 context.clone(),
2405 seed,
2406 );
2407 execute(&mut backend, circuit, &SimOptions::classical_only())?;
2408 return marginals_from_pauli_expectations(&backend, n);
2409 }
2410
2411 if let Some(backend) = try_native_marginal_backend(&kind, circuit, seed)? {
2412 return marginals_from_pauli_expectations(&*backend, n);
2413 }
2414
2415 let result = run_with(kind, circuit, seed)?;
2416 if let Some(probs) = &result.probabilities {
2417 Ok(MarginalsResult {
2418 marginals: probs.marginals(),
2419 metadata: result.metadata.clone(),
2420 })
2421 } else {
2422 Err(PrismError::BackendUnsupported {
2423 backend: "simulate".into(),
2424 operation: format!(
2425 "marginals for {} qubits without backend probability output",
2426 circuit.num_qubits
2427 ),
2428 })
2429 }
2430}
2431
2432pub fn run_expectation_values(
2455 circuit: &Circuit,
2456 observables: &[Vec<PauliTerm>],
2457 seed: u64,
2458) -> Result<Vec<f64>> {
2459 run_expectation_values_with(BackendKind::Auto, circuit, observables, seed)
2460}
2461
2462fn run_expectation_values_with(
2463 kind: BackendKind,
2464 circuit: &Circuit,
2465 observables: &[Vec<PauliTerm>],
2466 seed: u64,
2467) -> Result<Vec<f64>> {
2468 run_expectation_values_reported(kind, circuit, observables, seed)
2469 .map(ExpectationResult::into_values)
2470}
2471
2472fn run_expectation_values_reported(
2473 kind: BackendKind,
2474 circuit: &Circuit,
2475 observables: &[Vec<PauliTerm>],
2476 seed: u64,
2477) -> Result<ExpectationResult> {
2478 require_unitary_circuit(&kind, circuit, "expectation values require")?;
2479
2480 match &kind {
2481 BackendKind::StochasticPauli { num_samples } => {
2482 let mut values = Vec::with_capacity(observables.len());
2483 let mut std_errors = Vec::with_capacity(observables.len());
2484 for (i, obs) in observables.iter().enumerate() {
2485 let r = unified_pauli::run_spp_observable(
2486 circuit,
2487 obs,
2488 *num_samples,
2489 seed.wrapping_add(i as u64),
2490 )?;
2491 values.push(r.mean);
2492 std_errors.push(r.std_error);
2493 }
2494 Ok(ExpectationResult {
2495 values,
2496 std_errors: Some(std_errors),
2497 metadata: RunMetadata::approximate(ResolvedBackend::StochasticPauli)
2498 .with_shots(*num_samples),
2499 })
2500 }
2501 BackendKind::DeterministicPauli { epsilon, max_terms } => {
2502 let mut values = Vec::with_capacity(observables.len());
2503 for obs in observables {
2504 let r = unified_pauli::run_spd_observable(circuit, obs, *epsilon, *max_terms)?;
2505 values.push(r.mean);
2506 }
2507 Ok(analytic_expectations(values, spd_metadata(*epsilon)))
2508 }
2509 _ if kind.is_auto() || kind.is_stabilizer_family() => {
2510 if circuit.is_clifford_only() {
2511 let mut values = Vec::with_capacity(observables.len());
2512 for obs in observables {
2513 let r = unified_pauli::run_spd_observable(circuit, obs, 0.0, 0)?;
2514 values.push(r.mean);
2515 }
2516 Ok(analytic_expectations(values, spd_metadata(0.0)))
2517 } else if kind.is_auto() {
2518 if circuit.num_qubits > max_statevector_qubits() {
2519 return expectation_values_native(&kind, circuit, observables, seed);
2520 }
2521 expectation_values_statevector(&kind, circuit, observables, seed)
2522 } else {
2523 Err(PrismError::IncompatibleBackend {
2524 backend: format!("{kind:?}"),
2525 reason: "stabilizer backends require a Clifford-only circuit".into(),
2526 })
2527 }
2528 }
2529 BackendKind::Statevector => {
2530 expectation_values_statevector(&kind, circuit, observables, seed)
2531 }
2532 #[cfg(feature = "gpu")]
2533 BackendKind::StatevectorGpu { .. } => {
2534 expectation_values_statevector(&kind, circuit, observables, seed)
2535 }
2536 other => expectation_values_native(other, circuit, observables, seed),
2537 }
2538}
2539
2540pub fn run_observable_expectation(
2569 circuit: &Circuit,
2570 observable: &PauliObservable,
2571 seed: u64,
2572) -> Result<ObservableExpectation> {
2573 run_observable_expectation_reported(BackendKind::Auto, circuit, observable, seed)
2574}
2575
2576fn run_observable_expectation_reported(
2577 kind: BackendKind,
2578 circuit: &Circuit,
2579 observable: &PauliObservable,
2580 seed: u64,
2581) -> Result<ObservableExpectation> {
2582 require_unitary_circuit(&kind, circuit, "expectation values require")?;
2583
2584 let grouped_statevector = match &kind {
2585 BackendKind::Statevector => true,
2586 #[cfg(feature = "gpu")]
2587 BackendKind::StatevectorGpu { .. } => true,
2588 _ => {
2589 kind.is_auto()
2590 && !circuit.is_clifford_only()
2591 && circuit.num_qubits <= max_statevector_qubits()
2592 }
2593 };
2594 if grouped_statevector {
2595 return grouped_expectation_statevector(&kind, circuit, observable, seed);
2596 }
2597
2598 let result =
2599 run_expectation_values_reported(kind, circuit, &observable_vecs(observable), seed)?;
2600 Ok(weighted_observable_result(
2601 observable,
2602 &result.values,
2603 result.std_errors.as_deref(),
2604 result.metadata,
2605 ))
2606}
2607
2608fn observable_vecs(observable: &PauliObservable) -> Vec<Vec<PauliTerm>> {
2609 observable
2610 .terms()
2611 .iter()
2612 .map(|(_, factors)| factors.clone())
2613 .collect()
2614}
2615
2616fn weighted_observable_result(
2620 observable: &PauliObservable,
2621 values: &[f64],
2622 std_errors: Option<&[f64]>,
2623 metadata: RunMetadata,
2624) -> ObservableExpectation {
2625 let coefficients = observable.terms().iter().map(|(c, _)| *c);
2626 let mean = coefficients.clone().zip(values).map(|(c, v)| c * v).sum();
2627 let std_error = std_errors.map(|errors| {
2628 coefficients
2629 .zip(errors)
2630 .map(|(c, e)| (c * e).powi(2))
2631 .sum::<f64>()
2632 .sqrt()
2633 });
2634 ObservableExpectation {
2635 mean,
2636 variance: None,
2637 group_variances: None,
2638 std_error,
2639 metadata,
2640 }
2641}
2642
2643fn grouped_expectation_statevector(
2653 kind: &BackendKind,
2654 circuit: &Circuit,
2655 observable: &PauliObservable,
2656 seed: u64,
2657) -> Result<ObservableExpectation> {
2658 let terms = observable.terms();
2659 for (_, factors) in terms {
2663 validate_observable(factors, circuit.num_qubits)?;
2664 }
2665
2666 let accel = accel_for(kind, Family::Statevector, circuit.num_qubits);
2667 let mut backend = build_statevector(&accel, seed);
2668 let expanded = expand_for_backend(&backend, circuit);
2669 let fused = fuse_for_backend(&backend, &expanded);
2670 backend.init(fused.num_qubits, fused.num_classical_bits)?;
2671 let masks = terms
2672 .iter()
2673 .map(|(_, factors)| pauli_masks(factors, circuit.num_qubits))
2674 .collect::<Result<Vec<_>>>()?;
2675 backend.apply_instructions(&fused.instructions)?;
2676 let metadata = backend_metadata(&backend);
2677
2678 let grouping = observable.grouping();
2679
2680 let mut combined = masks.clone();
2687 let mut pair_blocks: Vec<(usize, usize, Vec<f64>)> = Vec::new();
2688 let mut deferred: Vec<usize> = Vec::new();
2689 for (gi, group) in grouping.groups.iter().enumerate() {
2690 let members = &group.term_indices;
2691 if members.len() * (members.len() - 1) / 2 > MAX_PAIR_MASKS_PER_GROUP {
2692 deferred.push(gi);
2693 continue;
2694 }
2695 let first_mask = combined.len();
2696 let mut pair_coefficients = Vec::with_capacity(members.len() * (members.len() - 1) / 2);
2697 for (pos, &i) in members.iter().enumerate() {
2698 for &j in &members[pos + 1..] {
2699 let product_x = masks[i].0 ^ masks[j].0;
2700 let product_z = masks[i].1 ^ masks[j].1;
2701 combined.push((product_x, product_z, (product_x & product_z).count_ones()));
2702 pair_coefficients.push(2.0 * terms[i].0 * terms[j].0);
2703 }
2704 }
2705 pair_blocks.push((gi, first_mask, pair_coefficients));
2706 }
2707
2708 let (values, host) = match pauli_expectations_on_device(&backend, &combined) {
2711 Some(values) => (values?, None),
2712 None => {
2713 let state = backend.state_vector();
2714 let norm = crate::backend::state_norm_sqr(state);
2715 let values = pauli_expectations_from_masks(state, &combined, norm);
2716 (values, Some((state, norm)))
2717 }
2718 };
2719
2720 let mean: f64 = terms.iter().zip(&values).map(|((c, _), v)| c * v).sum();
2721 let mut group_variances = vec![0.0; grouping.groups.len()];
2722
2723 for (gi, first_mask, pair_coefficients) in &pair_blocks {
2724 let group = &grouping.groups[*gi];
2725 let m1: f64 = group
2726 .term_indices
2727 .iter()
2728 .map(|&i| terms[i].0 * values[i])
2729 .sum();
2730 let square_diag: f64 = group
2731 .term_indices
2732 .iter()
2733 .map(|&i| terms[i].0 * terms[i].0)
2734 .sum();
2735 let square_cross: f64 = pair_coefficients
2736 .iter()
2737 .zip(&values[*first_mask..])
2738 .map(|(c, v)| c * v)
2739 .sum();
2740 group_variances[*gi] = (square_diag + square_cross - m1 * m1).max(0.0);
2741 }
2742
2743 if !deferred.is_empty() {
2746 let exported;
2747 let (state, norm): (&[Complex64], f64) = match host {
2748 Some(host) => host,
2749 None => {
2750 exported = backend.export_statevector()?;
2751 (&exported, crate::backend::state_norm_sqr(&exported))
2752 }
2753 };
2754 let mut scratch: Option<StatevectorBackend> = None;
2755 for &gi in &deferred {
2756 let group = &grouping.groups[gi];
2757 let coefficients: Vec<f64> = group.term_indices.iter().map(|&i| terms[i].0).collect();
2758 let (m1, m2) = if group.is_z_only() {
2759 let zmasks: Vec<usize> = group.term_indices.iter().map(|&i| masks[i].1).collect();
2760 observable::weighted_group_moments(state, &zmasks, &coefficients, norm)
2761 } else {
2762 let zmasks: Vec<usize> = group
2763 .term_indices
2764 .iter()
2765 .map(|&i| masks[i].0 | masks[i].1)
2766 .collect();
2767 let rotation_circuit = group.basis_rotation_circuit(circuit.num_qubits);
2768 let rotation = crate::circuit::fusion::fuse_circuit(&rotation_circuit, true);
2769 let rotated = scratch.get_or_insert_with(|| StatevectorBackend::new(seed));
2770 rotated.init_from_amplitudes(state.to_vec(), 0)?;
2771 rotated.apply_instructions(&rotation.instructions)?;
2772 observable::weighted_group_moments(
2773 rotated.state_vector(),
2774 &zmasks,
2775 &coefficients,
2776 norm,
2777 )
2778 };
2779 group_variances[gi] = (m2 - m1 * m1).max(0.0);
2780 }
2781 }
2782
2783 let variance = group_variances.iter().sum();
2784 Ok(ObservableExpectation {
2785 mean,
2786 variance: Some(variance),
2787 group_variances: Some(group_variances),
2788 std_error: None,
2789 metadata,
2790 })
2791}
2792
2793const MAX_PAIR_MASKS_PER_GROUP: usize = 20;
2799
2800fn analytic_expectations(values: Vec<f64>, metadata: RunMetadata) -> ExpectationResult {
2803 ExpectationResult {
2804 values,
2805 std_errors: None,
2806 metadata,
2807 }
2808}
2809
2810#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2813enum Diagnostic {
2814 StateVector,
2815 ReducedDensityMatrix,
2816 Entropy,
2817 Overlap,
2818}
2819
2820impl Diagnostic {
2821 fn terminal(self) -> &'static str {
2822 match self {
2823 Diagnostic::StateVector => "a statevector",
2824 Diagnostic::ReducedDensityMatrix => "a reduced density matrix",
2825 Diagnostic::Entropy => "entanglement entropy",
2826 Diagnostic::Overlap => "a state overlap",
2827 }
2828 }
2829}
2830
2831fn plan_answers(plan: &BackendPlan, diagnostic: Diagnostic) -> bool {
2838 match diagnostic {
2839 Diagnostic::ReducedDensityMatrix => matches!(
2840 plan,
2841 BackendPlan::Statevector { .. }
2842 | BackendPlan::Sparse
2843 | BackendPlan::Factored
2844 | BackendPlan::ProductState
2845 | BackendPlan::DensityMatrix { .. }
2846 | BackendPlan::Stabilizer { .. }
2847 | BackendPlan::FactoredStabilizer
2848 ),
2849 Diagnostic::Entropy => matches!(
2850 plan,
2851 BackendPlan::Statevector { .. }
2852 | BackendPlan::Mps { .. }
2853 | BackendPlan::ProductState
2854 | BackendPlan::Stabilizer { .. }
2855 | BackendPlan::FactoredStabilizer
2856 ),
2857 Diagnostic::StateVector | Diagnostic::Overlap => {
2858 !matches!(plan, BackendPlan::DensityMatrix { .. })
2859 }
2860 }
2861}
2862
2863fn stateless_route(kind: &BackendKind, diagnostic: Diagnostic, route: &str) -> PrismError {
2866 PrismError::IncompatibleBackend {
2867 backend: format!("{kind:?}"),
2868 reason: format!(
2869 "{} needs a backend that holds a state; the {route} route returns \
2870 probabilities only",
2871 diagnostic.terminal()
2872 ),
2873 }
2874}
2875
2876fn check_diagnostic_width(backend: &dyn Backend, diagnostic: Diagnostic, k: usize) -> Result<()> {
2880 match diagnostic {
2881 Diagnostic::ReducedDensityMatrix => {
2882 crate::backend::reduced_density::reduced_density_side(backend.name(), k)?;
2883 Ok(())
2884 }
2885 Diagnostic::StateVector => {
2886 if k > crate::backend::schmidt::export_cap() {
2887 return Err(crate::backend::schmidt::export_cap_exceeded(
2888 backend.name(),
2889 format!("dense statevector of {k} qubits"),
2890 ));
2891 }
2892 Ok(())
2893 }
2894 Diagnostic::Entropy | Diagnostic::Overlap => Ok(()),
2895 }
2896}
2897
2898fn diagnostic_backend(
2908 kind: &BackendKind,
2909 circuit: &Circuit,
2910 initial_state: Option<&[Complex64]>,
2911 seed: u64,
2912 diagnostic: Diagnostic,
2913 subsystem_len: usize,
2914) -> Result<Box<dyn Backend>> {
2915 if let Some(state) = initial_state {
2916 let mut backend = backend_from_initial_state(kind, circuit, state, seed)?;
2917 check_diagnostic_width(&*backend, diagnostic, subsystem_len)?;
2918 apply_fused_circuit(&mut *backend, circuit)?;
2919 return Ok(backend);
2920 }
2921 if !kind.is_auto() {
2922 validate_explicit_backend(kind, circuit)?;
2923 }
2924 let (_, has_partial_independence) = analyze_independence(circuit);
2925 let mut plan = match resolve(kind, circuit, has_partial_independence) {
2926 ExecutionPlan::Backend(plan) => plan,
2927 ExecutionPlan::StabilizerRank => {
2928 return Err(stateless_route(kind, diagnostic, "stabilizer-rank"));
2929 }
2930 ExecutionPlan::StochasticPauli { .. } => {
2931 return Err(stateless_route(kind, diagnostic, "stochastic Pauli"));
2932 }
2933 ExecutionPlan::DeterministicPauli { .. } => {
2934 return Err(stateless_route(kind, diagnostic, "deterministic Pauli"));
2935 }
2936 ExecutionPlan::PauliPath => {
2937 return Err(stateless_route(kind, diagnostic, "Pauli path"));
2938 }
2939 };
2940 if kind.is_auto()
2941 && !plan_answers(&plan, diagnostic)
2942 && circuit.num_qubits <= max_statevector_qubits()
2943 {
2944 plan = plan_for_family(kind, Family::Statevector, circuit.num_qubits);
2945 }
2946 let mut backend: Box<dyn Backend> = plan.build(seed);
2947 check_diagnostic_width(&*backend, diagnostic, subsystem_len)?;
2948 execute(&mut *backend, circuit, &SimOptions::classical_only())?;
2949 Ok(backend)
2950}
2951
2952fn expectation_values_native(
2959 kind: &BackendKind,
2960 circuit: &Circuit,
2961 observables: &[Vec<PauliTerm>],
2962 seed: u64,
2963) -> Result<ExpectationResult> {
2964 if !kind.is_auto() {
2965 validate_explicit_backend(kind, circuit)?;
2966 }
2967 for observable in observables {
2970 validate_observable(observable, circuit.num_qubits)?;
2971 }
2972
2973 let (_, has_partial_independence) = analyze_independence(circuit);
2974 let ExecutionPlan::Backend(plan) = resolve(kind, circuit, has_partial_independence) else {
2975 return Err(PrismError::IncompatibleBackend {
2976 backend: format!("{kind:?}"),
2977 reason: "expectation values need a backend that holds a state; the stabilizer-rank \
2978 route returns probabilities only"
2979 .into(),
2980 });
2981 };
2982
2983 let mut backend = plan.build(seed);
2984 if !backend.supports_pauli_expectation() {
2985 return Err(PrismError::BackendUnsupported {
2986 backend: backend.name().to_string(),
2987 operation: "Pauli expectation values".to_string(),
2988 });
2989 }
2990 execute(&mut *backend, circuit, &SimOptions::classical_only())?;
2991 let values = backend.pauli_expectations(observables)?;
2992 Ok(analytic_expectations(values, backend_metadata(&*backend)))
2993}
2994
2995fn expectation_values_statevector(
2996 kind: &BackendKind,
2997 circuit: &Circuit,
2998 observables: &[Vec<PauliTerm>],
2999 seed: u64,
3000) -> Result<ExpectationResult> {
3001 for obs in observables {
3005 validate_observable(obs, circuit.num_qubits)?;
3006 }
3007
3008 let accel = accel_for(kind, Family::Statevector, circuit.num_qubits);
3009 let mut backend = build_statevector(&accel, seed);
3010 let expanded = expand_for_backend(&backend, circuit);
3011 let fused = fuse_for_backend(&backend, &expanded);
3012 backend.init(fused.num_qubits, fused.num_classical_bits)?;
3013 let masks = observables
3014 .iter()
3015 .map(|obs| pauli_masks(obs, circuit.num_qubits))
3016 .collect::<Result<Vec<_>>>()?;
3017 backend.apply_instructions(&fused.instructions)?;
3018
3019 let values = match pauli_expectations_on_device(&backend, &masks) {
3020 Some(values) => values?,
3021 None => {
3022 let state = backend.state_vector();
3023 let norm = crate::backend::state_norm_sqr(state);
3024 pauli_expectations_from_masks(state, &masks, norm)
3025 }
3026 };
3027 let metadata = backend_metadata(&backend);
3028 Ok(analytic_expectations(values, metadata))
3029}
3030
3031fn pauli_expectations_on_device(
3036 backend: &StatevectorBackend,
3037 masks: &[(usize, usize, u32)],
3038) -> Option<Result<Vec<f64>>> {
3039 if !backend.is_gpu_resident() {
3040 return None;
3041 }
3042 let request: Vec<(u64, u64)> = masks
3043 .iter()
3044 .map(|&(xmask, zmask, _)| (xmask as u64, zmask as u64))
3045 .chain(std::iter::once((0, 0)))
3046 .collect();
3047 let sums = match backend.gpu_pauli_sums(&request)? {
3048 Ok(sums) => sums,
3049 Err(e) => return Some(Err(e)),
3050 };
3051 let norm = sums[masks.len()].re;
3052 if norm == 0.0 {
3053 return Some(Ok(vec![0.0; masks.len()]));
3054 }
3055 Some(Ok(masks
3056 .iter()
3057 .zip(&sums)
3058 .map(|(&(_, _, num_y), sum)| (sum * i_pow(num_y)).re / norm)
3059 .collect()))
3060}
3061
3062#[cfg(feature = "distributed")]
3069fn run_shots_distributed(
3070 context: std::sync::Arc<crate::distributed::DistributedContext>,
3071 circuit: &Circuit,
3072 num_shots: usize,
3073 seed: u64,
3074) -> Result<ShotsResult> {
3075 use crate::backend::distributed_statevector::DistributedStatevectorBackend;
3076
3077 let meas_map = circuit.measurement_map();
3078 if meas_map.is_empty() {
3079 let mut backend = DistributedStatevectorBackend::new(context, seed);
3083 backend.init(circuit.num_qubits, circuit.num_classical_bits)?;
3084 return Ok(ShotsResult::from_shots(
3085 vec![vec![false; circuit.num_classical_bits]; num_shots],
3086 circuit.num_classical_bits,
3087 )
3088 .with_metadata(backend_metadata(&backend)));
3089 }
3090
3091 if circuit.has_terminal_measurements_only() {
3092 let stripped = circuit.without_measurements();
3093 let mut backend = DistributedStatevectorBackend::new(context, seed);
3094 execute(&mut backend, &stripped, &SimOptions::classical_only())?;
3095 let samples = backend.sample_basis_states(num_shots, seed)?;
3096 return Ok(ShotsResult::from_shots(
3097 shots_from_basis_samples(&samples, &meas_map, circuit.num_classical_bits),
3098 circuit.num_classical_bits,
3099 )
3100 .with_metadata(backend_metadata(&backend)));
3101 }
3102
3103 let probe = DistributedStatevectorBackend::new(context.clone(), seed);
3104 let expanded = expand_for_backend(&probe, circuit);
3105 let fused = fuse_for_backend(&probe, &expanded);
3106 let opts = SimOptions::classical_only();
3107 let mut shots = Vec::with_capacity(num_shots);
3108 let mut metadata = RunMetadata::exact(ResolvedBackend::Distributed);
3109 for i in 0..num_shots {
3110 let shot_seed = seed.wrapping_add(i as u64);
3111 let mut backend = DistributedStatevectorBackend::new(context.clone(), shot_seed);
3112 let result = execute_circuit(&mut backend, &fused, &opts)?;
3113 metadata.weaken_with(&result.metadata);
3114 shots.push(result.classical_bits);
3115 }
3116 Ok(ShotsResult::from_shots(shots, circuit.num_classical_bits).with_metadata(metadata))
3117}
3118
3119pub(crate) fn run_shots_with(
3121 kind: BackendKind,
3122 circuit: &Circuit,
3123 num_shots: usize,
3124 seed: u64,
3125) -> Result<ShotsResult> {
3126 #[cfg(feature = "distributed")]
3130 if let BackendKind::StatevectorDistributed { context } = &kind {
3131 return run_shots_distributed(context.clone(), circuit, num_shots, seed);
3132 }
3133
3134 let folded = circuit.fold_static_guards();
3138 let circuit = folded.as_ref();
3139
3140 let bits = circuit.num_classical_bits;
3141 let source = prepare_shot_source(&kind, circuit, num_shots, seed)?;
3142 let Some(metadata) = source.metadata() else {
3143 return run_shots_per_shot(kind, circuit, num_shots, seed);
3144 };
3145 let result = match source {
3146 ShotSource::Compiled {
3147 mut sampler,
3148 meas_map,
3149 ..
3150 } => {
3151 let packed = sampler.try_sample_bulk_packed(num_shots)?;
3152 ShotsResult::from_shots(
3153 packed_shots_to_classical_bits(&packed, &meas_map, bits),
3154 bits,
3155 )
3156 }
3157 ShotSource::TerminalStatevector { backend, meas_map } => {
3158 let shots = if backend.is_gpu_resident() {
3159 let probs = backend.probabilities()?;
3160 sample_shots_from_probs(&probs, &meas_map, bits, num_shots, seed)
3161 } else {
3162 sample_shots_from_state(
3163 backend.state_vector(),
3164 backend.probability_scale(),
3165 &meas_map,
3166 bits,
3167 num_shots,
3168 seed,
3169 )
3170 };
3171 ShotsResult::from_shots(shots, bits)
3172 }
3173 ShotSource::Native {
3174 mut backend,
3175 meas_map,
3176 } => {
3177 let samples = backend.sample_basis_states(num_shots, seed)?;
3178 ShotsResult::from_shots(shots_from_basis_samples(&samples, &meas_map, bits), bits)
3179 }
3180 ShotSource::TerminalProbabilities {
3181 probs, meas_map, ..
3182 } => ShotsResult::from_shots(sample_shots(&probs, &meas_map, bits, num_shots, seed), bits),
3183 ShotSource::StabilizerRank => {
3184 stabilizer_rank::run_stabilizer_rank_shots(circuit, num_shots, seed)?
3185 }
3186 ShotSource::PerShot => unreachable!("handled above"),
3187 };
3188 Ok(result.with_metadata(metadata))
3189}
3190
3191fn run_shots_per_shot(
3193 kind: BackendKind,
3194 circuit: &Circuit,
3195 num_shots: usize,
3196 seed: u64,
3197) -> Result<ShotsResult> {
3198 if !kind.is_auto() {
3200 validate_explicit_backend(&kind, circuit)?;
3201 }
3202
3203 let (decompose, has_partial_independence) = analyze_independence(circuit);
3204
3205 if matches!(kind, BackendKind::StabilizerRank) {
3206 return stabilizer_rank::run_stabilizer_rank_shots(circuit, num_shots, seed);
3207 }
3208 if matches!(
3209 kind,
3210 BackendKind::StochasticPauli { .. } | BackendKind::DeterministicPauli { .. }
3211 ) {
3212 return Err(crate::error::PrismError::IncompatibleBackend {
3213 backend: format!("{kind:?}"),
3214 reason: "Pauli propagation backends do not support mid-circuit measurements".into(),
3215 });
3216 }
3217 if kind.is_auto() && auto_stabilizer_rank_t_count(circuit, MAX_AUTO_T_COUNT_SHOTS).is_some() {
3218 return stabilizer_rank::run_stabilizer_rank_shots(circuit, num_shots, seed);
3219 }
3220
3221 if has_temporal_clifford_opportunity(&kind, circuit) {
3222 if decompose.is_none() {
3223 if let Some(tc) = plan_temporal_clifford(&kind, circuit) {
3224 let route = ResolvedBackend::Statevector;
3225 return collect_shots(circuit, num_shots, seed, route, |shot_seed| {
3226 let outcome = run_temporal_clifford(&tc, shot_seed, false)?;
3227 Ok((outcome.classical_bits, outcome.metadata))
3228 });
3229 }
3230 }
3231 let opts = SimOptions::classical_only();
3235 let route = resolve_backend(&kind, circuit, has_partial_independence).resolved();
3236 let plan = plan_probability_route(&kind, circuit);
3237 return collect_shots(circuit, num_shots, seed, route, |shot_seed| {
3238 let outcome = run_route(&kind, circuit, shot_seed, opts, &plan)?;
3239 Ok((outcome.classical_bits, outcome.metadata))
3240 });
3241 }
3242
3243 let opts = SimOptions::classical_only();
3244
3245 if let Some(ref comps) = decompose {
3246 let partitions = circuit.partition_subcircuits(comps);
3247 let block_plans: Vec<BackendPlan> = partitions
3248 .iter()
3249 .map(|(sub, _, _)| {
3250 if !kind.is_auto() {
3251 validate_explicit_backend(&kind, sub)?;
3252 }
3253 Ok(resolve_backend(&kind, sub, false))
3254 })
3255 .collect::<Result<_>>()?;
3256 let fused_blocks: Vec<std::borrow::Cow<'_, Circuit>> = partitions
3257 .iter()
3258 .zip(&block_plans)
3259 .map(|((sub, _, _), plan)| {
3260 let probe = plan.build(seed);
3261 let expanded = expand_for_backend(&*probe, sub);
3262 std::borrow::Cow::Owned(fuse_for_backend(&*probe, &expanded).into_owned())
3263 })
3264 .collect();
3265
3266 collect_shots(
3267 circuit,
3268 num_shots,
3269 seed,
3270 ResolvedBackend::Decomposed,
3271 |shot_seed| {
3272 let result = run_decomposed_prefused(
3273 &block_plans,
3274 comps,
3275 &partitions,
3276 &fused_blocks,
3277 shot_seed,
3278 &opts,
3279 circuit,
3280 )?;
3281 Ok((result.classical_bits, result.metadata))
3282 },
3283 )
3284 } else {
3285 let plan = resolve_backend(&kind, circuit, has_partial_independence);
3286 let probe = plan.build(seed);
3287 let expanded = expand_for_backend(&*probe, circuit);
3288 let fused = fuse_for_backend(&*probe, &expanded);
3289
3290 collect_shots(circuit, num_shots, seed, plan.resolved(), |shot_seed| {
3291 let mut backend = plan.build(shot_seed);
3292 let outcome = execute_circuit(&mut *backend, &fused, &opts)?;
3293 Ok((outcome.classical_bits, outcome.metadata))
3294 })
3295 }
3296}
3297
3298fn collect_shots(
3302 circuit: &Circuit,
3303 num_shots: usize,
3304 seed: u64,
3305 route: ResolvedBackend,
3306 mut shot: impl FnMut(u64) -> Result<(Vec<bool>, RunMetadata)>,
3307) -> Result<ShotsResult> {
3308 let mut shots = Vec::with_capacity(num_shots);
3309 let mut metadata = RunMetadata::exact(route);
3310 for i in 0..num_shots {
3311 let (bits, shot_metadata) = shot(seed.wrapping_add(i as u64))?;
3312 if i == 0 {
3313 metadata = shot_metadata;
3314 } else {
3315 metadata.weaken_with(&shot_metadata);
3316 }
3317 shots.push(bits);
3318 }
3319 Ok(ShotsResult::from_shots(shots, circuit.num_classical_bits).with_metadata(metadata))
3320}
3321
3322fn general_noise_plan(kind: &BackendKind, circuit: &Circuit) -> BackendPlan {
3327 let family = if !circuit.has_entangling_gates() {
3328 Family::ProductState
3329 } else if circuit.num_qubits > max_statevector_qubits() {
3330 if circuit.is_sparse_friendly() && circuit.num_qubits <= MAX_SPARSE_INDEX_QUBITS {
3331 Family::Sparse
3332 } else {
3333 Family::Mps
3334 }
3335 } else {
3336 Family::Statevector
3337 };
3338 plan_for_family(kind, family, circuit.num_qubits)
3339}
3340
3341pub(crate) fn run_shots_with_noise(
3349 kind: BackendKind,
3350 circuit: &Circuit,
3351 noise_model: &noise::NoiseModel,
3352 num_shots: usize,
3353 seed: u64,
3354) -> Result<ShotsResult> {
3355 noise_model.validate_for(circuit)?;
3356
3357 #[cfg(feature = "distributed")]
3362 if matches!(kind, BackendKind::StatevectorDistributed { .. }) {
3363 return Err(crate::error::PrismError::IncompatibleBackend {
3364 backend: format!("{kind:?}"),
3365 reason: "noisy shot sampling is not supported on the distributed backend; \
3366 trajectory execution cannot keep rank collectives in lockstep"
3367 .into(),
3368 });
3369 }
3370
3371 if kind.is_density_matrix() {
3372 let probs = exact_noisy_probabilities(&kind, circuit, noise_model, None, seed)?;
3373 return Ok(ShotsResult::from_shots(
3374 sample_exact_noisy_shots(&probs, circuit, noise_model, num_shots, seed),
3375 circuit.num_classical_bits,
3376 )
3377 .with_metadata(exact_mixture_metadata(&kind)));
3378 }
3379
3380 if !kind.supports_noisy_per_shot() {
3381 return Err(crate::error::PrismError::IncompatibleBackend {
3382 backend: format!("{kind:?}"),
3383 reason: "this backend holds no per-shot pure state to inject noise into; select \
3384 DensityMatrix for the exact mixed state, or a backend that evolves one \
3385 state per trajectory"
3386 .into(),
3387 });
3388 }
3389
3390 let is_stabilizer_kind = kind.is_stabilizer_family();
3391
3392 if is_stabilizer_kind && !noise_model.has_only_pauli_channels() {
3393 return Err(crate::error::PrismError::IncompatibleBackend {
3394 backend: format!("{kind:?}"),
3395 reason: format!(
3396 "stabilizer backends only support Pauli/depolarizing noise; use {} for amplitude damping, phase damping, thermal relaxation, or custom Kraus",
3397 BackendKind::general_noise_backend_names()
3398 ),
3399 });
3400 }
3401
3402 if !noise_model.has_only_pauli_channels() && !kind.supports_general_noise() {
3403 return Err(crate::error::PrismError::IncompatibleBackend {
3404 backend: format!("{kind:?}"),
3405 reason: format!(
3406 "non-Pauli noise requires {}",
3407 BackendKind::general_noise_backend_names()
3408 ),
3409 });
3410 }
3411
3412 if is_stabilizer_kind && !circuit.is_clifford_only() {
3413 return Err(crate::error::PrismError::IncompatibleBackend {
3414 backend: format!("{kind:?}"),
3415 reason: "circuit contains non-Clifford gates".into(),
3416 });
3417 }
3418
3419 if !kind.is_auto() {
3420 validate_explicit_backend(&kind, circuit)?;
3421 }
3422
3423 if noise_model.has_only_pauli_channels() {
3424 let use_compiled = (kind.is_auto()
3425 || matches!(
3426 kind,
3427 BackendKind::Stabilizer | BackendKind::FactoredStabilizer
3428 ))
3429 && supports_compiled_measurement_sampling(circuit)
3430 || {
3431 #[cfg(feature = "gpu")]
3432 {
3433 matches!(kind, BackendKind::StabilizerGpu { .. })
3434 && supports_compiled_measurement_sampling(circuit)
3435 }
3436 #[cfg(not(feature = "gpu"))]
3437 {
3438 false
3439 }
3440 };
3441
3442 if use_compiled {
3443 #[cfg(feature = "gpu")]
3444 if let BackendKind::StabilizerGpu { context } = &kind {
3445 return noise::run_shots_noisy_with_gpu(
3446 circuit,
3447 noise_model,
3448 num_shots,
3449 seed,
3450 context.clone(),
3451 );
3452 }
3453 return noise::run_shots_noisy(circuit, noise_model, num_shots, seed);
3454 }
3455 }
3456
3457 let plan = if kind.is_auto() && !noise_model.has_only_pauli_channels() {
3458 general_noise_plan(&kind, circuit)
3459 } else {
3460 resolve_backend(&kind, circuit, false)
3461 };
3462 let has_pauli_rot = crate::circuit::any_gate(&circuit.instructions, &mut |gate| {
3467 matches!(gate, crate::gates::Gate::PauliRot(_))
3468 });
3469 if has_pauli_rot
3470 && !matches!(plan, BackendPlan::Statevector { .. })
3471 && !plan.build(seed).supports_pauli_rotation()
3472 {
3473 return Err(crate::error::PrismError::IncompatibleBackend {
3474 backend: format!("{:?}", plan.resolved()),
3475 reason: "noisy trajectories apply the instruction stream raw so noise events \
3476 stay aligned to it, which leaves no room for the Pauli-rotation \
3477 lowering this backend needs; run on the statevector, or expand the \
3478 rotations with circuit::expand_pauli_rotations and attach the noise \
3479 model to the expanded circuit"
3480 .into(),
3481 });
3482 }
3483 if noise_model.has_two_qubit_kraus() && !plan.build(seed).supports_two_qubit_kraus() {
3488 return Err(crate::error::PrismError::IncompatibleBackend {
3489 backend: format!("{:?}", plan.resolved()),
3490 reason: "a two-qubit Kraus channel needs the two-qubit reduced density matrix \
3491 its branch probabilities are drawn from, which only the host \
3492 statevector provides; run on BackendKind::Statevector, or evaluate \
3493 the channel exactly on BackendKind::DensityMatrix"
3494 .into(),
3495 });
3496 }
3497 let route = plan.resolved();
3498 trajectory::run_trajectories(
3499 |s| plan.build(s),
3500 circuit,
3501 noise_model,
3502 num_shots,
3503 seed,
3504 plan.is_gpu(),
3505 route,
3506 )
3507}
3508
3509#[cfg(test)]
3510mod tests;
3511
3512#[cfg(all(test, feature = "gpu"))]
3513mod gpu_stub_tests;
3514
3515#[cfg(test)]
3516mod terminal_candidate_matrix_tests;
3517
3518#[cfg(test)]
3519mod diagnostic_terminal_tests;