Skip to main content

bloq_vm/
runtime.rs

1//! Dynamic execution of lowered VM instruction streams.
2
3mod causal;
4
5use std::collections::HashSet;
6
7use serde::{Deserialize, Serialize};
8
9use crate::backend::{Gate1Q, Pauli as BackendPauli, PauliBasis, PauliString, SimError, Simulator};
10use crate::decoder::{DecoderDecision, DecoderError, MockDecoderConfig};
11use crate::instruction::{
12    BitId, BoolOp, Clifford1, Instruction, LogicalOutput, MemoryCycle, Pauli, PauliProduct,
13    Program, QuantumOp, QuantumStream, RecordId, RecordParity, ResourceId, SourceRole, Stream,
14    TaskId, TaskOrigin,
15};
16
17pub(crate) const TIME_EPSILON: f64 = 1e-9;
18
19/// Hard bounds for one dynamic run.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21pub struct RuntimeLimits {
22    /// Maximum task, moment, and repeated-memory executions.
23    pub max_steps: u64,
24    /// Maximum attempts of one repeat-until-success task.
25    pub max_attempts: u32,
26}
27
28impl Default for RuntimeLimits {
29    fn default() -> Self {
30        Self {
31            max_steps: 1_000_000,
32            max_attempts: 100,
33        }
34    }
35}
36
37/// Runtime and mock-hardware settings.
38#[derive(Debug, Clone)]
39pub struct RuntimeConfig {
40    /// Seed shared by the simulator and mock decoder through separate streams.
41    pub seed: u64,
42    /// State injected at every external logical input arrival.
43    pub input_state: LogicalInputState,
44    /// Mock streaming-decoder assumptions.
45    pub decoder: MockDecoderConfig,
46    /// Idle depolarization rate per runtime time unit; an interval uses
47    /// `min(duration * rate, 0.75)`.
48    pub idle_error_rate: f64,
49    /// Execution caps.
50    pub limits: RuntimeLimits,
51}
52
53impl Default for RuntimeConfig {
54    fn default() -> Self {
55        Self {
56            seed: 0,
57            input_state: LogicalInputState::Plus,
58            decoder: MockDecoderConfig::default(),
59            idle_error_rate: 0.0,
60            limits: RuntimeLimits::default(),
61        }
62    }
63}
64
65/// Encoded state injected when a logical input source is released.
66#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(rename_all = "snake_case")]
68pub enum LogicalInputState {
69    /// Logical `|+>`.
70    #[default]
71    Plus,
72    /// Logical `|0>`.
73    Zero,
74}
75
76/// Final engine state plus the complete execution trace.
77#[derive(Debug)]
78pub struct RunResult {
79    /// Final quantum state, available to physical-verification hooks.
80    pub simulator: Simulator,
81    /// JSON-ready run trace.
82    pub artifact: ExecutionArtifact,
83}
84
85impl RunResult {
86    /// Read a signed VM Pauli expectation from the untouched terminal state.
87    ///
88    /// # Errors
89    ///
90    /// Returns a fixed-register or backend error.
91    pub fn expectation(&self, operator: &PauliProduct) -> Result<f64, RuntimeError> {
92        let operator = engine_pauli_product(operator, self.simulator.num_qubits())?;
93        Ok(self.simulator.peek_observable_expectation(&operator)?)
94    }
95
96    /// Read the terminal `(X, Y, Z)` Bloch vector with VM frame corrections.
97    ///
98    /// # Errors
99    ///
100    /// Returns [`RuntimeError::MissingBit`] for an unavailable frame or a
101    /// fixed-register/backend error for malformed logical operators.
102    pub fn logical_bloch(&self, output: &LogicalOutput) -> Result<(f64, f64, f64), RuntimeError> {
103        let frame_x = self
104            .artifact
105            .final_bits
106            .get(output.frame_x as usize)
107            .copied()
108            .flatten()
109            .ok_or(RuntimeError::MissingBit(output.frame_x))?;
110        let frame_z = self
111            .artifact
112            .final_bits
113            .get(output.frame_z as usize)
114            .copied()
115            .flatten()
116            .ok_or(RuntimeError::MissingBit(output.frame_z))?;
117        let width = self.simulator.num_qubits();
118        let mut logical_x = engine_pauli_product(&output.x, width)?;
119        let mut logical_z = engine_pauli_product(&output.z, width)?;
120        logical_x.phase_shift(2 * i32::from(frame_z));
121        logical_z.phase_shift(2 * i32::from(frame_x));
122        let mut logical_y = &logical_x * &logical_z;
123        logical_y.phase_shift(1);
124        Ok((
125            self.simulator.peek_observable_expectation(&logical_x)?,
126            self.simulator.peek_observable_expectation(&logical_y)?,
127            self.simulator.peek_observable_expectation(&logical_z)?,
128        ))
129    }
130}
131
132/// Inspectable output of one dynamic run.
133#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
134pub struct ExecutionArtifact {
135    /// Reproduction settings and dense-task source map.
136    pub metadata: RunMetadata,
137    /// Every raw measurement, including rejected attempts.
138    pub measurements: Vec<MeasurementRecord>,
139    /// Signed detector outcomes.
140    pub detectors: Vec<DetectorResult>,
141    /// Named raw and physical observable outcomes.
142    pub observables: Vec<ObservableResult>,
143    /// Actual task intervals.
144    pub timing: Vec<TaskTiming>,
145    /// RUS attempt history.
146    pub retries: Vec<RetryRecord>,
147    /// Mock decoder outcomes.
148    pub decoder_decisions: Vec<DecoderDecisionRecord>,
149    /// Dynamic waits, corrections, selections, and state transitions.
150    pub events: Vec<ExecutionEvent>,
151    /// Whether this shot terminated without a committed result.
152    pub discarded: bool,
153    /// Terminal reason for a discarded shot.
154    pub stop_reason: Option<StopReason>,
155    /// Physical stop time.
156    pub finished_at: f64,
157    /// Highest stabilizer-frame rank reached by the selected engine.
158    pub peak_rank: usize,
159    /// Final dense classical registers; all unavailable on a discarded shot.
160    pub final_bits: Vec<Option<bool>>,
161}
162
163/// Settings and source labels needed to reproduce and read an artifact.
164#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
165pub struct RunMetadata {
166    /// Selected backend name.
167    pub backend: String,
168    /// Root random seed.
169    pub seed: u64,
170    /// External logical input state.
171    pub input_state: LogicalInputState,
172    /// Mock decoder assumptions.
173    pub decoder: MockDecoderConfig,
174    /// Mock-decoder latency used by this program, in physical memory rounds.
175    pub decoder_latency_rounds: u32,
176    /// Dynamic-idle depolarization rate per runtime time unit.
177    pub idle_error_rate: f64,
178    /// Runtime work and retry caps.
179    pub limits: RuntimeLimits,
180    /// Dense-task source map.
181    pub tasks: Vec<TaskMetadata>,
182}
183
184/// Human-readable metadata for one dense VM task.
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186pub struct TaskMetadata {
187    /// Dense task id.
188    pub task: TaskId,
189    /// Lowering-provided IR scope, node, and role.
190    pub label: String,
191    /// VM instruction kind.
192    pub instruction: String,
193    /// Backend-independent compiled-IR function and source geometry.
194    pub origin: Option<TaskOrigin>,
195    /// Dense causal predecessors used by the runtime scheduler.
196    pub dependencies: Vec<TaskId>,
197    /// Tasks whose completion releases this held patch.
198    pub wait_until: Vec<TaskId>,
199    /// Dense classical result register, when produced.
200    pub output: Option<BitId>,
201}
202
203impl ExecutionArtifact {
204    /// Render this artifact as compact JSON.
205    ///
206    /// # Errors
207    ///
208    /// Propagates JSON serialization errors.
209    pub fn to_json(&self) -> Result<String, serde_json::Error> {
210        serde_json::to_string(self)
211    }
212
213    /// Render this artifact for inspection or storage.
214    ///
215    /// # Errors
216    ///
217    /// Propagates JSON serialization errors.
218    pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
219        serde_json::to_string_pretty(self)
220    }
221}
222
223/// One raw record write.
224#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
225pub struct MeasurementRecord {
226    /// Dense record id.
227    pub record: RecordId,
228    /// `true` is the `-1` outcome.
229    pub value: bool,
230    /// Task that measured it.
231    pub task: TaskId,
232    /// RUS attempt epoch, if any.
233    pub attempt: Option<u64>,
234    /// Measurement completion time.
235    pub time: f64,
236    /// Whether this record belongs to the accepted execution path.
237    pub committed: bool,
238}
239
240/// One signed detector evaluation.
241#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
242pub struct DetectorResult {
243    /// Stable order within this artifact.
244    pub sequence: u32,
245    /// Owning task.
246    pub task: TaskId,
247    /// Fault-syndrome value, including the authored sign.
248    pub value: bool,
249    /// RUS attempt epoch, if any.
250    pub attempt: Option<u64>,
251    /// Evaluation time.
252    pub time: f64,
253    /// Whether this detector belongs to the accepted execution path.
254    pub committed: bool,
255}
256
257/// One named observable evaluation.
258#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
259pub struct ObservableResult {
260    /// Authored observable index.
261    pub index: u32,
262    /// Owning task.
263    pub task: TaskId,
264    /// SEM-OBS dataflow parity, excluding boundary bindings.
265    pub raw: Option<bool>,
266    /// Physical report value after one signed joint boundary measurement.
267    /// Full physical report value, including deferred boundary bindings.
268    pub value: Option<bool>,
269    /// RUS attempt epoch, if any.
270    pub attempt: Option<u64>,
271    /// Observable task time.
272    pub time: f64,
273    /// Whether this observable belongs to the accepted execution path.
274    pub committed: bool,
275}
276
277/// One cached streaming-decoder solve in its task context.
278#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
279pub struct DecoderDecisionRecord {
280    /// First task that requested the solve.
281    pub task: TaskId,
282    /// Causal decoder decision.
283    pub decision: DecoderDecision,
284    /// Whether this solve belongs to the accepted execution path.
285    pub committed: bool,
286}
287
288/// Actual interval occupied by one task execution.
289#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
290pub struct TaskTiming {
291    /// Dense task id.
292    pub task: TaskId,
293    /// Lowering-provided source label.
294    pub label: String,
295    /// VM instruction kind.
296    pub instruction: String,
297    /// RUS attempt epoch, if any.
298    pub attempt: Option<u64>,
299    /// Start time.
300    pub start: f64,
301    /// Finish time.
302    pub end: f64,
303}
304
305/// One complete RUS attempt.
306#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
307pub struct RetryRecord {
308    /// Owning RUS task.
309    pub task: TaskId,
310    /// Zero-based local attempt number.
311    pub attempt: u32,
312    /// Globally unique attempt epoch.
313    pub epoch: u64,
314    /// Attempt start time.
315    pub start: f64,
316    /// Attempt finish time, including decoder latency.
317    pub end: f64,
318    /// Whether this attempt committed its state and aliases.
319    pub accepted: bool,
320}
321
322/// Why a shot ended without a committed result.
323#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
324#[serde(rename_all = "snake_case")]
325pub enum StopReason {
326    /// An authored discard predicate fired.
327    Discard,
328    /// A RUS task exhausted its configured attempt cap.
329    AttemptLimit,
330}
331
332/// Origin of a recorded syndrome-memory round.
333#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
334#[serde(rename_all = "snake_case")]
335pub enum MemoryKind {
336    /// Authored memory-round padding.
337    StaticPadding,
338    /// Runtime padding while a join sibling remained active.
339    DynamicWait,
340    /// Mandatory factory GAP rounds after physical cultivation completes.
341    FactoryGap,
342}
343
344/// Runtime cause for a dynamically inserted hold interval.
345///
346/// When blockers overlap, classification uses this conservative precedence:
347/// a direct physical sibling or resource is [`Self::Synchronization`], then an
348/// unfinished causal query is [`Self::CausalCut`], and only an issued decoder
349/// solve is [`Self::DecoderLatency`]. The reason is sampled when the whole
350/// interval is issued and does not change partway through a QEC round. Factory
351/// GAP rounds remain identified by [`MemoryKind::FactoryGap`].
352#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
353#[serde(rename_all = "snake_case")]
354pub enum WaitReason {
355    /// A directly joined physical sibling or factory resource is unfinished.
356    Synchronization,
357    /// Measurements or other causal inputs needed to issue a query are unfinished.
358    CausalCut,
359    /// Query inputs are complete and an issued decoder solve is still running.
360    DecoderLatency,
361}
362
363/// Time-stamped dynamic events.
364#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
365#[serde(tag = "kind", rename_all = "snake_case")]
366pub enum ExecutionEvent {
367    /// An independently released source became eligible to execute.
368    SourceReleased {
369        /// Source task.
370        task: TaskId,
371        /// Lowered source role.
372        role: String,
373        /// Absolute release time.
374        time: f64,
375    },
376    /// An external encoded input arrived.
377    InputArrival {
378        /// Owning source task.
379        task: TaskId,
380        /// Logical input port.
381        port: [i32; 3],
382        /// Injected logical state.
383        state: LogicalInputState,
384        /// Arrival time.
385        time: f64,
386    },
387    /// A physical moment was issued onto free qubits.
388    MomentIssued {
389        /// Owning task.
390        task: TaskId,
391        /// RUS attempt epoch, if any.
392        attempt: Option<u64>,
393        /// Zero-based moment index within its physical stream.
394        moment: u32,
395        /// Issue time.
396        start: f64,
397        /// Scheduled completion time.
398        end: f64,
399    },
400    /// A physical moment completed and mutated the simulator.
401    MomentCompleted {
402        /// Owning task.
403        task: TaskId,
404        /// RUS attempt epoch, if any.
405        attempt: Option<u64>,
406        /// Zero-based moment index.
407        moment: u32,
408        /// Completion time.
409        time: f64,
410    },
411    /// A newly available selected restart parity aborted cultivation early.
412    EarlyRestart {
413        /// Owning RUS task.
414        task: TaskId,
415        /// Zero-based local attempt.
416        attempt: u32,
417        /// Inspection reason naming the selected check.
418        reason: String,
419        /// First time the rejecting parity was fully available.
420        time: f64,
421    },
422    /// A causal decoder deadline was established.
423    DecoderDeadline {
424        /// Decoder output task.
425        task: TaskId,
426        /// Causal observable task.
427        observable: TaskId,
428        /// Whether factory completion, rather than a measurement cut, anchors latency.
429        factory: bool,
430        /// Last contributing measurement time when present.
431        measurements_ready_at: Option<f64>,
432        /// Time at which the runtime issued this decoder query.
433        requested_at: f64,
434        /// Decision availability time.
435        ready_at: f64,
436    },
437    /// Every incoming seam reached a join.
438    JoinReleased {
439        /// Wait task representing the held seam.
440        task: TaskId,
441        /// Release time after any in-flight round completed.
442        time: f64,
443    },
444    /// One guarded quantum alternative was selected.
445    QuantumAlternative {
446        /// Quantum task.
447        task: TaskId,
448        /// Zero-based selected alternative.
449        alternative: u32,
450        /// Human-readable selected gate names, including dynamic S corrections.
451        operations: Vec<String>,
452        /// Selection time.
453        time: f64,
454    },
455    /// A record-controlled Pauli was considered.
456    ConditionalCorrection {
457        /// Quantum task.
458        task: TaskId,
459        /// Gating record.
460        record: RecordId,
461        /// Pauli axis.
462        pauli: String,
463        /// Whether the correction fired.
464        applied: bool,
465        /// Correction time.
466        time: f64,
467    },
468    /// A source-isolated RUS attempt began.
469    RusAttemptStarted {
470        /// Owning RUS task.
471        task: TaskId,
472        /// Zero-based local attempt number.
473        attempt: u32,
474        /// Globally unique attempt epoch.
475        epoch: u64,
476        /// Start time.
477        time: f64,
478    },
479    /// Explicit timed preparation before a retried attempt.
480    RetryPrepare {
481        /// Owning RUS task.
482        task: TaskId,
483        /// Zero-based local attempt number.
484        attempt: u32,
485        /// Preparation start.
486        start: f64,
487        /// Preparation finish.
488        end: f64,
489    },
490    /// A RUS attempt rejected without committing aliases.
491    RusAttemptRejected {
492        /// Owning RUS task.
493        task: TaskId,
494        /// Zero-based local attempt number.
495        attempt: u32,
496        /// Globally unique attempt epoch.
497        epoch: u64,
498        /// Rejection time.
499        time: f64,
500    },
501    /// A RUS attempt accepted and committed.
502    RusAttemptAccepted {
503        /// Owning RUS task.
504        task: TaskId,
505        /// Zero-based local attempt number.
506        attempt: u32,
507        /// Globally unique attempt epoch.
508        epoch: u64,
509        /// Acceptance time.
510        time: f64,
511    },
512    /// An accepted factory output became available.
513    FactoryReady {
514        /// Runtime resource id.
515        resource: ResourceId,
516        /// Ready time.
517        time: f64,
518    },
519    /// One complete syndrome-memory round ran.
520    MemoryRound {
521        /// Waiting or padding task.
522        task: TaskId,
523        /// Zero-based round within the task.
524        round: u32,
525        /// Static or dynamic origin.
526        memory_kind: MemoryKind,
527        /// Dynamic hold cause sampled when the round was issued.
528        /// Authored static padding and factory GAP use `memory_kind` instead.
529        wait_reason: Option<WaitReason>,
530        /// Round start.
531        start: f64,
532        /// Round finish.
533        end: f64,
534    },
535    /// A patch performed an authored idle or a residual dynamic wait.
536    Idle {
537        /// Idling task.
538        task: TaskId,
539        /// Dynamic hold cause sampled when the idle was issued.
540        /// Authored idle instructions have no wait reason.
541        wait_reason: Option<WaitReason>,
542        /// Idle start.
543        start: f64,
544        /// Idle finish.
545        end: f64,
546    },
547    /// The shot stopped and no result committed.
548    Discarded {
549        /// Task that stopped the shot.
550        task: TaskId,
551        /// Stop reason.
552        reason: StopReason,
553        /// Stop time.
554        time: f64,
555    },
556}
557
558/// Failures on the *dynamic causal runtime* path.
559///
560/// That path is [`run()`] and the timing, retry, noise, and mock-decoding
561/// machinery it drives. The noiseless physical verifier is the other execution
562/// engine in this crate and reports [`ExecError`](crate::ExecError) instead.
563#[derive(Debug, thiserror::Error)]
564#[non_exhaustive]
565pub enum RuntimeError {
566    /// A dense runtime or artifact identifier exhausted `u32`.
567    #[error("{0} identifier space exhausted")]
568    IdOverflow(&'static str),
569    /// A runtime table could not fit or allocate its initial capacity.
570    #[error("cannot allocate {register} runtime table: {source}")]
571    RegisterAllocation {
572        /// Runtime table being initialized.
573        register: &'static str,
574        /// Capacity or allocation failure from the table.
575        #[source]
576        source: std::collections::TryReserveError,
577    },
578    /// A public VM program or runtime setting violated its fixed-register contract.
579    #[error("invalid VM program: {0}")]
580    InvalidProgram(&'static str),
581    /// A task id or dependency was absent.
582    #[error("task {0} is unavailable at its scheduled use")]
583    MissingTask(TaskId),
584    /// A required classical bit was absent.
585    #[error("classical bit {0} is unavailable")]
586    MissingBit(BitId),
587    /// A required record was absent.
588    #[error("measurement record {0} is unavailable")]
589    MissingRecord(RecordId),
590    /// The run exhausted its operation-work cap.
591    #[error("runtime step cap {limit} exceeded")]
592    StepLimit {
593        /// Configured cap.
594        limit: u64,
595    },
596    /// No guarded quantum alternative matched available input bits.
597    #[error("quantum task has no selected alternative")]
598    NoQuantumAlternative,
599    /// The selected simulator rejected an operation.
600    #[error("quantum backend error: {0}")]
601    Backend(#[from] SimError),
602    /// Mock decoder configuration or timing was invalid.
603    #[error("mock decoder error: {0}")]
604    Decoder(#[from] DecoderError),
605    /// Finite instruction durations overflowed the runtime clock.
606    #[error("runtime clock overflowed")]
607    InvalidTiming,
608}
609
610/// Execute one lowered program from time zero.
611///
612/// # Errors
613///
614/// Returns a typed program, resource-limit, decoder, or backend failure.
615pub fn run(program: &Program, config: RuntimeConfig) -> Result<RunResult, RuntimeError> {
616    causal::run(program, config)
617}
618
619fn validate_program(program: &Program) -> Result<(), RuntimeError> {
620    let tasks = program.tasks.len();
621    if let Some(last_task) = tasks.checked_sub(1) {
622        u32::try_from(last_task).map_err(|_| RuntimeError::IdOverflow("task"))?;
623    }
624    let bits = program.bit_count as usize;
625    let records = program.record_count as usize;
626    let qubits = program.qubit_count as usize;
627    if !program.task_origins.is_empty() && program.task_origins.len() != tasks {
628        return Err(RuntimeError::InvalidProgram("invalid task origin table"));
629    }
630    validate_stream(&program.entry, tasks, bits)?;
631    let mut input_tasks = HashSet::new();
632    for input in &program.inputs {
633        let mut data_qubits = HashSet::new();
634        if input.task as usize >= tasks
635            || program.tasks[input.task as usize].source != Some(SourceRole::LogicalInput)
636            || !input_tasks.insert(input.task)
637            || input
638                .data_qubits
639                .iter()
640                .any(|&qubit| qubit as usize >= qubits || !data_qubits.insert(qubit))
641        {
642            return Err(RuntimeError::InvalidProgram("invalid logical input"));
643        }
644        for stabilizer in &input.stabilizers {
645            validate_product(stabilizer, qubits)?;
646        }
647        validate_product(&input.x, qubits)?;
648        validate_product(&input.z, qubits)?;
649    }
650    for output in &program.outputs {
651        if output.frame_x as usize >= bits || output.frame_z as usize >= bits {
652            return Err(RuntimeError::InvalidProgram("invalid output frame"));
653        }
654        validate_product(&output.x, qubits)?;
655        validate_product(&output.z, qubits)?;
656    }
657    for task in &program.tasks {
658        if !valid_duration(task.release)
659            || !valid_duration(task.duration)
660            || task.dependencies.iter().any(|&id| id as usize >= tasks)
661            || task.activation.is_some_and(|id| id as usize >= bits)
662            || task.output.is_some_and(|id| id as usize >= bits)
663            || task.qubits.iter().any(|&id| id as usize >= qubits)
664        {
665            return Err(RuntimeError::InvalidProgram("invalid task operand"));
666        }
667        validate_instruction(&task.instruction, tasks, bits, records, qubits)?;
668    }
669    for task in &program.tasks {
670        if let Instruction::Rus { .. } = &task.instruction {
671            validate_rus_isolation(program, task)?;
672        }
673    }
674    Ok(())
675}
676
677fn validate_rus_isolation(
678    program: &Program,
679    task: &crate::instruction::Task,
680) -> Result<(), RuntimeError> {
681    let Instruction::Rus {
682        body,
683        owned_qubits,
684        retry_prepare,
685        decoder_hold,
686        cultivation_exits,
687        ..
688    } = &task.instruction
689    else {
690        return Ok(());
691    };
692    let owned = owned_qubits.iter().copied().collect::<HashSet<_>>();
693    let outside_operation = |operation: &QuantumOp| {
694        quantum_op_qubits(operation)
695            .iter()
696            .any(|qubit| !owned.contains(qubit))
697    };
698    let outside_stream = |stream: &QuantumStream| {
699        stream
700            .moments
701            .iter()
702            .flat_map(|moment| &moment.operations)
703            .any(outside_operation)
704    };
705    let outside_cycle = |cycle: &MemoryCycle| {
706        outside_stream(&cycle.stream)
707            || cycle
708                .boundary_flows
709                .iter()
710                .flat_map(|flow| flow.start.terms.iter().chain(flow.end.terms.iter()))
711                .any(|(qubit, _)| !owned.contains(qubit))
712    };
713    if owned.len() != owned_qubits.len()
714        || decoder_hold.is_empty() != cultivation_exits.is_empty()
715        || (task.source == Some(SourceRole::Factory) && decoder_hold.is_empty())
716    {
717        return Err(RuntimeError::InvalidProgram("invalid RUS isolation"));
718    }
719
720    if outside_stream(retry_prepare) {
721        return Err(RuntimeError::InvalidProgram(
722            "RUS retry preparation touches unowned support",
723        ));
724    }
725
726    let mut members = HashSet::new();
727    for &member in &body.tasks {
728        if !members.insert(member) {
729            continue;
730        }
731        let member_task = &program.tasks[member as usize];
732        let outside_instruction = match &member_task.instruction {
733            Instruction::Quantum(quantum) => quantum
734                .alternatives
735                .iter()
736                .any(|alternative| outside_stream(&alternative.stream)),
737            Instruction::MemoryRounds { stream, .. } => outside_stream(stream),
738            Instruction::Idle { operations, .. } => operations.iter().any(outside_operation),
739            Instruction::WaitFor { memory, .. } => memory.iter().any(outside_cycle),
740            Instruction::Bind(bindings) => bindings
741                .iter()
742                .flat_map(|binding| &binding.operator.terms)
743                .any(|(qubit, _)| !owned.contains(qubit)),
744            Instruction::Rus { .. } => {
745                return Err(RuntimeError::InvalidProgram(
746                    "nested dynamic RUS is unsupported",
747                ));
748            }
749            Instruction::Eval(_)
750            | Instruction::Accumulate(_)
751            | Instruction::ReadoutRecipe { .. }
752            | Instruction::Observable { .. }
753            | Instruction::Decode(_)
754            | Instruction::Discard(_)
755            | Instruction::SignalReady { .. } => false,
756        };
757        if member_task
758            .qubits
759            .iter()
760            .any(|qubit| !owned.contains(qubit))
761            || outside_instruction
762        {
763            return Err(RuntimeError::InvalidProgram(
764                "RUS body touches qubits outside its isolated source support",
765            ));
766        }
767    }
768    if cultivation_exits.iter().any(|exit| !members.contains(exit)) {
769        return Err(RuntimeError::InvalidProgram(
770            "RUS cultivation exit is outside its body",
771        ));
772    }
773    if decoder_hold.iter().any(outside_cycle) {
774        return Err(RuntimeError::InvalidProgram(
775            "RUS decoder hold has invalid data support",
776        ));
777    }
778    Ok(())
779}
780
781fn validate_stream(stream: &Stream, tasks: usize, bits: usize) -> Result<(), RuntimeError> {
782    if stream.tasks.iter().any(|&id| id as usize >= tasks)
783        || stream.bindings.iter().any(|&id| id as usize >= tasks)
784        || stream.value.is_some_and(|id| id as usize >= bits)
785    {
786        return Err(RuntimeError::InvalidProgram("invalid stream operand"));
787    }
788    Ok(())
789}
790
791fn validate_instruction(
792    instruction: &Instruction,
793    tasks: usize,
794    bits: usize,
795    records: usize,
796    qubits: usize,
797) -> Result<(), RuntimeError> {
798    match instruction {
799        Instruction::Quantum(quantum) => {
800            for alternative in &quantum.alternatives {
801                if alternative
802                    .when
803                    .iter()
804                    .any(|&(bit, _)| bit as usize >= bits)
805                {
806                    return Err(RuntimeError::InvalidProgram("invalid quantum selector"));
807                }
808                if alternative
809                    .selected_records
810                    .iter()
811                    .chain(alternative.excluded_records.iter())
812                    .any(|&record| record as usize >= records)
813                {
814                    return Err(RuntimeError::InvalidProgram("invalid quantum record"));
815                }
816                validate_quantum_stream(&alternative.stream, records, qubits)?;
817                for parity in alternative
818                    .detectors
819                    .iter()
820                    .chain(alternative.restarts.iter())
821                {
822                    validate_parity(parity, records)?;
823                }
824            }
825        }
826        Instruction::Eval(expression) | Instruction::Discard(expression) => {
827            validate_bool(expression, bits)?;
828        }
829        Instruction::Accumulate(parity) => validate_parity(parity, records)?,
830        Instruction::Bind(bindings) => {
831            for binding in bindings {
832                validate_product(&binding.operator, qubits)?;
833            }
834        }
835        Instruction::ReadoutRecipe {
836            bits: inputs,
837            bindings,
838        }
839        | Instruction::Observable {
840            bits: inputs,
841            bindings,
842            ..
843        } => {
844            if inputs.iter().any(|&id| id as usize >= bits)
845                || bindings.iter().any(|&id| id as usize >= tasks)
846            {
847                return Err(RuntimeError::InvalidProgram("invalid observable operand"));
848            }
849        }
850        Instruction::Decode(request) => {
851            if request.observable as usize >= tasks
852                || request.raw as usize >= bits
853                || !valid_duration(request.round_duration)
854            {
855                return Err(RuntimeError::InvalidProgram("invalid decoder operand"));
856            }
857        }
858        Instruction::Rus {
859            body,
860            restart,
861            owned_qubits,
862            attempt_bits,
863            attempt_records,
864            retry_prepare,
865            decoder_hold,
866            cultivation_exits,
867            ..
868        } => {
869            validate_stream(body, tasks, bits)?;
870            validate_bool(restart, bits)?;
871            if owned_qubits.iter().any(|&id| id as usize >= qubits)
872                || attempt_bits.iter().any(|&id| id as usize >= bits)
873                || attempt_records.iter().any(|&id| id as usize >= records)
874                || cultivation_exits.iter().any(|&id| id as usize >= tasks)
875            {
876                return Err(RuntimeError::InvalidProgram("invalid RUS operand"));
877            }
878            for cycle in decoder_hold {
879                validate_memory_cycle(cycle, records, qubits)?;
880            }
881            validate_quantum_stream(retry_prepare, records, qubits)?;
882        }
883        Instruction::WaitFor { until, memory } => {
884            if until.iter().any(|&id| id as usize >= tasks) {
885                return Err(RuntimeError::InvalidProgram("invalid wait operand"));
886            }
887            for cycle in memory {
888                validate_memory_cycle(cycle, records, qubits)?;
889            }
890        }
891        Instruction::SignalReady { .. } => {}
892        Instruction::MemoryRounds {
893            stream, detectors, ..
894        } => {
895            validate_quantum_stream(stream, records, qubits)?;
896            for parity in detectors {
897                validate_parity(parity, records)?;
898            }
899        }
900        Instruction::Idle {
901            duration,
902            noise_ratio,
903            operations,
904        } => {
905            if !valid_duration(*duration) || !valid_duration(*noise_ratio) {
906                return Err(RuntimeError::InvalidProgram("invalid idle timing"));
907            }
908            for operation in operations {
909                validate_quantum_op(operation, records, qubits)?;
910            }
911        }
912    }
913    Ok(())
914}
915
916fn validate_memory_cycle(
917    cycle: &MemoryCycle,
918    records: usize,
919    qubits: usize,
920) -> Result<(), RuntimeError> {
921    let stream_duration = stream_duration(&cycle.stream)?;
922    if stream_duration <= 0.0
923        || cycle.round_duration <= 0.0
924        || !cycle.round_duration.is_finite()
925        || (stream_duration - cycle.round_duration).abs() > TIME_EPSILON
926    {
927        return Err(RuntimeError::InvalidProgram("invalid memory duration"));
928    }
929    validate_quantum_stream(&cycle.stream, records, qubits)?;
930    for parity in &cycle.detectors {
931        validate_parity(parity, records)?;
932    }
933    for initializer in &cycle.initializers {
934        if initializer.record as usize >= records {
935            return Err(RuntimeError::InvalidProgram("invalid frontier record"));
936        }
937        validate_parity(&initializer.parity, records)?;
938    }
939    for flow in &cycle.boundary_flows {
940        validate_product(&flow.start, qubits)?;
941        validate_product(&flow.end, qubits)?;
942        validate_parity(&flow.parity, records)?;
943        if flow
944            .frontier_record
945            .is_some_and(|record| record as usize >= records)
946        {
947            return Err(RuntimeError::InvalidProgram("invalid frontier record"));
948        }
949    }
950    Ok(())
951}
952
953fn validate_quantum_stream(
954    stream: &QuantumStream,
955    records: usize,
956    qubits: usize,
957) -> Result<(), RuntimeError> {
958    for moment in &stream.moments {
959        if !valid_duration(moment.duration) {
960            return Err(RuntimeError::InvalidProgram("invalid moment duration"));
961        }
962        for operation in &moment.operations {
963            validate_quantum_op(operation, records, qubits)?;
964        }
965    }
966    Ok(())
967}
968
969fn validate_quantum_op(
970    operation: &QuantumOp,
971    records: usize,
972    qubits: usize,
973) -> Result<(), RuntimeError> {
974    let valid_qubit = |qubit: u32| (qubit as usize) < qubits;
975    let valid_record = |record: u32| (record as usize) < records;
976    let valid = match operation {
977        QuantumOp::Gate1 { qubit, .. }
978        | QuantumOp::Pauli { qubit, .. }
979        | QuantumOp::T { qubit, .. }
980        | QuantumOp::Reset { qubit, .. } => valid_qubit(*qubit),
981        QuantumOp::Gate2 {
982            control, target, ..
983        } => valid_qubit(*control) && valid_qubit(*target) && control != target,
984        QuantumOp::Measure {
985            observable,
986            records: outputs,
987            flip_probability,
988        } => {
989            validate_product(observable, qubits)?;
990            valid_probability(*flip_probability)
991                && !outputs.is_empty()
992                && outputs.iter().all(|&record| valid_record(record))
993        }
994        QuantumOp::ConditionalPauli { qubit, control, .. } => {
995            valid_qubit(*qubit) && valid_record(*control)
996        }
997        QuantumOp::Depolarize1 {
998            probability,
999            qubits: operands,
1000        }
1001        | QuantumOp::PauliError {
1002            probability,
1003            qubits: operands,
1004            ..
1005        } => valid_probability(*probability) && operands.iter().all(|&qubit| valid_qubit(qubit)),
1006        QuantumOp::Depolarize2 { probability, pairs } => {
1007            valid_probability(*probability)
1008                && pairs
1009                    .iter()
1010                    .all(|&(left, right)| left != right && valid_qubit(left) && valid_qubit(right))
1011        }
1012    };
1013    if valid {
1014        Ok(())
1015    } else {
1016        Err(RuntimeError::InvalidProgram("invalid quantum operand"))
1017    }
1018}
1019
1020fn quantum_op_qubits(operation: &QuantumOp) -> Vec<u32> {
1021    match operation {
1022        QuantumOp::Gate1 { qubit, .. }
1023        | QuantumOp::Pauli { qubit, .. }
1024        | QuantumOp::T { qubit, .. }
1025        | QuantumOp::Reset { qubit, .. }
1026        | QuantumOp::ConditionalPauli { qubit, .. } => vec![*qubit],
1027        QuantumOp::Gate2 {
1028            control, target, ..
1029        } => vec![*control, *target],
1030        QuantumOp::Measure { observable, .. } => {
1031            observable.terms.iter().map(|&(qubit, _)| qubit).collect()
1032        }
1033        QuantumOp::Depolarize1 { qubits, .. } | QuantumOp::PauliError { qubits, .. } => {
1034            qubits.to_vec()
1035        }
1036        QuantumOp::Depolarize2 { pairs, .. } => pairs
1037            .iter()
1038            .flat_map(|&(left, right)| [left, right])
1039            .collect(),
1040    }
1041}
1042
1043fn validate_product(product: &PauliProduct, qubits: usize) -> Result<(), RuntimeError> {
1044    let mut seen = HashSet::new();
1045    if product
1046        .terms
1047        .iter()
1048        .any(|&(qubit, _)| qubit as usize >= qubits || !seen.insert(qubit))
1049    {
1050        Err(RuntimeError::InvalidProgram("invalid Pauli operand"))
1051    } else {
1052        Ok(())
1053    }
1054}
1055
1056fn validate_parity(parity: &RecordParity, records: usize) -> Result<(), RuntimeError> {
1057    if parity
1058        .records
1059        .iter()
1060        .any(|&record| record as usize >= records)
1061    {
1062        Err(RuntimeError::InvalidProgram("invalid record operand"))
1063    } else {
1064        Ok(())
1065    }
1066}
1067
1068fn validate_bool(expression: &BoolOp, bits: usize) -> Result<(), RuntimeError> {
1069    match expression {
1070        BoolOp::Call { body, inputs } => {
1071            if inputs.iter().any(|&bit| bit as usize >= bits) {
1072                return Err(RuntimeError::InvalidProgram("invalid function argument"));
1073            }
1074            validate_bool(body, inputs.len())
1075        }
1076        BoolOp::Const(_) => Ok(()),
1077        BoolOp::Copy(bit) => ((*bit as usize) < bits)
1078            .then_some(())
1079            .ok_or(RuntimeError::InvalidProgram("invalid bit operand")),
1080        BoolOp::Not(inner) => validate_bool(inner, bits),
1081        BoolOp::Parity { inputs, .. } => inputs
1082            .iter()
1083            .all(|&bit| (bit as usize) < bits)
1084            .then_some(())
1085            .ok_or(RuntimeError::InvalidProgram("invalid bit operand")),
1086        BoolOp::Xor(operands) | BoolOp::And(operands) | BoolOp::Or(operands) => {
1087            for operand in operands {
1088                validate_bool(operand, bits)?;
1089            }
1090            Ok(())
1091        }
1092        BoolOp::Select {
1093            condition,
1094            when_false,
1095            when_true,
1096        } => {
1097            validate_bool(condition, bits)?;
1098            validate_bool(when_false, bits)?;
1099            validate_bool(when_true, bits)
1100        }
1101    }
1102}
1103
1104fn stream_duration(stream: &QuantumStream) -> Result<f64, RuntimeError> {
1105    stream.moments.iter().try_fold(0.0, |duration, moment| {
1106        if valid_duration(moment.duration) {
1107            Ok(duration + moment.duration)
1108        } else {
1109            Err(RuntimeError::InvalidProgram("invalid moment duration"))
1110        }
1111    })
1112}
1113
1114fn quantum_operation_names(stream: &QuantumStream) -> Vec<String> {
1115    stream
1116        .moments
1117        .iter()
1118        .flat_map(|moment| &moment.operations)
1119        .filter_map(|operation| match operation {
1120            QuantumOp::Gate1 { gate, .. } => Some(format!("{gate:?}")),
1121            QuantumOp::Gate2 {
1122                control_basis,
1123                target_basis,
1124                ..
1125            } => Some(format!("C{control_basis:?}{target_basis:?}")),
1126            QuantumOp::Pauli { basis, .. } => Some(format!("{basis:?}")),
1127            QuantumOp::T { adjoint, .. } => Some(if *adjoint { "T_DAG" } else { "T" }.into()),
1128            QuantumOp::ConditionalPauli { basis, .. } => Some(format!("conditional_{basis:?}")),
1129            QuantumOp::Measure { .. }
1130            | QuantumOp::Reset { .. }
1131            | QuantumOp::Depolarize1 { .. }
1132            | QuantumOp::Depolarize2 { .. }
1133            | QuantumOp::PauliError { .. } => None,
1134        })
1135        .collect()
1136}
1137
1138fn instruction_kind(instruction: &Instruction) -> &'static str {
1139    match instruction {
1140        Instruction::Quantum(_) => "quantum",
1141        Instruction::Eval(_) => "eval",
1142        Instruction::Accumulate(_) => "accumulate",
1143        Instruction::Bind(_) => "bind",
1144        Instruction::ReadoutRecipe { .. } => "readout_recipe",
1145        Instruction::Observable { .. } => "observable",
1146        Instruction::Decode(_) => "decode",
1147        Instruction::Discard(_) => "discard",
1148        Instruction::Rus { .. } => "rus",
1149        Instruction::WaitFor { .. } => "wait_for",
1150        Instruction::SignalReady { .. } => "signal_ready",
1151        Instruction::MemoryRounds { .. } => "memory_rounds",
1152        Instruction::Idle { .. } => "idle",
1153    }
1154}
1155
1156fn valid_probability(value: f64) -> bool {
1157    value.is_finite() && (0.0..=1.0).contains(&value)
1158}
1159
1160fn valid_duration(value: f64) -> bool {
1161    value.is_finite() && value >= 0.0
1162}
1163
1164fn max_time(left: Option<f64>, right: Option<f64>) -> Option<f64> {
1165    match (left, right) {
1166        (Some(left), Some(right)) => Some(left.max(right)),
1167        (Some(value), None) | (None, Some(value)) => Some(value),
1168        (None, None) => None,
1169    }
1170}
1171
1172fn basis(pauli: Pauli) -> PauliBasis {
1173    match pauli {
1174        Pauli::X => PauliBasis::X,
1175        Pauli::Y => PauliBasis::Y,
1176        Pauli::Z => PauliBasis::Z,
1177    }
1178}
1179
1180fn backend_pauli(pauli: Pauli) -> BackendPauli {
1181    match pauli {
1182        Pauli::X => BackendPauli::X,
1183        Pauli::Y => BackendPauli::Y,
1184        Pauli::Z => BackendPauli::Z,
1185    }
1186}
1187
1188fn set_pauli(product: &mut PauliString, qubit: usize, pauli: Pauli) {
1189    product.set(qubit, backend_pauli(pauli));
1190}
1191
1192fn engine_pauli_product(
1193    product: &PauliProduct,
1194    qubit_count: usize,
1195) -> Result<PauliString, RuntimeError> {
1196    let mut out = PauliString::new(qubit_count);
1197    let mut seen = HashSet::new();
1198    for &(qubit, axis) in &product.terms {
1199        if (qubit as usize) >= qubit_count || !seen.insert(qubit) {
1200            return Err(RuntimeError::InvalidProgram("invalid Pauli operand"));
1201        }
1202        set_pauli(&mut out, qubit as usize, axis);
1203    }
1204    if product.negative {
1205        out.phase_shift(2);
1206    }
1207    Ok(out)
1208}
1209
1210fn gate1(gate: Clifford1) -> Gate1Q {
1211    match gate {
1212        Clifford1::H => Gate1Q::H,
1213        Clifford1::H_XY => Gate1Q::Hxy,
1214        Clifford1::H_YZ => Gate1Q::Hyz,
1215        Clifford1::H_NXY => Gate1Q::Hnxy,
1216        Clifford1::H_NXZ => Gate1Q::Hnxz,
1217        Clifford1::H_NYZ => Gate1Q::Hnyz,
1218        Clifford1::SQRT_X => Gate1Q::SqrtX,
1219        Clifford1::SQRT_X_DAG => Gate1Q::SqrtXDag,
1220        Clifford1::SQRT_Y => Gate1Q::SqrtY,
1221        Clifford1::SQRT_Y_DAG => Gate1Q::SqrtYDag,
1222        Clifford1::S => Gate1Q::S,
1223        Clifford1::S_DAG => Gate1Q::SDag,
1224        Clifford1::C_XYZ => Gate1Q::Cxyz,
1225        Clifford1::C_ZYX => Gate1Q::Czyx,
1226        Clifford1::C_NXYZ => Gate1Q::Cnxyz,
1227        Clifford1::C_XNYZ => Gate1Q::Cxnyz,
1228        Clifford1::C_XYNZ => Gate1Q::Cxynz,
1229        Clifford1::C_NZYX => Gate1Q::Cnzyx,
1230        Clifford1::C_ZNYX => Gate1Q::Cznyx,
1231        Clifford1::C_ZYNX => Gate1Q::Czynx,
1232    }
1233}
1234
1235#[cfg(test)]
1236mod tests {
1237    use super::*;
1238    use crate::instruction::{Moment, QuantumAlternative, QuantumTask, Task};
1239
1240    #[test]
1241    fn select_evaluates_unselected_arm_strictly() {
1242        let program = Program {
1243            qubit_count: 0,
1244            bit_count: 2,
1245            tasks: vec![Task {
1246                label: "strict select".into(),
1247                release: 0.0,
1248                source: None,
1249                dependencies: Box::default(),
1250                activation: None,
1251                output: Some(0),
1252                qubits: Box::default(),
1253                duration: 0.0,
1254                instruction: Instruction::Eval(BoolOp::Select {
1255                    condition: Box::new(BoolOp::Const(false)),
1256                    when_false: Box::new(BoolOp::Const(true)),
1257                    when_true: Box::new(BoolOp::Copy(1)),
1258                }),
1259            }],
1260            entry: Stream {
1261                tasks: vec![0].into_boxed_slice(),
1262                ..Stream::default()
1263            },
1264            ..Program::default()
1265        };
1266        assert_eq!(
1267            run(&program, RuntimeConfig::default())
1268                .unwrap()
1269                .artifact
1270                .final_bits[0],
1271            None
1272        );
1273    }
1274
1275    #[test]
1276    fn idle_error_rate_is_not_probability_bounded() {
1277        let mut config = RuntimeConfig {
1278            idle_error_rate: 4.0,
1279            ..RuntimeConfig::default()
1280        };
1281        run(&Program::default(), config.clone()).unwrap();
1282        config.idle_error_rate = -1.0;
1283        assert!(matches!(
1284            run(&Program::default(), config),
1285            Err(RuntimeError::InvalidProgram(
1286                "invalid runtime configuration"
1287            ))
1288        ));
1289    }
1290
1291    #[test]
1292    fn rus_rejects_unowned_operation_targets_even_when_support_omits_them() {
1293        let stream = QuantumStream {
1294            moments: vec![Moment {
1295                duration: 1.0,
1296                operations: vec![QuantumOp::Reset {
1297                    basis: Pauli::Z,
1298                    qubit: 1,
1299                }]
1300                .into_boxed_slice(),
1301                ..Moment::default()
1302            }]
1303            .into_boxed_slice(),
1304        };
1305        let body = Task {
1306            label: "body".into(),
1307            release: 0.0,
1308            source: None,
1309            dependencies: Box::default(),
1310            activation: None,
1311            output: None,
1312            qubits: Box::new([0]),
1313            duration: 1.0,
1314            instruction: Instruction::Quantum(QuantumTask {
1315                alternatives: vec![QuantumAlternative {
1316                    stream: stream.clone(),
1317                    ..QuantumAlternative::default()
1318                }]
1319                .into_boxed_slice(),
1320            }),
1321        };
1322        let owner = Task {
1323            instruction: Instruction::Rus {
1324                body: Stream {
1325                    tasks: Box::new([0]),
1326                    ..Stream::default()
1327                },
1328                restart: BoolOp::Const(false),
1329                owned_qubits: Box::new([0]),
1330                attempt_bits: Box::default(),
1331                attempt_records: Box::default(),
1332                resource: 0,
1333                retry_prepare: QuantumStream::default(),
1334                decoder_hold: Box::default(),
1335                cultivation_exits: Box::default(),
1336            },
1337            ..body.clone()
1338        };
1339        let mut program = Program {
1340            qubit_count: 2,
1341            tasks: vec![body, owner],
1342            entry: Stream {
1343                tasks: Box::new([1]),
1344                ..Stream::default()
1345            },
1346            ..Program::default()
1347        };
1348        assert!(matches!(
1349            run(&program, RuntimeConfig::default()),
1350            Err(RuntimeError::InvalidProgram(
1351                "RUS body touches qubits outside its isolated source support"
1352            ))
1353        ));
1354
1355        program.tasks[0].instruction = Instruction::Eval(BoolOp::Const(false));
1356        let Instruction::Rus {
1357            decoder_hold,
1358            cultivation_exits,
1359            ..
1360        } = &mut program.tasks[1].instruction
1361        else {
1362            unreachable!()
1363        };
1364        *decoder_hold = Box::new([MemoryCycle {
1365            stream,
1366            round_duration: 1.0,
1367            ..MemoryCycle::default()
1368        }]);
1369        *cultivation_exits = Box::new([0]);
1370        assert!(matches!(
1371            run(&program, RuntimeConfig::default()),
1372            Err(RuntimeError::InvalidProgram(
1373                "RUS decoder hold has invalid data support"
1374            ))
1375        ));
1376    }
1377
1378    #[test]
1379    fn fixed_register_rejects_out_of_range_quantum_operand() {
1380        let program = Program {
1381            qubit_count: 1,
1382            tasks: vec![Task {
1383                release: 0.0,
1384                source: None,
1385                dependencies: Box::default(),
1386                activation: None,
1387                output: None,
1388                qubits: Box::default(),
1389                duration: 0.0,
1390                label: "bad reset".into(),
1391                instruction: Instruction::Quantum(QuantumTask {
1392                    alternatives: vec![QuantumAlternative {
1393                        when: Box::default(),
1394                        stream: QuantumStream {
1395                            moments: vec![Moment {
1396                                kind: None,
1397                                duration: 0.0,
1398                                operations: vec![QuantumOp::Reset {
1399                                    basis: Pauli::Z,
1400                                    qubit: 1,
1401                                }]
1402                                .into_boxed_slice(),
1403                            }]
1404                            .into_boxed_slice(),
1405                        },
1406                        selected_records: Box::default(),
1407                        excluded_records: Box::default(),
1408                        detectors: Box::default(),
1409                        restarts: Box::default(),
1410                    }]
1411                    .into_boxed_slice(),
1412                }),
1413            }],
1414            entry: Stream {
1415                tasks: vec![0].into_boxed_slice(),
1416                ..Stream::default()
1417            },
1418            decoder_latency_rounds: 10,
1419            ..Program::default()
1420        };
1421        assert!(matches!(
1422            run(&program, RuntimeConfig::default()),
1423            Err(RuntimeError::InvalidProgram("invalid quantum operand"))
1424        ));
1425        assert!(matches!(
1426            validate_product(
1427                &PauliProduct {
1428                    negative: false,
1429                    terms: vec![(0, Pauli::X), (0, Pauli::Z)].into_boxed_slice(),
1430                },
1431                1,
1432            ),
1433            Err(RuntimeError::InvalidProgram("invalid Pauli operand"))
1434        ));
1435    }
1436
1437    #[test]
1438    fn vm_y_product_keeps_the_backend_hermitian_phase() {
1439        let product = engine_pauli_product(
1440            &PauliProduct {
1441                negative: true,
1442                terms: vec![(0, Pauli::Y)].into_boxed_slice(),
1443            },
1444            1,
1445        )
1446        .unwrap();
1447        Simulator::with_seed(1, 0)
1448            .measure_observable(&product)
1449            .unwrap();
1450    }
1451
1452    #[test]
1453    fn logical_bloch_applies_crossed_frame_signs() {
1454        let mut simulator = Simulator::with_seed(1, 0);
1455        simulator.h(0);
1456        let result = RunResult {
1457            simulator,
1458            artifact: ExecutionArtifact {
1459                final_bits: vec![Some(false), Some(true)],
1460                ..ExecutionArtifact::default()
1461            },
1462        };
1463        let output = LogicalOutput {
1464            port: [0; 3],
1465            x: PauliProduct {
1466                negative: false,
1467                terms: vec![(0, Pauli::X)].into_boxed_slice(),
1468            },
1469            z: PauliProduct {
1470                negative: false,
1471                terms: vec![(0, Pauli::Z)].into_boxed_slice(),
1472            },
1473            frame_x: 0,
1474            frame_z: 1,
1475        };
1476        assert_eq!(result.logical_bloch(&output).unwrap(), (-1.0, 0.0, 0.0));
1477    }
1478}