Repository: rigetti/quil-rs (language: python)

Issue:
The computed duration of `erfsquare` waveform templates is incorrect
`ScheduledBasicBlock::get_waveform_duration_seconds` assumes that the duration of all built-in waveforms is equal to their `duration` parameter.  This isn't true for `erfsquare`, though; the duration is just that of the main pulse, and the `padleft` and `padright` parameters specify silent padding on either side, which contributes to the total duration.

To see this, consider the following program:

```
DEFFRAME 0 "a":
    SAMPLE-RATE: 1e9
PULSE 0 "a" erfsquare(duration: 1.0, padleft: 0.2, padright: 0.3)
PULSE 0 "a" erfsquare(duration: 0.1, padleft: 0.7, padright: 0.7)
PULSE 0 "a" erfsquare(duration: 0.5, padleft: 0.6, padright: 0.4)
FENCE
```

If provided as a test case to `program::schedule::tests::schedule_seconds`, it will produce instruction timings of `[0.0, 1.0, 1.1, 1.6]`, when it should instead produce `[0.0, 1.5, 3.0, 4.5]`.


Discussion / hints:

Fix patch:
diff --git a/quil-rs/src/program/analysis/control_flow_graph.rs b/quil-rs/src/program/analysis/control_flow_graph.rs
index 98f60170..1128d9d0 100644
--- a/quil-rs/src/program/analysis/control_flow_graph.rs
+++ b/quil-rs/src/program/analysis/control_flow_graph.rs
@@ -745,8 +745,8 @@ DEFCAL CZ q0 q1:
     FENCE q0 q1
     SET-PHASE q0 "flux_tx_cz" 0.0
     SET-PHASE q1 "flux_tx_iswap" 0.0
-    NONBLOCKING PULSE q0 "flux_tx_cz" erf_square(duration: 6.000000000000001e-08)
-    NONBLOCKING PULSE q1 "flux_tx_iswap" erf_square(duration: 6.000000000000001e-08)
+    NONBLOCKING PULSE q0 "flux_tx_cz" erf_square(duration: 6.000000000000001e-08, pad_left: 0, pad_right: 0)
+    NONBLOCKING PULSE q1 "flux_tx_iswap" erf_square(duration: 6.000000000000001e-08, pad_left: 0, pad_right: 0)
     SHIFT-PHASE q0 "flux_tx_cz" 1.0
     SHIFT-PHASE q1 "flux_tx_iswap" 1.0
     FENCE q0 q1
diff --git a/quil-rs/src/program/scheduling/schedule.rs b/quil-rs/src/program/scheduling/schedule.rs
index 58d762c9..ac848fc0 100644
--- a/quil-rs/src/program/scheduling/schedule.rs
+++ b/quil-rs/src/program/scheduling/schedule.rs
@@ -210,9 +210,9 @@ impl<'p> ScheduledBasicBlock<'p> {
     fn get_waveform_duration_seconds(
         program: &Program,
         instruction: &Instruction,
-        waveform_invocation: &WaveformInvocation,
+        WaveformInvocation { name, parameters }: &WaveformInvocation,
     ) -> Option<Seconds> {
-        if let Some(definition) = program.waveforms.get(&waveform_invocation.name) {
+        if let Some(definition) = program.waveforms.get(name) {
             let sample_count = definition.matrix.len();
             let common_sample_rate =
                 program
@@ -243,11 +243,22 @@ impl<'p> ScheduledBasicBlock<'p> {
                 .map(|sample_rate| sample_count as f64 / sample_rate)
                 .map(Seconds)
         } else {
-            waveform_invocation
-                .parameters
-                .get("duration")
-                .and_then(|v| v.to_real().ok())
-                .map(Seconds)
+            // Per the Quil spec, all waveform templates have a "duration"
+            // parameter, and "erf_square" also has "pad_left" and "pad_right".
+            // We explicitly choose to be more flexible here, and allow any
+            // built-in waveform templates to have "pad_*" parameters, as well
+            // as allow "erf_square" to omit them.
+            let parameter = |parameter_name| {
+                parameters
+                    .get(parameter_name)
+                    .and_then(|v| v.to_real().ok())
+                    .map(Seconds)
+            };
+            Some(
+                parameter("duration")?
+                    + parameter("pad_left").unwrap_or(Seconds::zero())
+                    + parameter("pad_right").unwrap_or(Seconds::zero()),
+            )
         }
     }
 
@@ -404,6 +415,16 @@ PULSE 0 "a" flat(duration: 1.0)
     #[case(
         r#"DEFFRAME 0 "a":
     SAMPLE-RATE: 1e9
+PULSE 0 "a" erf_square(duration: 1.0, pad_left: 0.2, pad_right: 0.3)
+PULSE 0 "a" erf_square(duration: 0.1, pad_left: 0.7, pad_right: 0.7)
+PULSE 0 "a" erf_square(duration: 0.5, pad_left: 0.6, pad_right: 0.4)
+FENCE
+"#,
+        Ok(vec![0.0, 1.5, 3.0, 4.5])
+    )]
+    #[case(
+        r#"DEFFRAME 0 "a":
+    SAMPLE-RATE: 1e9
 DEFFRAME 0 "b":
     SAMPLE-RATE: 1e9
 NONBLOCKING PULSE 0 "a" flat(duration: 1.0)


Test patch:
diff --git a/quil-py/tests_py/program/test_program.py b/quil-py/tests_py/program/test_program.py
index 86f81179..93e35247 100644
--- a/quil-py/tests_py/program/test_program.py
+++ b/quil-py/tests_py/program/test_program.py
@@ -134,8 +134,8 @@ def test_basic_block_fixed_schedule():
     FENCE q0 q1
     SET-PHASE q0 "flux_tx_cz" 0.0
     SET-PHASE q1 "flux_tx_iswap" 0.0
-    NONBLOCKING PULSE q0 "flux_tx_cz" erf_square(duration: 6.000000000000001e-08)
-    NONBLOCKING PULSE q1 "flux_tx_iswap" erf_square(duration: 6.000000000000001e-08)
+    NONBLOCKING PULSE q0 "flux_tx_cz" erf_square(duration: 6.000000000000001e-08, pad_left: 0.1e-08, pad_right: 0.1e-08)
+    NONBLOCKING PULSE q1 "flux_tx_iswap" erf_square(duration: 6.000000000000001e-08, pad_left: 0.1e-08, pad_right: 0.1e-08)
     SHIFT-PHASE q0 "flux_tx_cz" 1.0
     SHIFT-PHASE q1 "flux_tx_iswap" 1.0
     FENCE q0 q1