1mod 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21pub struct RuntimeLimits {
22 pub max_steps: u64,
24 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#[derive(Debug, Clone)]
39pub struct RuntimeConfig {
40 pub seed: u64,
42 pub input_state: LogicalInputState,
44 pub decoder: MockDecoderConfig,
46 pub idle_error_rate: f64,
49 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(rename_all = "snake_case")]
68pub enum LogicalInputState {
69 #[default]
71 Plus,
72 Zero,
74}
75
76#[derive(Debug)]
78pub struct RunResult {
79 pub simulator: Simulator,
81 pub artifact: ExecutionArtifact,
83}
84
85impl RunResult {
86 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 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#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
134pub struct ExecutionArtifact {
135 pub metadata: RunMetadata,
137 pub measurements: Vec<MeasurementRecord>,
139 pub detectors: Vec<DetectorResult>,
141 pub observables: Vec<ObservableResult>,
143 pub timing: Vec<TaskTiming>,
145 pub retries: Vec<RetryRecord>,
147 pub decoder_decisions: Vec<DecoderDecisionRecord>,
149 pub events: Vec<ExecutionEvent>,
151 pub discarded: bool,
153 pub stop_reason: Option<StopReason>,
155 pub finished_at: f64,
157 pub peak_rank: usize,
159 pub final_bits: Vec<Option<bool>>,
161}
162
163#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
165pub struct RunMetadata {
166 pub backend: String,
168 pub seed: u64,
170 pub input_state: LogicalInputState,
172 pub decoder: MockDecoderConfig,
174 pub decoder_latency_rounds: u32,
176 pub idle_error_rate: f64,
178 pub limits: RuntimeLimits,
180 pub tasks: Vec<TaskMetadata>,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186pub struct TaskMetadata {
187 pub task: TaskId,
189 pub label: String,
191 pub instruction: String,
193 pub origin: Option<TaskOrigin>,
195 pub dependencies: Vec<TaskId>,
197 pub wait_until: Vec<TaskId>,
199 pub output: Option<BitId>,
201}
202
203impl ExecutionArtifact {
204 pub fn to_json(&self) -> Result<String, serde_json::Error> {
210 serde_json::to_string(self)
211 }
212
213 pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
219 serde_json::to_string_pretty(self)
220 }
221}
222
223#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
225pub struct MeasurementRecord {
226 pub record: RecordId,
228 pub value: bool,
230 pub task: TaskId,
232 pub attempt: Option<u64>,
234 pub time: f64,
236 pub committed: bool,
238}
239
240#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
242pub struct DetectorResult {
243 pub sequence: u32,
245 pub task: TaskId,
247 pub value: bool,
249 pub attempt: Option<u64>,
251 pub time: f64,
253 pub committed: bool,
255}
256
257#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
259pub struct ObservableResult {
260 pub index: u32,
262 pub task: TaskId,
264 pub raw: Option<bool>,
266 pub value: Option<bool>,
269 pub attempt: Option<u64>,
271 pub time: f64,
273 pub committed: bool,
275}
276
277#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
279pub struct DecoderDecisionRecord {
280 pub task: TaskId,
282 pub decision: DecoderDecision,
284 pub committed: bool,
286}
287
288#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
290pub struct TaskTiming {
291 pub task: TaskId,
293 pub label: String,
295 pub instruction: String,
297 pub attempt: Option<u64>,
299 pub start: f64,
301 pub end: f64,
303}
304
305#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
307pub struct RetryRecord {
308 pub task: TaskId,
310 pub attempt: u32,
312 pub epoch: u64,
314 pub start: f64,
316 pub end: f64,
318 pub accepted: bool,
320}
321
322#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
324#[serde(rename_all = "snake_case")]
325pub enum StopReason {
326 Discard,
328 AttemptLimit,
330}
331
332#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
334#[serde(rename_all = "snake_case")]
335pub enum MemoryKind {
336 StaticPadding,
338 DynamicWait,
340 FactoryGap,
342}
343
344#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
353#[serde(rename_all = "snake_case")]
354pub enum WaitReason {
355 Synchronization,
357 CausalCut,
359 DecoderLatency,
361}
362
363#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
365#[serde(tag = "kind", rename_all = "snake_case")]
366pub enum ExecutionEvent {
367 SourceReleased {
369 task: TaskId,
371 role: String,
373 time: f64,
375 },
376 InputArrival {
378 task: TaskId,
380 port: [i32; 3],
382 state: LogicalInputState,
384 time: f64,
386 },
387 MomentIssued {
389 task: TaskId,
391 attempt: Option<u64>,
393 moment: u32,
395 start: f64,
397 end: f64,
399 },
400 MomentCompleted {
402 task: TaskId,
404 attempt: Option<u64>,
406 moment: u32,
408 time: f64,
410 },
411 EarlyRestart {
413 task: TaskId,
415 attempt: u32,
417 reason: String,
419 time: f64,
421 },
422 DecoderDeadline {
424 task: TaskId,
426 observable: TaskId,
428 factory: bool,
430 measurements_ready_at: Option<f64>,
432 requested_at: f64,
434 ready_at: f64,
436 },
437 JoinReleased {
439 task: TaskId,
441 time: f64,
443 },
444 QuantumAlternative {
446 task: TaskId,
448 alternative: u32,
450 operations: Vec<String>,
452 time: f64,
454 },
455 ConditionalCorrection {
457 task: TaskId,
459 record: RecordId,
461 pauli: String,
463 applied: bool,
465 time: f64,
467 },
468 RusAttemptStarted {
470 task: TaskId,
472 attempt: u32,
474 epoch: u64,
476 time: f64,
478 },
479 RetryPrepare {
481 task: TaskId,
483 attempt: u32,
485 start: f64,
487 end: f64,
489 },
490 RusAttemptRejected {
492 task: TaskId,
494 attempt: u32,
496 epoch: u64,
498 time: f64,
500 },
501 RusAttemptAccepted {
503 task: TaskId,
505 attempt: u32,
507 epoch: u64,
509 time: f64,
511 },
512 FactoryReady {
514 resource: ResourceId,
516 time: f64,
518 },
519 MemoryRound {
521 task: TaskId,
523 round: u32,
525 memory_kind: MemoryKind,
527 wait_reason: Option<WaitReason>,
530 start: f64,
532 end: f64,
534 },
535 Idle {
537 task: TaskId,
539 wait_reason: Option<WaitReason>,
542 start: f64,
544 end: f64,
546 },
547 Discarded {
549 task: TaskId,
551 reason: StopReason,
553 time: f64,
555 },
556}
557
558#[derive(Debug, thiserror::Error)]
564#[non_exhaustive]
565pub enum RuntimeError {
566 #[error("{0} identifier space exhausted")]
568 IdOverflow(&'static str),
569 #[error("cannot allocate {register} runtime table: {source}")]
571 RegisterAllocation {
572 register: &'static str,
574 #[source]
576 source: std::collections::TryReserveError,
577 },
578 #[error("invalid VM program: {0}")]
580 InvalidProgram(&'static str),
581 #[error("task {0} is unavailable at its scheduled use")]
583 MissingTask(TaskId),
584 #[error("classical bit {0} is unavailable")]
586 MissingBit(BitId),
587 #[error("measurement record {0} is unavailable")]
589 MissingRecord(RecordId),
590 #[error("runtime step cap {limit} exceeded")]
592 StepLimit {
593 limit: u64,
595 },
596 #[error("quantum task has no selected alternative")]
598 NoQuantumAlternative,
599 #[error("quantum backend error: {0}")]
601 Backend(#[from] SimError),
602 #[error("mock decoder error: {0}")]
604 Decoder(#[from] DecoderError),
605 #[error("runtime clock overflowed")]
607 InvalidTiming,
608}
609
610pub 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}