use std::{
convert::{identity, Infallible},
f64::consts::{LN_2, PI},
iter::repeat_n,
};
use derive_where::derive_where;
use ndarray::{Array, Array1};
use num_complex::{c64, Complex64};
use serde::{Deserialize, Serialize};
use statrs::function::erf::erf;
#[cfg(feature = "stubs")]
use pyo3_stub_gen::derive::gen_stub_pyclass;
use crate::{
real,
units::{Cycles, Radians},
};
use super::{
parse,
sampling::{IqSamples, SamplingError},
Concrete, GeneralWaveformParameterError, GeneralWaveformParameters, Partial, Reference,
WaveformData,
};
#[cfg(feature = "python")]
pub mod quilpy;
mod macros;
use macros::*;
mod partiality;
use partiality::{ConcretizableFromTo, ConcretizableWaveform, IqSamplesFor, Sampleable};
#[derive_where(Clone, PartialEq, Debug)]
#[derive_where(Copy; T::Real, T::Complex)]
#[derive(derive_more::From)]
pub enum BuiltinWaveform<T: WaveformData> {
Flat(Flat<T>),
Gaussian(Gaussian<T>),
DragGaussian(DragGaussian<T>),
ErfSquare(ErfSquare<T>),
HermiteGaussian(HermiteGaussian<T>),
BoxcarKernel(BoxcarKernel),
RaisedCosine(RaisedCosine<T>),
}
#[derive_where(Clone, PartialEq, Debug)]
#[derive_where(Copy, Serialize, Deserialize; T::Real)]
pub struct CommonBuiltinParameters<T: WaveformData> {
pub duration: f64,
pub scale: Option<T::Real>,
pub phase: Option<Cycles<T::Real>>,
pub detuning: Option<T::Real>,
}
impl<T: WaveformData> parse::Extractable<T> for CommonBuiltinParameters<T> {
fn extract_from<P: GeneralWaveformParameters, EF64, ER, EC>(
parameters: &mut P,
concrete_real: impl FnMut(P::Value) -> Result<f64, EF64>,
mut real: impl FnMut(P::Value) -> Result<T::Real, ER>,
_complex: impl FnMut(P::Value) -> Result<T::Complex, EC>,
) -> Result<Self, GeneralWaveformParameterError<EF64, ER, EC>> {
let duration = parse::mandatory(
parameters,
"duration",
concrete_real,
GeneralWaveformParameterError::BadConcreteReal,
)?;
let mut optional_real = |name| {
parse::optional(
parameters,
name,
&mut real,
GeneralWaveformParameterError::BadReal,
)
};
Ok(Self {
duration,
scale: optional_real("scale")?,
phase: optional_real("phase")?.map(Cycles),
detuning: optional_real("detuning")?,
})
}
}
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "stubs", gen_stub_pyclass)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(
module = "quil._quil.waveform",
subclass,
get_all,
set_all,
eq,
from_py_object
)
)]
pub struct ExplicitCommonBuiltinParameters {
pub sample_count: u32,
pub scale: f64,
pub phase: Cycles<f64>,
pub detuning: f64,
}
const _USIZE_MUST_BE_AT_LEAST_32_BITS: () =
assert!(std::mem::size_of::<usize>() >= std::mem::size_of::<u32>());
impl<S: WaveformData> CommonBuiltinParameters<S> {
pub fn as_ref(&self) -> CommonBuiltinParameters<Reference<'_, S>> {
let Self {
duration,
scale,
phase,
detuning,
} = self;
CommonBuiltinParameters {
duration: *duration,
scale: scale.as_ref(),
phase: phase.as_ref().map(Cycles::as_ref),
detuning: detuning.as_ref(),
}
}
pub fn try_evaluate<T: WaveformData, E>(
self,
real: impl Fn(S::Real) -> Result<T::Real, E>,
complex: impl Fn(S::Complex) -> Result<T::Complex, E>,
) -> Result<CommonBuiltinParameters<T>, E> {
let _ = complex;
let Self {
duration,
scale,
phase,
detuning,
} = self;
Ok(CommonBuiltinParameters {
duration,
scale: scale.map(&real).transpose()?,
phase: phase.map(|phase| phase.try_map(&real)).transpose()?,
detuning: detuning.map(&real).transpose()?,
})
}
}
impl<T: WaveformData> CommonBuiltinParameters<Partial<T>> {
pub fn transpose(self) -> Option<CommonBuiltinParameters<T>> {
let Self {
duration,
scale,
phase,
detuning,
} = self;
let scale = match scale {
Some(None) => return None,
Some(Some(scale)) => Some(scale),
None => None,
};
let phase = match phase {
Some(Cycles(None)) => return None,
Some(Cycles(Some(phase))) => Some(Cycles(phase)),
None => None,
};
let detuning = match detuning {
Some(None) => return None,
Some(Some(detuning)) => Some(detuning),
None => None,
};
Some(CommonBuiltinParameters {
duration,
scale,
phase,
detuning,
})
}
}
impl<T: WaveformData> CommonBuiltinParameters<T> {
#[inline]
fn raw_resolve_with_sample_rate(
self,
sample_rate: f64,
) -> Result<partiality::Value<T, u32, ExplicitCommonBuiltinParameters>, SamplingError>
where
T: Sampleable,
{
let Self {
duration,
scale,
phase,
detuning,
} = self;
let sample_count_fract = duration * sample_rate;
let sample_count = sample_count_fract.round();
let misalignment = sample_count_fract - sample_count;
let max_misalignment = 1.0 / (sample_rate * 100.0);
if sample_count < 0.0 || sample_count >= f64::from(u32::MAX) {
Err(SamplingError::SampleCountOutOfRange {
duration,
sample_rate,
sample_count,
})
} else if misalignment.abs() >= max_misalignment {
Err(SamplingError::MisalignedDuration {
duration,
sample_rate,
misalignment,
max_misalignment,
})
} else {
let sample_count = sample_count as u32;
let evaluate_or =
|field: Option<_>, default| field.map(T::eval_real).unwrap_or(Ok(default));
let result = (|| {
Ok(ExplicitCommonBuiltinParameters {
sample_count,
scale: evaluate_or(scale, 1.0)?,
phase: evaluate_or(phase.map(|p| p.0), 0.0).map(Cycles)?,
detuning: evaluate_or(detuning, 0.0)?,
})
})();
Ok(match result {
Ok(total) => partiality::Value::Total(total),
Err(is_partial) => partiality::Value::Partial(is_partial, sample_count),
})
}
}
}
impl CommonBuiltinParameters<Concrete> {
#[inline]
pub fn resolve_with_sample_rate(
self,
sample_rate: f64,
) -> Result<ExplicitCommonBuiltinParameters, SamplingError> {
self.raw_resolve_with_sample_rate(sample_rate)
.map(partiality::Value::unwrap_total)
}
}
#[derive(Clone, PartialEq, Debug)]
pub enum IqSamplesOrPlaceholder {
Placeholder(IqSamples<()>),
Samples(IqSamples<Complex64>),
}
pub trait BuiltinWaveformParameters:
Into<BuiltinWaveform<Concrete>> + Copy + PartialEq + std::fmt::Debug + private::SealedConcrete
{
fn iq_values_at_sample_rate(
self,
common: CommonBuiltinParameters<Concrete>,
sample_rate: f64,
) -> Result<IqSamples<Complex64>, SamplingError>;
}
pub trait PartialBuiltinWaveformParameters:
Into<BuiltinWaveform<Partial<Concrete>>>
+ Copy
+ PartialEq
+ std::fmt::Debug
+ private::SealedPartial
{
type Concrete: BuiltinWaveformParameters;
fn concretize(self) -> Option<Self::Concrete>;
fn partial_iq_values_at_sample_rate(
self,
common: CommonBuiltinParameters<Partial<Concrete>>,
sample_rate: f64,
) -> Result<IqSamplesOrPlaceholder, SamplingError>;
}
define_waveforms! {
#[waveform_source(QuilT)]
pub struct Flat {
pub iq: Complex,
}
#[waveform_source(QuilT)]
pub struct Gaussian {
pub fwhm: Real,
pub t0: Real,
}
#[waveform_source(QuilT)]
pub struct DragGaussian {
pub fwhm: Real,
pub t0: Real,
pub anh: Real,
pub alpha: Real,
}
#[waveform_source(QuilT)]
pub struct ErfSquare {
pub risetime: Real,
pub pad_left: ConcreteReal,
pub pad_right: ConcreteReal,
}
#[waveform_source(Rigetti)]
pub struct HermiteGaussian {
pub fwhm: Real,
pub t0: Real,
pub anh: Real,
pub alpha: Real,
pub second_order_hrm_coeff: Real,
}
#[waveform_source(Rigetti)]
pub struct RaisedCosine {
pub rolloff: Real,
pub pad_left: ConcreteReal,
pub pad_right: ConcreteReal,
}
#[waveform_source(Rigetti)]
pub struct BoxcarKernel;
}
macro_rules! builtin_waveform_match {
(match $outer:ident {
$inner:ident => $body:expr$(,)?
}) => {
builtin_waveform_match! {
@match $outer {
($inner) => ($body),
(BoxcarKernel) => ($body),
}
}
};
(match $outer:ident {
$inner:ident => $body:expr,
BoxcarKernel => $boxcar_body:expr$(,)?
}) => {
builtin_waveform_match! {
@match $outer {
($inner) => ($body),
(BoxcarKernel) => ($boxcar_body),
}
}
};
(match $outer:ident {
$inner:ident => { $($body:stmt;)* $(trailing:stmt)? }
BoxcarKernel => $boxcar_body:expr$(,)?
}) => {
builtin_waveform_match! {
@match $outer {
($inner) => ({ $($body;)* $(trailing)? }),
(BoxcarKernel) => ($boxcar_body),
}
}
};
(@match $outer:ident {
($inner:ident) => ($body:expr),
(BoxcarKernel) => ($boxcar_body:expr),
}) => {
match $outer {
BuiltinWaveform::Flat($inner) => $body,
BuiltinWaveform::Gaussian($inner) => $body,
BuiltinWaveform::DragGaussian($inner) => $body,
BuiltinWaveform::ErfSquare($inner) => $body,
BuiltinWaveform::HermiteGaussian($inner) => $body,
BuiltinWaveform::RaisedCosine($inner) => $body,
BuiltinWaveform::BoxcarKernel($inner @ BoxcarKernel) => $boxcar_body,
}
};
}
impl<S: WaveformData> BuiltinWaveform<S> {
pub fn as_ref(&self) -> BuiltinWaveform<Reference<'_, S>> {
builtin_waveform_match! {
match self {
waveform => waveform.as_ref().into(),
BoxcarKernel => (*waveform).into(),
}
}
}
pub fn try_evaluate<T: WaveformData, E>(
self,
real: impl Fn(S::Real) -> Result<T::Real, E>,
complex: impl Fn(S::Complex) -> Result<T::Complex, E>,
) -> Result<BuiltinWaveform<T>, E> {
builtin_waveform_match! {
match self {
waveform => waveform.try_evaluate(real, complex).map(BuiltinWaveform::from),
BoxcarKernel => Ok(waveform.into()),
}
}
}
}
impl<T: WaveformData> BuiltinWaveform<Partial<T>> {
pub fn transpose(self) -> Option<BuiltinWaveform<T>> {
builtin_waveform_match! {
match self {
waveform => waveform.transpose().map(BuiltinWaveform::from),
BoxcarKernel => Some(waveform.into()),
}
}
}
}
impl BuiltinWaveformParameters for BuiltinWaveform<Concrete> {
fn iq_values_at_sample_rate(
self,
common: CommonBuiltinParameters<Concrete>,
sample_rate: f64,
) -> Result<IqSamples<Complex64>, SamplingError> {
builtin_waveform_match! {
match self {
waveform => waveform.iq_values_at_sample_rate(common, sample_rate),
}
}
}
}
impl PartialBuiltinWaveformParameters for BuiltinWaveform<Partial<Concrete>> {
type Concrete = BuiltinWaveform<Concrete>;
#[inline(always)]
fn concretize(self) -> Option<Self::Concrete> {
self.transpose()
}
fn partial_iq_values_at_sample_rate(
self,
common: CommonBuiltinParameters<Partial<Concrete>>,
sample_rate: f64,
) -> Result<IqSamplesOrPlaceholder, SamplingError> {
builtin_waveform_match! {
match self {
waveform => waveform.partial_iq_values_at_sample_rate(common, sample_rate),
}
}
}
}
impl<T: WaveformData> Flat<T> {
fn raw_iq_values_at_sample_rate(
self,
common: CommonBuiltinParameters<T>,
sample_rate: f64,
) -> Result<IqSamplesFor<T>, SamplingError>
where
CommonBuiltinParameters<T>: Copy,
Self: ConcretizableFromTo<T, Flat<Concrete>>,
{
let (waveform, explicit) = match resolve_for_flat_unless_detuned(
|| self.concretize(),
common,
sample_rate,
identity,
)? {
partiality::Value::Partial(is_partial, samples) => {
return Ok(partiality::Value::Partial(is_partial, samples))
}
partiality::Value::Total(result) => result,
};
let ExplicitCommonBuiltinParameters {
sample_count,
scale,
phase,
detuning,
} = explicit;
let Flat { iq } = waveform;
let sample_count = sample_count as usize;
let scaled_iq = scale * iq;
Ok(IqSamplesFor::Total(if detuning == 0.0 {
IqSamples::Flat {
iq: apply_phase(scaled_iq, phase),
sample_count,
}
} else {
let mut samples = vec![scaled_iq; sample_count];
apply_phase_and_detuning(&mut samples, phase, detuning, sample_rate);
IqSamples::Samples(samples)
}))
}
}
impl<T: WaveformData> Gaussian<T> {
fn raw_iq_values_at_sample_rate(
self,
common: CommonBuiltinParameters<T>,
sample_rate: f64,
) -> Result<IqSamplesFor<T>, SamplingError>
where
Self: ConcretizableFromTo<T, Gaussian<Concrete>>,
{
build_sample_per_time_step_and_adjust_for_common_parameters(
self,
common,
sample_rate,
|w| w.fwhm,
|waveform, sigma| {
let Gaussian { fwhm: _, t0 } = waveform;
move |el| real!((-0.5 * (el - t0).powf(2.0) / sigma.powf(2.0)).exp())
},
)
}
}
impl<T: WaveformData> DragGaussian<T> {
fn raw_iq_values_at_sample_rate(
self,
common: CommonBuiltinParameters<T>,
sample_rate: f64,
) -> Result<IqSamplesFor<T>, SamplingError>
where
Self: ConcretizableFromTo<T, DragGaussian<Concrete>>,
{
build_sample_per_time_step_and_adjust_for_common_parameters(
self,
common,
sample_rate,
|w| w.fwhm,
|waveform, sigma| {
let DragGaussian {
fwhm: _,
t0,
anh,
alpha,
} = waveform;
move |el| {
let env = (-0.5 * (el - t0).powf(2.0) / sigma.powf(2.0)).exp();
let env_mod =
(alpha * (1.0 / (2.0 * PI * anh * sigma.powf(2.0)))) * (el - t0) * env;
c64(env, env_mod)
}
},
)
}
}
impl<T: WaveformData> ErfSquare<T> {
fn raw_iq_values_at_sample_rate(
self,
common: CommonBuiltinParameters<T>,
sample_rate: f64,
) -> Result<IqSamplesFor<T>, SamplingError>
where
Self: ConcretizableFromTo<T, ErfSquare<Concrete>>,
{
let scale_is_zero = common
.scale
.is_some_and(|scale| T::eval_real(scale) == Ok(0.0));
let left_padding_samples = (self.pad_left * sample_rate).ceil() as usize;
let right_padding_samples = (self.pad_right * sample_rate).ceil() as usize;
let all_zero = |sample_count| {
IqSamplesFor::Total(IqSamples::Flat {
iq: c64(0.0, 0.0),
sample_count: left_padding_samples + sample_count + right_padding_samples,
})
};
match concretize_and_resolve(self, common, sample_rate)? {
partiality::Value::Partial(is_partial, sample_count) => Ok(if scale_is_zero {
all_zero(sample_count)
} else {
IqSamplesFor::Partial(
is_partial,
IqSamples::Samples(vec![
();
left_padding_samples
+ sample_count
+ right_padding_samples
]),
)
}),
partiality::Value::Total((explicit, waveform)) => {
if scale_is_zero {
return Ok(all_zero(explicit.sample_count as usize));
}
let ErfSquare {
risetime,
pad_left: _, pad_right: _, } = waveform;
let fwhm = 0.5 * risetime;
let t1 = fwhm;
let t2 = common.duration - fwhm;
Ok(IqSamplesFor::Total(
build_samples_and_adjust_for_common_parameters(
SamplingParameters { sample_rate, fwhm },
explicit,
|SamplingInfo { time_steps, sigma }| {
let waveform = time_steps.into_iter().map(move |el| {
real!(0.5 * (erf((el - t1) / sigma) - erf((el - t2) / sigma)))
});
let left_padding = repeat_n(real!(0.0), left_padding_samples);
let right_padding = repeat_n(real!(0.0), right_padding_samples);
left_padding.chain(waveform).chain(right_padding)
},
),
))
}
}
}
}
impl<T: WaveformData> HermiteGaussian<T> {
fn raw_iq_values_at_sample_rate(
self,
common: CommonBuiltinParameters<T>,
sample_rate: f64,
) -> Result<IqSamplesFor<T>, SamplingError>
where
Self: ConcretizableFromTo<T, HermiteGaussian<Concrete>>,
{
build_sample_per_time_step_and_adjust_for_common_parameters(
self,
common,
sample_rate,
|w| w.fwhm,
|waveform, sigma| {
let HermiteGaussian {
fwhm: _,
t0,
anh,
alpha,
second_order_hrm_coeff,
} = waveform;
let deriv_prefactor = -alpha / (2f64 * PI * anh);
move |el| {
let exp_t = 0.5 * (el - t0).powf(2.0) / sigma.powf(2.0);
let g = (-exp_t).exp();
let env = (1.0 - second_order_hrm_coeff * exp_t) * g;
let env_derived = deriv_prefactor * (el - t0) / sigma.powf(2.0)
* g
* (second_order_hrm_coeff * (exp_t - 1.0) - 1.0);
c64(env, env_derived)
}
},
)
}
}
impl<T: WaveformData> RaisedCosine<T> {
fn raw_iq_values_at_sample_rate(
self,
common: CommonBuiltinParameters<T>,
sample_rate: f64,
) -> Result<IqSamplesFor<T>, SamplingError>
where
Self: ConcretizableFromTo<T, RaisedCosine<Concrete>>,
{
let scale_is_zero = common
.scale
.is_some_and(|scale| T::eval_real(scale) == Ok(0.0));
let left_padding_samples = (self.pad_left * sample_rate).ceil() as usize;
let right_padding_samples = (self.pad_right * sample_rate).ceil() as usize;
let all_zero = |sample_count| {
IqSamplesFor::Total(IqSamples::Flat {
iq: c64(0.0, 0.0),
sample_count: left_padding_samples + sample_count + right_padding_samples,
})
};
let active_duration = common.duration;
match concretize_and_resolve(self, common, sample_rate)? {
partiality::Value::Partial(is_partial, sample_count) => Ok(if scale_is_zero {
all_zero(sample_count)
} else {
IqSamplesFor::Partial(
is_partial,
IqSamples::Samples(vec![
();
left_padding_samples
+ sample_count
+ right_padding_samples
]),
)
}),
partiality::Value::Total((explicit, waveform)) => {
if scale_is_zero {
return Ok(all_zero(explicit.sample_count as usize));
}
let RaisedCosine {
rolloff,
pad_left: _, pad_right: _, } = waveform;
let square_pulse = real!(1.0);
let cosine_pulse = |el: f64, rolloff_factor: f64| {
real!(
0.5 * (1.0
+ f64::cos(PI + 2.0 * PI * el / (rolloff_factor * active_duration)))
)
};
Ok(IqSamplesFor::Total(
build_samples_and_adjust_for_builtin_parameters(
sample_rate,
explicit,
|time_steps| {
let risetime = rolloff * active_duration / 2.0;
let falltime = active_duration - risetime;
let waveform = time_steps.into_iter().map(move |el| match rolloff {
0.0 => square_pulse,
1.0 => cosine_pulse(el, 1.0),
_ => {
if el < risetime {
cosine_pulse(el, rolloff)
} else if el <= falltime {
square_pulse
} else {
cosine_pulse(el - active_duration, rolloff)
}
}
});
let left_padding = repeat_n(real!(0.0), left_padding_samples);
let right_padding = repeat_n(real!(0.0), right_padding_samples);
left_padding.chain(waveform).chain(right_padding)
},
),
))
}
}
}
}
impl BoxcarKernel {
fn raw_iq_values_at_sample_rate<T: Sampleable>(
self,
common: CommonBuiltinParameters<T>,
sample_rate: f64,
) -> Result<IqSamplesFor<T>, SamplingError>
where
CommonBuiltinParameters<T>: Copy,
{
let (waveform, explicit) = match resolve_for_flat_unless_detuned(
|| Ok(self),
common,
sample_rate,
|never: Infallible| match never {},
)? {
partiality::Value::Partial(is_partial, samples) => {
return Ok(partiality::Value::Partial(is_partial, samples))
}
partiality::Value::Total(result) => result,
};
let ExplicitCommonBuiltinParameters {
sample_count,
scale,
phase,
detuning,
} = explicit;
let Self = waveform;
let sample_count = sample_count as usize;
Ok(IqSamplesFor::Total(if detuning == 0.0 {
let iq = polar_to_rectangular(scale / sample_count as f64, phase);
IqSamples::Flat { iq, sample_count }
} else {
let samples = (0..sample_count).map(|index| {
polar_to_rectangular(
scale / sample_count as f64,
Cycles(detuning * (index as f64) / sample_rate) + phase,
)
});
IqSamples::Samples(samples.collect())
}))
}
}
#[expect(
clippy::type_complexity,
reason = "the complexity is not that bad, or at least not bad in a way that's resolvable by \
using `type` definitions"
)]
fn resolve_for_flat_unless_detuned<T: Sampleable, W, E>(
waveform: impl FnOnce() -> Result<W, E>,
common: CommonBuiltinParameters<T>,
sample_rate: f64,
partiality: impl FnOnce(E) -> T::IsPartial,
) -> Result<partiality::Value<T, IqSamples<()>, (W, ExplicitCommonBuiltinParameters)>, SamplingError>
where
CommonBuiltinParameters<T>: Copy,
{
let placeholder = |is_partial, sample_count| {
Ok(partiality::Value::Partial(
is_partial,
if common
.detuning
.is_none_or(|detuning| T::eval_real(detuning) == Ok(0.0))
{
IqSamples::Flat {
iq: (),
sample_count,
}
} else {
IqSamples::Samples(vec![(); sample_count])
},
))
};
let explicit = match common.raw_resolve_with_sample_rate(sample_rate)? {
partiality::Value::Partial(is_partial, sample_count) => {
return placeholder(is_partial, sample_count as usize)
}
partiality::Value::Total(explicit) => explicit,
};
let waveform = match waveform() {
Err(is_partial) => {
return placeholder(partiality(is_partial), explicit.sample_count as usize)
}
Ok(waveform) => waveform,
};
Ok(partiality::Value::Total((waveform, explicit)))
}
#[derive(Clone, Copy, Debug)]
struct SamplingParameters {
sample_rate: f64,
fwhm: f64,
}
#[derive(Clone, Debug)]
struct SamplingInfo {
time_steps: Array1<f64>,
sigma: f64,
}
#[expect(
clippy::type_complexity,
reason = "the complexity here is essential to the abstraction, and cannot be brushed under the \
rug with `type` definitions"
)]
fn concretize_and_resolve<W: ConcretizableWaveform>(
waveform: W,
common: CommonBuiltinParameters<W::WaveformData>,
sample_rate: f64,
) -> Result<
partiality::Value<
W::WaveformData,
usize,
(
ExplicitCommonBuiltinParameters,
W::WithWaveformData<Concrete>,
),
>,
SamplingError,
> {
let explicit = match common.raw_resolve_with_sample_rate(sample_rate)? {
partiality::Value::Partial(is_partial, sample_count) => {
return Ok(partiality::Value::Partial(
is_partial,
sample_count as usize,
))
}
partiality::Value::Total(explicit) => explicit,
};
let waveform = match waveform.concretize() {
Err(is_partial) => {
return Ok(partiality::Value::Partial(
is_partial,
explicit.sample_count as usize,
))
}
Ok(waveform) => waveform,
};
Ok(partiality::Value::Total((explicit, waveform)))
}
fn build_samples_and_adjust_for_common_parameters<I: IntoIterator<Item = Complex64>>(
parameters: SamplingParameters,
common: ExplicitCommonBuiltinParameters,
build: impl FnOnce(SamplingInfo) -> I,
) -> IqSamples<Complex64> {
let SamplingParameters { sample_rate, fwhm } = parameters;
let sigma = 0.5 * fwhm / (2.0 * LN_2).sqrt();
build_samples_and_adjust_for_builtin_parameters(sample_rate, common, |time_steps| {
build(SamplingInfo { time_steps, sigma })
})
}
fn build_samples_and_adjust_for_builtin_parameters<I: IntoIterator<Item = Complex64>>(
sample_rate: f64,
common: ExplicitCommonBuiltinParameters,
build: impl FnOnce(Array1<f64>) -> I,
) -> IqSamples<Complex64> {
let ExplicitCommonBuiltinParameters {
sample_count,
scale,
phase,
detuning,
} = common;
let time_steps = Array::range(0.0, sample_count.into(), 1.0) / sample_rate;
let mut samples: Vec<_> = build(time_steps).into_iter().collect();
for (index, sample) in samples.iter_mut().enumerate() {
*sample =
apply_phase_and_detuning_at_index(scale * *sample, phase, detuning, sample_rate, index);
}
IqSamples::Samples(samples)
}
fn build_sample_per_time_step_and_adjust_for_common_parameters<W: ConcretizableWaveform, Sampler>(
waveform: W,
common: CommonBuiltinParameters<W::WaveformData>,
sample_rate: f64,
fwhm: impl FnOnce(&W::WithWaveformData<Concrete>) -> f64,
make_sampler: impl FnOnce(W::WithWaveformData<Concrete>, f64) -> Sampler,
) -> Result<IqSamplesFor<W::WaveformData>, SamplingError>
where
Sampler: Fn(f64) -> Complex64,
{
let scale_is_zero = common
.scale
.is_some_and(|scale| W::WaveformData::eval_real(scale) == Ok(0.0));
let all_zero = |sample_count| {
IqSamplesFor::Total(IqSamples::Flat {
iq: c64(0.0, 0.0),
sample_count,
})
};
match concretize_and_resolve(waveform, common, sample_rate)? {
partiality::Value::Partial(is_partial, sample_count) => Ok(if scale_is_zero {
all_zero(sample_count)
} else {
IqSamplesFor::Partial(is_partial, IqSamples::Samples(vec![(); sample_count]))
}),
partiality::Value::Total((explicit, waveform)) => {
if scale_is_zero {
return Ok(all_zero(explicit.sample_count as usize));
}
let fwhm = fwhm(&waveform);
Ok(IqSamplesFor::Total(
build_samples_and_adjust_for_common_parameters(
SamplingParameters { sample_rate, fwhm },
explicit,
|SamplingInfo { time_steps, sigma }| {
let sampler = make_sampler(waveform, sigma);
time_steps.into_iter().map(sampler)
},
),
))
}
}
}
#[inline]
pub fn apply_phase_and_detuning(
iq_values: &mut [Complex64],
phase: Cycles<f64>,
detuning: f64,
sample_rate: f64,
) {
for (index, value) in iq_values.iter_mut().enumerate() {
*value = apply_phase_and_detuning_at_index(*value, phase, detuning, sample_rate, index);
}
}
#[inline]
pub(super) fn apply_phase_and_detuning_at_index(
iq_value: Complex64,
phase: Cycles<f64>,
detuning: f64,
sample_rate: f64,
index: usize,
) -> Complex64 {
apply_phase(
iq_value,
Cycles(detuning * (index as f64) / sample_rate + phase.0),
)
}
#[inline]
fn apply_phase(iq_value: Complex64, phase: Cycles<f64>) -> Complex64 {
iq_value * Complex64::cis(Radians::from(phase).0)
}
#[inline]
fn polar_to_rectangular(magnitude: f64, angle: Cycles<f64>) -> Complex64 {
Complex64::from_polar(magnitude, Radians::from(angle).0)
}
#[cfg(test)]
mod tests {
use super::*;
fn format_snapshot_name(
waveform: impl BuiltinWaveformParameters,
common: CommonBuiltinParameters<Concrete>,
tag: &str,
) -> String {
format!("{waveform:?}__{common:?}__{tag}")
.replace(['{', '}', ':', ','], "")
.replace([' ', '.'], "_")
}
fn assert_almost_eq(left: Complex64, right: Complex64, epsilon: f64) {
assert!(
(left - right).norm() < epsilon,
"Expected {left} to be almost equal to {right} with epsilon {epsilon}"
);
}
#[rstest::rstest]
#[case(1.0, Cycles(0.0), Complex64::new(0.1, 0.0))]
#[case(1.0, Cycles(0.5), Complex64::new(-0.1, 0.0))]
#[case(-1.0, Cycles(0.0), Complex64::new(-0.1, 0.0))]
#[case(0.0, Cycles(0.0), Complex64::new(0.0, 0.0))]
fn boxcar_kernel(#[case] scale: f64, #[case] phase: Cycles<f64>, #[case] expected: Complex64) {
match BoxcarKernel
.iq_values_at_sample_rate(
CommonBuiltinParameters {
duration: 0.1,
scale: Some(scale),
phase: Some(phase),
detuning: None,
},
100.0,
)
.unwrap()
{
IqSamples::Flat { iq, sample_count } => {
assert_eq!(sample_count, 10);
assert_almost_eq(iq, expected, 1e-10);
}
IqSamples::Samples(samples) => {
panic!(
"Boxcar kernel must report a flat result, but got samples: {samples:?}",
samples = samples
);
}
};
}
#[rstest::rstest]
#[case(0.0, 0.0, Some(0.0))]
#[case(0.0, 1e9, Some(0.0))]
#[case(1e9, 0.0, Some(0.0))]
#[case(f64::EPSILON, 1.0, Some(0.0))]
#[case(-f64::EPSILON, 1.0, Some(0.0))]
#[case(0.9999999, 101.0, Some(101.0))]
#[case(1.0000001, 101.0, Some(101.0))]
#[case(0.99, 101.0, None)]
#[case(1.01, 101.0, None)]
#[case(8.800_000_000_000_001e-8, 1.0e9, Some(88.0))] #[case(0.5, 3.0, None)]
fn sample_count(
#[case] duration: f64,
#[case] sample_rate: f64,
#[case] expected: Option<f64>,
) {
let actual = CommonBuiltinParameters {
duration,
scale: None,
phase: None,
detuning: None,
}
.resolve_with_sample_rate(sample_rate);
match (actual, expected) {
(
Ok(ExplicitCommonBuiltinParameters {
sample_count: actual,
..
}),
Some(expected),
) => {
assert_eq!(
expected,
f64::from(actual),
"duration = {duration} s,\n\
sample_rate = {sample_rate} Hz,\n\
expected = {expected} samples,\n\
actual = {actual} samples"
)
}
(Err(_), None) => {}
(
Ok(ExplicitCommonBuiltinParameters {
sample_count: actual,
..
}),
None,
) => {
panic!(
"duration = {duration} s, sample_rate = {sample_rate} Hz: \
expected to be unable to generate a sample count, but generated {actual}",
duration = duration,
sample_rate = sample_rate,
actual = actual,
)
}
(Err(actual), Some(expected)) => {
panic!(
"duration = {duration} s, sample_rate = {sample_rate} Hz: \
expected a sample count of {expected} samples, but got the following error:\n\
{actual}",
duration = duration,
sample_rate = sample_rate,
expected = expected,
actual = actual,
)
}
}
}
#[rstest::rstest]
#[case(
ErfSquare { risetime: 1e-5, pad_left: 0.0, pad_right: 0.0 },
CommonBuiltinParameters { duration: 1e-4, scale: Some(1.0), phase: Some(Cycles(0.0)), detuning: Some(0.0)},
)]
#[case(
ErfSquare { risetime: 1e-5, pad_left: 0.0, pad_right: 0.0 },
CommonBuiltinParameters { duration: 1e-4, scale: Some(1.0), phase: Some(Cycles(0.5)), detuning: Some(0.0)},
)]
#[case(
ErfSquare { risetime: 1e-5, pad_left: 0.0, pad_right: 0.0 },
CommonBuiltinParameters { duration: 1e-4, scale: Some(-1.0), phase: Some(Cycles(0.0)), detuning: Some(0.0)},
)]
#[case(
ErfSquare { risetime: 1e-5, pad_left: 0.0, pad_right: 0.0 },
CommonBuiltinParameters { duration: 1e-4, scale: Some(0.0), phase: Some(Cycles(0.0)), detuning: Some(0.0)},
)]
#[case(
Gaussian { fwhm: 1e-5, t0: 0.0 },
CommonBuiltinParameters { duration: 1e-4, scale: Some(1.0), phase: Some(Cycles(0.0)), detuning: Some(0.0)},
)]
#[case(
Gaussian { fwhm: 1e-5, t0: 5e-5 },
CommonBuiltinParameters { duration: 1e-4, scale: Some(1.0), phase: Some(Cycles(0.0)), detuning: Some(1e6)},
)]
#[case(
Gaussian { fwhm: 2e-5, t0: 5e-5 },
CommonBuiltinParameters { duration: 1e-4, scale: Some(0.5), phase: Some(Cycles(0.0)), detuning: Some(0.0)},
)]
#[case(
Gaussian { fwhm: 4e-5, t0: 5e-5 },
CommonBuiltinParameters { duration: 1e-4, scale: Some(0.5), phase: Some(Cycles(0.5)), detuning: Some(0.0)},
)]
#[case(
Gaussian { fwhm: 4e-5, t0: 5e-5 },
CommonBuiltinParameters { duration: 1e-4, scale: Some(-1.0), phase: Some(Cycles(0.0)), detuning: Some(0.0)},
)]
#[case(
DragGaussian { fwhm: 1e-5, t0: 0.0, anh: 1e6, alpha: 1.0 },
CommonBuiltinParameters { duration: 1e-4, scale: Some(1.0), phase: Some(Cycles(0.0)), detuning: Some(0.0)},
)]
#[case(
DragGaussian { fwhm: 1e-5, t0: 0.0, anh: 1e6, alpha: 1.0 },
CommonBuiltinParameters { duration: 1e-4, scale: Some(1.0), phase: Some(Cycles(0.0)), detuning: Some(1e6)},
)]
#[case(
HermiteGaussian { fwhm: 1e-5, t0: 0.0, anh: 1e6, alpha: 1.0, second_order_hrm_coeff: 0.1 },
CommonBuiltinParameters { duration: 1e-4, scale: Some(1.0), phase: Some(Cycles(0.0)), detuning: Some(0.0)},
)]
#[case(
HermiteGaussian { fwhm: 1e-5, t0: 0.0, anh: 1e6, alpha: 1.0, second_order_hrm_coeff: 0.1 },
CommonBuiltinParameters { duration: 1e-4, scale: Some(1.0), phase: Some(Cycles(0.0)), detuning: Some(1e6)},
)]
#[case(
RaisedCosine { rolloff: 0.5, pad_left: 0.0, pad_right: 0.0 },
CommonBuiltinParameters { duration: 1e-4, scale: Some(1.0), phase: Some(Cycles(0.0)), detuning: Some(0.0)},
)]
#[case(
RaisedCosine { rolloff: 0.0, pad_left: 0.0, pad_right: 0.0 },
CommonBuiltinParameters { duration: 1e-4, scale: Some(1.0), phase: Some(Cycles(0.0)), detuning: Some(0.0)},
)]
#[case(
RaisedCosine { rolloff: 1.0, pad_left: 0.0, pad_right: 0.0 },
CommonBuiltinParameters { duration: 1e-4, scale: Some(1.0), phase: Some(Cycles(0.0)), detuning: Some(0.0)},
)]
fn into_iq_values(
#[case] parameters: impl BuiltinWaveformParameters,
#[case] common: CommonBuiltinParameters<Concrete>,
) {
let iq_values = parameters
.iq_values_at_sample_rate(common, 1e6)
.unwrap()
.into_iq_values();
let count = iq_values.len();
let all_values_zero = iq_values.iter().all(|el| el == &Complex64::new(0.0, 0.0));
if count <= 200 && !all_values_zero {
let split = iq_values.clone().into_iter().fold(
(vec![], vec![]),
|(mut reals, mut imags), el| {
reals.push(el.re);
imags.push(el.im);
(reals, imags)
},
);
let split = vec![split.0, split.1];
let res = rasciigraph::plot_many(
split,
rasciigraph::Config::default()
.with_width(count as u32 + 10)
.with_height(20),
);
insta::assert_snapshot!(format_snapshot_name(parameters, common, "plot"), res);
}
let neat_iq_values = iq_values
.iter()
.map(|el| format!("{:+.5e}, {:+.5e}", el.re, el.im))
.collect::<Vec<_>>()
.join("\n");
insta::assert_snapshot!(
format_snapshot_name(parameters, common, "data"),
neat_iq_values
)
}
}