Skip to content

Commit 129efe4

Browse files
committed
feat: error on misalignment in waveform sampling
1 parent c9b2b6e commit 129efe4

File tree

3 files changed

+252
-80
lines changed

3 files changed

+252
-80
lines changed

quil-rs/src/waveform/builtin.rs

Lines changed: 150 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use crate::{
1818
units::{Cycles, Radians},
1919
};
2020

21-
use super::sampling::IqSamples;
21+
use super::sampling::{IqSamples, SamplingError};
2222

2323
////////////////////////////////////////////////////////////////////////////////
2424
// General built-in waveform types
@@ -60,16 +60,18 @@ pub struct CommonBuiltinParameters {
6060
pub detuning: Option<f64>,
6161
}
6262

63-
/// Like [`CommonBuiltinParameters`], but with the defaults resolved.
63+
/// Like [`CommonBuiltinParameters`], but with the defaults resolved and the duration discretized.
6464
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
6565
#[cfg_attr(feature = "stubs", gen_stub_pyclass)]
6666
#[cfg_attr(
6767
feature = "python",
6868
pyo3::pyclass(module = "quil.waveforms", subclass, get_all, set_all, eq)
6969
)]
7070
pub struct ExplicitCommonBuiltinParameters {
71-
/// Full duration of the pulse (s)
72-
pub duration: f64,
71+
/// Integral number of samples that should be taken of the pulse
72+
///
73+
/// Corresponds to [`CommonBuiltinParameters::duration`].
74+
pub sample_count: f64,
7375
/// Scale to apply to waveform envelope
7476
pub scale: f64,
7577
/// Phase shift for the entire waveform
@@ -78,20 +80,54 @@ pub struct ExplicitCommonBuiltinParameters {
7880
pub detuning: f64,
7981
}
8082

81-
impl From<CommonBuiltinParameters> for ExplicitCommonBuiltinParameters {
82-
fn from(parameters: CommonBuiltinParameters) -> Self {
83+
impl CommonBuiltinParameters {
84+
/// Given a sample rate, return the corresponding [explicit
85+
/// parameters][ExplicitCommonBuiltinParameters].
86+
///
87+
/// For the three optional fields ([`scale`][Self::scale], [`phase`][Self::phase], and
88+
/// [`detuning`][Self::detuning]), the explicit version is either their original value (if
89+
/// present) or their default value (if missing).
90+
///
91+
/// For the [`duration`][Self::duration], the explicit version is [the integer number of
92+
/// samples][ExplicitCommonBuiltinParameters::sample_count] required to fill out the
93+
/// [`Self::duration`]. If the requested duration is misaligned with respect to the sample
94+
/// rate, returns an error.
95+
///
96+
/// *Misalignment* means that to fill the duration would require a nonintegral number of
97+
/// samples. This function will ignore any error less than 1% – that is, if the function would
98+
/// an integer number of samples plus one fractional sample of size between 99% and 101%, then
99+
/// that last sample is considered another integral sample.
100+
#[inline]
101+
pub fn resolve_with_sample_rate(
102+
self,
103+
sample_rate: f64,
104+
) -> Result<ExplicitCommonBuiltinParameters, SamplingError> {
83105
let CommonBuiltinParameters {
84106
duration,
85107
scale,
86108
phase,
87109
detuning,
88-
} = parameters;
89-
Self {
90-
duration,
110+
} = self;
111+
112+
let sample_count_fract = duration * sample_rate;
113+
let sample_count = sample_count_fract.round();
114+
let misalignment = sample_count_fract - sample_count;
115+
let max_misalignment = 1.0 / (sample_rate * 100.0);
116+
if misalignment.abs() >= max_misalignment {
117+
return Err(SamplingError::MisalignedDuration {
118+
duration,
119+
sample_rate,
120+
misalignment,
121+
max_misalignment,
122+
});
123+
}
124+
125+
Ok(ExplicitCommonBuiltinParameters {
126+
sample_count,
91127
scale: scale.unwrap_or(1.0),
92128
phase: phase.unwrap_or(Cycles(0.0)),
93129
detuning: detuning.unwrap_or(0.0),
94-
}
130+
})
95131
}
96132
}
97133

@@ -107,7 +143,7 @@ pub trait BuiltinWaveformParameters:
107143
self,
108144
common: CommonBuiltinParameters,
109145
sample_rate: f64,
110-
) -> IqSamples;
146+
) -> Result<IqSamples, SamplingError>;
111147
}
112148

113149
mod private {
@@ -266,7 +302,7 @@ impl BuiltinWaveformParameters for BuiltinWaveform {
266302
self,
267303
common: CommonBuiltinParameters,
268304
sample_rate: f64,
269-
) -> IqSamples {
305+
) -> Result<IqSamples, SamplingError> {
270306
match self {
271307
BuiltinWaveform::Flat(flat) => flat.iq_values_at_sample_rate(common, sample_rate),
272308

@@ -298,20 +334,19 @@ impl BuiltinWaveformParameters for Flat {
298334
self,
299335
common: CommonBuiltinParameters,
300336
sample_rate: f64,
301-
) -> IqSamples {
337+
) -> Result<IqSamples, SamplingError> {
302338
let ExplicitCommonBuiltinParameters {
303-
duration,
339+
sample_count,
304340
scale,
305341
phase,
306342
detuning,
307-
} = common.into();
343+
} = common.resolve_with_sample_rate(sample_rate)?;
308344
let Self { iq } = self;
309345

310-
// TODO: no verification of integer sample count
311-
let sample_count = ceiling_with_epsilon(duration * sample_rate) as usize;
346+
let sample_count = sample_count as usize;
312347
let scaled_iq = scale * iq;
313348

314-
if detuning == 0.0 {
349+
Ok(if detuning == 0.0 {
315350
IqSamples::Flat {
316351
iq: apply_phase(scaled_iq, phase),
317352
sample_count,
@@ -320,7 +355,7 @@ impl BuiltinWaveformParameters for Flat {
320355
let mut samples = vec![scaled_iq; sample_count];
321356
apply_phase_and_detuning(&mut samples, phase, detuning, sample_rate);
322357
IqSamples::Samples(samples)
323-
}
358+
})
324359
}
325360
}
326361

@@ -329,7 +364,7 @@ impl BuiltinWaveformParameters for Gaussian {
329364
self,
330365
common: CommonBuiltinParameters,
331366
sample_rate: f64,
332-
) -> IqSamples {
367+
) -> Result<IqSamples, SamplingError> {
333368
let Self { fwhm, t0 } = self;
334369

335370
build_samples_and_adjust_for_common_parameters(
@@ -349,7 +384,7 @@ impl BuiltinWaveformParameters for DragGaussian {
349384
self,
350385
common: CommonBuiltinParameters,
351386
sample_rate: f64,
352-
) -> IqSamples {
387+
) -> Result<IqSamples, SamplingError> {
353388
let Self {
354389
fwhm,
355390
t0,
@@ -379,7 +414,7 @@ impl BuiltinWaveformParameters for ErfSquare {
379414
self,
380415
common: CommonBuiltinParameters,
381416
sample_rate: f64,
382-
) -> IqSamples {
417+
) -> Result<IqSamples, SamplingError> {
383418
let CommonBuiltinParameters { duration, .. } = common;
384419
let Self {
385420
risetime,
@@ -413,7 +448,7 @@ impl BuiltinWaveformParameters for HermiteGaussian {
413448
self,
414449
common: CommonBuiltinParameters,
415450
sample_rate: f64,
416-
) -> IqSamples {
451+
) -> Result<IqSamples, SamplingError> {
417452
let Self {
418453
fwhm,
419454
t0,
@@ -447,19 +482,18 @@ impl BuiltinWaveformParameters for BoxcarKernel {
447482
self,
448483
common: CommonBuiltinParameters,
449484
sample_rate: f64,
450-
) -> IqSamples {
485+
) -> Result<IqSamples, SamplingError> {
451486
let ExplicitCommonBuiltinParameters {
452-
duration,
487+
sample_count,
453488
scale,
454489
phase,
455490
detuning,
456-
} = common.into();
491+
} = common.resolve_with_sample_rate(sample_rate)?;
457492
let Self = self; // Get errors if the definition changes
458493

459-
// TODO: no verification of integer sample count
460-
let sample_count = ceiling_with_epsilon(duration * sample_rate) as usize;
494+
let sample_count = sample_count as usize;
461495

462-
if detuning == 0.0 {
496+
Ok(if detuning == 0.0 {
463497
let iq = polar_to_rectangular(scale / sample_count as f64, phase);
464498
IqSamples::Flat { iq, sample_count }
465499
} else {
@@ -470,7 +504,7 @@ impl BuiltinWaveformParameters for BoxcarKernel {
470504
)
471505
});
472506
IqSamples::Samples(samples.collect())
473-
}
507+
})
474508
}
475509
}
476510

@@ -501,20 +535,19 @@ fn build_samples_and_adjust_for_common_parameters<I: IntoIterator<Item = Complex
501535
parameters: SamplingParameters,
502536
common: CommonBuiltinParameters,
503537
build: impl FnOnce(SamplingInfo) -> I,
504-
) -> IqSamples {
538+
) -> Result<IqSamples, SamplingError> {
505539
let SamplingParameters { sample_rate, fwhm } = parameters;
506540
let ExplicitCommonBuiltinParameters {
507-
duration,
541+
sample_count,
508542
scale,
509543
phase,
510544
detuning,
511-
} = common.into();
545+
} = common.resolve_with_sample_rate(sample_rate)?;
512546

513547
// It would be nice to be able to optimize `scale: 0.0` to `IqSamples::Flat`, but we'd have to
514548
// figure out how to take care of the `erf_squared` padding durations.
515549

516-
let length = ceiling_with_epsilon(duration * sample_rate);
517-
let time_steps = Array::range(0.0, length, 1.0) / sample_rate;
550+
let time_steps = Array::range(0.0, sample_count, 1.0) / sample_rate;
518551
let sigma = 0.5 * fwhm / (2.0 * LN_2).sqrt();
519552

520553
let mut samples: Vec<_> = build(SamplingInfo { time_steps, sigma })
@@ -526,7 +559,7 @@ fn build_samples_and_adjust_for_common_parameters<I: IntoIterator<Item = Complex
526559
apply_phase_and_detuning_at_index(scale * *sample, phase, detuning, sample_rate, index);
527560
}
528561

529-
IqSamples::Samples(samples)
562+
Ok(IqSamples::Samples(samples))
530563
}
531564

532565
/// Modulate and phase shift waveform IQ data in place.
@@ -562,20 +595,6 @@ fn apply_phase(iq_value: Complex64, phase: Cycles<f64>) -> Complex64 {
562595
iq_value * Complex64::cis(Radians::from(phase).0)
563596
}
564597

565-
/// A custom `ceil()` method that includes a machine-epsilon sized region above each integer in the
566-
/// set of values that are mapped to that integer. In other words:
567-
/// ceil_eps(x) = n, for all x and integers n s.t. n <= x < (n + n*epsilon)
568-
///
569-
/// To handle accumulated floating point errors in sweeps above typical floating point imprecision
570-
/// we make epsilon 10x larger than floating point epsilon.
571-
fn ceiling_with_epsilon(value: f64) -> f64 {
572-
// This isn't really what EPSILON is for:
573-
// <https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/>.
574-
// However, changing this now might break behavior, so we leave it be.
575-
let truncated = value - (value * 10.0 * f64::EPSILON);
576-
truncated.ceil()
577-
}
578-
579598
/// Convert polar coordinates to rectangular coordinates.
580599
fn polar_to_rectangular(magnitude: f64, angle: Cycles<f64>) -> Complex64 {
581600
Complex64::from_polar(magnitude, Radians::from(angle).0)
@@ -611,15 +630,18 @@ mod tests {
611630
#[case(-1.0, Cycles(0.0), Complex64::new(-0.1, 0.0))]
612631
#[case(0.0, Cycles(0.0), Complex64::new(0.0, 0.0))]
613632
fn boxcar_kernel(#[case] scale: f64, #[case] phase: Cycles<f64>, #[case] expected: Complex64) {
614-
match BoxcarKernel.iq_values_at_sample_rate(
615-
CommonBuiltinParameters {
616-
duration: 0.1,
617-
scale: Some(scale),
618-
phase: Some(phase),
619-
detuning: None,
620-
},
621-
100.0,
622-
) {
633+
match BoxcarKernel
634+
.iq_values_at_sample_rate(
635+
CommonBuiltinParameters {
636+
duration: 0.1,
637+
scale: Some(scale),
638+
phase: Some(phase),
639+
detuning: None,
640+
},
641+
100.0,
642+
)
643+
.unwrap()
644+
{
623645
IqSamples::Flat { iq, sample_count } => {
624646
assert_eq!(sample_count, 10);
625647
assert_almost_eq(iq, expected, 1e-10);
@@ -634,14 +656,74 @@ mod tests {
634656
}
635657

636658
#[rstest::rstest]
637-
#[case(0.0, 0.0)]
638-
#[case(-f64::EPSILON, 0.0)]
639-
#[case(f64::EPSILON, 1.0)]
640-
// Based on a past edge case
641-
#[case(8.800_000_000_000_001e-8 * 1.0e9, 88.0)]
642-
fn ceiling_with_epsilon(#[case] value: f64, #[case] expected: f64) {
643-
let result = super::ceiling_with_epsilon(value);
644-
assert_eq!(result, expected);
659+
#[case(0.0, 0.0, Some(0.0))]
660+
#[case(0.0, 1e9, Some(0.0))]
661+
#[case(1e9, 0.0, Some(0.0))]
662+
#[case(f64::EPSILON, 1.0, Some(0.0))]
663+
#[case(-f64::EPSILON, 1.0, Some(0.0))]
664+
#[case(0.9999999, 101.0, Some(101.0))]
665+
#[case(1.0000001, 101.0, Some(101.0))]
666+
#[case(0.99, 101.0, None)]
667+
#[case(1.01, 101.0, None)]
668+
#[case(8.800_000_000_000_001e-8, 1.0e9, Some(88.0))] // Based on a past edge case
669+
#[case(0.5, 3.0, None)]
670+
fn sample_count(
671+
#[case] duration: f64,
672+
#[case] sample_rate: f64,
673+
#[case] expected: Option<f64>,
674+
) {
675+
let actual = CommonBuiltinParameters {
676+
duration,
677+
scale: None,
678+
phase: None,
679+
detuning: None,
680+
}
681+
.resolve_with_sample_rate(sample_rate);
682+
683+
match (actual, expected) {
684+
(
685+
Ok(ExplicitCommonBuiltinParameters {
686+
sample_count: actual,
687+
..
688+
}),
689+
Some(expected),
690+
) => {
691+
assert_eq!(
692+
expected, actual,
693+
"duration = {duration} s,\n\
694+
sample_rate = {sample_rate} Hz,\n\
695+
expected = {expected} samples,\n\
696+
actual = {actual} samples"
697+
)
698+
}
699+
(Err(_), None) => {}
700+
(
701+
Ok(ExplicitCommonBuiltinParameters {
702+
sample_count: actual,
703+
..
704+
}),
705+
None,
706+
) => {
707+
panic!(
708+
"duration = {duration} s, sample_rate = {sample_rate} Hz: \
709+
expected to be unable to generate a sample count, but generated {actual}",
710+
duration = duration,
711+
sample_rate = sample_rate,
712+
actual = actual,
713+
)
714+
}
715+
(Err(actual), Some(expected)) => {
716+
panic!(
717+
"duration = {duration} s, sample_rate = {sample_rate} Hz: \
718+
expected a sample count of {expected} samples, but got the following error:\n\
719+
{actual}",
720+
duration = duration,
721+
sample_rate = sample_rate,
722+
expected = expected,
723+
actual = actual,
724+
)
725+
}
726+
}
645727
}
646728

647729
/// Assert that for some exemplar waveform templates, the right IQ values are generated.
@@ -717,6 +799,7 @@ mod tests {
717799
) {
718800
let iq_values = parameters
719801
.iq_values_at_sample_rate(common, 1e6)
802+
.unwrap()
720803
.into_iq_values();
721804
let count = iq_values.len();
722805

0 commit comments

Comments
 (0)