quil_rs/program/scheduling/
graph.rs

1//! Utilities for analysis of the dependency graph of a Quil Program
2
3// Copyright 2021 Rigetti Computing
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17use std::collections::{HashMap, HashSet};
18
19use petgraph::graphmap::GraphMap;
20use petgraph::Directed;
21
22use self::dependency_queue::DependencyQueue;
23use crate::instruction::{
24    ExternSignatureMap, FrameIdentifier, Instruction, InstructionHandler, Target,
25};
26use crate::program::analysis::{
27    BasicBlock, BasicBlockOwned, BasicBlockTerminator, ControlFlowGraph,
28};
29use crate::{instruction::InstructionRole, program::Program, quil::Quil};
30
31pub use crate::program::memory::MemoryAccessType;
32
33mod dependency_queue;
34
35#[derive(Debug, Clone, Copy)]
36pub enum ScheduleErrorVariant {
37    DuplicateLabel,
38    Extern,
39    UncalibratedInstruction,
40    UnresolvedCallInstruction,
41    ControlFlowNotBlockTerminator,
42    UnschedulableInstruction,
43}
44
45#[derive(Debug, Clone, thiserror::Error)]
46#[error(
47    "Error scheduling {}: {}: {variant:?}",
48    .instruction_node.map_or_else(||  "an instruction".to_string(), |node| node.to_string()),
49    .instruction.to_quil_or_debug(),
50)]
51pub struct ScheduleError {
52    pub instruction_node: Option<ScheduledGraphNode>,
53    pub instruction: Instruction,
54    pub variant: ScheduleErrorVariant,
55}
56
57pub type ScheduleResult<T> = Result<T, ScheduleError>;
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Hash, Ord)]
60pub enum ScheduledGraphNode {
61    BlockStart,
62    InstructionIndex(usize),
63    BlockEnd,
64}
65
66impl std::fmt::Display for ScheduledGraphNode {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        match self {
69            Self::BlockStart => write!(f, "the start of the block"),
70            Self::InstructionIndex(ix) => write!(f, "instruction {ix}"),
71            Self::BlockEnd => write!(f, "the end-of-block terminator"),
72        }
73    }
74}
75
76/// A MemoryAccessDependency expresses a dependency that one node has on another to complete
77/// some type of memory access prior to the dependent node's execution.
78#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
79struct MemoryAccessDependency {
80    /// What type of memory access must complete prior to the downstream instruction.
81    // NOTE: This must remain the first field for ordering to work as expected.
82    pub access_type: MemoryAccessType,
83
84    /// Which node is using the given `access_type`.
85    pub node_id: ScheduledGraphNode,
86}
87
88#[derive(Clone, Debug, Eq, PartialEq, Hash)]
89pub enum ExecutionDependency {
90    /// The downstream instruction must wait for the given operation to complete.
91    AwaitMemoryAccess(MemoryAccessType),
92
93    /// The schedule of the downstream instruction depends on the upstream instruction.
94    /// Per the Quil-T specification, the downstream instruction begins execution at
95    /// the time that its latest upstream neighbor completes.
96    Scheduled,
97
98    /// The ordering between these two instructions must remain unchanged
99    StableOrdering,
100}
101
102/// Add a dependency to an edge on the graph, whether that edge currently exists or not.
103macro_rules! add_dependency {
104    ($graph:expr, $source:expr => $target:expr, $dependency:expr) => {{
105        let source = $source;
106        let target = $target;
107        let dependency = $dependency;
108        match $graph.edge_weight_mut(source, target) {
109            Some(edge) => {
110                edge.insert(dependency);
111            }
112            None => {
113                let mut edge = HashSet::new();
114                edge.insert(dependency);
115                $graph.add_edge(source, target, edge);
116            }
117        }
118    }};
119}
120
121pub type DependencyGraph = GraphMap<ScheduledGraphNode, HashSet<ExecutionDependency>, Directed>;
122
123/// A [`ScheduledBasicBlock`] is a wrapper around a [`BasicBlock`] which includes a graph expressing the vector clock
124/// among the instructions according to the Quil specification.
125///
126/// If instruction A blocks instruction B (because of shared use of a frame), then there will be an edge from A to B
127/// in the graph.
128#[derive(Clone, Debug)]
129pub struct ScheduledBasicBlock<'a> {
130    basic_block: BasicBlock<'a>,
131    pub(super) graph: DependencyGraph,
132}
133
134/// How an instruction affects a frame.
135///
136/// An instruction may do one of two things:
137///
138/// 1. It may [*block*][Self::Blocking] the frame, which indicates that while it does not play on
139///    that frame itself, it must prevent any other instructions from playing on it.
140///
141/// 2. It may [*use*][Self::Using] the frame, which indicates that the instruction play on or
142///    modifies that frame.
143///
144/// These may be thought of as reads and writes of a frame, respectively: blocking does not have an
145/// affect on the frame but must be ordered with respect to uses, while uses must happen in a
146/// specific order.  This is relevant for [`dependency_queue::Access`].
147///
148/// See also [`crate::program::MatchedFrames`] for the type that keeps track of the specific frames
149/// that are blocked or used by an instruction.
150#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)]
151pub enum InstructionFrameInteraction {
152    Blocking,
153    Using,
154}
155
156impl<'a> ScheduledBasicBlock<'a> {
157    /// Build a scheduled basic block from a basic block and a program.
158    pub fn build(
159        basic_block: BasicBlock<'a>,
160        program: &'a Program,
161        custom_handler: &mut InstructionHandler,
162    ) -> ScheduleResult<Self> {
163        let mut graph: DependencyGraph = GraphMap::new();
164        // Root node
165        graph.add_node(ScheduledGraphNode::BlockStart);
166
167        // The set of classical instructions that do not have outgoing edges (i.e. there are no
168        // downstream instructions that depend on them). After iterating over all instructions,
169        // the set of trailing classical instructions will need an outgoing edge to the block end.
170        let mut trailing_classical_instructions: HashSet<ScheduledGraphNode> = HashSet::new();
171
172        // Store the instruction index of the last instruction to block that frame
173        let mut last_instruction_by_frame: HashMap<
174            FrameIdentifier,
175            DependencyQueue<InstructionFrameInteraction>,
176        > = HashMap::new();
177
178        let mut last_timed_instruction_by_frame: HashMap<
179            FrameIdentifier,
180            DependencyQueue<InstructionFrameInteraction>,
181        > = HashMap::new();
182
183        // Keep track of memory accesses (reads/writes) to each region, so that they can be
184        // serialized.  This serialization is done by memory region; that is, writes to `region[0]`
185        // are not allowed to be concurrent with writes to `region[1]`.
186        //
187        // This could be refined to serialize writes to each memory region *and offset*, so writes
188        // to `region[0]` could happen in parallel with writes to `region[1]`, although this would
189        // require further refinement of [`Instruction::get_memory_accesses`] and the
190        // [`crate::program::memory::MemoryAccesses`] type.  In particular, we would need to capture
191        // accesses that read from/write to potentially an entire region, such as `LOAD` and
192        // `STORE`, as well as accesses that only access a statically known index.
193        let mut pending_memory_access: HashMap<String, DependencyQueue<MemoryAccessType>> =
194            HashMap::new();
195
196        let extern_signature_map = ExternSignatureMap::try_from(program.extern_pragma_map.clone())
197            .map_err(|(pragma, _)| ScheduleError {
198                instruction_node: None,
199                instruction: Instruction::Pragma(pragma),
200                variant: ScheduleErrorVariant::Extern,
201            })?;
202
203        let terminator = basic_block.terminator().clone().into_instruction();
204        let terminator_ref = terminator.as_ref();
205
206        let instructions_iter = basic_block
207            .instructions()
208            .iter()
209            .enumerate()
210            .map(|(index, instr)| (ScheduledGraphNode::InstructionIndex(index), *instr))
211            .chain(terminator_ref.map(|instr| (ScheduledGraphNode::BlockEnd, instr)));
212
213        for (node, instruction) in instructions_iter {
214            graph.add_node(node);
215
216            let accesses = custom_handler
217                .memory_accesses(instruction, &extern_signature_map)
218                .map_err(|_| ScheduleError {
219                    instruction_node: Some(node),
220                    instruction: instruction.clone(),
221                    variant: ScheduleErrorVariant::UnresolvedCallInstruction,
222                })?;
223
224            let memory_dependencies = [
225                (accesses.reads, MemoryAccessType::Read),
226                (accesses.writes, MemoryAccessType::Write),
227                (accesses.captures, MemoryAccessType::Capture),
228            ]
229            .into_iter()
230            .flat_map(|(regions, access_type)| {
231                regions
232                    .into_iter()
233                    .flat_map(|region| {
234                        pending_memory_access
235                            .entry(region)
236                            .or_default()
237                            // NOTE: This mutates the underlying `DependencyQueue` by registering
238                            // the instruction node.
239                            .record_access_and_get_dependencies(node, access_type)
240                    })
241                    // We have to `collect` into a `Vec` to finish our accesses to
242                    // `pending_memory_access`.
243                    .collect::<Vec<_>>()
244            })
245            .collect::<Vec<_>>();
246            let no_memory_dependencies = memory_dependencies.is_empty();
247            for memory_dependency in memory_dependencies {
248                // Test to make sure that no instructions depend directly on themselves
249                if memory_dependency.node_id != node {
250                    let execution_dependency =
251                        ExecutionDependency::AwaitMemoryAccess(memory_dependency.access_type);
252                    add_dependency!(graph, memory_dependency.node_id => node, execution_dependency);
253                    // This memory dependency now has an outgoing edge, so it is no longer a
254                    // trailing classical instruction. If the memory dependency is not a classical
255                    // instruction, this has no effect.
256                    trailing_classical_instructions.remove(&memory_dependency.node_id);
257                }
258            }
259
260            match custom_handler.role_for_instruction(instruction) {
261                // Classical instructions must be ordered by appearance in the program
262                InstructionRole::ClassicalCompute => {
263                    // If this instruction has no memory dependencies, it is a leading classical
264                    // instruction and needs an incoming edge from the block start.
265                    if no_memory_dependencies {
266                        add_dependency!(graph, ScheduledGraphNode::BlockStart => node, ExecutionDependency::StableOrdering);
267                    }
268                    trailing_classical_instructions.insert(node);
269                    Ok(())
270                }
271                InstructionRole::RFControl => {
272                    let matched_frames = custom_handler.matching_frames(instruction, program);
273                    let is_scheduled = custom_handler.is_scheduled(instruction);
274
275                    if let Some(matched_frames) = matched_frames {
276                        for frame in matched_frames.used {
277                            if is_scheduled {
278                                let previous_node_ids = last_timed_instruction_by_frame
279                                    .entry((*frame).clone())
280                                    .or_default()
281                                    .record_access_and_get_dependencies(
282                                        node,
283                                        InstructionFrameInteraction::Using,
284                                    );
285
286                                for previous_node_id in previous_node_ids {
287                                    add_dependency!(graph, previous_node_id => node, ExecutionDependency::Scheduled);
288                                }
289                            }
290
291                            let previous_node_ids = last_instruction_by_frame
292                                .entry((*frame).clone())
293                                .or_default()
294                                .record_access_and_get_dependencies(
295                                    node,
296                                    InstructionFrameInteraction::Using,
297                                );
298
299                            for previous_node_id in previous_node_ids {
300                                add_dependency!(graph, previous_node_id => node, ExecutionDependency::StableOrdering);
301                            }
302                        }
303
304                        for frame in matched_frames.blocked {
305                            if is_scheduled {
306                                let previous_node_ids = last_timed_instruction_by_frame
307                                    .entry((*frame).clone())
308                                    .or_default()
309                                    .record_access_and_get_dependencies(
310                                        node,
311                                        InstructionFrameInteraction::Blocking,
312                                    );
313                                for previous_node_id in previous_node_ids {
314                                    add_dependency!(graph, previous_node_id => node, ExecutionDependency::Scheduled);
315                                }
316                            }
317
318                            let previous_node_ids = last_instruction_by_frame
319                                .entry((*frame).clone())
320                                .or_default()
321                                .record_access_and_get_dependencies(
322                                    node,
323                                    InstructionFrameInteraction::Blocking,
324                                );
325                            for previous_node_id in previous_node_ids {
326                                add_dependency!(graph, previous_node_id => node, ExecutionDependency::StableOrdering);
327                            }
328                        }
329                    }
330
331                    Ok(())
332                }
333                InstructionRole::ControlFlow => {
334                    if let ScheduledGraphNode::BlockEnd = node {
335                        Ok(())
336                    } else {
337                        Err(ScheduleError {
338                            instruction_node: Some(node),
339                            instruction: instruction.clone(),
340                            variant: ScheduleErrorVariant::ControlFlowNotBlockTerminator,
341                        })
342                    }
343                }
344                InstructionRole::ProgramComposition => Err(ScheduleError {
345                    instruction_node: Some(node),
346                    instruction: instruction.clone(),
347                    variant: ScheduleErrorVariant::UnschedulableInstruction,
348                }),
349            }?;
350        }
351
352        // Link all pending dependency nodes to the end of the block, to ensure that the block
353        // does not terminate until these are complete
354        for trailing_classical_instruction in trailing_classical_instructions {
355            add_dependency!(graph, trailing_classical_instruction => ScheduledGraphNode::BlockEnd, ExecutionDependency::StableOrdering);
356        }
357
358        for node in last_timed_instruction_by_frame
359            .into_values()
360            .flat_map(DependencyQueue::into_pending_dependencies)
361        {
362            add_dependency!(graph, node => ScheduledGraphNode::BlockEnd, ExecutionDependency::Scheduled);
363        }
364
365        for node in last_instruction_by_frame
366            .into_values()
367            .flat_map(DependencyQueue::into_pending_dependencies)
368        {
369            add_dependency!(graph, node => ScheduledGraphNode::BlockEnd, ExecutionDependency::StableOrdering);
370        }
371
372        // Maintain the invariant that the block start node has a connecting path to the block end node.
373        if basic_block.instructions().is_empty() {
374            add_dependency!(graph, ScheduledGraphNode::BlockStart => ScheduledGraphNode::BlockEnd, ExecutionDependency::StableOrdering);
375        }
376
377        Ok(ScheduledBasicBlock { graph, basic_block })
378    }
379
380    pub fn get_dependency_graph(&self) -> &DependencyGraph {
381        &self.graph
382    }
383
384    pub fn instructions(&'a self) -> &'a [&'a Instruction] {
385        self.basic_block.instructions()
386    }
387
388    /// Return a particular-indexed instruction (if present).
389    pub fn get_instruction(&self, node_id: usize) -> Option<&Instruction> {
390        self.instructions().get(node_id).copied()
391    }
392
393    pub fn label(&self) -> Option<&Target> {
394        self.basic_block.label()
395    }
396
397    /// Return the count of executable instructions in this block.
398    pub fn len(&self) -> usize {
399        self.instructions().len()
400    }
401
402    /// Return true if this block contains no executable instructions.
403    pub fn is_empty(&self) -> bool {
404        self.instructions().is_empty()
405    }
406
407    pub fn terminator(&self) -> &BasicBlockTerminator<'a> {
408        self.basic_block.terminator()
409    }
410
411    pub fn basic_block(&self) -> &BasicBlock<'a> {
412        &self.basic_block
413    }
414}
415
416/// A program broken down into its [`ScheduledBasicBlock`]s.  All instruction-level scheduling in a
417/// program is intra-block; the only dependencies between basic blocks are those resulting from
418/// execution flow.  For instance, we do *not* track memory dependencies from a write in one block to
419/// a read in a subsequent block.
420#[derive(Clone, Debug)]
421pub struct ScheduledProgram<'a> {
422    basic_blocks: Vec<ScheduledBasicBlock<'a>>,
423}
424
425impl<'a> ScheduledProgram<'a> {
426    /// Structure a sequential program
427    pub fn from_program(
428        program: &'a Program,
429        custom_handler: &mut InstructionHandler,
430    ) -> ScheduleResult<Self> {
431        let control_flow_graph = ControlFlowGraph::from(program);
432        Ok(Self {
433            basic_blocks: control_flow_graph
434                .into_blocks()
435                .into_iter()
436                .map(|block| ScheduledBasicBlock::build(block, program, custom_handler))
437                .collect::<ScheduleResult<Vec<_>>>()?,
438        })
439    }
440
441    pub fn basic_blocks(&self) -> &[ScheduledBasicBlock<'_>] {
442        self.basic_blocks.as_ref()
443    }
444
445    pub fn into_basic_blocks(self) -> Vec<ScheduledBasicBlock<'a>> {
446        self.basic_blocks
447    }
448}
449
450#[derive(Clone, Debug)]
451pub struct ScheduledBasicBlockOwned {
452    basic_block: BasicBlockOwned,
453    graph: DependencyGraph,
454}
455
456impl<'a> From<&'a ScheduledBasicBlockOwned> for ScheduledBasicBlock<'a> {
457    fn from(block: &'a ScheduledBasicBlockOwned) -> Self {
458        Self {
459            basic_block: (&block.basic_block).into(),
460            graph: block.graph.clone(),
461        }
462    }
463}
464
465impl From<ScheduledBasicBlock<'_>> for ScheduledBasicBlockOwned {
466    fn from(block: ScheduledBasicBlock) -> Self {
467        Self {
468            basic_block: block.basic_block.into(),
469            graph: block.graph.clone(),
470        }
471    }
472}
473
474#[cfg(all(test, feature = "graphviz-dot"))]
475mod graphviz_dot_tests {
476    use super::*;
477
478    use crate::program::scheduling::graphviz_dot::tests::build_dot_format_snapshot_test_case;
479
480    mod custom_handler {
481        use super::*;
482
483        use crate::instruction::Pragma;
484        use crate::instruction::PragmaArgument;
485        use crate::program::frame::FrameMatchCondition;
486        use crate::program::{MatchedFrames, MemoryAccesses};
487
488        /// Generates a custom [`InstructionHandler`] that specially handles two `PRAGMA` instructions:
489        ///
490        /// - `NO-OP` is considered a `ClassicalCompute` instruction that does nothing
491        /// - `RAW-INSTRUCTION` is an `RFControl` instruction that is scheduled on all frames by default
492        ///   or the frame names specified as arguments, and reads from `ro`.
493        ///
494        /// Note that any program being tested must define at least one frame for `RAW-INSTRUCTION` to
495        /// have any effect.
496        fn get_custom_handler() -> InstructionHandler {
497            const NO_OP: &str = "NO-OP";
498            const RAW_INSTRUCTION: &str = "RAW-INSTRUCTION";
499
500            InstructionHandler::default()
501                .set_is_scheduled(|instruction| match instruction {
502                    Instruction::Pragma(Pragma { name, .. }) if name == NO_OP => Some(false),
503                    Instruction::Pragma(Pragma { name, .. }) if name == RAW_INSTRUCTION => {
504                        Some(true)
505                    }
506                    _ => None,
507                })
508                .set_role_for_instruction(|instruction| match instruction {
509                    Instruction::Pragma(Pragma { name, .. }) if name == NO_OP => {
510                        Some(InstructionRole::ClassicalCompute)
511                    }
512                    Instruction::Pragma(Pragma { name, .. }) if name == RAW_INSTRUCTION => {
513                        Some(InstructionRole::RFControl)
514                    }
515                    _ => None,
516                })
517                .set_matching_frames(|instruction, program| match instruction {
518                    Instruction::Pragma(Pragma { name, .. }) if name == NO_OP => Some(None),
519                    Instruction::Pragma(Pragma {
520                        name, arguments, ..
521                    }) if name == RAW_INSTRUCTION => Some(Some({
522                        let frame_condition = if arguments.is_empty() {
523                            FrameMatchCondition::All
524                        } else {
525                            FrameMatchCondition::AnyOfNames(
526                                arguments
527                                    .iter()
528                                    .filter_map(|arg| match arg {
529                                        PragmaArgument::Identifier(name) => Some(name.as_str()),
530                                        PragmaArgument::Integer(_) => None,
531                                    })
532                                    .collect(),
533                            )
534                        };
535
536                        let used = program
537                            .frames
538                            .get_matching_keys_for_condition(frame_condition);
539
540                        MatchedFrames {
541                            used,
542                            blocked: HashSet::new(),
543                        }
544                    })),
545                    _ => None,
546                })
547                .set_memory_accesses(|instruction| match instruction {
548                    Instruction::Pragma(Pragma { name, .. }) if name == NO_OP => {
549                        Some(MemoryAccesses::default())
550                    }
551                    Instruction::Pragma(Pragma { name, .. }) if name == RAW_INSTRUCTION => Some({
552                        MemoryAccesses {
553                            captures: HashSet::new(),
554                            reads: [String::from("ro")].into(),
555                            writes: HashSet::new(),
556                        }
557                    }),
558                    _ => None,
559                })
560        }
561
562        build_dot_format_snapshot_test_case! {
563            only_pragmas_without_frames,
564            r#"
565DEFFRAME 0 "quux":
566    SAMPLE-RATE: 1.0
567    INITIAL-FREQUENCY: 1e8
568PRAGMA NO-OP
569PRAGMA RAW-INSTRUCTION
570PRAGMA RAW-INSTRUCTION
571PRAGMA NO-OP
572PRAGMA RAW-INSTRUCTION
573"#,
574            &mut get_custom_handler(),
575        }
576
577        build_dot_format_snapshot_test_case! {
578            only_pragmas_with_frames,
579            r#"
580DEFFRAME 0 "foo":
581    SAMPLE-RATE: 1.0
582    INITIAL-FREQUENCY: 1e8
583DEFFRAME 1 "bar":
584    SAMPLE-RATE: 1.0
585    INITIAL-FREQUENCY: 1e8
586
587PRAGMA NO-OP
588PRAGMA RAW-INSTRUCTION foo
589PRAGMA RAW-INSTRUCTION bar
590PRAGMA NO-OP
591PRAGMA RAW-INSTRUCTION foo bar
592PRAGMA NO-OP
593PRAGMA RAW-INSTRUCTION foo
594"#,
595            &mut get_custom_handler(),
596        }
597
598        build_dot_format_snapshot_test_case! {
599            mixed_pragmas_and_pulses,
600            r#"
601DEFFRAME 0 "foo":
602    SAMPLE-RATE: 1.0
603    INITIAL-FREQUENCY: 1e8
604DEFFRAME 1 "bar":
605    SAMPLE-RATE: 1.0
606    INITIAL-FREQUENCY: 1e8
607
608PRAGMA NO-OP
609PRAGMA RAW-INSTRUCTION foo
610PULSE 1 "bar" gaussian(duration: 1, fwhm: 2, t0: 3)
611PRAGMA RAW-INSTRUCTION foo bar
612PRAGMA NO-OP
613PULSE 0 "foo" gaussian(duration: 1, fwhm: 2, t0: 3)
614PRAGMA RAW-INSTRUCTION bar
615PULSE 0 "foo" gaussian(duration: 1, fwhm: 2, t0: 3)
616PULSE 1 "bar" gaussian(duration: 1, fwhm: 2, t0: 3)
617PRAGMA NO-OP
618PRAGMA RAW-INSTRUCTION foo
619"#,
620            &mut get_custom_handler(),
621        }
622    }
623
624    // Because any instruction that reads a particular region must be preceded by any earlier instructions that write to/ capture that memory region,
625    // we expect an edge from the first load to the second (0 -> 1).
626    build_dot_format_snapshot_test_case! {
627        classical_write_read_load_load,
628        r#"
629DECLARE params1 REAL[1]
630DECLARE params2 REAL[1]
631DECLARE params3 REAL[1]
632DECLARE integers INTEGER[1]
633LOAD params2[0] params3 integers[0] # writes params2 
634LOAD params1[0] params2 integers[0] # reads params2
635"#
636    }
637
638    // Because any instruction that reads a particular region must be preceded by any earlier instructions that write to/ capture that memory region,
639    // we expect an edge from the mul to the load (0 -> 1).
640    build_dot_format_snapshot_test_case! {
641        classical_write_read_mul_load,
642        r#"
643DECLARE params1 REAL[1]
644DECLARE params2 REAL[1]
645DECLARE integers INTEGER[1]
646
647MUL params2[0] 2 # reads and writes params2
648LOAD params1[0] params2 integers[0] # just reads params2
649"#
650    }
651
652    // Because any instruction that reads a particular region must be preceded by any earlier instructions that write to/ capture that memory region,
653    // we expect an edge from the mul to the add (0 -> 1).
654    build_dot_format_snapshot_test_case! {
655        classical_write_read_add_mul,
656        r#"
657DECLARE params1 REAL[1]
658DECLARE params2 REAL[1]
659DECLARE integers INTEGER[1]
660
661ADD params1[0] 1 # this reads and writes params1
662MUL params1[0] 2 # this reads and writes params1
663"#
664    }
665
666    // Because any instruction that reads a particular region must precede any later instructions that write to/ capture that memory region,
667    // we expect an edge from the first load to the second (0, 1).
668    build_dot_format_snapshot_test_case! {
669        classical_read_write_load_load,
670        r#"
671DECLARE params1 REAL[1]
672DECLARE params2 REAL[1]
673DECLARE integers INTEGER[1]
674
675LOAD params1[0] params2 integers[0] # reads params2
676LOAD params2[0] params3 integers[0] # writes params2
677"#
678    }
679
680    // Because any instruction that reads a particular region must precede any later instructions that write to/ capture that memory region,
681    // we expect an edge from the load to the mul (0, 1).
682    build_dot_format_snapshot_test_case! {
683        classical_read_write_load_mul,
684        r#"
685DECLARE params1 REAL[1]
686DECLARE params2 REAL[1]
687DECLARE integers INTEGER[1]
688
689LOAD params1[0] params2 integers[0] # reads params2
690MUL params2[0] 2                    # reads and writes params2
691"#
692    }
693
694    // Because memory reading and writing dependencies also apply to RfControl instructions, we
695    // expect edges from the first load to the first shift-phase (0 -> 1) as well as to the
696    // second load (0 -> 2), from the first shift-phase to the second load (1 -> 2), and from the
697    // second load to the second shift-phase (2 -> 3).
698    build_dot_format_snapshot_test_case! {
699        quantum_write_parameterized_operations,
700        r#"
701DEFFRAME 0 "rx":
702    INITIAL-FREQUENCY: 1e8
703DEFFRAME 1 "rx":
704    INITIAL-FREQUENCY: 1e8
705
706DECLARE params1 REAL[1]
707DECLARE params2 REAL[1]
708DECLARE integers INTEGER[1]
709
710LOAD params2[0] params1 integers[0] # writes params2
711SHIFT-PHASE 0 "rf" params2[0]       # reads params2
712LOAD params2[0] params1 integers[1] # writes params2
713SHIFT-PHASE 1 "rf" params2[0]       # reads params2
714"#
715    }
716
717    // Because a pragma by default will have no memory accesses, it should only have edges from the block start and to the block end.
718    build_dot_format_snapshot_test_case! {
719        classical_no_memory_pragma,
720        r#"PRAGMA example"#
721    }
722
723    build_dot_format_snapshot_test_case! {
724        write_capture_read,
725        r#"
726DECLARE bits BIT[1]
727DECLARE integers INTEGER[1]
728LOAD bits[0] bits2 integers[0] # write
729NONBLOCKING CAPTURE 0 "Transmon-0_readout_rx" flat(duration: 2.0000000000000003e-06, iq: 1.0, scale: 1.0, phase: 0.8745492960861506, detuning: 0.0) bits[0] # capture
730LOAD bits3[0] bits integers[0] # read
731"#
732    }
733
734    build_dot_format_snapshot_test_case! {
735        write_write_read,
736        r#"
737DECLARE bits BIT[1]
738DECLARE bits2 BIT[1]
739DECLARE bits3 BIT[1]
740DECLARE integers INTEGER[1]
741LOAD bits[0] bits2 integers[0] # write
742LOAD bits[0] bits3 integers[0] # write
743LOAD bits4[0] bits integers[0] # read
744"#
745    }
746
747    build_dot_format_snapshot_test_case! {
748        memory_dependency_not_in_block_terminator,
749        r#"
750DECLARE ro BIT
751DECLARE depends_on_ro BIT
752
753NONBLOCKING CAPTURE 0 "ro_rx" flat(duration: 2.0000000000000003e-06, iq: 1.0, scale: 1.0, phase: 0.8745492960861506, detuning: 0.0) ro
754MOVE depends_on_ro ro
755JUMP @eq
756LABEL @eq
757PULSE 0 "ro_tx" gaussian(duration: 1, fwhm: 2, t0: 3)
758"#
759    }
760
761    build_dot_format_snapshot_test_case! {
762        memory_dependency_in_block_terminator,
763        r#"
764DECLARE ro BIT
765
766NONBLOCKING CAPTURE 0 "ro_rx" flat(duration: 2.0000000000000003e-06, iq: 1.0, scale: 1.0, phase: 0.8745492960861506, detuning: 0.0) ro
767JUMP-WHEN @eq ro
768LABEL @eq
769PULSE 0 "ro_tx" gaussian(duration: 1, fwhm: 2, t0: 3)
770"#
771    }
772
773    build_dot_format_snapshot_test_case! {
774        no_memory_dependency_across_blocks,
775        r#"
776DECLARE ro BIT
777DECLARE depends_on_ro BIT
778
779NONBLOCKING CAPTURE 0 "ro_rx" flat(duration: 2.0000000000000003e-06, iq: 1.0, scale: 1.0, phase: 0.8745492960861506, detuning: 0.0) ro
780JUMP @eq
781LABEL @eq
782MOVE depends_on_ro ro
783PULSE 0 "ro_tx" gaussian(duration: 1, fwhm: 2, t0: 3)
784"#
785    }
786
787    build_dot_format_snapshot_test_case! {
788        memory_dependency_one_variable,
789        r#"DEFFRAME 0 "frame0":
790    DIRECTION: "tx"
791    INITIAL-FREQUENCY: 6864214214.214214
792    CENTER-FREQUENCY: 7250000000.0
793    HARDWARE-OBJECT: "{\"instrument_name\": \"tsunami00\", \"card_index\": 0, \"channel_type\": \"QGSx2Channel\", \"channel_index\": 0, \"sequencer_index\": 0, \"nco_index\": 0}"
794    SAMPLE-RATE: 1000000000.0
795DEFFRAME 1 "frame1":
796    DIRECTION: "tx"
797    INITIAL-FREQUENCY: 6864214214.214214
798    CENTER-FREQUENCY: 7250000000.0
799    HARDWARE-OBJECT: "{\"instrument_name\": \"tsunami00\", \"card_index\": 1, \"channel_type\": \"QGSx2Channel\", \"channel_index\": 0, \"sequencer_index\": 0, \"nco_index\": 0}"
800    SAMPLE-RATE: 1000000000.0
801
802DELAY 2e-8
803
804DECLARE phase REAL
805MOVE phase 0.1
806
807SET-PHASE 0 "frame0" 2*pi*phase
808SET-PHASE 1 "frame1" 2*pi*phase
809
810PULSE 0 "frame0" flat(iq: 1, duration: 4e-9)
811PULSE 1 "frame1" flat(iq: 1, duration: 4e-9)
812"#
813    }
814
815    build_dot_format_snapshot_test_case! {
816        memory_dependency_array,
817        r#"DEFFRAME 0 "frame0":
818    DIRECTION: "tx"
819    INITIAL-FREQUENCY: 6864214214.214214
820    CENTER-FREQUENCY: 7250000000.0
821    HARDWARE-OBJECT: "{\"instrument_name\": \"tsunami00\", \"card_index\": 0, \"channel_type\": \"QGSx2Channel\", \"channel_index\": 0, \"sequencer_index\": 0, \"nco_index\": 0}"
822    SAMPLE-RATE: 1000000000.0
823DEFFRAME 1 "frame1":
824    DIRECTION: "tx"
825    INITIAL-FREQUENCY: 6864214214.214214
826    CENTER-FREQUENCY: 7250000000.0
827    HARDWARE-OBJECT: "{\"instrument_name\": \"tsunami00\", \"card_index\": 1, \"channel_type\": \"QGSx2Channel\", \"channel_index\": 0, \"sequencer_index\": 0, \"nco_index\": 0}"
828    SAMPLE-RATE: 1000000000.0
829
830DELAY 2e-8
831
832DECLARE phase REAL[2]
833MOVE phase[0] 0.1
834MOVE phase[1] 0.1
835
836SET-PHASE 0 "frame0" 2*pi*phase[0]
837SET-PHASE 1 "frame1" 2*pi*phase[1]
838
839PULSE 0 "frame0" flat(iq: 1, duration: 4e-9)
840PULSE 1 "frame1" flat(iq: 1, duration: 4e-9)
841"#
842    }
843
844    build_dot_format_snapshot_test_case! {
845        memory_dependency_two_variables,
846        r#"DEFFRAME 0 "frame0":
847    DIRECTION: "tx"
848    INITIAL-FREQUENCY: 6864214214.214214
849    CENTER-FREQUENCY: 7250000000.0
850    HARDWARE-OBJECT: "{\"instrument_name\": \"tsunami00\", \"card_index\": 0, \"channel_type\": \"QGSx2Channel\", \"channel_index\": 0, \"sequencer_index\": 0, \"nco_index\": 0}"
851    SAMPLE-RATE: 1000000000.0
852DEFFRAME 1 "frame1":
853    DIRECTION: "tx"
854    INITIAL-FREQUENCY: 6864214214.214214
855    CENTER-FREQUENCY: 7250000000.0
856    HARDWARE-OBJECT: "{\"instrument_name\": \"tsunami00\", \"card_index\": 1, \"channel_type\": \"QGSx2Channel\", \"channel_index\": 0, \"sequencer_index\": 0, \"nco_index\": 0}"
857    SAMPLE-RATE: 1000000000.0
858
859DELAY 2e-8
860
861DECLARE phase0 REAL
862DECLARE phase1 REAL
863MOVE phase0 0.1
864MOVE phase1 0.1
865
866SET-PHASE 0 "frame0" 2*pi*phase0
867SET-PHASE 1 "frame1" 2*pi*phase1
868
869PULSE 0 "frame0" flat(iq: 1, duration: 4e-9)
870PULSE 1 "frame1" flat(iq: 1, duration: 4e-9)
871"#
872    }
873}