use num_traits::{Float, FloatConst};
use thiserror::Error;
use crate::FrequencyTransferFunction;
use crate::sampling::{samples_per_period, whole_samples, DurationError};
pub fn multisine<T: Float>(
tlen: T,
ts: T,
fundamental_frequency: T,
harmonics: &[usize],
amplitude: impl Fn(usize) -> T,
seed: u64,
) -> Result<Vec<T>, ExcitationError> {
let n_samples = whole_samples(tlen, ts).map_err(|kind| duration_error(kind, tlen, ts))?;
let period = samples_per_period(fundamental_frequency, ts).map_err(|kind| {
let (fundamental_frequency, ts) = (fundamental_frequency.to_f64().unwrap_or(f64::NAN), ts.to_f64().unwrap_or(f64::NAN));
match kind {
DurationError::Invalid => ExcitationError::InvalidFundamental { fundamental_frequency, ts },
DurationError::Fractional => ExcitationError::FractionalPeriod { fundamental_frequency, ts },
}
})?;
check_harmonics(period, harmonics)?;
let mut state = seed;
let mut lines: Vec<(usize, f64, f64)> = Vec::with_capacity(harmonics.len());
for &h in harmonics {
let a = amplitude(h).to_f64().filter(|a| a.is_finite() && *a != 0.0).ok_or(ExcitationError::InvalidAmplitude { harmonic: h })?;
lines.push((h, a, 2.0 * std::f64::consts::PI * uniform(&mut state)));
}
let power = lines.iter().map(|(_, a, _)| a * a).sum::<f64>() / 2.0;
if !(power > 0.0 && power.is_finite()) {
return Err(ExcitationError::NoPower);
}
let scale = 1.0 / power.sqrt();
let theta = |h: usize| 2.0 * std::f64::consts::PI * h as f64 / period as f64;
let drift = |tau: f64| lines.iter().map(|&(h, a, phi)| a * (phi + theta(h) * tau).cos() / (2.0 * (theta(h) / 2.0).sin())).sum::<f64>();
let bisect = |mut lo: f64, mut hi: f64| {
let positive = drift(lo) > 0.0;
for _ in 0..60 {
let mid = 0.5 * (lo + hi);
if (drift(mid) > 0.0) == positive { lo = mid } else { hi = mid }
}
0.5 * (lo + hi)
};
let tau = (0..period)
.find_map(|i| {
let (lo, hi) = (i as f64, i as f64 + 1.0);
let (d_lo, d_hi) = (drift(lo), drift(hi));
if d_lo == 0.0 {
Some(lo)
} else if (d_lo > 0.0) != (d_hi > 0.0) {
Some(bisect(lo, hi))
} else {
None
}
})
.unwrap_or(0.0);
let lines: Vec<(usize, f64, f64)> = lines.iter().map(|&(h, a, phi)| (h, a, phi + theta(h) * tau)).collect();
Ok((0..n_samples)
.map(|k| {
let value: f64 = lines
.iter()
.map(|&(h, a, psi)| a * (std::f64::consts::PI * ((h * (2 * k + 1)) % (2 * period)) as f64 / period as f64 + psi).sin())
.sum();
T::from(scale * value).unwrap()
})
.collect())
}
pub fn flat_output_multisine<T: Float + FloatConst + 'static>(
model: impl Into<FrequencyTransferFunction<T>>,
tlen: T,
ts: T,
fundamental_frequency: T,
harmonics: &[usize],
flat_below: usize,
seed: u64,
) -> Result<Vec<T>, ExcitationError> {
let model = model.into();
let omega = |h: usize| T::from(2.0 * std::f64::consts::PI * h as f64).unwrap() * fundamental_frequency;
multisine(tlen, ts, fundamental_frequency, harmonics, |h| T::one() / model.response(omega(h.max(flat_below))).norm(), seed)
}
fn duration_error<T: Float>(kind: DurationError, duration: T, ts: T) -> ExcitationError {
let (duration, ts) = (duration.to_f64().unwrap_or(f64::NAN), ts.to_f64().unwrap_or(f64::NAN));
match kind {
DurationError::Invalid => ExcitationError::InvalidDuration { duration, ts },
DurationError::Fractional => ExcitationError::FractionalDuration { duration, ts },
}
}
fn check_harmonics(period: usize, harmonics: &[usize]) -> Result<(), ExcitationError> {
for (i, &h) in harmonics.iter().enumerate() {
if h == 0 || 2 * h >= period {
return Err(ExcitationError::InvalidHarmonic { harmonic: h, samples_per_period: period });
}
if harmonics[..i].contains(&h) {
return Err(ExcitationError::DuplicateHarmonic { harmonic: h });
}
}
Ok(())
}
#[derive(Clone, Debug, Error, PartialEq)]
pub enum ExcitationError {
#[error("duration {duration} s is negative or not finite, or sampling period {ts} s not positive and finite")]
InvalidDuration { duration: f64, ts: f64 },
#[error("duration {duration} s is not a whole number of sampling periods {ts} s")]
FractionalDuration { duration: f64, ts: f64 },
#[error("fundamental frequency {fundamental_frequency} Hz is not positive and finite, or sampling period {ts} s not positive and finite")]
InvalidFundamental { fundamental_frequency: f64, ts: f64 },
#[error("the period of the fundamental frequency {fundamental_frequency} Hz is not a whole number of sampling periods {ts} s")]
FractionalPeriod { fundamental_frequency: f64, ts: f64 },
#[error("harmonic {harmonic} is not within 1 <= h < N / 2 (N = {samples_per_period} samples per period)")]
InvalidHarmonic { harmonic: usize, samples_per_period: usize },
#[error("harmonic {harmonic} is given more than once")]
DuplicateHarmonic { harmonic: usize },
#[error("the amplitude of harmonic {harmonic} is zero or not finite")]
InvalidAmplitude { harmonic: usize },
#[error("no harmonics, or their total power is not finite")]
NoPower,
}
pub fn chirp<T: Float>(tlen: T, ts: T, f0: T, f1: T) -> Result<Vec<T>, ExcitationError> {
let n_samples = whole_samples(tlen, ts).map_err(|kind| duration_error(kind, tlen, ts))?;
let (ts, f0, f1) = (ts.to_f64().unwrap(), f0.to_f64().unwrap(), f1.to_f64().unwrap());
let duration = n_samples as f64 * ts;
Ok((0..n_samples)
.map(|k| {
let t = k as f64 * ts;
T::from((2.0 * std::f64::consts::PI * (f0 * t + (f1 - f0) * t * t / (2.0 * duration))).sin()).unwrap()
})
.collect())
}
fn uniform(state: &mut u64) -> f64 {
*state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = *state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
((z ^ (z >> 31)) >> 11) as f64 / (1u64 << 53) as f64
}
#[cfg(test)]
mod tests {
use super::*;
use crate::TransferFunction;
fn line_amplitude(u: &[f64], period: usize, h: usize) -> f64 {
let (mut re, mut im) = (0.0, 0.0);
for (k, v) in u[..period].iter().enumerate() {
let theta = 2.0 * std::f64::consts::PI * (h * k % period) as f64 / period as f64;
re += v * theta.cos();
im -= v * theta.sin();
}
2.0 * (re * re + im * im).sqrt() / period as f64
}
#[test]
fn multisine_has_the_given_amplitudes() {
let (period, harmonics) = (1000, (5..=200).step_by(5).collect::<Vec<usize>>());
let u: Vec<f64> = multisine(3.0, 1e-3, 1.0, &harmonics, |h| 1.0 / h as f64, 7).unwrap();
assert_eq!(u.len(), 3 * period);
assert!((0..2 * period).all(|k| (u[k] - u[k + period]).abs() < 1e-12));
let rms = (u[..period].iter().map(|v| v * v).sum::<f64>() / period as f64).sqrt();
assert!((rms - 1.0).abs() < 1e-12, "rms {rms}");
let c = 1.0 / (harmonics.iter().map(|&h| 1.0 / (h * h) as f64).sum::<f64>() / 2.0).sqrt();
for h in 1..period / 2 {
let expected = if harmonics.contains(&h) { c / h as f64 } else { 0.0 };
assert!((line_amplitude(&u, period, h) - expected).abs() < 1e-12, "harmonic {h}");
}
assert_eq!(u, multisine::<f64>(3.0, 1e-3, 1.0, &harmonics, |h| 1.0 / h as f64, 7).unwrap());
assert_ne!(u, multisine::<f64>(3.0, 1e-3, 1.0, &harmonics, |h| 1.0 / h as f64, 8).unwrap());
let u: Vec<f64> = multisine(0.3, 1e-3, 10.0, &[1, 2, 3], |_| 1.0, 1).unwrap();
assert_eq!(u.len(), 300);
for h in 1..50 {
let expected = if h <= 3 { (2.0f64 / 3.0).sqrt() } else { 0.0 };
assert!((line_amplitude(&u, 100, h) - expected).abs() < 1e-12, "harmonic {h}");
}
}
#[test]
fn multisine_does_not_drift_through_integrators() {
let (period, harmonics) = (1000, (1..=20).collect::<Vec<usize>>());
let u: Vec<f64> = multisine(10.0, 1e-3, 1.0, &harmonics, |_| 1.0, 3).unwrap();
let (mut v, mut x) = (0.0, 0.0);
let mut positions = vec![];
for (k, &uk) in u.iter().enumerate() {
v += uk;
x += v;
if (k + 1) % period == 0 {
assert!(v.abs() < 1e-9, "velocity {v} after {} periods", (k + 1) / period);
positions.push(x);
}
}
assert!(positions.windows(2).all(|p| (p[1] - p[0]).abs() < 1e-6), "{positions:?}");
}
#[test]
fn multisine_does_not_drift_through_a_sampled_double_integrator() {
use crate::{tf, DiscreteSystem, discretize::Zoh};
let (ts, period) = (1e-3, 1000);
for seed in 0..5 {
let u: Vec<f64> = multisine(6.0, ts, 1.0, &(1..=30).collect::<Vec<usize>>(), |h| h as f64, seed).unwrap();
let mut plant = DiscreteSystem::try_from(&tf!("1 / s^2").discretize(Zoh, ts).unwrap()).unwrap();
let x: Vec<f64> = u.iter().map(|&uk| plant.update(uk)).collect();
let ends: Vec<f64> = (1..=6).map(|p| x[p * period - 1]).collect();
let range = x.iter().fold(0.0f64, |m, v| m.max(v.abs()));
assert!(ends.windows(2).all(|e| (e[1] - e[0]).abs() < 1e-9 * range), "seed {seed}: {ends:?}");
}
}
#[test]
fn flat_output_multisine_flattens_the_output() {
use crate::tf;
let (ts, period) = (1e-3, 1000);
let g = tf!("1000 / (s^2 + 20 s + 1000)");
let harmonics: Vec<usize> = (1..=100).collect();
let u: Vec<f64> = flat_output_multisine(&g, 1.0, ts, 1.0, &harmonics, 10, 5).unwrap();
let g_jw = g.frequency_transfer_function();
let gain = |h: usize| g_jw.response(2.0 * std::f64::consts::PI * h as f64).norm();
let output = line_amplitude(&u, period, 10) * gain(10);
for &h in &harmonics {
let a = line_amplitude(&u, period, h);
if h >= 10 {
assert!((a * gain(h) - output).abs() < 1e-9 * output, "harmonic {h}");
} else {
assert!((a - line_amplitude(&u, period, 10)).abs() < 1e-12, "harmonic {h}");
}
}
}
#[test]
fn multisine_rejects_invalid_arguments() {
let ms = |harmonics: &[usize], amplitude: f64| multisine::<f64>(0.1, 1e-3, 10.0, harmonics, |_| amplitude, 1);
let invalid = |harmonic| Err(ExcitationError::InvalidHarmonic { harmonic, samples_per_period: 100 });
assert_eq!(ms(&[0, 1], 1.0), invalid(0));
assert_eq!(ms(&[1, 50], 1.0), invalid(50));
assert_eq!(ms(&[1, 70], 1.0), invalid(70));
assert_eq!(ms(&[1, 2, 1], 1.0), Err(ExcitationError::DuplicateHarmonic { harmonic: 1 }));
assert_eq!(ms(&[3], 0.0), Err(ExcitationError::InvalidAmplitude { harmonic: 3 }));
assert_eq!(ms(&[3], f64::NAN), Err(ExcitationError::InvalidAmplitude { harmonic: 3 }));
assert_eq!(ms(&[], 1.0), Err(ExcitationError::NoPower));
assert_eq!(ms(&[1, 2], 1e200), Err(ExcitationError::NoPower));
assert!(ms(&[1, 49], 1.0).is_ok());
let resonance = TransferFunction::continuous(&[1.0], &[1.0, 0.0, (2.0 * std::f64::consts::PI * 20.0).powi(2)]);
assert_eq!(flat_output_multisine::<f64>(&resonance, 0.1, 1e-3, 10.0, &[1, 2, 3], 0, 1), Err(ExcitationError::InvalidAmplitude { harmonic: 2 }));
assert!(flat_output_multisine::<f64>(&resonance, 0.1, 1e-3, 10.0, &[1, 3], 0, 1).is_ok());
}
#[test]
fn durations_and_fundamental_must_be_whole_samples() {
let ms = |tlen: f64, f0: f64| multisine::<f64>(tlen, 1e-3, f0, &[1], |_| 1.0, 1);
assert_eq!(ms(1.0005, 10.0), Err(ExcitationError::FractionalDuration { duration: 1.0005, ts: 1e-3 }));
assert!(matches!(ms(-1.0, 10.0), Err(ExcitationError::InvalidDuration { .. })));
assert_eq!(ms(1.0, 3.0), Err(ExcitationError::FractionalPeriod { fundamental_frequency: 3.0, ts: 1e-3 }));
for f0 in [0.0, -1.0, f64::NAN] {
assert!(matches!(ms(1.0, f0), Err(ExcitationError::InvalidFundamental { .. })), "{f0}");
}
assert!(matches!(multisine::<f64>(1.0, 0.0, 10.0, &[1], |_| 1.0, 1), Err(ExcitationError::InvalidDuration { .. })));
assert_eq!(chirp::<f64>(1.0005, 1e-3, 1.0, 2.0), Err(ExcitationError::FractionalDuration { duration: 1.0005, ts: 1e-3 }));
assert_eq!(ms(0.0, 10.0), Ok(vec![]));
assert_eq!(multisine::<f32>(0.1, 1e-4, 20.0, &[1, 2], |_| 1.0, 1).unwrap().len(), 1000);
}
#[test]
fn chirp_sweeps_linearly() {
let u: Vec<f64> = chirp(1.0, 1e-3, 1.0, 11.0).unwrap();
assert_eq!(u.len(), 1000);
assert!(u[0].abs() < 1e-12 && u.iter().all(|v| v.abs() <= 1.0));
let crossings: Vec<usize> = (450..560).filter(|&k| u[k] <= 0.0 && u[k + 1] > 0.0 || u[k] >= 0.0 && u[k + 1] < 0.0).collect();
let spacing = (crossings[crossings.len() - 1] - crossings[0]) as f64 / (crossings.len() - 1) as f64 * 1e-3;
assert!((spacing - 1.0 / 12.0).abs() < 0.01, "spacing {spacing}");
}
}