paulistrings/engine/
mod.rs

1//! The propagation front door [`propagate`], one partition of the partitioned layer loop; one layer is applied in `bucketed`.
2
3pub(crate) mod backend;
4pub(crate) mod bucketed;
5mod coset;
6#[cfg(feature = "cuda")]
7mod cuda_context;
8pub(crate) mod driver;
9#[cfg(feature = "cuda")]
10pub mod gpu;
11pub(crate) mod knob;
12mod merge;
13pub(crate) mod partitioned;
14pub(crate) mod plan;
15#[cfg(feature = "phase-timing")]
16pub(crate) mod stats;
17pub(crate) mod timing;
18pub(crate) mod trace;
19pub(crate) mod transport;
20
21use crate::circuit::Circuit;
22use crate::pauli_sum::hash::PartitionRows;
23use crate::pauli_sum::storage::{DEFAULT_MIN_BUCKETS, DEFAULT_TARGET_BUCKET_LEN};
24use crate::pauli_sum::PauliSum;
25use crate::truncation::TruncationPolicy;
26use backend::HostPartition;
27use bucketed::LayerScratch;
28use driver::{run_layers, PartitionContext, PartitionWork};
29use trace::assemble;
30use transport::SoloTransport;
31
32/// `log` target for the engine's progress events, partitioned or not, and the truncation policies' own.
33pub(crate) const LOG_TARGET: &str = "paulistrings::propagate";
34
35/// The tail every driver's `INFO` progress line ends with: `1 partition`, `4 partitions [placement]`, or one rank's share, `partition 0/4 [placement]`.
36pub(crate) fn progress_scope(
37    rank: Option<usize>,
38    partitions: usize,
39    placement: Option<&str>,
40) -> String {
41    let mut scope = match (rank, partitions) {
42        (Some(rank), _) => format!("partition {rank}/{partitions}"),
43        (None, 1) => "1 partition".to_string(),
44        (None, _) => format!("{partitions} partitions"),
45    };
46    if let Some(placement) = placement {
47        scope.push_str(&format!(" [{placement}]"));
48    }
49    scope
50}
51
52/// Propagation direction: channels in order, or in reverse with adjoints for backpropagating observables.
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub enum Direction {
55    /// Apply channels in the order they were pushed onto the [`Circuit`].
56    Forward,
57    /// Apply channels in reverse order, using each channel's [`Channel::apply_adjoint`](crate::Channel::apply_adjoint).
58    Heisenberg,
59}
60
61/// Tuning knobs for [`propagate_with`]; [`Default`] is what [`propagate`] uses.
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
63pub struct PropagateOptions {
64    /// Terms per bucket the per-layer partition targets.
65    ///
66    /// Above the `min_buckets` floor raising this alone does nothing; both fields must move together to get fewer buckets, and rebucketing is grow-only, so lowering either mid-run never coarsens a partition.
67    /// At least 1 ([`validate`](Self::validate)).
68    pub target_bucket_len: usize,
69    /// Floor on the per-layer bucket count once the sum is worth splitting.
70    ///
71    /// At least 16, so a sum of at most 1024 terms keeps one bucket ([`validate`](Self::validate)).
72    pub min_buckets: usize,
73}
74
75/// The smallest accepted [`PropagateOptions::min_buckets`].
76const SMALLEST_MIN_BUCKETS: usize = 16;
77
78impl PropagateOptions {
79    /// `Err` with the message every entry point panics with unless `target_bucket_len >= 1` and `min_buckets >= 16`.
80    pub fn validate(&self) -> Result<(), String> {
81        if self.target_bucket_len == 0 {
82            return Err("target_bucket_len must be at least 1, got 0".to_string());
83        }
84        if self.min_buckets < SMALLEST_MIN_BUCKETS {
85            return Err(format!(
86                "min_buckets must be at least {SMALLEST_MIN_BUCKETS}, got {}",
87                self.min_buckets
88            ));
89        }
90        Ok(())
91    }
92
93    /// Panics with [`validate`](Self::validate)'s message.
94    pub(crate) fn assert_valid(&self) {
95        if let Err(message) = self.validate() {
96            panic!("{message}");
97        }
98    }
99}
100
101impl Default for PropagateOptions {
102    fn default() -> Self {
103        Self { target_bucket_len: DEFAULT_TARGET_BUCKET_LEN, min_buckets: DEFAULT_MIN_BUCKETS }
104    }
105}
106
107/// Propagate `sum` through `circuit` under `policy`.
108///
109/// Under [`Direction::Heisenberg`] the channels run in reverse through [`Channel::apply_adjoint`](crate::Channel::apply_adjoint).
110/// The sum stays bucketed throughout, so stepping one sum through repeated calls costs only the layers.
111///
112/// # Panics
113///
114/// If a channel's preparation declines: its support exceeds `MAX_LOCAL_SUPPORT` (two qubits), which only [`PauliRotation`](crate::PauliRotation) may, or it writes outside its declared support.
115///
116/// # Progress logging
117///
118/// Through the [`log`] facade under the target `paulistrings::propagate`: `INFO` on entry and exit, `DEBUG` once per layer, named by [`Channel::debug_name`](crate::Channel::debug_name).
119/// Every driver writes the same lines, ending in the partitions they speak for (`1 partition` here).
120pub fn propagate<const W: usize, T>(
121    circuit: &Circuit<W>,
122    sum: PauliSum<W>,
123    policy: &T,
124    direction: Direction,
125) -> PauliSum<W>
126where
127    T: TruncationPolicy<W> + ?Sized,
128{
129    let mut scratch = LayerScratch::<W>::new();
130    propagate_with(circuit, sum, policy, direction, &mut scratch, PropagateOptions::default())
131}
132
133/// [`propagate`] with a caller-held [`LayerScratch`] and [`PropagateOptions`].
134///
135/// The scratch keeps its buffer capacity across calls and carries the opt-in one-partition [`PartitionTrace`](crate::PartitionTrace) (and, under `phase-timing`, the phase counters).
136/// Log events are emitted on the calling thread, never inside a parallel layer.
137///
138/// # Panics
139///
140/// As [`propagate`], and if `options` fail [`PropagateOptions::validate`].
141pub fn propagate_with<const W: usize, T>(
142    circuit: &Circuit<W>,
143    sum: PauliSum<W>,
144    policy: &T,
145    direction: Direction,
146    scratch: &mut LayerScratch<W>,
147    options: PropagateOptions,
148) -> PauliSum<W>
149where
150    T: TruncationPolicy<W> + ?Sized,
151{
152    options.assert_valid();
153    let num_channels = circuit.channels.len();
154    let terms_in = sum.len();
155    let started = std::time::Instant::now();
156    log::info!(
157        target: LOG_TARGET,
158        "propagate: {terms_in} terms through {num_channels} channels ({direction:?}), {}",
159        progress_scope(None, 1, None),
160    );
161
162    // One partition of `run_layers` on the caller's pool, over the no-op `SoloTransport`.
163    let tracing = scratch.trace.is_some();
164    let rows = PartitionRows::none(sum.num_qubits());
165    let mut work = PartitionWork {
166        local: HostPartition::with_layer_scratch(sum, std::mem::take(scratch)),
167        rows: Vec::with_capacity(if tracing { num_channels } else { 0 }),
168    };
169    let context = PartitionContext { rows: &rows, rank: 0, size: 1, tracing };
170    run_layers(circuit, policy, direction, options, context, &mut work, &SoloTransport);
171    let (sum, layer_scratch) = work.local.into_parts();
172    *scratch = layer_scratch;
173    if let Some(trace) = scratch.trace.as_mut() {
174        assemble(trace, vec![work.rows], 1);
175    }
176
177    log::info!(
178        target: LOG_TARGET,
179        "propagate: {num_channels} layers applied, {terms_in} -> {} terms, {:.3} s, {}",
180        sum.len(),
181        started.elapsed().as_secs_f64(),
182        progress_scope(None, 1, None),
183    );
184
185    sum
186}
187
188#[cfg(test)]
189mod tests;