1pub mod braket;
5mod branching;
6pub mod calibration;
7mod clifford_register;
8pub mod compiled;
9mod decomposed;
10mod dispatch;
11mod dynamic;
12pub mod gradient;
13pub mod homological;
14mod metadata;
15pub mod noise;
16mod observable;
17mod probability;
18pub(crate) mod shots;
19pub mod stabilizer_rank;
20mod terminal_sampling;
21mod trajectory;
22pub mod unified_pauli;
23
24pub use braket::ResultValue;
25pub(crate) use decomposed::merge_probabilities;
26use decomposed::{
27 MIN_DECOMPOSITION_QUBITS, PreparedBlocks, run_decomposed, run_decomposed_prefused,
28 should_decompose,
29};
30pub use dispatch::BackendKind;
31use dispatch::{
32 AUTO_MPS_BOND_DIM, BackendPlan, ExecutionPlan, Family, MAX_AUTO_T_COUNT_SHOTS,
33 MAX_STABILIZER_RANK_QUBITS, MIN_BLOCK_FOR_FACTORED_STAB, MIN_FACTORED_STABILIZER_QUBITS,
34 MIN_QUBITS_FOR_SPD_AUTO, accel_for, approximate_route_name, auto_selects_cpu_statevector,
35 auto_spd_work_budget, build_statevector, has_temporal_clifford_opportunity, initial_state_plan,
36 mps_apply_cost, plan_for_family, plan_temporal_clifford, resolve, resolve_backend,
37 run_temporal_clifford, stabilizer_rank_budget, validate_explicit_backend,
38};
39pub use dynamic::{SimulateProgram, simulate_program};
40pub use metadata::{
41 BondReport, Engine, Exactness, ExpectationResult, Placement, ResolvedBackend, RunMetadata,
42};
43#[cfg(feature = "distributed")]
44pub(crate) use observable::pauli_sandwich;
45pub use observable::{ObservableExpectation, PauliObservable};
46pub(crate) use observable::{
47 finish_expectations, i_pow, pauli_expectation_from_masks, pauli_expectations_from_masks,
48 pauli_masks, pauli_sandwiches_from_masks, validate_observable,
49};
50pub use probability::{FactoredBlock, Probabilities, ProbabilitiesIter};
51pub use shots::{ShotsResult, bitstring};
52
53use std::collections::HashMap;
54
55use num_complex::Complex64;
56
57use crate::backend::sparse::MAX_SPARSE_INDEX_QUBITS;
58use crate::backend::statevector::StatevectorBackend;
59use crate::backend::{Backend, max_statevector_qubits};
60use crate::circuit::{Circuit, Instruction, SaveRecord, SaveSpec, SavedValue};
61use crate::error::{PrismError, Result};
62use crate::sim::noise::NoiseModel;
63use shots::{
64 counts_from_basis_samples, packed_shots_to_classical_bits, record_counts_to_classical_bits,
65 sample_shots, shots_from_basis_samples,
66};
67use terminal_sampling::{
68 sample_counts_from_probs, sample_counts_from_state, sample_shots_from_probs,
69 sample_shots_from_state,
70};
71use unified_pauli::{PauliTerm, SpdTruncation};
72
73type TerminalStatevector = (StatevectorBackend, Vec<(usize, usize)>);
74
75#[derive(Debug, Clone, Copy)]
76pub(crate) struct SimOptions {
77 pub(crate) probabilities: bool,
78}
79
80impl Default for SimOptions {
81 fn default() -> Self {
82 Self {
83 probabilities: true,
84 }
85 }
86}
87
88impl SimOptions {
89 pub(crate) fn classical_only() -> Self {
90 Self {
91 probabilities: false,
92 }
93 }
94}
95
96#[derive(Debug, Clone)]
98pub struct RunOutcome {
99 pub classical_bits: Vec<bool>,
102 pub probabilities: Option<Probabilities>,
108 pub metadata: RunMetadata,
110 pub saves: Vec<SaveRecord>,
113}
114
115#[derive(Debug, Clone)]
117pub struct CountsResult {
118 pub counts: HashMap<Vec<u64>, u64>,
121 pub num_classical_bits: usize,
122 pub metadata: RunMetadata,
123}
124
125impl CountsResult {
126 pub fn into_counts(self) -> HashMap<Vec<u64>, u64> {
127 self.counts
128 }
129}
130
131#[derive(Debug, Clone)]
133pub struct MarginalsResult {
134 pub marginals: Vec<(f64, f64)>,
136 pub metadata: RunMetadata,
137}
138
139impl MarginalsResult {
140 pub fn into_vec(self) -> Vec<(f64, f64)> {
141 self.marginals
142 }
143}
144
145#[derive(Debug, Clone)]
148pub struct ReducedDensityMatrix {
149 pub qubits: Vec<usize>,
151 pub data: Vec<Complex64>,
156 pub metadata: RunMetadata,
157}
158
159impl ReducedDensityMatrix {
160 pub fn purity(&self) -> f64 {
162 self.data.iter().map(|entry| entry.norm_sqr()).sum()
163 }
164}
165
166#[derive(Debug, Clone)]
169pub struct ObservableVariance {
170 pub variance: f64,
172 pub mean: f64,
174 pub metadata: RunMetadata,
175}
176
177#[derive(Debug, Clone)]
180pub struct EntropyResult {
181 pub subsystem: Vec<usize>,
184 pub entropy: f64,
186 pub schmidt_values: Option<Vec<f64>>,
191 pub metadata: RunMetadata,
192}
193
194#[derive(Debug, Clone)]
197pub struct OverlapResult {
198 pub fidelity: f64,
203 pub left: RunMetadata,
206 pub right: RunMetadata,
208}
209
210#[derive(Debug, Clone, Copy)]
212pub struct Unseeded;
213
214#[derive(Debug, Clone, Copy)]
216pub struct Seeded {
217 seed: u64,
218}
219
220#[derive(Debug, Clone, Copy)]
223pub(crate) enum StartState<'c> {
224 Amplitudes(&'c [Complex64]),
225 DensityMatrix(&'c [Complex64]),
226}
227
228impl StartState<'_> {
229 fn load(self, backend: &mut dyn Backend, num_classical_bits: usize) -> Result<()> {
231 match self {
232 Self::Amplitudes(amplitudes) => {
233 backend.init_from_amplitudes(amplitudes.to_vec(), num_classical_bits)
234 }
235 Self::DensityMatrix(rho) => {
236 backend.init_from_density_matrix(rho.to_vec(), num_classical_bits)
237 }
238 }
239 }
240}
241
242pub struct Simulate<'c, SeedState> {
244 circuit: &'c Circuit,
245 kind: BackendKind,
246 seed: SeedState,
247 noise_model: Option<&'c noise::NoiseModel>,
248 initial_state: Option<StartState<'c>>,
249 require_exact: bool,
250}
251
252impl<'c, SeedState> Simulate<'c, SeedState> {
253 #[inline]
255 pub fn backend(mut self, kind: BackendKind) -> Self {
256 self.kind = kind;
257 self
258 }
259
260 #[inline]
277 pub fn require_exact(mut self) -> Self {
278 self.require_exact = true;
279 self
280 }
281
282 #[inline]
296 pub fn noise(mut self, model: &'c noise::NoiseModel) -> Self {
297 self.noise_model = Some(model);
298 self
299 }
300
301 #[inline]
316 pub fn initial_state(mut self, amplitudes: &'c [Complex64]) -> Self {
317 self.initial_state = Some(StartState::Amplitudes(amplitudes));
318 self
319 }
320
321 #[inline]
340 pub fn initial_density_matrix(mut self, rho: &'c [Complex64]) -> Self {
341 self.initial_state = Some(StartState::DensityMatrix(rho));
342 self
343 }
344
345 #[cfg(feature = "gpu")]
347 #[inline]
348 pub fn gpu(self, context: std::sync::Arc<crate::gpu::GpuContext>) -> Self {
349 self.backend(BackendKind::StatevectorGpu { context })
350 }
351
352 #[cfg(feature = "gpu")]
358 #[inline]
359 pub fn gpu_auto(self, context: std::sync::Arc<crate::gpu::GpuContext>) -> Self {
360 self.backend(BackendKind::AutoGpu { context })
361 }
362
363 #[cfg(feature = "distributed")]
368 pub fn distributed(
369 self,
370 context: std::sync::Arc<crate::distributed::DistributedContext>,
371 ) -> Self {
372 self.backend(BackendKind::StatevectorDistributed { context })
373 }
374}
375
376impl<'c> Simulate<'c, Unseeded> {
377 #[inline]
379 pub fn seed(self, seed: u64) -> Simulate<'c, Seeded> {
380 Simulate {
381 circuit: self.circuit,
382 kind: self.kind,
383 seed: Seeded { seed },
384 noise_model: self.noise_model,
385 initial_state: self.initial_state,
386 require_exact: self.require_exact,
387 }
388 }
389}
390
391impl<'c> Simulate<'c, Seeded> {
392 #[inline]
393 fn seed_value(&self) -> u64 {
394 self.seed.seed
395 }
396
397 fn require_no_initial_state_under_noise(&self, terminal: &str) -> Result<()> {
400 if self.initial_state.is_some() {
401 return Err(reject_initial_state(
402 &self.kind,
403 terminal,
404 "noisy trajectory replay starts every shot from |0...0>; read the exact mixture \
405 with `run`, `marginals`, or `expectation_values` on the density-matrix backend",
406 ));
407 }
408 Ok(())
409 }
410
411 #[inline]
420 pub fn run(self) -> Result<RunOutcome> {
421 let seed = self.seed_value();
422 if self.require_exact {
423 reject_approximate_route(&self.kind, self.circuit)?;
424 }
425 if let Some(noise_model) = self.noise_model {
426 require_exact_mixture(&self.kind, "a single run")?;
427 reject_readout_at(self.circuit, noise_model, "a single run")?;
428 let probabilities = exact_noisy_probabilities(
429 &self.kind,
430 self.circuit,
431 noise_model,
432 self.initial_state,
433 seed,
434 )?;
435 let classical_bits =
436 sample_exact_noisy_shots(&probabilities, self.circuit, noise_model, 1, seed)
437 .swap_remove(0);
438 let outcome = RunOutcome {
439 classical_bits,
440 probabilities: Some(probabilities),
441 metadata: exact_mixture_metadata(&self.kind),
442 saves: Vec::new(),
443 };
444 ensure_saves_recorded(self.circuit, &outcome)?;
445 return Ok(outcome);
446 }
447 if let Some(state) = self.initial_state {
448 return run_from_initial_state(
449 &self.kind,
450 self.circuit,
451 state,
452 seed,
453 &SimOptions::default(),
454 );
455 }
456 let outcome = run_with_internal(self.kind, self.circuit, seed, SimOptions::default())?;
457 ensure_exact_result(self.require_exact, &outcome.metadata)?;
458 Ok(outcome)
459 }
460
461 fn reject_saves(&self, terminal: &str) -> Result<()> {
464 if self.circuit.save_count() == 0 {
465 return Ok(());
466 }
467 Err(PrismError::IncompatibleBackend {
468 backend: format!("{:?}", self.kind),
469 reason: format!(
470 "the circuit carries {} save point(s), which only `run` returns, so \
471 `{terminal}` declines it",
472 self.circuit.save_count()
473 ),
474 })
475 }
476
477 fn reject_approximate_shot_route(&self) -> Result<()> {
480 let sampling = self.noise_model.is_none() && self.initial_state.is_none();
481 reject_predicted_route(approximate_route_name(&self.kind, self.circuit, sampling))
482 }
483
484 #[inline]
487 pub fn shots(self, num_shots: usize) -> Result<ShotsResult> {
488 self.reject_saves("shots")?;
489 let seed = self.seed_value();
490 let require_exact = self.require_exact;
491 if require_exact {
492 self.reject_approximate_shot_route()?;
493 }
494 let result = if let Some(noise_model) = self.noise_model {
495 self.require_no_initial_state_under_noise("shot sampling")?;
496 run_shots_with_noise(self.kind, self.circuit, noise_model, num_shots, seed)?
497 } else if let Some(state) = self.initial_state {
498 shots_from_initial_state(&self.kind, self.circuit, state, num_shots, seed)?
499 } else {
500 run_shots_with(self.kind, self.circuit, num_shots, seed)?
501 };
502 ensure_exact_result(require_exact, &result.metadata)?;
503 Ok(result)
504 }
505
506 #[inline]
513 pub fn sample_counts(self, num_shots: usize) -> Result<CountsResult> {
514 self.reject_saves("sample_counts")?;
515 let seed = self.seed_value();
516 if self.require_exact {
517 self.reject_approximate_shot_route()?;
518 }
519 let (counts, metadata) = if let Some(noise_model) = self.noise_model {
520 self.require_no_initial_state_under_noise("count sampling")?;
521 let shots =
522 run_shots_with_noise(self.kind, self.circuit, noise_model, num_shots, seed)?;
523 (shots.counts(), shots.metadata)
524 } else if let Some(state) = self.initial_state {
525 let shots = shots_from_initial_state(&self.kind, self.circuit, state, num_shots, seed)?;
526 (shots.counts(), shots.metadata)
527 } else {
528 run_counts_with(self.kind, self.circuit, num_shots, seed)?
529 };
530 ensure_exact_result(self.require_exact, &metadata)?;
531 Ok(CountsResult {
532 counts,
533 num_classical_bits: self.circuit.num_classical_bits,
534 metadata,
535 })
536 }
537
538 #[inline]
544 pub fn marginals(self) -> Result<MarginalsResult> {
545 self.reject_saves("marginals")?;
546 let seed = self.seed_value();
547 if self.require_exact {
548 reject_approximate_route(&self.kind, self.circuit)?;
549 }
550 if let Some(noise_model) = self.noise_model {
551 require_exact_mixture(&self.kind, "marginals")?;
552 reject_readout_at(self.circuit, noise_model, "marginals")?;
553 let probs = exact_noisy_probabilities(
554 &self.kind,
555 self.circuit,
556 noise_model,
557 self.initial_state,
558 seed,
559 )?;
560 return Ok(MarginalsResult {
561 marginals: probs.marginals(),
562 metadata: exact_mixture_metadata(&self.kind),
563 });
564 }
565 let result = if let Some(state) = self.initial_state {
566 marginals_from_initial_state(&self.kind, self.circuit, state, seed)?
567 } else {
568 run_marginals_result_with(self.kind, self.circuit, seed)?
569 };
570 ensure_exact_result(self.require_exact, &result.metadata)?;
571 Ok(result)
572 }
573
574 #[inline]
590 pub fn expectation_values(self, observables: &[Vec<PauliTerm>]) -> Result<Vec<f64>> {
591 self.reject_saves("expectation_values")?;
592 self.expectation_values_reported(observables)
593 .map(ExpectationResult::into_values)
594 }
595
596 pub fn expectation_values_reported(
599 self,
600 observables: &[Vec<PauliTerm>],
601 ) -> Result<ExpectationResult> {
602 let seed = self.seed_value();
603 if self.require_exact {
604 reject_approximate_route(&self.kind, self.circuit)?;
605 }
606 if let Some(noise_model) = self.noise_model {
607 reject_readout_at(self.circuit, noise_model, "expectation values")?;
608 }
609 if let BackendKind::PauliPath { epsilon, max_terms } = self.kind {
610 reject_pauli_path_initial_state(self.initial_state)?;
611 return pauli_path_expectations(
612 self.circuit,
613 self.noise_model,
614 observables,
615 epsilon,
616 max_terms,
617 );
618 }
619 if let Some(noise_model) = self.noise_model {
620 require_exact_mixture(&self.kind, "expectation values")?;
621 require_unitary_circuit(&self.kind, self.circuit, "expectation values require")?;
622 let values = noise::dm_expectation_values(
623 &self.kind,
624 self.circuit,
625 observables,
626 Some(noise_model),
627 self.initial_state,
628 seed,
629 )?;
630 return Ok(analytic_expectations(
631 values,
632 exact_mixture_metadata(&self.kind),
633 ));
634 }
635 if let Some(state) = self.initial_state {
636 require_unitary_circuit(&self.kind, self.circuit, "expectation values require")?;
637 return expectation_values_from_initial_state(
638 &self.kind,
639 self.circuit,
640 state,
641 observables,
642 seed,
643 );
644 }
645 let result = run_expectation_values_reported(self.kind, self.circuit, observables, seed)?;
646 ensure_exact_result(self.require_exact, &result.metadata)?;
647 Ok(result)
648 }
649
650 pub fn observable_expectation(
661 self,
662 observable: &PauliObservable,
663 ) -> Result<ObservableExpectation> {
664 self.reject_saves("observable_expectation")?;
665 self.observable_expectation_ref(observable)
666 }
667
668 fn observable_expectation_ref(
672 &self,
673 observable: &PauliObservable,
674 ) -> Result<ObservableExpectation> {
675 let seed = self.seed_value();
676 if self.require_exact {
677 reject_approximate_route(&self.kind, self.circuit)?;
678 }
679 if let Some(noise_model) = self.noise_model {
680 reject_readout_at(self.circuit, noise_model, "observable expectation")?;
681 }
682 if let BackendKind::PauliPath { epsilon, max_terms } = self.kind {
683 reject_pauli_path_initial_state(self.initial_state)?;
684 let result = pauli_path_expectations(
685 self.circuit,
686 self.noise_model,
687 &observable_vecs(observable),
688 epsilon,
689 max_terms,
690 )?;
691 let metadata = result.metadata;
692 return Ok(weighted_observable_result(
693 observable,
694 &result.values,
695 None,
696 metadata,
697 ));
698 }
699 if let Some(noise_model) = self.noise_model {
700 require_exact_mixture(&self.kind, "expectation values")?;
701 require_unitary_circuit(&self.kind, self.circuit, "expectation values require")?;
702 let values = noise::dm_expectation_values(
703 &self.kind,
704 self.circuit,
705 &observable_vecs(observable),
706 Some(noise_model),
707 self.initial_state,
708 seed,
709 )?;
710 return Ok(weighted_observable_result(
711 observable,
712 &values,
713 None,
714 exact_mixture_metadata(&self.kind),
715 ));
716 }
717 if let Some(state) = self.initial_state {
718 require_unitary_circuit(&self.kind, self.circuit, "expectation values require")?;
719 let result = expectation_values_from_initial_state(
720 &self.kind,
721 self.circuit,
722 state,
723 &observable_vecs(observable),
724 seed,
725 )?;
726 return Ok(weighted_observable_result(
727 observable,
728 &result.values,
729 result.std_errors.as_deref(),
730 result.metadata,
731 ));
732 }
733 let result =
734 run_observable_expectation_reported(self.kind.clone(), self.circuit, observable, seed)?;
735 ensure_exact_result(self.require_exact, &result.metadata)?;
736 Ok(result)
737 }
738
739 pub fn observable_variance(self, observable: &PauliObservable) -> Result<ObservableVariance> {
754 self.reject_saves("observable_variance")?;
755 let (offset, traceless) = observable.split_identity();
756 let mean = self.observable_expectation_ref(observable)?;
757 let second = self.observable_expectation_ref(&traceless.square())?;
758 let centered = mean.mean - offset;
759 Ok(ObservableVariance {
760 variance: second.mean - centered * centered,
761 mean: mean.mean,
762 metadata: mean.metadata,
763 })
764 }
765
766 pub fn probabilities_of(self, qubits: &[usize]) -> Result<Vec<f64>> {
779 self.reject_saves("probabilities_of")?;
780 crate::backend::schmidt::validate_qubit_set(qubits, self.circuit.num_qubits)?;
781 let kind = format!("{:?}", self.kind);
782 let outcome = self.run()?;
783 let probabilities = outcome
784 .probabilities
785 .ok_or(PrismError::BackendUnsupported {
786 backend: kind,
787 operation: "a probability distribution to marginalize".into(),
788 })?;
789 Ok(probabilities.subset_marginal(qubits))
790 }
791
792 pub fn state_vector(self) -> Result<Vec<Complex64>> {
807 self.reject_saves("state_vector")?;
808 let seed = self.seed_value();
809 let diagnostic = Diagnostic::StateVector;
810 require_unitary_circuit(&self.kind, self.circuit, "a statevector requires")?;
811 if self.require_exact {
812 reject_approximate_route(&self.kind, self.circuit)?;
813 }
814 if self.noise_model.is_some() {
815 return Err(PrismError::IncompatibleBackend {
816 backend: format!("{:?}", self.kind),
817 reason: format!(
818 "{} is a pure state; a noise model evolves a mixture, which \
819 `reduced_density_matrix` over the whole register reports",
820 diagnostic.terminal()
821 ),
822 });
823 }
824 let backend = diagnostic_backend(
825 &self.kind,
826 self.circuit,
827 self.initial_state,
828 seed,
829 diagnostic,
830 self.circuit.num_qubits,
831 )?;
832 ensure_exact_result(self.require_exact, &backend_metadata(&*backend))?;
833 backend.export_statevector()
834 }
835
836 pub fn reduced_density_matrix(self, qubits: &[usize]) -> Result<ReducedDensityMatrix> {
851 self.reject_saves("reduced_density_matrix")?;
852 let seed = self.seed_value();
853 let diagnostic = Diagnostic::ReducedDensityMatrix;
854 let terminal = diagnostic.terminal();
855 crate::backend::schmidt::validate_qubit_set(qubits, self.circuit.num_qubits)?;
856 require_unitary_circuit(
857 &self.kind,
858 self.circuit,
859 "a reduced density matrix requires",
860 )?;
861 if self.require_exact {
862 reject_approximate_route(&self.kind, self.circuit)?;
863 }
864 if let Some(noise_model) = self.noise_model {
865 reject_readout_at(self.circuit, noise_model, terminal)?;
866 require_exact_mixture(&self.kind, terminal)?;
867 let mut mixture = noise::evolve_density_matrix(
868 &self.kind,
869 self.circuit,
870 Some(noise_model),
871 self.initial_state,
872 seed,
873 )?;
874 return Ok(ReducedDensityMatrix {
875 qubits: qubits.to_vec(),
876 data: mixture.reduced_density_matrix(qubits)?,
877 metadata: exact_mixture_metadata(&self.kind),
878 });
879 }
880 let mut backend = diagnostic_backend(
881 &self.kind,
882 self.circuit,
883 self.initial_state,
884 seed,
885 diagnostic,
886 qubits.len(),
887 )?;
888 let metadata = backend_metadata(&*backend);
889 ensure_exact_result(self.require_exact, &metadata)?;
890 Ok(ReducedDensityMatrix {
891 qubits: qubits.to_vec(),
892 data: backend.reduced_density_matrix(qubits)?,
893 metadata,
894 })
895 }
896
897 pub fn entanglement_entropy(self, subsystem: &[usize]) -> Result<EntropyResult> {
914 self.reject_saves("entanglement_entropy")?;
915 let seed = self.seed_value();
916 let diagnostic = Diagnostic::Entropy;
917 let terminal = diagnostic.terminal();
918 crate::backend::schmidt::validate_subsystem(subsystem, self.circuit.num_qubits)?;
919 require_unitary_circuit(&self.kind, self.circuit, "entanglement entropy requires")?;
920 if self.require_exact {
921 reject_approximate_route(&self.kind, self.circuit)?;
922 }
923 if let Some(noise_model) = self.noise_model {
924 reject_readout_at(self.circuit, noise_model, terminal)?;
925 require_exact_mixture(&self.kind, terminal)?;
926 let mut mixture = noise::evolve_density_matrix(
927 &self.kind,
928 self.circuit,
929 Some(noise_model),
930 self.initial_state,
931 seed,
932 )?;
933 return Ok(EntropyResult {
934 subsystem: subsystem.to_vec(),
935 entropy: mixture.entanglement_entropy(subsystem)?,
936 schmidt_values: None,
937 metadata: exact_mixture_metadata(&self.kind),
938 });
939 }
940 let mut backend = diagnostic_backend(
941 &self.kind,
942 self.circuit,
943 self.initial_state,
944 seed,
945 diagnostic,
946 subsystem.len(),
947 )?;
948 let metadata = backend_metadata(&*backend);
949 ensure_exact_result(self.require_exact, &metadata)?;
950 let (entropy, schmidt_values) = match backend.schmidt_values(subsystem) {
955 Ok(values) => (
956 crate::backend::schmidt::entropy_of_schmidt_values(&values),
957 Some(values),
958 ),
959 Err(declined) => match backend.entanglement_entropy(subsystem) {
960 Ok(entropy) => (entropy, None),
961 Err(_) => return Err(declined),
962 },
963 };
964 Ok(EntropyResult {
965 subsystem: subsystem.to_vec(),
966 entropy,
967 schmidt_values,
968 metadata,
969 })
970 }
971
972 pub fn overlap(self, other: Simulate<'_, Seeded>) -> Result<OverlapResult> {
988 self.reject_saves("overlap")?;
989 let diagnostic = Diagnostic::Overlap;
990 if self.circuit.num_qubits != other.circuit.num_qubits {
991 return Err(PrismError::InvalidParameter {
992 message: format!(
993 "{} needs two circuits of the same width; got {} and {} qubits",
994 diagnostic.terminal(),
995 self.circuit.num_qubits,
996 other.circuit.num_qubits
997 ),
998 });
999 }
1000 let (left_backend, left) = self.overlap_side(diagnostic)?;
1001 let (right_backend, right) = other.overlap_side(diagnostic)?;
1002 Ok(OverlapResult {
1003 fidelity: left_backend.overlap_sq(&*right_backend)?,
1004 left,
1005 right,
1006 })
1007 }
1008
1009 fn overlap_side(self, diagnostic: Diagnostic) -> Result<(Box<dyn Backend>, RunMetadata)> {
1013 let seed = self.seed_value();
1014 let terminal = diagnostic.terminal();
1015 require_unitary_circuit(&self.kind, self.circuit, "a state overlap requires")?;
1016 if self.require_exact {
1017 reject_approximate_route(&self.kind, self.circuit)?;
1018 }
1019 if self.noise_model.is_some() {
1020 return Err(PrismError::IncompatibleBackend {
1021 backend: format!("{:?}", self.kind),
1022 reason: format!(
1023 "{terminal} is an inner product of two pure states, and a noise model \
1024 evolves a mixture, whose fidelity is a different computation; drop \
1025 the model, or compare the mixtures through `reduced_density_matrix`"
1026 ),
1027 });
1028 }
1029 let backend = diagnostic_backend(
1030 &self.kind,
1031 self.circuit,
1032 self.initial_state,
1033 seed,
1034 diagnostic,
1035 0,
1036 )?;
1037 let metadata = backend_metadata(&*backend);
1038 ensure_exact_result(self.require_exact, &metadata)?;
1039 Ok((backend, metadata))
1040 }
1041
1042 #[inline]
1051 pub fn expectation_gradient(
1052 self,
1053 hamiltonian: &[(f64, Vec<PauliTerm>)],
1054 params: &crate::circuit::Parameters,
1055 ) -> Result<gradient::ExpectationGradient> {
1056 self.reject_saves("expectation_gradient")?;
1057 let seed = self.seed_value();
1058 if self.require_exact {
1059 reject_approximate_route(&self.kind, self.circuit)?;
1060 }
1061 if self.noise_model.is_some() {
1062 return Err(PrismError::IncompatibleBackend {
1063 backend: format!("{:?}", self.kind),
1064 reason: "the adjoint method backpropagates through a pure state, so no backend \
1065 has a noisy adjoint path; drop the noise model, or take \
1066 `expectation_gradient_shift` on the density-matrix backend"
1067 .into(),
1068 });
1069 }
1070 if self.initial_state.is_some() {
1071 return Err(reject_initial_state(
1072 &self.kind,
1073 "the adjoint gradient",
1074 "the backward pass reconstructs the input register by inverting the circuit from \
1075 |0...0>, so a start state would have to be inverted with it",
1076 ));
1077 }
1078 if !(self.kind.is_auto() || matches!(self.kind, BackendKind::Statevector)) {
1079 return Err(PrismError::IncompatibleBackend {
1080 backend: format!("{:?}", self.kind),
1081 reason:
1082 "adjoint gradients run on the statevector backend; select Auto or Statevector"
1083 .into(),
1084 });
1085 }
1086 gradient::run_expectation_gradient(self.circuit, hamiltonian, params, seed)
1087 }
1088
1089 #[inline]
1104 pub fn expectation_gradient_shift(
1105 self,
1106 hamiltonian: &[(f64, Vec<PauliTerm>)],
1107 params: &crate::circuit::Parameters,
1108 ) -> Result<gradient::ExpectationGradient> {
1109 self.reject_saves("expectation_gradient_shift")?;
1110 let seed = self.seed_value();
1111 if self.require_exact {
1112 reject_approximate_route(&self.kind, self.circuit)?;
1113 }
1114 if self.noise_model.is_some()
1115 && !self.kind.is_density_matrix()
1116 && !matches!(self.kind, BackendKind::PauliPath { .. })
1117 {
1118 return Err(PrismError::IncompatibleBackend {
1119 backend: format!("{:?}", self.kind),
1120 reason: "a parameter-shift gradient under a noise model evaluates the exact \
1121 mixed state, which only the density-matrix backend holds; select \
1122 `BackendKind::DensityMatrix` or its device sibling, or drop the noise \
1123 model"
1124 .into(),
1125 });
1126 }
1127 gradient::shift_gradient(
1128 &self.kind,
1129 self.circuit,
1130 hamiltonian,
1131 params,
1132 self.noise_model,
1133 self.initial_state,
1134 seed,
1135 )
1136 }
1137}
1138
1139#[inline]
1161pub fn simulate(circuit: &Circuit) -> Simulate<'_, Unseeded> {
1162 Simulate {
1163 circuit,
1164 kind: BackendKind::Auto,
1165 seed: Unseeded,
1166 noise_model: None,
1167 initial_state: None,
1168 require_exact: false,
1169 }
1170}
1171
1172fn reject_pauli_path(terminal: &str) -> PrismError {
1174 PrismError::IncompatibleBackend {
1175 backend: "PauliPath".into(),
1176 reason: format!(
1177 "{terminal} is not served by Pauli path propagation, which answers \
1178 `expectation_values` and `observable_expectation` only"
1179 ),
1180 }
1181}
1182
1183fn reject_pauli_path_initial_state(state: Option<StartState<'_>>) -> Result<()> {
1184 match state {
1185 Some(_) => Err(reject_pauli_path("a start state")),
1186 None => Ok(()),
1187 }
1188}
1189
1190fn pauli_path_expectations(
1197 circuit: &Circuit,
1198 noise: Option<&NoiseModel>,
1199 observables: &[Vec<PauliTerm>],
1200 epsilon: f64,
1201 max_terms: usize,
1202) -> Result<ExpectationResult> {
1203 let empty;
1204 let noise = match noise {
1205 Some(model) => model,
1206 None => {
1207 empty = NoiseModel {
1208 after_gate: vec![Vec::new(); circuit.instructions.len()],
1209 readout: vec![None; circuit.num_classical_bits],
1210 };
1211 &empty
1212 }
1213 };
1214 let mut values = Vec::with_capacity(observables.len());
1215 let mut discarded = 0.0f64;
1216 for obs in observables {
1217 let result =
1218 unified_pauli::run_pauli_path_observable(circuit, noise, obs, epsilon, max_terms)?;
1219 discarded = discarded.max(result.total_discarded);
1220 values.push(result.mean);
1221 }
1222 let metadata = if discarded > 0.0 {
1223 RunMetadata::approximate(ResolvedBackend::PauliPath)
1224 } else {
1225 RunMetadata::exact(ResolvedBackend::PauliPath)
1226 };
1227 Ok(analytic_expectations(values, metadata))
1228}
1229
1230fn require_exact_mixture(kind: &BackendKind, terminal: &str) -> Result<()> {
1231 if kind.is_density_matrix() {
1232 return Ok(());
1233 }
1234 Err(PrismError::IncompatibleBackend {
1235 backend: format!("{kind:?}"),
1236 reason: format!(
1237 "{terminal} under a noise model reads the exact mixed state, which only the \
1238 density-matrix backend holds; select it, or average trajectories through `shots` \
1239 or `sample_counts`"
1240 ),
1241 })
1242}
1243
1244fn reject_readout_at(
1253 circuit: &Circuit,
1254 noise_model: &noise::NoiseModel,
1255 terminal: &str,
1256) -> Result<()> {
1257 let inert = noise_model
1258 .readout
1259 .iter()
1260 .take(circuit.num_classical_bits)
1261 .all(|entry| entry.as_ref().is_none_or(|readout| readout.is_inert()));
1262 if inert {
1263 return Ok(());
1264 }
1265 Err(PrismError::InvalidParameter {
1266 message: format!(
1267 "{terminal} answers from the mixed state, which readout error is not part of: it \
1268 acts on the measurement record and is indexed by classical bit, not qubit. Drop \
1269 it from the model, or use `shots` or `sample_counts`, which apply it"
1270 ),
1271 })
1272}
1273
1274fn reject_initial_state(kind: &BackendKind, terminal: &str, instead: &str) -> PrismError {
1277 PrismError::IncompatibleBackend {
1278 backend: format!("{kind:?}"),
1279 reason: format!("{terminal} does not accept a start state; {instead}"),
1280 }
1281}
1282
1283fn check_initial_state_len(state: StartState<'_>, num_qubits: usize) -> Result<()> {
1290 let (buffer, bits, entries) = match state {
1291 StartState::Amplitudes(amplitudes) => (amplitudes, num_qubits, "amplitudes"),
1292 StartState::DensityMatrix(rho) => (rho, 2 * num_qubits, "density matrix entries"),
1293 };
1294 let want = (bits < usize::BITS as usize).then(|| 1usize << bits);
1295 if want == Some(buffer.len()) {
1296 return Ok(());
1297 }
1298 let needs = match want {
1299 Some(count) => count.to_string(),
1300 None => format!("2^{bits}"),
1301 };
1302 Err(PrismError::InvalidParameter {
1303 message: format!(
1304 "start state has {} {entries}, but a {num_qubits}-qubit circuit needs {needs}",
1305 buffer.len()
1306 ),
1307 })
1308}
1309
1310fn backend_from_initial_state(
1312 kind: &BackendKind,
1313 circuit: &Circuit,
1314 state: StartState<'_>,
1315 seed: u64,
1316) -> Result<Box<dyn Backend>> {
1317 if !kind.is_auto() {
1318 validate_explicit_backend(kind, circuit)?;
1319 }
1320 check_initial_state_len(state, circuit.num_qubits)?;
1321 let mut backend = initial_state_plan(kind, circuit.num_qubits, state)?.build(seed);
1322 state.load(&mut *backend, circuit.num_classical_bits)?;
1323 Ok(backend)
1324}
1325
1326fn expand_for_backend<'c>(
1330 backend: &dyn Backend,
1331 circuit: &'c Circuit,
1332) -> std::borrow::Cow<'c, Circuit> {
1333 use std::borrow::Cow;
1334 let expanded = if backend.supports_qft_block() {
1335 crate::circuit::recognize_qft_blocks(circuit)
1336 } else {
1337 crate::circuit::expand_qft_blocks(circuit)
1338 };
1339 if backend.supports_pauli_rotation() {
1340 return expanded;
1341 }
1342 match expanded {
1343 Cow::Borrowed(borrowed) => crate::circuit::expand_pauli_rotations(borrowed),
1344 Cow::Owned(owned) => {
1345 let rotations = crate::circuit::expand_pauli_rotations(&owned);
1346 if let Cow::Owned(expanded_rotations) = rotations {
1347 return Cow::Owned(expanded_rotations);
1348 }
1349 Cow::Owned(owned)
1350 }
1351 }
1352}
1353
1354fn fuse_for_backend<'a>(
1357 backend: &dyn Backend,
1358 circuit: &'a Circuit,
1359) -> std::borrow::Cow<'a, Circuit> {
1360 crate::circuit::fusion::fuse_circuit_for_width(
1361 circuit,
1362 backend.supports_fused_gates(),
1363 backend.fusion_state_qubits(circuit.num_qubits),
1364 )
1365}
1366
1367fn apply_fused_without_saves(
1373 backend: &mut dyn Backend,
1374 circuit: &Circuit,
1375 terminal: &str,
1376) -> Result<()> {
1377 if circuit.save_count() > 0 {
1378 return Err(PrismError::IncompatibleBackend {
1379 backend: backend.name().to_string(),
1380 reason: format!(
1381 "save points are returned by `run`, and `{terminal}` has nowhere to put them"
1382 ),
1383 });
1384 }
1385 apply_fused_circuit(backend, circuit).map(|_| ())
1386}
1387
1388fn apply_fused_circuit(backend: &mut dyn Backend, circuit: &Circuit) -> Result<Vec<SaveRecord>> {
1391 let expanded = expand_for_backend(&*backend, circuit);
1392 let fused = fuse_for_backend(&*backend, &expanded);
1393 apply_recording_saves(backend, &fused.instructions)
1394}
1395
1396fn run_from_initial_state(
1397 kind: &BackendKind,
1398 circuit: &Circuit,
1399 state: StartState<'_>,
1400 seed: u64,
1401 opts: &SimOptions,
1402) -> Result<RunOutcome> {
1403 let mut backend = backend_from_initial_state(kind, circuit, state, seed)?;
1404 let saves = apply_fused_circuit(&mut *backend, circuit)?;
1405
1406 let probabilities = if opts.probabilities {
1407 try_backend_probabilities(&*backend)?
1408 } else {
1409 None
1410 };
1411 Ok(RunOutcome {
1412 classical_bits: backend.classical_results().to_vec(),
1413 probabilities,
1414 metadata: backend_metadata(&*backend),
1415 saves,
1416 })
1417}
1418
1419fn shots_from_initial_state(
1423 kind: &BackendKind,
1424 circuit: &Circuit,
1425 state: StartState<'_>,
1426 num_shots: usize,
1427 seed: u64,
1428) -> Result<ShotsResult> {
1429 let bits = circuit.num_classical_bits;
1430 if circuit.has_terminal_measurements_only() && !circuit.has_resets() {
1431 let stripped = circuit.without_measurements();
1432 let mut backend = backend_from_initial_state(kind, &stripped, state, seed)?;
1433 apply_fused_circuit(&mut *backend, &stripped)?;
1434 let meas_map = circuit.measurement_map();
1435 let shots = if backend.supports_native_sampling() {
1436 let samples = backend.sample_basis_states(num_shots, seed)?;
1437 Some(shots_from_basis_samples(&samples, &meas_map, bits))
1438 } else {
1439 try_backend_probabilities(&*backend)?
1440 .map(|probs| sample_shots(&probs, &meas_map, bits, num_shots, seed))
1441 };
1442 if let Some(shots) = shots {
1443 return Ok(
1444 ShotsResult::from_shots(shots, bits).with_metadata(backend_metadata(&*backend))
1445 );
1446 }
1447 }
1448
1449 if !kind.is_auto() {
1452 validate_explicit_backend(kind, circuit)?;
1453 }
1454 check_initial_state_len(state, circuit.num_qubits)?;
1455 let plan = initial_state_plan(kind, circuit.num_qubits, state)?;
1456 let probe = plan.build(seed);
1457 let expanded = expand_for_backend(&*probe, circuit);
1458 let fused = fuse_for_backend(&*probe, &expanded);
1459
1460 let route = plan.resolved();
1461 let states = [(route, circuit.num_qubits)];
1462 collect_shots(
1463 circuit,
1464 num_shots,
1465 seed,
1466 route,
1467 kind,
1468 &states,
1469 |shot_seed| {
1470 let mut backend = plan.build(shot_seed);
1471 state.load(&mut *backend, circuit.num_classical_bits)?;
1472 backend.apply_instructions(&fused.instructions)?;
1473 Ok((
1474 backend.classical_results().to_vec(),
1475 backend_metadata(&*backend),
1476 ))
1477 },
1478 )
1479}
1480
1481fn marginals_from_initial_state(
1482 kind: &BackendKind,
1483 circuit: &Circuit,
1484 state: StartState<'_>,
1485 seed: u64,
1486) -> Result<MarginalsResult> {
1487 let mut backend = backend_from_initial_state(kind, circuit, state, seed)?;
1488 apply_fused_without_saves(&mut *backend, circuit, "marginals")?;
1489 if backend.supports_pauli_expectation() {
1490 return marginals_from_pauli_expectations(&*backend, circuit.num_qubits);
1491 }
1492 Ok(MarginalsResult {
1493 marginals: Probabilities::Dense(backend.probabilities()?).marginals(),
1494 metadata: backend_metadata(&*backend),
1495 })
1496}
1497
1498fn expectation_values_from_initial_state(
1499 kind: &BackendKind,
1500 circuit: &Circuit,
1501 state: StartState<'_>,
1502 observables: &[Vec<PauliTerm>],
1503 seed: u64,
1504) -> Result<ExpectationResult> {
1505 let masks = observables
1507 .iter()
1508 .map(|obs| pauli_masks(obs, circuit.num_qubits))
1509 .collect::<Result<Vec<_>>>()?;
1510
1511 let mut backend = backend_from_initial_state(kind, circuit, state, seed)?;
1512 apply_fused_without_saves(&mut *backend, circuit, "expectation_values")?;
1513 let metadata = backend_metadata(&*backend);
1514 if backend.supports_pauli_expectation() {
1515 let values = backend.pauli_expectations(observables)?;
1516 return Ok(analytic_expectations(values, metadata));
1517 }
1518
1519 let evolved = backend.export_statevector()?;
1520 let (values, _) = pauli_expectations_from_masks(&evolved, &masks);
1521 Ok(analytic_expectations(values, metadata))
1522}
1523
1524fn ensure_exact_result(require_exact: bool, metadata: &RunMetadata) -> Result<()> {
1531 if require_exact && !metadata.is_exact() {
1532 return Err(PrismError::IncompatibleBackend {
1533 backend: format!("{:?}", metadata.backend),
1534 reason: "require_exact rejects an approximate result; this engine discarded state weight while running, which the route could not predict"
1535 .into(),
1536 });
1537 }
1538 Ok(())
1539}
1540
1541fn reject_approximate_route(kind: &BackendKind, circuit: &Circuit) -> Result<()> {
1543 reject_predicted_route(approximate_route_name(kind, circuit, false))
1544}
1545
1546fn reject_predicted_route(engine: Option<&'static str>) -> Result<()> {
1549 match engine {
1550 Some(engine) => Err(PrismError::IncompatibleBackend {
1551 backend: engine.into(),
1552 reason: "require_exact rejects a route that can discard state weight; drop the \
1553 requirement to accept the approximation, which the result reports, or \
1554 select a backend that represents this circuit exactly"
1555 .into(),
1556 }),
1557 None => Ok(()),
1558 }
1559}
1560
1561fn require_unitary_circuit(kind: &BackendKind, circuit: &Circuit, subject: &str) -> Result<()> {
1565 if has_nonunitary_or_classical_ops(circuit) {
1566 return Err(PrismError::IncompatibleBackend {
1567 backend: format!("{kind:?}"),
1568 reason: format!(
1569 "{subject} a unitary circuit without measurements, resets, or conditionals"
1570 ),
1571 });
1572 }
1573 Ok(())
1574}
1575
1576fn exact_mixture_metadata(kind: &BackendKind) -> RunMetadata {
1579 let metadata = RunMetadata::exact(ResolvedBackend::DensityMatrix);
1580 #[cfg(feature = "gpu")]
1581 if matches!(kind, BackendKind::DensityMatrixGpu { .. }) {
1582 let mut on_device = metadata;
1583 on_device.placement = Placement::Device;
1584 return on_device;
1585 }
1586 #[cfg(not(feature = "gpu"))]
1587 let _ = kind;
1588 metadata
1589}
1590
1591fn exact_noisy_probabilities(
1595 kind: &BackendKind,
1596 circuit: &Circuit,
1597 noise_model: &noise::NoiseModel,
1598 initial_state: Option<StartState<'_>>,
1599 seed: u64,
1600) -> Result<Probabilities> {
1601 Ok(Probabilities::Dense(noise::density_matrix_probabilities(
1602 kind,
1603 circuit,
1604 noise_model,
1605 initial_state,
1606 seed,
1607 )?))
1608}
1609
1610fn sample_exact_noisy_shots(
1615 probs: &Probabilities,
1616 circuit: &Circuit,
1617 noise_model: &noise::NoiseModel,
1618 num_shots: usize,
1619 seed: u64,
1620) -> Vec<Vec<bool>> {
1621 let bits = circuit.num_classical_bits;
1622 let mut shots = sample_shots(probs, &circuit.measurement_map(), bits, num_shots, seed);
1623 if noise_model.readout.iter().any(Option::is_some) {
1624 let readout = trajectory::written_readout(circuit, &noise_model.readout);
1625 let mut rng = trajectory::noise_rng(seed);
1626 for shot in &mut shots {
1627 trajectory::apply_readout_errors(shot, &readout, &mut rng);
1628 }
1629 }
1630 shots
1631}
1632
1633#[inline]
1634fn probs_only_result(probs: Vec<f64>, metadata: RunMetadata) -> RunOutcome {
1635 RunOutcome {
1636 probabilities: Some(Probabilities::Dense(probs)),
1637 classical_bits: vec![],
1638 metadata,
1639 saves: Vec::new(),
1640 }
1641}
1642
1643fn ensure_saves_recorded(circuit: &Circuit, outcome: &RunOutcome) -> Result<()> {
1650 let wanted = circuit.save_count();
1651 if wanted == outcome.saves.len() {
1652 return Ok(());
1653 }
1654 Err(PrismError::IncompatibleBackend {
1655 backend: format!("{:?}", outcome.metadata.backend),
1656 reason: format!(
1657 "circuit carries {wanted} save point(s) and the route that ran recorded {}",
1658 outcome.saves.len()
1659 ),
1660 })
1661}
1662
1663fn try_backend_probabilities(backend: &dyn Backend) -> Result<Option<Probabilities>> {
1664 if let Some(factored) = backend.block_probabilities() {
1665 return Ok(Some(factored));
1666 }
1667 match backend.probabilities() {
1668 Ok(probs) => Ok(Some(Probabilities::Dense(probs))),
1669 Err(PrismError::BackendUnsupported { .. }) => Ok(None),
1670 Err(err) => Err(err),
1671 }
1672}
1673
1674fn execute(backend: &mut dyn Backend, circuit: &Circuit, opts: &SimOptions) -> Result<RunOutcome> {
1676 let expanded = expand_for_backend(&*backend, circuit);
1677 let fused = fuse_for_backend(&*backend, &expanded);
1678 execute_circuit(backend, &fused, opts)
1679}
1680
1681pub(crate) struct PreparedRoute {
1689 plan: BackendPlan,
1690 supports_fused: bool,
1691 fusion_state_qubits: usize,
1692 held: Option<(u64, Box<dyn Backend + Send>)>,
1696 draws_randomness: bool,
1699 values: PreparedValues,
1700 observable: PreparedObservable,
1701}
1702
1703#[derive(Clone, Copy)]
1705enum PreparedValues {
1706 Held,
1707 TensorFirst,
1710 Simulate,
1712}
1713
1714#[derive(Clone, Copy)]
1716enum PreparedObservable {
1717 Held,
1719 Grouped,
1721 Simulate,
1723}
1724
1725impl PreparedRoute {
1726 pub(crate) fn supports_fused(&self) -> bool {
1729 self.supports_fused
1730 }
1731
1732 pub(crate) fn fusion_state_qubits(&self) -> usize {
1735 self.fusion_state_qubits
1736 }
1737
1738 pub(crate) fn run(&mut self, circuit: &Circuit, seed: u64) -> Result<RunOutcome> {
1740 if self.draws_randomness {
1741 let mut backend = self.plan.build(seed);
1742 return execute_circuit(&mut *backend, circuit, &SimOptions::default());
1743 }
1744 execute_circuit(self.held_backend(seed), circuit, &SimOptions::default())
1745 }
1746
1747 pub(crate) fn expectation_values(
1750 &mut self,
1751 bound: &Circuit,
1752 applied: &Circuit,
1753 observables: &[Vec<PauliTerm>],
1754 seed: u64,
1755 ) -> Option<Result<Vec<f64>>> {
1756 match self.values {
1757 PreparedValues::Simulate => return None,
1758 PreparedValues::TensorFirst => {
1759 if let Some(result) = tensor_route_expectations(bound, observables) {
1760 return Some(result.map(ExpectationResult::into_values));
1761 }
1762 }
1763 PreparedValues::Held => {}
1764 }
1765 Some(
1766 self.held_expectation_values(applied, observables, seed)
1767 .map(|(values, _)| values),
1768 )
1769 }
1770
1771 pub(crate) fn observable_expectation(
1774 &mut self,
1775 applied: &Circuit,
1776 observable: &PauliObservable,
1777 seed: u64,
1778 ) -> Option<Result<ObservableExpectation>> {
1779 match self.observable {
1780 PreparedObservable::Simulate => None,
1781 PreparedObservable::Held => Some(
1782 self.held_expectation_values(applied, &observable_vecs(observable), seed)
1783 .map(|(values, metadata)| {
1784 weighted_observable_result(observable, &values, None, metadata)
1785 }),
1786 ),
1787 PreparedObservable::Grouped => Some(self.held_grouped(applied, observable, seed)),
1788 }
1789 }
1790
1791 fn held_backend(&mut self, seed: u64) -> &mut dyn Backend {
1792 if !matches!(&self.held, Some((s, _)) if *s == seed) {
1793 self.held = Some((seed, self.plan.build(seed)));
1794 }
1795 let (_, backend) = self.held.as_mut().expect("just built");
1796 &mut **backend
1797 }
1798
1799 fn held_expectation_values(
1800 &mut self,
1801 circuit: &Circuit,
1802 observables: &[Vec<PauliTerm>],
1803 seed: u64,
1804 ) -> Result<(Vec<f64>, RunMetadata)> {
1805 for observable in observables {
1806 validate_observable(observable, circuit.num_qubits)?;
1807 }
1808 let backend = self.held_backend(seed);
1809 execute_circuit(&mut *backend, circuit, &SimOptions::classical_only())?;
1810 Ok((
1811 backend.pauli_expectations(observables)?,
1812 backend_metadata(backend),
1813 ))
1814 }
1815
1816 fn held_grouped(
1817 &mut self,
1818 circuit: &Circuit,
1819 observable: &PauliObservable,
1820 seed: u64,
1821 ) -> Result<ObservableExpectation> {
1822 for (_, factors) in observable.terms() {
1823 validate_observable(factors, circuit.num_qubits)?;
1824 }
1825 let backend = self.held_backend(seed);
1826 execute_circuit(&mut *backend, circuit, &SimOptions::classical_only())?;
1827 let statevector = backend
1828 .as_any()
1829 .and_then(|any| any.downcast_ref::<StatevectorBackend>())
1830 .expect("a grouped answer is settled only on a statevector plan");
1831 grouped_expectation_on_state(statevector, observable)
1832 }
1833}
1834
1835pub(crate) fn prepared_route(kind: &BackendKind, template: &Circuit) -> Option<PreparedRoute> {
1839 if !kind.is_auto() && validate_explicit_backend(kind, template).is_err() {
1840 return None;
1841 }
1842 let ProbabilityRoute::Direct {
1843 has_partial_independence,
1844 } = plan_probability_route(kind, template)
1845 else {
1846 return None;
1847 };
1848 let ExecutionPlan::Backend(plan) = resolve(kind, template, has_partial_independence) else {
1849 return None;
1850 };
1851 let probe = plan.build(0);
1852 let has_qft_block = crate::circuit::any_gate(&template.instructions, &mut |gate| {
1853 matches!(gate, crate::gates::Gate::QftBlock { .. })
1854 });
1855 if has_qft_block && !probe.supports_qft_block() {
1856 return None;
1857 }
1858 let has_pauli_rot = crate::circuit::any_gate(&template.instructions, &mut |gate| {
1859 matches!(gate, crate::gates::Gate::PauliRot(_))
1860 });
1861 if has_pauli_rot && !probe.supports_pauli_rotation() {
1862 return None;
1863 }
1864 let (values, observable) = prepared_expectations(kind, template, &*probe);
1865 Some(PreparedRoute {
1866 supports_fused: probe.supports_fused_gates(),
1867 fusion_state_qubits: probe.fusion_state_qubits(template.num_qubits),
1868 plan,
1869 held: None,
1870 draws_randomness: draws_randomness(template),
1871 values,
1872 observable,
1873 })
1874}
1875
1876fn prepared_expectations(
1886 kind: &BackendKind,
1887 template: &Circuit,
1888 probe: &dyn Backend,
1889) -> (PreparedValues, PreparedObservable) {
1890 let clifford_route =
1891 (kind.is_auto() || kind.is_stabilizer_family()) && template.is_clifford_only();
1892 if draws_randomness(template)
1893 || template.save_count() > 0
1894 || clifford_route
1895 || !probe.supports_pauli_expectation()
1896 {
1897 return (PreparedValues::Simulate, PreparedObservable::Simulate);
1898 }
1899 let fits_dense = template.num_qubits <= max_statevector_qubits();
1900 let values = if kind.is_auto() && fits_dense && template.num_qubits >= TENSOR_ROUTE_MIN_QUBITS {
1901 PreparedValues::TensorFirst
1902 } else {
1903 PreparedValues::Held
1904 };
1905 let grouped = match kind {
1906 BackendKind::Statevector => true,
1907 #[cfg(feature = "gpu")]
1908 BackendKind::StatevectorGpu { .. } => true,
1909 _ => kind.is_auto() && fits_dense,
1910 };
1911 let holds_statevector = probe
1912 .as_any()
1913 .is_some_and(|any| any.is::<StatevectorBackend>());
1914 let observable = match (grouped, holds_statevector) {
1915 (false, _) => PreparedObservable::Held,
1916 (true, true) => PreparedObservable::Grouped,
1917 (true, false) => PreparedObservable::Simulate,
1918 };
1919 (values, observable)
1920}
1921
1922fn execute_circuit(
1924 backend: &mut dyn Backend,
1925 circuit: &Circuit,
1926 opts: &SimOptions,
1927) -> Result<RunOutcome> {
1928 backend.init(circuit.num_qubits, circuit.num_classical_bits)?;
1929 let saves = apply_recording_saves(backend, &circuit.instructions)?;
1930
1931 let probabilities = if opts.probabilities {
1932 try_backend_probabilities(backend)?
1933 } else {
1934 None
1935 };
1936
1937 Ok(RunOutcome {
1938 classical_bits: backend.classical_results().to_vec(),
1939 probabilities,
1940 metadata: backend_metadata(backend),
1941 saves,
1942 })
1943}
1944
1945fn apply_recording_saves(
1951 backend: &mut dyn Backend,
1952 instructions: &[Instruction],
1953) -> Result<Vec<SaveRecord>> {
1954 let first = instructions
1955 .iter()
1956 .position(|i| matches!(i, Instruction::Save { .. }));
1957 let Some(first) = first else {
1958 backend.apply_instructions(instructions)?;
1959 return Ok(Vec::new());
1960 };
1961
1962 let mut saves = Vec::new();
1963 let mut start = 0;
1964 for (i, inst) in instructions.iter().enumerate().skip(first) {
1965 let Instruction::Save { spec, label, .. } = inst else {
1966 continue;
1967 };
1968 backend.apply_instructions(&instructions[start..i])?;
1969 saves.push(SaveRecord {
1970 label: label.clone(),
1971 value: read_save(backend, *spec, label)?,
1972 });
1973 start = i + 1;
1974 }
1975 backend.apply_instructions(&instructions[start..])?;
1976 Ok(saves)
1977}
1978
1979fn read_save(backend: &mut dyn Backend, spec: SaveSpec, label: &str) -> Result<SavedValue> {
1985 let named = |err: crate::error::PrismError| match err {
1986 crate::error::PrismError::IncompatibleBackend { backend, reason } => {
1987 crate::error::PrismError::IncompatibleBackend {
1988 backend,
1989 reason: format!("save point `{label}`: {reason}"),
1990 }
1991 }
1992 other => other,
1993 };
1994 match spec {
1995 SaveSpec::StateVector => backend
1996 .export_statevector()
1997 .map(SavedValue::StateVector)
1998 .map_err(named),
1999 SaveSpec::Probabilities => backend
2000 .probabilities()
2001 .map(SavedValue::Probabilities)
2002 .map_err(named),
2003 SaveSpec::DensityMatrix => {
2004 let whole: Vec<usize> = (0..backend.num_qubits()).collect();
2005 backend
2006 .reduced_density_matrix(&whole)
2007 .map(SavedValue::DensityMatrix)
2008 .map_err(named)
2009 }
2010 }
2011}
2012
2013pub(crate) fn backend_metadata(backend: &dyn Backend) -> RunMetadata {
2018 let mut metadata =
2019 RunMetadata::new(backend.resolved(), backend.exactness(), backend.placement());
2020 metadata.bond = backend.bond_report();
2021 metadata
2022}
2023
2024#[cfg(test)]
2025fn run(circuit: &Circuit, seed: u64) -> Result<RunOutcome> {
2026 run_with(BackendKind::Auto, circuit, seed)
2027}
2028
2029pub(crate) fn run_with(kind: BackendKind, circuit: &Circuit, seed: u64) -> Result<RunOutcome> {
2032 run_with_internal(kind, circuit, seed, SimOptions::default())
2033}
2034
2035fn run_with_internal(
2036 kind: BackendKind,
2037 circuit: &Circuit,
2038 seed: u64,
2039 opts: SimOptions,
2040) -> Result<RunOutcome> {
2041 if !kind.is_auto() {
2042 validate_explicit_backend(&kind, circuit)?;
2043 }
2044 #[cfg(feature = "distributed")]
2049 if matches!(kind, BackendKind::StatevectorDistributed { .. }) {
2050 let mut backend = resolve_backend(&kind, circuit, false).build(seed);
2051 if opts.probabilities {
2052 crate::backend::dense_probability_len(backend.name(), circuit.num_qubits)?;
2057 }
2058 return execute(&mut *backend, circuit, &opts);
2059 }
2060 let route = plan_probability_route(&kind, circuit);
2061 let outcome = run_route(&kind, circuit, seed, opts, &route)?;
2062 ensure_saves_recorded(circuit, &outcome)?;
2063 Ok(outcome)
2064}
2065
2066fn run_route(
2069 kind: &BackendKind,
2070 circuit: &Circuit,
2071 seed: u64,
2072 opts: SimOptions,
2073 route: &ProbabilityRoute,
2074) -> Result<RunOutcome> {
2075 match route {
2076 ProbabilityRoute::FactoredStabilizer => {
2077 let mut backend =
2078 crate::backend::factored_stabilizer::FactoredStabilizerBackend::new(seed);
2079 let fs_opts = if circuit.num_qubits > 64 {
2080 SimOptions {
2081 probabilities: false,
2082 }
2083 } else {
2084 opts
2085 };
2086 execute(&mut backend, circuit, &fs_opts)
2087 }
2088 ProbabilityRoute::Decomposed(components) => {
2089 run_decomposed(kind, components, circuit, seed, &opts)
2090 }
2091 ProbabilityRoute::TemporalClifford {
2092 has_partial_independence,
2093 } => match plan_temporal_clifford(kind, circuit) {
2094 Some(tc) => run_temporal_clifford(&tc, seed, opts.probabilities),
2095 None => run_direct(kind, circuit, seed, opts, *has_partial_independence),
2096 },
2097 ProbabilityRoute::Direct {
2098 has_partial_independence,
2099 } => run_direct(kind, circuit, seed, opts, *has_partial_independence),
2100 }
2101}
2102
2103fn run_direct(
2104 kind: &BackendKind,
2105 circuit: &Circuit,
2106 seed: u64,
2107 opts: SimOptions,
2108 has_partial_independence: bool,
2109) -> Result<RunOutcome> {
2110 match resolve(kind, circuit, has_partial_independence) {
2111 ExecutionPlan::Backend(plan) => {
2112 let mut backend = plan.build(seed);
2113 execute(&mut *backend, circuit, &opts)
2114 }
2115 ExecutionPlan::StabilizerRank => {
2116 let sr = stabilizer_rank::run_stabilizer_rank(circuit, seed)?;
2117 Ok(probs_only_result(
2118 sr.probabilities,
2119 RunMetadata::exact(ResolvedBackend::StabilizerRank),
2120 ))
2121 }
2122 ExecutionPlan::StochasticPauli { num_samples } => {
2123 Err(crate::error::PrismError::IncompatibleBackend {
2124 backend: format!(
2125 "{:?}",
2126 BackendKind::StochasticPauli { num_samples }
2127 ),
2128 reason: "StochasticPauli produces marginal estimates only; use `simulate(...).marginals()`".into(),
2129 })
2130 }
2131 ExecutionPlan::DeterministicPauli { truncation } => {
2132 Err(crate::error::PrismError::IncompatibleBackend {
2133 backend: format!(
2134 "{:?}",
2135 BackendKind::DeterministicPauli { truncation }
2136 ),
2137 reason: "DeterministicPauli produces marginals only; use `simulate(...).marginals()`".into(),
2138 })
2139 }
2140 ExecutionPlan::PauliPath => Err(reject_pauli_path("a single run")),
2141 }
2142}
2143
2144pub fn run_on(backend: &mut dyn Backend, circuit: &Circuit) -> Result<RunOutcome> {
2147 execute(backend, circuit, &SimOptions::default())
2148}
2149
2150pub fn run_on_state(
2156 backend: &mut dyn Backend,
2157 circuit: &Circuit,
2158 initial_state: &[Complex64],
2159) -> Result<RunOutcome> {
2160 check_initial_state_len(StartState::Amplitudes(initial_state), circuit.num_qubits)?;
2161 backend.init_from_amplitudes(initial_state.to_vec(), circuit.num_classical_bits)?;
2162 let saves = apply_fused_circuit(backend, circuit)?;
2163 Ok(RunOutcome {
2164 classical_bits: backend.classical_results().to_vec(),
2165 probabilities: try_backend_probabilities(backend)?,
2166 metadata: backend_metadata(backend),
2167 saves,
2168 })
2169}
2170
2171pub fn run_batch(circuits: &[Circuit], kind: BackendKind, seed: u64) -> Result<Vec<RunOutcome>> {
2195 #[cfg(feature = "parallel")]
2196 if circuits.len() > 1
2197 && circuits
2198 .iter()
2199 .all(|c| runs_split_across_workers(&kind, c.num_qubits))
2200 {
2201 use rayon::prelude::*;
2202 let outcomes: Vec<Result<RunOutcome>> = circuits
2203 .par_iter()
2204 .map_init(
2205 || None,
2206 |held, circuit| run_batch_entry(&kind, circuit, seed, held),
2207 )
2208 .collect();
2209 return outcomes.into_iter().collect();
2210 }
2211
2212 let mut held = None;
2213 circuits
2214 .iter()
2215 .map(|circuit| run_batch_entry(&kind, circuit, seed, &mut held))
2216 .collect()
2217}
2218
2219type HeldBackend = Option<(dispatch::BackendPlan, usize, Box<dyn Backend + Send>)>;
2220
2221fn run_batch_entry(
2222 kind: &BackendKind,
2223 circuit: &Circuit,
2224 seed: u64,
2225 held: &mut HeldBackend,
2226) -> Result<RunOutcome> {
2227 let Some(plan) = batch_plan(kind, circuit) else {
2228 *held = None;
2229 return run_with_internal(kind.clone(), circuit, seed, SimOptions::default());
2230 };
2231 if !kind.is_auto() {
2232 validate_explicit_backend(kind, circuit)?;
2233 }
2234
2235 let reusable = matches!(
2236 &*held,
2237 Some((p, width, _)) if *width == circuit.num_qubits && dispatch::same_plan(p, &plan)
2238 );
2239 if !reusable {
2240 *held = Some((plan.clone(), circuit.num_qubits, plan.build(seed)));
2241 }
2242 let (_, _, backend) = held.as_mut().expect("just built");
2243 execute(&mut **backend, circuit, &SimOptions::default())
2244}
2245
2246#[cfg(all(feature = "parallel", not(miri)))]
2251const RUN_SPLIT_QUBITS: usize = 17;
2252#[cfg(all(feature = "parallel", miri))]
2253const RUN_SPLIT_QUBITS: usize = crate::backend::PARALLEL_THRESHOLD_QUBITS;
2254
2255#[cfg(feature = "parallel")]
2260pub(crate) fn runs_split_across_workers(kind: &BackendKind, num_qubits: usize) -> bool {
2261 if bound_to_one_context(kind) {
2262 return false;
2263 }
2264 let width = if kind.is_density_matrix() {
2265 2 * num_qubits
2266 } else {
2267 num_qubits
2268 };
2269 width < RUN_SPLIT_QUBITS
2270}
2271
2272#[cfg(feature = "parallel")]
2276fn shots_split_across_workers(kind: &BackendKind, states: &[(ResolvedBackend, usize)]) -> bool {
2277 !bound_to_one_context(kind)
2278 && states
2279 .iter()
2280 .all(|&(family, width)| state_splits_across_workers(family, width))
2281}
2282
2283#[cfg(feature = "parallel")]
2292pub(crate) fn state_splits_across_workers(family: ResolvedBackend, width: usize) -> bool {
2293 use crate::backend::{MIN_QUBITS_FOR_PAR_GATES, PARALLEL_THRESHOLD_QUBITS};
2294 match family {
2295 ResolvedBackend::Stabilizer | ResolvedBackend::FactoredStabilizer => {
2296 width < MIN_QUBITS_FOR_PAR_GATES
2297 }
2298 ResolvedBackend::ProductState => true,
2299 ResolvedBackend::DensityMatrix => 2 * width < PARALLEL_THRESHOLD_QUBITS,
2300 _ => width < PARALLEL_THRESHOLD_QUBITS,
2301 }
2302}
2303
2304#[cfg(feature = "parallel")]
2305fn bound_to_one_context(kind: &BackendKind) -> bool {
2306 match kind {
2307 #[cfg(feature = "gpu")]
2308 BackendKind::AutoGpu { .. }
2309 | BackendKind::StatevectorGpu { .. }
2310 | BackendKind::DensityMatrixGpu { .. }
2311 | BackendKind::StabilizerGpu { .. } => true,
2312 #[cfg(feature = "distributed")]
2313 BackendKind::StatevectorDistributed { .. } => true,
2314 _ => false,
2315 }
2316}
2317
2318fn batch_plan(kind: &BackendKind, circuit: &Circuit) -> Option<dispatch::BackendPlan> {
2323 if draws_randomness(circuit) {
2324 return None;
2325 }
2326 let ProbabilityRoute::Direct {
2327 has_partial_independence,
2328 } = plan_probability_route(kind, circuit)
2329 else {
2330 return None;
2331 };
2332 match resolve(kind, circuit, has_partial_independence) {
2333 ExecutionPlan::Backend(plan) => Some(plan),
2334 _ => None,
2335 }
2336}
2337
2338fn draws_randomness(circuit: &Circuit) -> bool {
2341 circuit.instructions.iter().any(|i| {
2342 matches!(
2343 i,
2344 Instruction::Measure { .. }
2345 | Instruction::Reset { .. }
2346 | Instruction::Conditional { .. }
2347 | Instruction::Region(_)
2348 )
2349 })
2350}
2351
2352pub fn run_qasm(qasm: &str, seed: u64) -> Result<RunOutcome> {
2354 let circuit = crate::circuit::openqasm::parse(qasm)?;
2355 simulate(&circuit).seed(seed).run()
2356}
2357
2358#[cfg(test)]
2359fn run_shots(circuit: &Circuit, num_shots: usize, seed: u64) -> Result<ShotsResult> {
2360 run_shots_with(BackendKind::Auto, circuit, num_shots, seed)
2361}
2362
2363pub(crate) fn supports_compiled_measurement_sampling(circuit: &Circuit) -> bool {
2364 circuit.is_clifford_only()
2365 && !circuit.has_resets()
2366 && circuit.has_terminal_measurements_only()
2367 && circuit
2368 .instructions
2369 .iter()
2370 .any(|inst| matches!(inst, Instruction::Measure { .. }))
2371}
2372
2373fn supports_deferred_measurement_sampling(circuit: &Circuit) -> bool {
2374 circuit.is_clifford_only()
2375 && (circuit.has_resets() || !circuit.has_terminal_measurements_only())
2376 && circuit
2377 .instructions
2378 .iter()
2379 .any(|inst| matches!(inst, Instruction::Measure { .. }))
2380 && !circuit.instructions.iter().any(|inst| {
2381 matches!(
2382 inst,
2383 Instruction::Conditional { .. } | Instruction::Region(_)
2384 )
2385 })
2386}
2387
2388fn is_clifford_sampler_kind(kind: &BackendKind) -> bool {
2389 if kind.is_auto() {
2390 return true;
2391 }
2392 match kind {
2393 BackendKind::Stabilizer | BackendKind::FactoredStabilizer => true,
2394 #[cfg(feature = "gpu")]
2395 BackendKind::StabilizerGpu { .. } => true,
2396 _ => false,
2397 }
2398}
2399
2400fn should_use_compiled_clifford_sampling(
2401 kind: &BackendKind,
2402 circuit: &Circuit,
2403 num_shots: usize,
2404) -> bool {
2405 num_shots >= 1
2406 && supports_compiled_measurement_sampling(circuit)
2407 && is_clifford_sampler_kind(kind)
2408}
2409
2410fn should_use_deferred_clifford_sampling(
2411 kind: &BackendKind,
2412 circuit: &Circuit,
2413 num_shots: usize,
2414) -> bool {
2415 num_shots >= 2
2416 && supports_deferred_measurement_sampling(circuit)
2417 && is_clifford_sampler_kind(kind)
2418}
2419
2420fn compile_measurements_for_kind(
2421 kind: &BackendKind,
2422 circuit: &Circuit,
2423 seed: u64,
2424) -> Result<compiled::CompiledSampler> {
2425 #[cfg(not(feature = "gpu"))]
2426 let _ = kind;
2427
2428 let sampler = compiled::compile_measurements(circuit, seed)?;
2429
2430 #[cfg(feature = "gpu")]
2431 if let BackendKind::StabilizerGpu { context } = kind {
2432 return Ok(sampler.with_gpu(context.clone()));
2433 }
2434
2435 Ok(sampler)
2436}
2437
2438type Decomposition = (Vec<Vec<usize>>, bool);
2440
2441fn analyze_independence(circuit: &Circuit) -> (Option<Decomposition>, bool) {
2446 if circuit.num_qubits >= MIN_DECOMPOSITION_QUBITS {
2447 let (components, clifford_only) = circuit.subsystems_and_clifford();
2448 if components.len() > 1 {
2449 if should_decompose(&components, circuit.num_qubits) {
2450 return (Some((components, clifford_only)), false);
2451 }
2452 return (None, true);
2453 }
2454 }
2455 (None, false)
2456}
2457
2458fn auto_clifford_t_budget(circuit: &Circuit) -> Option<(usize, usize)> {
2461 (circuit.is_clifford_plus_t() && circuit.has_t_gates()).then(|| {
2462 (
2463 circuit.t_count(),
2464 stabilizer_rank_budget(circuit.num_qubits),
2465 )
2466 })
2467}
2468
2469pub(super) fn auto_stabilizer_rank_t_count(circuit: &Circuit, max_t: usize) -> Option<usize> {
2473 let (t, sr_budget) = auto_clifford_t_budget(circuit)?;
2474 (t <= max_t && t <= sr_budget).then_some(t)
2475}
2476
2477enum ProbabilityRoute {
2483 FactoredStabilizer,
2484 Decomposed(Vec<Vec<usize>>),
2485 TemporalClifford {
2488 has_partial_independence: bool,
2489 },
2490 Direct {
2491 has_partial_independence: bool,
2492 },
2493}
2494
2495fn plan_probability_route(kind: &BackendKind, circuit: &Circuit) -> ProbabilityRoute {
2496 let (decompose, has_partial_independence) = analyze_independence(circuit);
2497 if let Some((components, clifford_only)) = decompose {
2498 let max_block = components.iter().map(|c| c.len()).max().unwrap_or(0);
2499 if kind.is_auto()
2500 && clifford_only
2501 && circuit.num_qubits >= MIN_FACTORED_STABILIZER_QUBITS
2502 && max_block >= MIN_BLOCK_FOR_FACTORED_STAB
2503 {
2504 return ProbabilityRoute::FactoredStabilizer;
2505 }
2506 return ProbabilityRoute::Decomposed(components);
2507 }
2508 if has_temporal_clifford_opportunity(kind, circuit) {
2509 return ProbabilityRoute::TemporalClifford {
2510 has_partial_independence,
2511 };
2512 }
2513 ProbabilityRoute::Direct {
2514 has_partial_independence,
2515 }
2516}
2517
2518fn auto_terminal_statevector_candidate(circuit: &Circuit) -> bool {
2521 match plan_probability_route(&BackendKind::Auto, circuit) {
2522 ProbabilityRoute::Direct {
2523 has_partial_independence,
2524 } => auto_selects_cpu_statevector(circuit, has_partial_independence),
2525 _ => false,
2526 }
2527}
2528
2529fn terminal_statevector_candidate(kind: &BackendKind, circuit: &Circuit) -> bool {
2530 if kind.is_auto() {
2531 return auto_terminal_statevector_candidate(circuit);
2532 }
2533 match kind {
2534 BackendKind::Statevector => true,
2535 #[cfg(feature = "gpu")]
2536 BackendKind::StatevectorGpu { .. } => true,
2537 _ => false,
2538 }
2539}
2540
2541fn try_terminal_statevector_backend(
2542 kind: &BackendKind,
2543 circuit: &Circuit,
2544 seed: u64,
2545) -> Result<Option<TerminalStatevector>> {
2546 if !circuit.has_terminal_measurements_only() {
2547 return Ok(None);
2548 }
2549
2550 let meas_map = circuit.measurement_map();
2551 if meas_map.is_empty() {
2552 return Ok(None);
2553 }
2554
2555 let stripped = circuit.without_measurements();
2556 if !terminal_statevector_candidate(kind, &stripped) {
2557 return Ok(None);
2558 }
2559
2560 let accel = accel_for(kind, Family::Statevector, stripped.num_qubits);
2561 let mut backend = build_statevector(&accel, seed);
2562 let expanded = expand_for_backend(&backend, &stripped);
2563 let fused = fuse_for_backend(&backend, &expanded);
2564 backend.init(fused.num_qubits, fused.num_classical_bits)?;
2565 backend.apply_instructions(&fused.instructions)?;
2566
2567 Ok(Some((backend, meas_map)))
2568}
2569
2570fn try_native_terminal_backend(
2585 kind: &BackendKind,
2586 stripped: &Circuit,
2587 seed: u64,
2588) -> Result<Option<Box<dyn Backend>>> {
2589 if !kind.is_auto() {
2590 validate_explicit_backend(kind, stripped)?;
2591 }
2592 let (decomposed, has_partial_independence) = match plan_probability_route(kind, stripped) {
2593 ProbabilityRoute::Direct {
2594 has_partial_independence,
2595 } => (false, has_partial_independence),
2596 ProbabilityRoute::Decomposed(_) => (true, false),
2597 _ => return Ok(None),
2598 };
2599 let ExecutionPlan::Backend(plan) = resolve(kind, stripped, has_partial_independence) else {
2600 return Ok(None);
2601 };
2602 if decomposed && !matches!(plan, BackendPlan::ProductState) {
2603 return Ok(None);
2604 }
2605 let mut backend = plan.build(seed);
2606 if !backend.supports_native_sampling() {
2607 return Ok(None);
2608 }
2609 execute(&mut *backend, stripped, &SimOptions::classical_only())?;
2610 Ok(Some(backend))
2611}
2612
2613fn try_native_marginal_backend(
2627 kind: &BackendKind,
2628 circuit: &Circuit,
2629 seed: u64,
2630) -> Result<Option<Box<dyn Backend>>> {
2631 if !kind.is_auto() {
2632 validate_explicit_backend(kind, circuit)?;
2633 }
2634 let (decomposed, has_partial_independence) = match plan_probability_route(kind, circuit) {
2635 ProbabilityRoute::Direct {
2636 has_partial_independence,
2637 } => (false, has_partial_independence),
2638 ProbabilityRoute::Decomposed(_) => (true, false),
2639 _ => return Ok(None),
2640 };
2641 let ExecutionPlan::Backend(plan) = resolve(kind, circuit, has_partial_independence) else {
2642 return Ok(None);
2643 };
2644 if decomposed && !matches!(plan, BackendPlan::ProductState) {
2645 return Ok(None);
2646 }
2647 let mut backend = plan.build(seed);
2648 if !backend.supports_pauli_expectation() {
2649 return Ok(None);
2650 }
2651 execute(&mut *backend, circuit, &SimOptions::classical_only())?;
2652 Ok(Some(backend))
2653}
2654
2655enum ShotSource {
2659 Compiled {
2662 sampler: Box<compiled::CompiledSampler>,
2663 meas_map: Vec<(usize, usize)>,
2664 deferred: bool,
2665 },
2666 TerminalStatevector {
2667 backend: Box<StatevectorBackend>,
2668 meas_map: Vec<(usize, usize)>,
2669 },
2670 Native {
2672 backend: Box<dyn Backend>,
2673 meas_map: Vec<(usize, usize)>,
2674 },
2675 TerminalProbabilities {
2678 probs: Probabilities,
2679 meas_map: Vec<(usize, usize)>,
2680 metadata: RunMetadata,
2681 },
2682 StabilizerRank,
2683 PerShot,
2684}
2685
2686impl ShotSource {
2687 fn metadata(&self) -> Option<RunMetadata> {
2691 match self {
2692 ShotSource::Compiled { .. } => Some(
2693 RunMetadata::exact(ResolvedBackend::CompiledStabilizer)
2694 .with_engine(Engine::CompiledSampler),
2695 ),
2696 ShotSource::TerminalStatevector { backend, .. } => Some(backend_metadata(&**backend)),
2697 ShotSource::Native { backend, .. } => Some(backend_metadata(&**backend)),
2698 ShotSource::TerminalProbabilities { metadata, .. } => Some(metadata.clone()),
2699 ShotSource::StabilizerRank => Some(RunMetadata::exact(ResolvedBackend::StabilizerRank)),
2700 ShotSource::PerShot => None,
2701 }
2702 }
2703}
2704
2705const MIN_AUTO_CLIFFORD_REGISTER_QUBITS: usize = 16;
2709
2710fn auto_prefers_clifford_register(circuit: &Circuit) -> bool {
2714 circuit.num_qubits >= MIN_AUTO_CLIFFORD_REGISTER_QUBITS
2715 && circuit.has_terminal_measurements_only()
2716 && !circuit.has_resets()
2717 && auto_clifford_t_budget(circuit).is_some_and(|(t, _)| {
2718 t < circuit.num_qubits && t <= clifford_register::MAX_REGISTER_QUBITS
2719 })
2720}
2721
2722fn auto_samples_wide_by_stabilizer_rank(circuit: &Circuit) -> bool {
2725 circuit.has_terminal_measurements_only()
2726 && circuit.num_qubits > MAX_STABILIZER_RANK_QUBITS
2727 && auto_stabilizer_rank_t_count(circuit, MAX_AUTO_T_COUNT_SHOTS).is_some()
2728}
2729
2730fn auto_samples_by_stabilizer_rank(circuit: &Circuit) -> bool {
2734 let folded = circuit.fold_static_guards();
2735 let circuit = folded.as_ref();
2736 if circuit.has_terminal_measurements_only() {
2737 auto_prefers_clifford_register(circuit) || auto_samples_wide_by_stabilizer_rank(circuit)
2738 } else {
2739 auto_stabilizer_rank_t_count(circuit, MAX_AUTO_T_COUNT_SHOTS).is_some()
2740 }
2741}
2742
2743fn prepare_shot_source(
2749 kind: &BackendKind,
2750 circuit: &Circuit,
2751 num_shots: usize,
2752 seed: u64,
2753) -> Result<ShotSource> {
2754 if should_use_compiled_clifford_sampling(kind, circuit, num_shots) {
2755 return Ok(ShotSource::Compiled {
2756 sampler: Box::new(compile_measurements_for_kind(kind, circuit, seed)?),
2757 meas_map: circuit.measurement_map(),
2758 deferred: false,
2759 });
2760 }
2761
2762 if should_use_deferred_clifford_sampling(kind, circuit, num_shots)
2763 && let Ok(deferred) = compiled::defer_measure_reset_circuit(circuit)
2764 {
2765 return Ok(ShotSource::Compiled {
2766 sampler: Box::new(compile_measurements_for_kind(kind, &deferred, seed)?),
2767 meas_map: deferred.measurement_map(),
2768 deferred: true,
2769 });
2770 }
2771
2772 if kind.is_auto() && auto_prefers_clifford_register(circuit) {
2773 return Ok(ShotSource::StabilizerRank);
2774 }
2775
2776 if let Some((backend, meas_map)) = try_terminal_statevector_backend(kind, circuit, seed)? {
2777 return Ok(ShotSource::TerminalStatevector {
2778 backend: Box::new(backend),
2779 meas_map,
2780 });
2781 }
2782
2783 if matches!(kind, BackendKind::StabilizerRank) && circuit.has_t_gates() {
2784 return Ok(ShotSource::StabilizerRank);
2785 }
2786 if kind.is_auto() && auto_samples_wide_by_stabilizer_rank(circuit) {
2787 return Ok(ShotSource::StabilizerRank);
2788 }
2789
2790 if circuit.has_terminal_measurements_only()
2791 || (kind.is_density_matrix() && circuit.has_terminal_measurements_under_reset_channel())
2792 {
2793 let stripped = circuit.without_measurements();
2794 if let Some(backend) = try_native_terminal_backend(kind, &stripped, seed)? {
2795 return Ok(ShotSource::Native {
2796 backend,
2797 meas_map: circuit.measurement_map(),
2798 });
2799 }
2800 let result = run_with_internal(kind.clone(), &stripped, seed, SimOptions::default())?;
2801 if let Some(probs) = result.probabilities {
2802 return Ok(ShotSource::TerminalProbabilities {
2803 probs,
2804 meas_map: circuit.measurement_map(),
2805 metadata: result.metadata,
2806 });
2807 }
2808 }
2809
2810 Ok(ShotSource::PerShot)
2811}
2812
2813#[cfg(test)]
2814fn run_counts(circuit: &Circuit, num_shots: usize, seed: u64) -> Result<HashMap<Vec<u64>, u64>> {
2815 run_counts_with(BackendKind::Auto, circuit, num_shots, seed).map(|(counts, _)| counts)
2816}
2817
2818pub(crate) fn run_counts_with(
2831 kind: BackendKind,
2832 circuit: &Circuit,
2833 num_shots: usize,
2834 seed: u64,
2835) -> Result<(HashMap<Vec<u64>, u64>, RunMetadata)> {
2836 #[cfg(feature = "distributed")]
2837 if matches!(kind, BackendKind::StatevectorDistributed { .. }) {
2838 let shots = run_shots_with(kind, circuit, num_shots, seed)?;
2839 return Ok((shots.counts(), shots.metadata));
2840 }
2841
2842 let folded = circuit.fold_static_guards();
2843 let circuit = folded.as_ref();
2844
2845 let bits = circuit.num_classical_bits;
2846 let source = prepare_shot_source(&kind, circuit, num_shots, seed)?;
2847 let Some(metadata) = source.metadata() else {
2848 let shots = run_shots_per_shot(kind, circuit, num_shots, seed, ShotRows::CountedOnly)?;
2849 return Ok((shots.counts(), shots.metadata));
2850 };
2851 let counts = match source {
2852 ShotSource::Compiled {
2853 mut sampler,
2854 meas_map,
2855 deferred,
2856 } => {
2857 if deferred {
2858 let packed = sampler.try_sample_bulk_packed(num_shots)?;
2859 counts_of(
2860 packed_shots_to_classical_bits(&packed, &meas_map, bits),
2861 bits,
2862 )
2863 } else {
2864 record_counts_to_classical_bits(
2865 sampler.try_sample_counts(num_shots)?,
2866 &meas_map,
2867 bits,
2868 )
2869 }
2870 }
2871 ShotSource::TerminalStatevector { backend, meas_map } => {
2872 if backend.is_gpu_resident() {
2873 let probs = backend.probabilities()?;
2874 sample_counts_from_probs(&probs, &meas_map, bits, num_shots, seed)
2875 } else {
2876 sample_counts_from_state(
2877 backend.state_vector(),
2878 backend.probability_scale(),
2879 &meas_map,
2880 bits,
2881 num_shots,
2882 seed,
2883 )
2884 }
2885 }
2886 ShotSource::Native {
2887 mut backend,
2888 meas_map,
2889 } => {
2890 let samples = backend.sample_basis_states(num_shots, seed)?;
2891 counts_from_basis_samples(&samples, &meas_map, bits)
2892 }
2893 ShotSource::TerminalProbabilities {
2894 probs, meas_map, ..
2895 } => counts_of(sample_shots(&probs, &meas_map, bits, num_shots, seed), bits),
2896 ShotSource::StabilizerRank => {
2897 stabilizer_rank::run_stabilizer_rank_shots(circuit, num_shots, seed)?.counts()
2898 }
2899 ShotSource::PerShot => unreachable!("handled above"),
2900 };
2901 Ok((counts, metadata.with_shots(num_shots)))
2902}
2903
2904fn counts_of(shots: Vec<Vec<bool>>, num_classical_bits: usize) -> HashMap<Vec<u64>, u64> {
2905 ShotsResult::from_shots(shots, num_classical_bits).counts()
2906}
2907
2908#[cfg(test)]
2909fn run_marginals(circuit: &Circuit, seed: u64) -> Result<Vec<(f64, f64)>> {
2910 run_marginals_result_with(BackendKind::Auto, circuit, seed).map(MarginalsResult::into_vec)
2911}
2912
2913#[cfg(test)]
2914fn run_marginals_with(kind: BackendKind, circuit: &Circuit, seed: u64) -> Result<Vec<(f64, f64)>> {
2915 run_marginals_result_with(kind, circuit, seed).map(MarginalsResult::into_vec)
2916}
2917
2918fn marginals_from_pauli_expectations(
2921 backend: &dyn Backend,
2922 num_qubits: usize,
2923) -> Result<MarginalsResult> {
2924 let observables: Vec<Vec<PauliTerm>> = (0..num_qubits).map(|q| vec![PauliTerm::z(q)]).collect();
2925 let expectations = backend.pauli_expectations(&observables)?;
2926 Ok(MarginalsResult {
2927 marginals: expectations_to_marginals(&expectations),
2928 metadata: backend_metadata(backend),
2929 })
2930}
2931
2932fn spd_metadata(truncation: &SpdTruncation) -> RunMetadata {
2940 if truncation.can_discard() {
2941 RunMetadata::approximate(ResolvedBackend::DeterministicPauli)
2942 } else {
2943 RunMetadata::exact(ResolvedBackend::DeterministicPauli)
2944 }
2945}
2946
2947pub(crate) fn expectations_to_marginals(expectations: &[f64]) -> Vec<(f64, f64)> {
2948 expectations
2949 .iter()
2950 .map(|ez| {
2951 let p0 = ((1.0 + ez) / 2.0).clamp(0.0, 1.0);
2952 (p0, 1.0 - p0)
2953 })
2954 .collect()
2955}
2956
2957pub(super) fn has_nonunitary_or_classical_ops(circuit: &Circuit) -> bool {
2958 circuit.instructions.iter().any(|inst| {
2959 matches!(
2960 inst,
2961 Instruction::Measure { .. }
2962 | Instruction::Reset { .. }
2963 | Instruction::Conditional { .. }
2964 | Instruction::Region(_)
2965 )
2966 })
2967}
2968
2969fn supports_pauli_marginal_backend(circuit: &Circuit) -> bool {
2970 circuit.is_clifford_plus_t() && !has_nonunitary_or_classical_ops(circuit)
2971}
2972
2973fn validate_pauli_marginal_backend(kind: &BackendKind, circuit: &Circuit) -> Result<()> {
2976 if has_nonunitary_or_classical_ops(circuit) {
2977 return Err(PrismError::IncompatibleBackend {
2978 backend: format!("{kind:?}"),
2979 reason: "Pauli marginal backends require a unitary circuit without measurements, resets, or conditionals".into(),
2980 });
2981 }
2982 Ok(())
2983}
2984
2985fn run_marginals_result_with(
2986 kind: BackendKind,
2987 circuit: &Circuit,
2988 seed: u64,
2989) -> Result<MarginalsResult> {
2990 let n = circuit.num_qubits;
2991
2992 match &kind {
2993 BackendKind::StochasticPauli { num_samples } => {
2994 validate_pauli_marginal_backend(&kind, circuit)?;
2995 let spp = unified_pauli::run_spp(circuit, *num_samples, seed)?;
2996 return Ok(MarginalsResult {
2997 marginals: expectations_to_marginals(&spp.expectations),
2998 metadata: RunMetadata::approximate(ResolvedBackend::StochasticPauli)
2999 .with_shots(*num_samples),
3000 });
3001 }
3002 BackendKind::DeterministicPauli { truncation } => {
3003 validate_pauli_marginal_backend(&kind, circuit)?;
3004 let spd = unified_pauli::run_spd_with(circuit, truncation)?;
3005 return Ok(MarginalsResult {
3006 marginals: expectations_to_marginals(&spd.expectations),
3007 metadata: spd_metadata(truncation),
3008 });
3009 }
3010 _ => {}
3011 }
3012
3013 if kind.is_auto()
3014 && supports_pauli_marginal_backend(circuit)
3015 && circuit.has_t_gates()
3016 && n >= MIN_QUBITS_FOR_SPD_AUTO
3017 {
3018 let exact = SpdTruncation::Threshold {
3019 epsilon: 0.0,
3020 max_terms: 0,
3021 };
3022 let spd = if n <= max_statevector_qubits() {
3023 unified_pauli::run_spd_exact_within(circuit, auto_spd_work_budget(circuit))?
3024 } else {
3025 Some(unified_pauli::run_spd_with(circuit, &exact)?)
3026 };
3027 if let Some(spd) = spd {
3028 return Ok(MarginalsResult {
3029 marginals: expectations_to_marginals(&spd.expectations),
3030 metadata: spd_metadata(&exact),
3031 });
3032 }
3033 }
3034
3035 #[cfg(feature = "distributed")]
3038 if let BackendKind::StatevectorDistributed { context } = &kind {
3039 let mut backend =
3040 crate::backend::distributed_statevector::DistributedStatevectorBackend::new(
3041 context.clone(),
3042 seed,
3043 );
3044 execute(&mut backend, circuit, &SimOptions::classical_only())?;
3045 return marginals_from_pauli_expectations(&backend, n);
3046 }
3047
3048 if kind.is_auto() && !has_nonunitary_or_classical_ops(circuit) {
3051 let observables: Vec<Vec<PauliTerm>> = (0..n).map(|q| vec![PauliTerm::z(q)]).collect();
3052 let tensor_route = if n <= max_statevector_qubits() {
3053 tensor_route_expectations(circuit, &observables)
3054 } else {
3055 tensor_route_over_mps(circuit, &observables)
3056 };
3057 if let Some(result) = tensor_route {
3058 let result = result?;
3059 return Ok(MarginalsResult {
3060 marginals: expectations_to_marginals(&result.values),
3061 metadata: result.metadata,
3062 });
3063 }
3064 }
3065
3066 if let Some(backend) = try_native_marginal_backend(&kind, circuit, seed)? {
3067 return marginals_from_pauli_expectations(&*backend, n);
3068 }
3069
3070 let result = run_with(kind, circuit, seed)?;
3071 if let Some(probs) = &result.probabilities {
3072 Ok(MarginalsResult {
3073 marginals: probs.marginals(),
3074 metadata: result.metadata.clone(),
3075 })
3076 } else {
3077 Err(PrismError::BackendUnsupported {
3078 backend: "simulate".into(),
3079 operation: format!(
3080 "marginals for {} qubits without backend probability output",
3081 circuit.num_qubits
3082 ),
3083 })
3084 }
3085}
3086
3087pub fn run_expectation_values(
3110 circuit: &Circuit,
3111 observables: &[Vec<PauliTerm>],
3112 seed: u64,
3113) -> Result<Vec<f64>> {
3114 run_expectation_values_with(BackendKind::Auto, circuit, observables, seed)
3115}
3116
3117fn run_expectation_values_with(
3118 kind: BackendKind,
3119 circuit: &Circuit,
3120 observables: &[Vec<PauliTerm>],
3121 seed: u64,
3122) -> Result<Vec<f64>> {
3123 run_expectation_values_reported(kind, circuit, observables, seed)
3124 .map(ExpectationResult::into_values)
3125}
3126
3127fn run_expectation_values_reported(
3128 kind: BackendKind,
3129 circuit: &Circuit,
3130 observables: &[Vec<PauliTerm>],
3131 seed: u64,
3132) -> Result<ExpectationResult> {
3133 require_unitary_circuit(&kind, circuit, "expectation values require")?;
3134
3135 match &kind {
3136 BackendKind::StochasticPauli { num_samples } => {
3137 let mut values = Vec::with_capacity(observables.len());
3138 let mut std_errors = Vec::with_capacity(observables.len());
3139 for (i, obs) in observables.iter().enumerate() {
3140 let r = unified_pauli::run_spp_observable(
3141 circuit,
3142 obs,
3143 *num_samples,
3144 mix_seed(seed, i),
3145 )?;
3146 values.push(r.mean);
3147 std_errors.push(r.std_error);
3148 }
3149 Ok(ExpectationResult {
3150 values,
3151 std_errors: Some(std_errors),
3152 metadata: RunMetadata::approximate(ResolvedBackend::StochasticPauli)
3153 .with_shots(*num_samples),
3154 })
3155 }
3156 BackendKind::DeterministicPauli { truncation } => {
3157 let mut values = Vec::with_capacity(observables.len());
3158 for obs in observables {
3159 let r = unified_pauli::run_spd_observable_with(circuit, obs, truncation)?;
3160 values.push(r.mean);
3161 }
3162 Ok(analytic_expectations(values, spd_metadata(truncation)))
3163 }
3164 _ if kind.is_auto() || kind.is_stabilizer_family() => {
3165 if circuit.is_clifford_only() {
3166 let mut values = Vec::with_capacity(observables.len());
3167 for obs in observables {
3168 let r = unified_pauli::run_spd_observable(circuit, obs, 0.0, 0)?;
3169 values.push(r.mean);
3170 }
3171 Ok(analytic_expectations(
3172 values,
3173 spd_metadata(&SpdTruncation::Threshold {
3174 epsilon: 0.0,
3175 max_terms: 0,
3176 }),
3177 ))
3178 } else if kind.is_auto() {
3179 if circuit.num_qubits > max_statevector_qubits() {
3180 if let Some(result) = tensor_route_over_mps(circuit, observables) {
3181 return result;
3182 }
3183 return expectation_values_native(&kind, circuit, observables, seed);
3184 }
3185 if let Some(result) = tensor_route_expectations(circuit, observables) {
3186 return result;
3187 }
3188 expectation_values_statevector(&kind, circuit, observables, seed)
3189 } else {
3190 Err(PrismError::IncompatibleBackend {
3191 backend: format!("{kind:?}"),
3192 reason: "stabilizer backends require a Clifford-only circuit".into(),
3193 })
3194 }
3195 }
3196 BackendKind::Statevector => {
3197 expectation_values_statevector(&kind, circuit, observables, seed)
3198 }
3199 #[cfg(feature = "gpu")]
3200 BackendKind::StatevectorGpu { .. } => {
3201 expectation_values_statevector(&kind, circuit, observables, seed)
3202 }
3203 BackendKind::TensorNetwork => {
3206 validate_explicit_backend(&kind, circuit)?;
3207 for observable in observables {
3208 validate_observable(observable, circuit.num_qubits)?;
3209 }
3210 let values =
3211 crate::backend::tensornetwork::expectations_zero_state(circuit, observables)?;
3212 Ok(analytic_expectations(
3213 values,
3214 RunMetadata::exact(ResolvedBackend::TensorNetwork),
3215 ))
3216 }
3217 other => expectation_values_native(other, circuit, observables, seed),
3218 }
3219}
3220
3221pub fn run_observable_expectation(
3250 circuit: &Circuit,
3251 observable: &PauliObservable,
3252 seed: u64,
3253) -> Result<ObservableExpectation> {
3254 run_observable_expectation_reported(BackendKind::Auto, circuit, observable, seed)
3255}
3256
3257fn run_observable_expectation_reported(
3258 kind: BackendKind,
3259 circuit: &Circuit,
3260 observable: &PauliObservable,
3261 seed: u64,
3262) -> Result<ObservableExpectation> {
3263 require_unitary_circuit(&kind, circuit, "expectation values require")?;
3264
3265 let grouped_statevector = match &kind {
3266 BackendKind::Statevector => true,
3267 #[cfg(feature = "gpu")]
3268 BackendKind::StatevectorGpu { .. } => true,
3269 _ => {
3270 kind.is_auto()
3271 && !circuit.is_clifford_only()
3272 && circuit.num_qubits <= max_statevector_qubits()
3273 }
3274 };
3275 if grouped_statevector {
3276 return grouped_expectation_statevector(&kind, circuit, observable, seed);
3277 }
3278
3279 let result =
3280 run_expectation_values_reported(kind, circuit, &observable_vecs(observable), seed)?;
3281 Ok(weighted_observable_result(
3282 observable,
3283 &result.values,
3284 result.std_errors.as_deref(),
3285 result.metadata,
3286 ))
3287}
3288
3289fn observable_vecs(observable: &PauliObservable) -> Vec<Vec<PauliTerm>> {
3290 observable
3291 .terms()
3292 .iter()
3293 .map(|(_, factors)| factors.clone())
3294 .collect()
3295}
3296
3297fn weighted_observable_result(
3301 observable: &PauliObservable,
3302 values: &[f64],
3303 std_errors: Option<&[f64]>,
3304 metadata: RunMetadata,
3305) -> ObservableExpectation {
3306 let coefficients = observable.terms().iter().map(|(c, _)| *c);
3307 let mean = coefficients.clone().zip(values).map(|(c, v)| c * v).sum();
3308 let std_error = std_errors.map(|errors| {
3309 coefficients
3310 .zip(errors)
3311 .map(|(c, e)| (c * e).powi(2))
3312 .sum::<f64>()
3313 .sqrt()
3314 });
3315 ObservableExpectation {
3316 mean,
3317 variance: None,
3318 group_variances: None,
3319 std_error,
3320 metadata,
3321 }
3322}
3323
3324fn grouped_expectation_statevector(
3334 kind: &BackendKind,
3335 circuit: &Circuit,
3336 observable: &PauliObservable,
3337 seed: u64,
3338) -> Result<ObservableExpectation> {
3339 let terms = observable.terms();
3340 for (_, factors) in terms {
3344 validate_observable(factors, circuit.num_qubits)?;
3345 }
3346
3347 let accel = accel_for(kind, Family::Statevector, circuit.num_qubits);
3348 let mut backend = build_statevector(&accel, seed);
3349 let expanded = expand_for_backend(&backend, circuit);
3350 let fused = fuse_for_backend(&backend, &expanded);
3351 backend.init(fused.num_qubits, fused.num_classical_bits)?;
3352 backend.apply_instructions(&fused.instructions)?;
3353 grouped_expectation_on_state(&backend, observable)
3354}
3355
3356fn grouped_expectation_on_state(
3359 backend: &StatevectorBackend,
3360 observable: &PauliObservable,
3361) -> Result<ObservableExpectation> {
3362 let num_qubits = backend.num_qubits();
3363 let terms = observable.terms();
3364 let masks = terms
3365 .iter()
3366 .map(|(_, factors)| pauli_masks(factors, num_qubits))
3367 .collect::<Result<Vec<_>>>()?;
3368 let metadata = backend_metadata(backend);
3369
3370 let grouping = observable.grouping();
3371
3372 let mut combined = Vec::with_capacity(masks.len());
3380 let mut pair_blocks: Vec<(usize, usize, usize, Vec<f64>)> = Vec::new();
3381 let mut deferred: Vec<usize> = Vec::new();
3382 for (gi, group) in grouping.groups.iter().enumerate() {
3383 let members = &group.term_indices;
3384 if members.len() * (members.len() - 1) / 2 > MAX_PAIR_MASKS_PER_GROUP {
3385 deferred.push(gi);
3386 continue;
3387 }
3388 let first_term = combined.len();
3389 combined.extend(members.iter().map(|&i| masks[i]));
3390 let first_pair = combined.len();
3391 let mut pair_coefficients = Vec::with_capacity(members.len() * (members.len() - 1) / 2);
3392 for (pos, &i) in members.iter().enumerate() {
3393 for &j in &members[pos + 1..] {
3394 let product_x = masks[i].0 ^ masks[j].0;
3395 let product_z = masks[i].1 ^ masks[j].1;
3396 combined.push((product_x, product_z, (product_x & product_z).count_ones()));
3397 pair_coefficients.push(2.0 * terms[i].0 * terms[j].0);
3398 }
3399 }
3400 pair_blocks.push((gi, first_term, first_pair, pair_coefficients));
3401 }
3402
3403 let (values, host) = match backend.pauli_expectations_on_device(&combined) {
3406 Some(values) => (values?, None),
3407 None => {
3408 let state = backend.state_vector();
3409 let (values, norm) = pauli_expectations_from_masks(state, &combined);
3410 (values, Some((state, norm)))
3411 }
3412 };
3413
3414 let mut mean: f64 = terms
3415 .iter()
3416 .filter(|(_, factors)| factors.is_empty())
3417 .map(|(c, _)| c)
3418 .sum();
3419 let mut group_variances = vec![0.0; grouping.groups.len()];
3420
3421 for (gi, first_term, first_pair, pair_coefficients) in &pair_blocks {
3422 let group = &grouping.groups[*gi];
3423 let m1: f64 = group
3424 .term_indices
3425 .iter()
3426 .zip(&values[*first_term..])
3427 .map(|(&i, v)| terms[i].0 * v)
3428 .sum();
3429 let square_diag: f64 = group
3430 .term_indices
3431 .iter()
3432 .map(|&i| terms[i].0 * terms[i].0)
3433 .sum();
3434 let square_cross: f64 = pair_coefficients
3435 .iter()
3436 .zip(&values[*first_pair..])
3437 .map(|(c, v)| c * v)
3438 .sum();
3439 mean += m1;
3440 group_variances[*gi] = (square_diag + square_cross - m1 * m1).max(0.0);
3441 }
3442
3443 if !deferred.is_empty() {
3448 let requests: Vec<(Vec<u64>, Vec<f64>, u64, u64)> = deferred
3449 .iter()
3450 .map(|&gi| {
3451 let group = &grouping.groups[gi];
3452 let (x_bits, y_bits) = if group.is_z_only() {
3453 (0, 0)
3454 } else {
3455 group.rotation_masks()
3456 };
3457 (
3458 group
3459 .term_indices
3460 .iter()
3461 .map(|&i| (masks[i].0 | masks[i].1) as u64)
3462 .collect(),
3463 group.term_indices.iter().map(|&i| terms[i].0).collect(),
3464 x_bits as u64,
3465 y_bits as u64,
3466 )
3467 })
3468 .collect();
3469 let on_device = match host {
3470 Some(_) => None,
3471 None => backend.group_moments_on_device(&requests).transpose()?,
3472 };
3473 let moments = match on_device {
3474 Some(moments) => moments,
3475 None => {
3476 let exported;
3477 let (state, norm): (&[Complex64], f64) = match host {
3478 Some(host) => host,
3479 None => {
3480 exported = backend.export_statevector()?;
3481 (&exported, crate::backend::state_norm_sqr(&exported))
3482 }
3483 };
3484 host_group_moments(state, norm, &requests)
3485 }
3486 };
3487 for (&gi, (m1, m2)) in deferred.iter().zip(moments) {
3488 mean += m1;
3489 group_variances[gi] = (m2 - m1 * m1).max(0.0);
3490 }
3491 }
3492
3493 let variance = group_variances.iter().sum();
3494 Ok(ObservableExpectation {
3495 mean,
3496 variance: Some(variance),
3497 group_variances: Some(group_variances),
3498 std_error: None,
3499 metadata,
3500 })
3501}
3502
3503pub(crate) fn host_group_moments(
3506 state: &[Complex64],
3507 norm: f64,
3508 requests: &[(Vec<u64>, Vec<f64>, u64, u64)],
3509) -> Vec<(f64, f64)> {
3510 let mut rotated = Vec::new();
3511 requests
3512 .iter()
3513 .map(|(zmasks, coefficients, x_bits, y_bits)| {
3514 let zmasks: Vec<usize> = zmasks.iter().map(|&z| z as usize).collect();
3515 let rotation = x_bits | y_bits;
3516 if rotation == 0 {
3517 return observable::weighted_group_moments(state, &zmasks, coefficients, norm);
3518 }
3519 observable::rotate_to_z_basis(state, &mut rotated, *x_bits as usize, *y_bits as usize);
3520 let growth = 2f64.powi(rotation.count_ones() as i32);
3521 observable::weighted_group_moments(&rotated, &zmasks, coefficients, norm * growth)
3522 })
3523 .collect()
3524}
3525
3526const MAX_PAIR_MASKS_PER_GROUP: usize = 20;
3532
3533fn analytic_expectations(values: Vec<f64>, metadata: RunMetadata) -> ExpectationResult {
3536 ExpectationResult {
3537 values,
3538 std_errors: None,
3539 metadata,
3540 }
3541}
3542
3543#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3546enum Diagnostic {
3547 StateVector,
3548 ReducedDensityMatrix,
3549 Entropy,
3550 Overlap,
3551}
3552
3553impl Diagnostic {
3554 fn terminal(self) -> &'static str {
3555 match self {
3556 Diagnostic::StateVector => "a statevector",
3557 Diagnostic::ReducedDensityMatrix => "a reduced density matrix",
3558 Diagnostic::Entropy => "entanglement entropy",
3559 Diagnostic::Overlap => "a state overlap",
3560 }
3561 }
3562}
3563
3564fn plan_answers(plan: &BackendPlan, diagnostic: Diagnostic) -> bool {
3571 match diagnostic {
3572 Diagnostic::ReducedDensityMatrix => matches!(
3573 plan,
3574 BackendPlan::Statevector { .. }
3575 | BackendPlan::Sparse
3576 | BackendPlan::Factored
3577 | BackendPlan::ProductState
3578 | BackendPlan::DensityMatrix { .. }
3579 | BackendPlan::Stabilizer { .. }
3580 | BackendPlan::FactoredStabilizer
3581 ),
3582 Diagnostic::Entropy => matches!(
3583 plan,
3584 BackendPlan::Statevector { .. }
3585 | BackendPlan::Mps { .. }
3586 | BackendPlan::ProductState
3587 | BackendPlan::Stabilizer { .. }
3588 | BackendPlan::FactoredStabilizer
3589 ),
3590 Diagnostic::StateVector | Diagnostic::Overlap => {
3591 !matches!(plan, BackendPlan::DensityMatrix { .. })
3592 }
3593 }
3594}
3595
3596fn stateless_route(kind: &BackendKind, diagnostic: Diagnostic, route: &str) -> PrismError {
3599 PrismError::IncompatibleBackend {
3600 backend: format!("{kind:?}"),
3601 reason: format!(
3602 "{} needs a backend that holds a state; the {route} route returns \
3603 probabilities only",
3604 diagnostic.terminal()
3605 ),
3606 }
3607}
3608
3609fn check_diagnostic_width(backend: &dyn Backend, diagnostic: Diagnostic, k: usize) -> Result<()> {
3613 match diagnostic {
3614 Diagnostic::ReducedDensityMatrix => {
3615 crate::backend::reduced_density::reduced_density_side(backend.name(), k)?;
3616 Ok(())
3617 }
3618 Diagnostic::StateVector => {
3619 if k > crate::backend::schmidt::export_cap() {
3620 return Err(crate::backend::schmidt::export_cap_exceeded(
3621 backend.name(),
3622 "a dense statevector".to_string(),
3623 k,
3624 ));
3625 }
3626 Ok(())
3627 }
3628 Diagnostic::Entropy | Diagnostic::Overlap => Ok(()),
3629 }
3630}
3631
3632fn diagnostic_backend(
3642 kind: &BackendKind,
3643 circuit: &Circuit,
3644 initial_state: Option<StartState<'_>>,
3645 seed: u64,
3646 diagnostic: Diagnostic,
3647 subsystem_len: usize,
3648) -> Result<Box<dyn Backend>> {
3649 if let Some(state) = initial_state {
3650 let mut backend = backend_from_initial_state(kind, circuit, state, seed)?;
3651 check_diagnostic_width(&*backend, diagnostic, subsystem_len)?;
3652 apply_fused_without_saves(&mut *backend, circuit, "state diagnostics")?;
3653 return Ok(backend);
3654 }
3655 if !kind.is_auto() {
3656 validate_explicit_backend(kind, circuit)?;
3657 }
3658 let (_, has_partial_independence) = analyze_independence(circuit);
3659 let mut plan = match resolve(kind, circuit, has_partial_independence) {
3660 ExecutionPlan::Backend(plan) => plan,
3661 ExecutionPlan::StabilizerRank => {
3662 return Err(stateless_route(kind, diagnostic, "stabilizer-rank"));
3663 }
3664 ExecutionPlan::StochasticPauli { .. } => {
3665 return Err(stateless_route(kind, diagnostic, "stochastic Pauli"));
3666 }
3667 ExecutionPlan::DeterministicPauli { .. } => {
3668 return Err(stateless_route(kind, diagnostic, "deterministic Pauli"));
3669 }
3670 ExecutionPlan::PauliPath => {
3671 return Err(stateless_route(kind, diagnostic, "Pauli path"));
3672 }
3673 };
3674 if kind.is_auto()
3675 && !plan_answers(&plan, diagnostic)
3676 && circuit.num_qubits <= max_statevector_qubits()
3677 {
3678 plan = plan_for_family(kind, Family::Statevector, circuit.num_qubits);
3679 }
3680 let mut backend: Box<dyn Backend> = plan.build(seed);
3681 check_diagnostic_width(&*backend, diagnostic, subsystem_len)?;
3682 execute(&mut *backend, circuit, &SimOptions::classical_only())?;
3683 Ok(backend)
3684}
3685
3686const TENSOR_ROUTE_MIN_QUBITS: usize = 18;
3690
3691fn tensor_route_expectations(
3697 circuit: &Circuit,
3698 observables: &[Vec<PauliTerm>],
3699) -> Option<Result<ExpectationResult>> {
3700 if circuit.num_qubits < TENSOR_ROUTE_MIN_QUBITS || observables.is_empty() {
3701 return None;
3702 }
3703 for observable in observables {
3704 if let Err(e) = validate_observable(observable, circuit.num_qubits) {
3705 return Some(Err(e));
3706 }
3707 }
3708 let planned = match crate::backend::tensornetwork::plan_bounded_expectations(
3709 circuit,
3710 observables,
3711 crate::backend::tensornetwork::AUTO_EXPECTATION_PEAK_BOUND,
3712 )? {
3713 Ok(planned) => planned,
3714 Err(e) => return Some(Err(e)),
3715 };
3716 Some(planned.contract().map(|values| {
3717 analytic_expectations(values, RunMetadata::exact(ResolvedBackend::TensorNetwork))
3718 }))
3719}
3720
3721const TENSOR_ROUTE_MPS_COST_PER_OBSERVABLE: f64 = 1.5e5;
3727
3728const TENSOR_ELEMENT_MPS_COST: f64 = 3.0;
3731
3732fn tensor_route_over_mps(
3739 circuit: &Circuit,
3740 observables: &[Vec<PauliTerm>],
3741) -> Option<Result<ExpectationResult>> {
3742 if observables.is_empty() {
3743 return None;
3744 }
3745 let mps_cost = mps_apply_cost(circuit, AUTO_MPS_BOND_DIM);
3746 if mps_cost < TENSOR_ROUTE_MPS_COST_PER_OBSERVABLE * observables.len() as f64 {
3747 return None;
3748 }
3749 let (_, has_partial_independence) = analyze_independence(circuit);
3750 let ExecutionPlan::Backend(plan) =
3751 resolve(&BackendKind::Auto, circuit, has_partial_independence)
3752 else {
3753 return None;
3754 };
3755 if !matches!(plan, BackendPlan::Mps { .. }) {
3756 return None;
3757 }
3758 for observable in observables {
3759 if let Err(e) = validate_observable(observable, circuit.num_qubits) {
3760 return Some(Err(e));
3761 }
3762 }
3763 let planned = match crate::backend::tensornetwork::plan_bounded_expectations(
3764 circuit,
3765 observables,
3766 crate::backend::tensornetwork::AUTO_EXPECTATION_PEAK_BOUND,
3767 )? {
3768 Ok(planned) => planned,
3769 Err(e) => return Some(Err(e)),
3770 };
3771 if planned.total_elements() as f64 * TENSOR_ELEMENT_MPS_COST >= mps_cost {
3772 return None;
3773 }
3774 Some(planned.contract().map(|values| {
3775 analytic_expectations(values, RunMetadata::exact(ResolvedBackend::TensorNetwork))
3776 }))
3777}
3778
3779fn expectation_values_native(
3786 kind: &BackendKind,
3787 circuit: &Circuit,
3788 observables: &[Vec<PauliTerm>],
3789 seed: u64,
3790) -> Result<ExpectationResult> {
3791 if !kind.is_auto() {
3792 validate_explicit_backend(kind, circuit)?;
3793 }
3794 for observable in observables {
3797 validate_observable(observable, circuit.num_qubits)?;
3798 }
3799
3800 let (_, has_partial_independence) = analyze_independence(circuit);
3801 let ExecutionPlan::Backend(plan) = resolve(kind, circuit, has_partial_independence) else {
3802 return Err(PrismError::IncompatibleBackend {
3803 backend: format!("{kind:?}"),
3804 reason: "expectation values need a backend that holds a state; the stabilizer-rank \
3805 route returns probabilities only"
3806 .into(),
3807 });
3808 };
3809
3810 let mut backend = plan.build(seed);
3811 if !backend.supports_pauli_expectation() {
3812 return Err(PrismError::BackendUnsupported {
3813 backend: backend.name().to_string(),
3814 operation: "Pauli expectation values".to_string(),
3815 });
3816 }
3817 execute(&mut *backend, circuit, &SimOptions::classical_only())?;
3818 let values = backend.pauli_expectations(observables)?;
3819 Ok(analytic_expectations(values, backend_metadata(&*backend)))
3820}
3821
3822fn expectation_values_statevector(
3823 kind: &BackendKind,
3824 circuit: &Circuit,
3825 observables: &[Vec<PauliTerm>],
3826 seed: u64,
3827) -> Result<ExpectationResult> {
3828 for obs in observables {
3832 validate_observable(obs, circuit.num_qubits)?;
3833 }
3834
3835 let accel = accel_for(kind, Family::Statevector, circuit.num_qubits);
3836 let mut backend = build_statevector(&accel, seed);
3837 let expanded = expand_for_backend(&backend, circuit);
3838 let fused = fuse_for_backend(&backend, &expanded);
3839 backend.init(fused.num_qubits, fused.num_classical_bits)?;
3840 let masks = observables
3841 .iter()
3842 .map(|obs| pauli_masks(obs, circuit.num_qubits))
3843 .collect::<Result<Vec<_>>>()?;
3844 backend.apply_instructions(&fused.instructions)?;
3845
3846 let values = match backend.pauli_expectations_on_device(&masks) {
3847 Some(values) => values?,
3848 None => pauli_expectations_from_masks(backend.state_vector(), &masks).0,
3849 };
3850 let metadata = backend_metadata(&backend);
3851 Ok(analytic_expectations(values, metadata))
3852}
3853
3854#[cfg(feature = "distributed")]
3861fn run_shots_distributed(
3862 context: std::sync::Arc<crate::distributed::DistributedContext>,
3863 circuit: &Circuit,
3864 num_shots: usize,
3865 seed: u64,
3866) -> Result<ShotsResult> {
3867 use crate::backend::distributed_statevector::DistributedStatevectorBackend;
3868
3869 let meas_map = circuit.measurement_map();
3870 if meas_map.is_empty() {
3871 let mut backend = DistributedStatevectorBackend::new(context, seed);
3875 backend.init(circuit.num_qubits, circuit.num_classical_bits)?;
3876 return Ok(ShotsResult::from_shots(
3877 vec![vec![false; circuit.num_classical_bits]; num_shots],
3878 circuit.num_classical_bits,
3879 )
3880 .with_metadata(backend_metadata(&backend)));
3881 }
3882
3883 if circuit.has_terminal_measurements_only() {
3884 let stripped = circuit.without_measurements();
3885 let mut backend = DistributedStatevectorBackend::new(context, seed);
3886 execute(&mut backend, &stripped, &SimOptions::classical_only())?;
3887 let samples = backend.sample_basis_states(num_shots, seed)?;
3888 return Ok(ShotsResult::from_shots(
3889 shots_from_basis_samples(&samples, &meas_map, circuit.num_classical_bits),
3890 circuit.num_classical_bits,
3891 )
3892 .with_metadata(backend_metadata(&backend)));
3893 }
3894
3895 let probe = DistributedStatevectorBackend::new(context.clone(), seed);
3896 let expanded = expand_for_backend(&probe, circuit);
3897 let fused = fuse_for_backend(&probe, &expanded);
3898 let opts = SimOptions::classical_only();
3899 let mut shots = Vec::with_capacity(num_shots);
3900 let mut metadata = RunMetadata::exact(ResolvedBackend::Distributed);
3901 for i in 0..num_shots {
3902 let shot_seed = mix_seed(seed, i);
3903 let mut backend = DistributedStatevectorBackend::new(context.clone(), shot_seed);
3904 let result = execute_circuit(&mut backend, &fused, &opts)?;
3905 metadata.weaken_with(&result.metadata);
3906 shots.push(result.classical_bits);
3907 }
3908 Ok(ShotsResult::from_shots(shots, circuit.num_classical_bits).with_metadata(metadata))
3909}
3910
3911pub(crate) fn run_shots_with(
3913 kind: BackendKind,
3914 circuit: &Circuit,
3915 num_shots: usize,
3916 seed: u64,
3917) -> Result<ShotsResult> {
3918 #[cfg(feature = "distributed")]
3922 if let BackendKind::StatevectorDistributed { context } = &kind {
3923 return run_shots_distributed(context.clone(), circuit, num_shots, seed);
3924 }
3925
3926 let folded = circuit.fold_static_guards();
3930 let circuit = folded.as_ref();
3931
3932 let bits = circuit.num_classical_bits;
3933 let source = prepare_shot_source(&kind, circuit, num_shots, seed)?;
3934 let Some(metadata) = source.metadata() else {
3935 return run_shots_per_shot(kind, circuit, num_shots, seed, ShotRows::Exchangeable);
3936 };
3937 let result = match source {
3938 ShotSource::Compiled {
3939 mut sampler,
3940 meas_map,
3941 ..
3942 } => {
3943 let packed = sampler.try_sample_bulk_packed(num_shots)?;
3944 ShotsResult::from_shots(
3945 packed_shots_to_classical_bits(&packed, &meas_map, bits),
3946 bits,
3947 )
3948 }
3949 ShotSource::TerminalStatevector { backend, meas_map } => {
3950 let shots = if backend.is_gpu_resident() {
3951 let probs = backend.probabilities()?;
3952 sample_shots_from_probs(&probs, &meas_map, bits, num_shots, seed)
3953 } else {
3954 sample_shots_from_state(
3955 backend.state_vector(),
3956 backend.probability_scale(),
3957 &meas_map,
3958 bits,
3959 num_shots,
3960 seed,
3961 )
3962 };
3963 ShotsResult::from_shots(shots, bits)
3964 }
3965 ShotSource::Native {
3966 mut backend,
3967 meas_map,
3968 } => {
3969 let samples = backend.sample_basis_states(num_shots, seed)?;
3970 ShotsResult::from_shots(shots_from_basis_samples(&samples, &meas_map, bits), bits)
3971 }
3972 ShotSource::TerminalProbabilities {
3973 probs, meas_map, ..
3974 } => ShotsResult::from_shots(sample_shots(&probs, &meas_map, bits, num_shots, seed), bits),
3975 ShotSource::StabilizerRank => {
3976 stabilizer_rank::run_stabilizer_rank_shots(circuit, num_shots, seed)?
3977 }
3978 ShotSource::PerShot => unreachable!("handled above"),
3979 };
3980 Ok(result.with_metadata(metadata))
3981}
3982
3983#[derive(Clone, Copy, PartialEq, Eq)]
3986enum ShotRows {
3987 Exchangeable,
3988 CountedOnly,
3989}
3990
3991fn run_shots_per_shot(
3995 kind: BackendKind,
3996 circuit: &Circuit,
3997 num_shots: usize,
3998 seed: u64,
3999 rows: ShotRows,
4000) -> Result<ShotsResult> {
4001 if !kind.is_auto() {
4003 validate_explicit_backend(&kind, circuit)?;
4004 }
4005
4006 let (decompose, has_partial_independence) = analyze_independence(circuit);
4007
4008 if matches!(kind, BackendKind::StabilizerRank) {
4009 return stabilizer_rank::run_stabilizer_rank_shots(circuit, num_shots, seed);
4010 }
4011 if matches!(
4012 kind,
4013 BackendKind::StochasticPauli { .. } | BackendKind::DeterministicPauli { .. }
4014 ) {
4015 return Err(crate::error::PrismError::IncompatibleBackend {
4016 backend: format!("{kind:?}"),
4017 reason: "Pauli propagation backends do not support mid-circuit measurements".into(),
4018 });
4019 }
4020 if kind.is_auto() && auto_stabilizer_rank_t_count(circuit, MAX_AUTO_T_COUNT_SHOTS).is_some() {
4021 return stabilizer_rank::run_stabilizer_rank_shots(circuit, num_shots, seed);
4022 }
4023
4024 if has_temporal_clifford_opportunity(&kind, circuit) {
4025 if decompose.is_none()
4026 && let Some(tc) = plan_temporal_clifford(&kind, circuit)
4027 {
4028 let route = ResolvedBackend::Statevector;
4029 let states = [(route, circuit.num_qubits)];
4030 return collect_shots(
4031 circuit,
4032 num_shots,
4033 seed,
4034 route,
4035 &kind,
4036 &states,
4037 |shot_seed| {
4038 let outcome = run_temporal_clifford(&tc, shot_seed, false)?;
4039 Ok((outcome.classical_bits, outcome.metadata))
4040 },
4041 );
4042 }
4043 let opts = SimOptions::classical_only();
4047 let route = resolve_backend(&kind, circuit, has_partial_independence).resolved();
4048 let plan = plan_probability_route(&kind, circuit);
4049 let blocks = match &plan {
4050 ProbabilityRoute::Decomposed(components) => {
4051 Some(PreparedBlocks::new(&kind, components.clone(), circuit))
4052 }
4053 _ => None,
4054 };
4055 let states = [(route, circuit.num_qubits)];
4056 return collect_shots(
4057 circuit,
4058 num_shots,
4059 seed,
4060 route,
4061 &kind,
4062 &states,
4063 |shot_seed| {
4064 let outcome = match &blocks {
4065 Some(blocks) => blocks.run(&kind, circuit, shot_seed, &opts)?,
4066 None => run_route(&kind, circuit, shot_seed, opts, &plan)?,
4067 };
4068 Ok((outcome.classical_bits, outcome.metadata))
4069 },
4070 );
4071 }
4072
4073 let opts = SimOptions::classical_only();
4074
4075 if let Some((ref comps, _)) = decompose {
4076 let partitions = circuit.partition_subcircuits(comps);
4077 let block_plans: Vec<BackendPlan> = partitions
4078 .iter()
4079 .map(|(sub, _, _)| {
4080 if !kind.is_auto() {
4081 validate_explicit_backend(&kind, sub)?;
4082 }
4083 Ok(resolve_backend(&kind, sub, false))
4084 })
4085 .collect::<Result<_>>()?;
4086 let fused_blocks: Vec<std::borrow::Cow<'_, Circuit>> = partitions
4087 .iter()
4088 .zip(&block_plans)
4089 .map(|((sub, _, _), plan)| {
4090 let probe = plan.build(seed);
4091 let expanded = expand_for_backend(&*probe, sub);
4092 std::borrow::Cow::Owned(fuse_for_backend(&*probe, &expanded).into_owned())
4093 })
4094 .collect();
4095
4096 let block_states: Vec<(ResolvedBackend, usize)> = block_plans
4097 .iter()
4098 .zip(comps)
4099 .map(|(plan, comp)| (plan.resolved(), comp.len()))
4100 .collect();
4101 collect_shots(
4102 circuit,
4103 num_shots,
4104 seed,
4105 ResolvedBackend::Decomposed,
4106 &kind,
4107 &block_states,
4108 |shot_seed| {
4109 let result = run_decomposed_prefused(
4110 &block_plans,
4111 comps,
4112 &partitions,
4113 &fused_blocks,
4114 shot_seed,
4115 &opts,
4116 circuit,
4117 )?;
4118 Ok((result.classical_bits, result.metadata))
4119 },
4120 )
4121 } else {
4122 let plan = resolve_backend(&kind, circuit, has_partial_independence);
4123 let probe = plan.build(seed);
4124 let expanded = expand_for_backend(&*probe, circuit);
4125 let fused = fuse_for_backend(&*probe, &expanded);
4126
4127 let route = plan.resolved();
4128 let states = [(route, circuit.num_qubits)];
4129 if plan.is_host_statevector() && circuit.num_qubits < max_statevector_qubits() {
4130 #[cfg(feature = "parallel")]
4131 let parallel = shots_split_across_workers(&kind, &states)
4132 && circuit.num_qubits >= branching::MIN_QUBITS_FOR_PARALLEL_SPLITS;
4133 #[cfg(not(feature = "parallel"))]
4134 let parallel = false;
4135 let limits = branching::BranchLimits::for_width(circuit.num_qubits, parallel);
4136 return branching::branch_shots(
4137 &fused,
4138 num_shots,
4139 seed,
4140 &limits,
4141 rows == ShotRows::Exchangeable,
4142 );
4143 }
4144 collect_shots(
4145 circuit,
4146 num_shots,
4147 seed,
4148 route,
4149 &kind,
4150 &states,
4151 |shot_seed| {
4152 let mut backend = plan.build(shot_seed);
4153 let outcome = execute_circuit(&mut *backend, &fused, &opts)?;
4154 Ok((outcome.classical_bits, outcome.metadata))
4155 },
4156 )
4157 }
4158}
4159
4160pub(crate) const fn splitmix64(mut z: u64) -> u64 {
4161 z = z.wrapping_add(0x9E37_79B9_7F4A_7C15);
4162 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
4163 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
4164 z ^ (z >> 31)
4165}
4166
4167pub(crate) const fn mix_seed(seed: u64, index: usize) -> u64 {
4171 splitmix64(splitmix64(seed) ^ index as u64)
4172}
4173
4174fn collect_shots(
4184 circuit: &Circuit,
4185 num_shots: usize,
4186 seed: u64,
4187 route: ResolvedBackend,
4188 kind: &BackendKind,
4189 states: &[(ResolvedBackend, usize)],
4190 shot: impl Fn(u64) -> Result<(Vec<bool>, RunMetadata)> + Sync,
4191) -> Result<ShotsResult> {
4192 #[cfg(feature = "parallel")]
4193 if num_shots > 1 && shots_split_across_workers(kind, states) {
4194 use rayon::prelude::*;
4195 let runs: Vec<Result<(Vec<bool>, RunMetadata)>> = (0..num_shots)
4196 .into_par_iter()
4197 .map(|i| shot(mix_seed(seed, i)))
4198 .collect();
4199 return fold_shots(circuit, route, runs);
4200 }
4201 #[cfg(not(feature = "parallel"))]
4202 let _ = (kind, states);
4203 fold_shots(
4204 circuit,
4205 route,
4206 (0..num_shots).map(|i| shot(mix_seed(seed, i))),
4207 )
4208}
4209
4210fn fold_shots(
4211 circuit: &Circuit,
4212 route: ResolvedBackend,
4213 runs: impl IntoIterator<Item = Result<(Vec<bool>, RunMetadata)>>,
4214) -> Result<ShotsResult> {
4215 let runs = runs.into_iter();
4216 let mut shots = Vec::with_capacity(runs.size_hint().0);
4217 let mut metadata = RunMetadata::exact(route);
4218 for (i, run) in runs.enumerate() {
4219 let (bits, shot_metadata) = run?;
4220 if i == 0 {
4221 metadata = shot_metadata;
4222 } else {
4223 metadata.weaken_with(&shot_metadata);
4224 }
4225 shots.push(bits);
4226 }
4227 Ok(ShotsResult::from_shots(shots, circuit.num_classical_bits).with_metadata(metadata))
4228}
4229
4230fn general_noise_plan(
4236 kind: &BackendKind,
4237 circuit: &Circuit,
4238 noise_model: &NoiseModel,
4239) -> BackendPlan {
4240 let family = if !circuit.has_entangling_gates() && !noise_model.has_two_qubit_kraus() {
4241 Family::ProductState
4242 } else if circuit.num_qubits > max_statevector_qubits() {
4243 if circuit.is_sparse_friendly() && circuit.num_qubits <= MAX_SPARSE_INDEX_QUBITS {
4244 Family::Sparse
4245 } else {
4246 Family::Mps
4247 }
4248 } else {
4249 Family::Statevector
4250 };
4251 plan_for_family(kind, family, circuit.num_qubits)
4252}
4253
4254pub(crate) fn run_shots_with_noise(
4267 kind: BackendKind,
4268 circuit: &Circuit,
4269 noise_model: &noise::NoiseModel,
4270 num_shots: usize,
4271 seed: u64,
4272) -> Result<ShotsResult> {
4273 noise_model.validate_for(circuit)?;
4274
4275 #[cfg(feature = "distributed")]
4280 if matches!(kind, BackendKind::StatevectorDistributed { .. }) {
4281 return Err(crate::error::PrismError::IncompatibleBackend {
4282 backend: format!("{kind:?}"),
4283 reason: "noisy shot sampling is not supported on the distributed backend; \
4284 trajectory execution cannot keep rank collectives in lockstep"
4285 .into(),
4286 });
4287 }
4288
4289 if kind.is_density_matrix() {
4290 let probs = exact_noisy_probabilities(&kind, circuit, noise_model, None, seed)?;
4291 return Ok(ShotsResult::from_shots(
4292 sample_exact_noisy_shots(&probs, circuit, noise_model, num_shots, seed),
4293 circuit.num_classical_bits,
4294 )
4295 .with_metadata(exact_mixture_metadata(&kind)));
4296 }
4297
4298 if !kind.supports_noisy_per_shot() {
4299 return Err(crate::error::PrismError::IncompatibleBackend {
4300 backend: format!("{kind:?}"),
4301 reason: "this backend holds no per-shot pure state to inject noise into; select \
4302 DensityMatrix for the exact mixed state, or a backend that evolves one \
4303 state per trajectory"
4304 .into(),
4305 });
4306 }
4307
4308 let is_stabilizer_kind = kind.is_stabilizer_family();
4309
4310 if is_stabilizer_kind && !noise_model.has_only_pauli_channels() {
4311 return Err(crate::error::PrismError::IncompatibleBackend {
4312 backend: format!("{kind:?}"),
4313 reason: format!(
4314 "stabilizer backends only support Pauli/depolarizing noise; use {} for amplitude damping, phase damping, thermal relaxation, or custom Kraus",
4315 BackendKind::general_noise_backend_names()
4316 ),
4317 });
4318 }
4319
4320 if !noise_model.has_only_pauli_channels() && !kind.supports_general_noise() {
4321 return Err(crate::error::PrismError::IncompatibleBackend {
4322 backend: format!("{kind:?}"),
4323 reason: format!(
4324 "non-Pauli noise requires {}",
4325 BackendKind::general_noise_backend_names()
4326 ),
4327 });
4328 }
4329
4330 if is_stabilizer_kind && !circuit.is_clifford_only() {
4331 return Err(crate::error::PrismError::IncompatibleBackend {
4332 backend: format!("{kind:?}"),
4333 reason: "circuit contains non-Clifford gates".into(),
4334 });
4335 }
4336
4337 if !kind.is_auto() {
4338 validate_explicit_backend(&kind, circuit)?;
4339 }
4340
4341 if noise_model.has_only_pauli_channels() {
4342 let use_compiled = (kind.is_auto()
4343 || matches!(
4344 kind,
4345 BackendKind::Stabilizer | BackendKind::FactoredStabilizer
4346 ))
4347 && supports_compiled_measurement_sampling(circuit)
4348 || {
4349 #[cfg(feature = "gpu")]
4350 {
4351 matches!(kind, BackendKind::StabilizerGpu { .. })
4352 && supports_compiled_measurement_sampling(circuit)
4353 }
4354 #[cfg(not(feature = "gpu"))]
4355 {
4356 false
4357 }
4358 };
4359
4360 if use_compiled {
4361 #[cfg(feature = "gpu")]
4362 if let BackendKind::StabilizerGpu { context } = &kind {
4363 return noise::run_shots_noisy_with_gpu(
4364 circuit,
4365 noise_model,
4366 num_shots,
4367 seed,
4368 context.clone(),
4369 );
4370 }
4371 return noise::run_shots_noisy(circuit, noise_model, num_shots, seed);
4372 }
4373 }
4374
4375 let plan = if kind.is_auto() && !noise_model.has_only_pauli_channels() {
4376 general_noise_plan(&kind, circuit, noise_model)
4377 } else {
4378 resolve_backend(&kind, circuit, false)
4379 };
4380 let has_pauli_rot = crate::circuit::any_gate(&circuit.instructions, &mut |gate| {
4385 matches!(gate, crate::gates::Gate::PauliRot(_))
4386 });
4387 if has_pauli_rot
4388 && !matches!(plan, BackendPlan::Statevector { .. })
4389 && !plan.build(seed).supports_pauli_rotation()
4390 {
4391 return Err(crate::error::PrismError::IncompatibleBackend {
4392 backend: format!("{:?}", plan.resolved()),
4393 reason: "noisy trajectories apply the instruction stream raw so noise events \
4394 stay aligned to it, which leaves no room for the Pauli-rotation \
4395 lowering this backend needs; run on the statevector, or expand the \
4396 rotations with circuit::expand_pauli_rotations and attach the noise \
4397 model to the expanded circuit"
4398 .into(),
4399 });
4400 }
4401 if noise_model.has_two_qubit_kraus() && !plan.build(seed).supports_two_qubit_kraus() {
4406 return Err(crate::error::PrismError::IncompatibleBackend {
4407 backend: format!("{:?}", plan.resolved()),
4408 reason: "a two-qubit Kraus channel needs the pair's reduced density matrix \
4409 its branch probabilities are drawn from, and a kernel for the branch \
4410 operator, which is not unitary. A tableau and a product state have no \
4411 such kernel, and a tensor network traces out no arbitrary pair; run on \
4412 a statevector, sparse, factored or MPS route, or evaluate the channel \
4413 exactly on BackendKind::DensityMatrix"
4414 .into(),
4415 });
4416 }
4417 let host_statevector = matches!(plan, BackendPlan::Statevector { .. }) && !plan.is_gpu();
4418 let dense_tensor_network = matches!(plan, BackendPlan::TensorNetwork { .. })
4419 && crate::backend::tensor_probability_len("tensor network", circuit.num_qubits).is_ok();
4420 if (host_statevector || dense_tensor_network)
4421 && let Some(groups) = trajectory::PauliGroups::sample(circuit, noise_model, num_shots, seed)
4422 {
4423 let build = |s| plan.build(s);
4424 return trajectory::run_pauli_groups(
4425 &groups,
4426 circuit,
4427 noise_model,
4428 seed,
4429 dense_tensor_network.then_some(&build as trajectory::GroupBackendFactory<'_>),
4430 plan.resolved(),
4431 );
4432 }
4433 if host_statevector
4435 && circuit.num_qubits + 1 < max_statevector_qubits()
4436 && let Some(groups) =
4437 trajectory::PauliGroups::sample_replayed(circuit, noise_model, num_shots, seed)
4438 {
4439 return trajectory::run_replayed_groups(
4440 &groups,
4441 circuit,
4442 noise_model,
4443 seed,
4444 plan.resolved(),
4445 );
4446 }
4447 let route = plan.resolved();
4448 trajectory::run_trajectories(
4449 |s| plan.build(s),
4450 circuit,
4451 noise_model,
4452 num_shots,
4453 seed,
4454 plan.is_gpu(),
4455 route,
4456 )
4457}
4458
4459#[cfg(test)]
4460mod tests;
4461
4462#[cfg(all(test, feature = "gpu"))]
4463mod gpu_stub_tests;
4464
4465#[cfg(test)]
4466mod terminal_candidate_matrix_tests;
4467
4468#[cfg(test)]
4469mod diagnostic_terminal_tests;