use std::f64::consts::PI;
use crate::maps::MapFn;
use crate::types::{DmdError, C64};
#[derive(Debug, Clone, Copy)]
pub enum Observable {
Identity,
SinPi,
CosPi,
SinPiXY,
CosPiXY,
Sin2Pi,
Cos2Pi,
TrigProduct,
}
impl Observable {
pub fn eval(&self, state: &[f64]) -> f64 {
match self {
Observable::Identity => state[0],
Observable::SinPi => (PI * state[0]).sin(),
Observable::CosPi => (PI * state[0]).cos(),
Observable::SinPiXY => (PI * state[0]).sin() * (PI * state[1]).sin(),
Observable::CosPiXY => (PI * state[0]).cos() * (PI * state[1]).cos(),
Observable::Sin2Pi => (2.0 * PI * state[0]).sin(),
Observable::Cos2Pi => (2.0 * PI * state[0]).cos(),
Observable::TrigProduct => (3.0 * PI * state[0]).sin() * (17.0 * PI * state[0]).sin(),
}
}
pub fn name(&self) -> &str {
match self {
Observable::Identity => "identity",
Observable::SinPi => "sin_pi",
Observable::CosPi => "cos_pi",
Observable::SinPiXY => "sin_pi_xy",
Observable::CosPiXY => "cos_pi_xy",
Observable::Sin2Pi => "sin_2pi",
Observable::Cos2Pi => "cos_2pi",
Observable::TrigProduct => "trig_product",
}
}
}
#[derive(Debug, Clone)]
pub struct HtaResult {
pub hta: C64,
pub magnitude: f64,
pub phase: f64,
pub omega: f64,
pub period: f64,
pub n_iter: usize,
pub observable: String,
}
pub fn harmonic_time_average(
initial_condition: &[f64],
map: &dyn MapFn,
observable: &Observable,
omega: f64,
n_iter: usize,
) -> Result<HtaResult, DmdError> {
if n_iter == 0 {
return Err(DmdError::InvalidInput("n_iter must be positive".into()));
}
let mut state = initial_condition.to_vec();
let mut sum = C64::zero();
for k in 0..n_iter {
let f_val = observable.eval(&state);
let phase = 2.0 * PI * k as f64 * omega;
let weight = C64::new(phase.cos(), phase.sin());
sum += weight * C64::new(f_val, 0.0);
state = map.step(&state);
}
let hta = sum / n_iter as f64;
let magnitude = hta.norm();
let phase = hta.arg();
Ok(HtaResult {
hta,
magnitude,
phase,
omega,
period: 1.0 / omega,
n_iter,
observable: observable.name().to_string(),
})
}
pub fn hta_from_values(f_values: &[f64], omega: f64) -> C64 {
let n = f_values.len();
if n == 0 {
return C64::zero();
}
let mut sum = C64::zero();
for k in 0..n {
let phase = 2.0 * PI * k as f64 * omega;
let weight = C64::new(phase.cos(), phase.sin());
sum += weight * C64::new(f_values[k], 0.0);
}
sum / n as f64
}
#[derive(Debug, Clone)]
pub struct HtaConvergenceResult {
pub times: Vec<usize>,
pub hta_magnitudes: Vec<f64>,
pub convergence_rate: Option<f64>,
pub dynamics_type: DynamicsType,
pub omega: f64,
pub period: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DynamicsType {
ResonatingPeriodic,
Chaotic,
NonResonatingPeriodic,
Indeterminate,
}
impl std::fmt::Display for DynamicsType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DynamicsType::ResonatingPeriodic => write!(f, "resonating_periodic"),
DynamicsType::Chaotic => write!(f, "chaotic"),
DynamicsType::NonResonatingPeriodic => write!(f, "non_resonating_periodic"),
DynamicsType::Indeterminate => write!(f, "indeterminate"),
}
}
}
pub fn hta_convergence(
initial_condition: &[f64],
map: &dyn MapFn,
observable: &Observable,
omega: f64,
n_iter: usize,
sample_times: Option<&[usize]>,
) -> Result<HtaConvergenceResult, DmdError> {
if n_iter < 100 {
return Err(DmdError::InvalidInput(
"need at least 100 iterations for convergence analysis".into(),
));
}
let mut state = initial_condition.to_vec();
let mut f_values = Vec::with_capacity(n_iter);
for _ in 0..n_iter {
f_values.push(observable.eval(&state));
state = map.step(&state);
}
let times: Vec<usize> = match sample_times {
Some(t) => t.iter().copied().filter(|&t| t <= n_iter).collect(),
None => {
let n_samples = ((n_iter as f64).log10() * 5.0).floor() as usize;
let n_samples = n_samples.min(20).max(5);
let log_min = 2.0_f64; let log_max = (n_iter as f64).log10();
let mut times = Vec::new();
for i in 0..n_samples {
let log_t = log_min + (log_max - log_min) * i as f64 / (n_samples - 1) as f64;
let t = 10.0_f64.powf(log_t).round() as usize;
if t <= n_iter && (times.is_empty() || *times.last().unwrap() != t) {
times.push(t);
}
}
times
}
};
let mut hta_magnitudes = Vec::with_capacity(times.len());
for &t in × {
let hta = hta_from_values(&f_values[..t], omega);
hta_magnitudes.push(hta.norm());
}
let use_idx: Vec<usize> = times
.iter()
.enumerate()
.filter(|(_, &t)| t >= 1000)
.map(|(i, _)| i)
.collect();
let use_idx = if use_idx.len() < 3 {
(0..times.len()).collect()
} else {
use_idx
};
let convergence_rate = if use_idx.len() >= 2 {
let log_t: Vec<f64> = use_idx.iter().map(|&i| (times[i] as f64).log10()).collect();
let log_hta: Vec<f64> = use_idx
.iter()
.map(|&i| (hta_magnitudes[i] + 1e-15).log10())
.collect();
let n = log_t.len() as f64;
let sum_x: f64 = log_t.iter().sum();
let sum_y: f64 = log_hta.iter().sum();
let sum_x2: f64 = log_t.iter().map(|x| x * x).sum();
let sum_xy: f64 = log_t.iter().zip(&log_hta).map(|(x, y)| x * y).sum();
let denom = n * sum_x2 - sum_x * sum_x;
if denom.abs() > 1e-14 {
Some((n * sum_xy - sum_x * sum_y) / denom)
} else {
None
}
} else {
None
};
let dynamics_type = match convergence_rate {
Some(rate) => {
if rate > -0.3 {
DynamicsType::ResonatingPeriodic
} else if rate > -0.75 {
DynamicsType::Chaotic
} else {
DynamicsType::NonResonatingPeriodic
}
}
None => DynamicsType::Indeterminate,
};
Ok(HtaConvergenceResult {
times,
hta_magnitudes,
convergence_rate,
dynamics_type,
omega,
period: 1.0 / omega,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PhaseSpaceClass {
Resonating = 1,
Chaotic = 2,
NonResonating = 3,
}
pub fn classify_phase_space(
hta_magnitudes: &[f64],
resonating_threshold: f64,
chaotic_threshold: f64,
) -> Vec<PhaseSpaceClass> {
hta_magnitudes
.iter()
.map(|&m| {
if m >= resonating_threshold {
PhaseSpaceClass::Resonating
} else if m >= chaotic_threshold {
PhaseSpaceClass::Chaotic
} else {
PhaseSpaceClass::NonResonating
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::maps::StandardMap;
fn assert_near(a: f64, b: f64, eps: f64) {
assert!(
(a - b).abs() < eps,
"expected {a} ≈ {b} (diff = {})",
(a - b).abs()
);
}
#[test]
fn test_observable_eval() {
let state = vec![0.5, 0.3];
assert_near(Observable::Identity.eval(&state), 0.5, 1e-12);
assert_near(Observable::SinPi.eval(&state), (PI * 0.5).sin(), 1e-12);
assert_near(Observable::CosPi.eval(&state), (PI * 0.5).cos(), 1e-12);
assert_near(
Observable::SinPiXY.eval(&state),
(PI * 0.5).sin() * (PI * 0.3).sin(),
1e-12,
);
}
#[test]
fn test_hta_zero_frequency() {
let map = StandardMap { epsilon: 0.0 };
let result =
harmonic_time_average(&[0.5, 0.3], &map, &Observable::Identity, 1e-10, 100).unwrap();
assert!(result.magnitude > 0.0);
}
#[test]
fn test_hta_basic() {
let map = StandardMap { epsilon: 0.12 };
let result = harmonic_time_average(
&[0.5, 0.3],
&map,
&Observable::SinPi,
0.5, 10000,
)
.unwrap();
assert!(result.magnitude >= 0.0);
assert_near(result.omega, 0.5, 1e-12);
assert_near(result.period, 2.0, 1e-12);
assert_eq!(result.n_iter, 10000);
}
#[test]
fn test_hta_from_values() {
let omega = 0.1;
let n = 10000;
let values: Vec<f64> = (0..n)
.map(|k| (2.0 * PI * k as f64 * omega).cos())
.collect();
let hta = hta_from_values(&values, omega);
assert!(hta.norm() > 0.4);
}
#[test]
fn test_hta_convergence_analysis() {
let map = StandardMap { epsilon: 0.12 };
let result = hta_convergence(
&[0.5, 0.25], &map,
&Observable::SinPi,
0.5,
50000,
None,
)
.unwrap();
assert!(!result.times.is_empty());
assert_eq!(result.hta_magnitudes.len(), result.times.len());
assert!(result.convergence_rate.is_some());
}
#[test]
fn test_classify_phase_space() {
let mags = vec![0.15, 0.001, 1e-5, 0.08, 0.0005];
let classes = classify_phase_space(&mags, 0.01, 1e-4);
assert_eq!(classes[0], PhaseSpaceClass::Resonating);
assert_eq!(classes[1], PhaseSpaceClass::Chaotic);
assert_eq!(classes[2], PhaseSpaceClass::NonResonating);
assert_eq!(classes[3], PhaseSpaceClass::Resonating);
assert_eq!(classes[4], PhaseSpaceClass::Chaotic);
}
#[test]
fn test_hta_error_zero_iter() {
let map = StandardMap::default();
assert!(harmonic_time_average(&[0.5, 0.3], &map, &Observable::SinPi, 0.5, 0).is_err());
}
}