use crate::error::{IntegrateError, IntegrateResult};
use scirs2_core::ndarray::Array1;
use scirs2_core::numeric::Complex64;
use scirs2_fft::{fft, ifft};
struct Lcg {
state: u64,
}
impl Lcg {
fn new(seed: u64) -> Self {
let state = seed
.wrapping_mul(6_364_136_223_846_793_005_u64)
.wrapping_add(1_442_695_040_888_963_407_u64);
Self { state }
}
fn next_f64(&mut self) -> f64 {
self.state = self.state.wrapping_add(0x9e37_79b9_7f4a_7c15_u64);
let mut z = self.state;
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9_u64);
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb_u64);
z = z ^ (z >> 31);
(z >> 11) as f64 * (1.0_f64 / (1u64 << 53) as f64)
}
fn next_normal(&mut self) -> f64 {
loop {
let u1 = self.next_f64();
let u2 = self.next_f64();
if u1 > 1e-300 {
let mag = (-2.0 * u1.ln()).sqrt();
let theta = std::f64::consts::TAU * u2;
return mag * theta.cos(); }
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FbmMethod {
Hosking,
DaviesHarte,
}
#[derive(Debug, Clone)]
pub struct FbmConfig {
pub hurst: f64,
pub n_steps: usize,
pub dt: f64,
pub seed: u64,
pub method: FbmMethod,
}
impl Default for FbmConfig {
fn default() -> Self {
Self {
hurst: 0.7,
n_steps: 256,
dt: 1.0 / 256.0,
seed: 42,
method: FbmMethod::DaviesHarte,
}
}
}
pub struct FractionalBrownianMotion {
config: FbmConfig,
}
impl FractionalBrownianMotion {
pub fn new(config: FbmConfig) -> Self {
Self { config }
}
fn validate(&self) -> IntegrateResult<()> {
if self.config.hurst <= 0.0 || self.config.hurst >= 1.0 {
return Err(IntegrateError::InvalidInput(format!(
"Hurst exponent must be in (0, 1), got {}",
self.config.hurst
)));
}
if self.config.n_steps == 0 {
return Err(IntegrateError::InvalidInput(
"n_steps must be at least 1".to_string(),
));
}
if self.config.dt <= 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"dt must be positive, got {}",
self.config.dt
)));
}
Ok(())
}
pub fn increments(&self) -> IntegrateResult<Array1<f64>> {
self.validate()?;
match self.config.method {
FbmMethod::Hosking => self.increments_hosking(),
FbmMethod::DaviesHarte => self.increments_davies_harte(),
}
}
pub fn sample_path(&self) -> IntegrateResult<Array1<f64>> {
let xi = self.increments()?;
let n = xi.len();
let mut path = Array1::zeros(n + 1);
let mut cum = 0.0_f64;
for i in 0..n {
cum += xi[i];
path[i + 1] = cum;
}
Ok(path)
}
pub fn increment_variance(&self) -> f64 {
self.config.dt.powf(2.0 * self.config.hurst)
}
fn increments_hosking(&self) -> IntegrateResult<Array1<f64>> {
let n = self.config.n_steps;
let h = self.config.hurst;
let dt = self.config.dt;
let h2 = 2.0 * h;
let gamma_lag = |lag: usize| -> f64 {
let l = lag as f64;
0.5 * dt.powf(h2) * ((l + 1.0).powf(h2) - 2.0 * l.powf(h2) + (l - 1.0).abs().powf(h2))
};
let cov_elem = |i: usize, j: usize| -> f64 {
let lag = i.abs_diff(j);
gamma_lag(lag)
};
let mut l = vec![0.0_f64; n * n];
for i in 0..n {
for j in 0..=i {
let c_ij = cov_elem(i + 1, j + 1); let mut sum = c_ij;
for k in 0..j {
sum -= l[i * n + k] * l[j * n + k];
}
if i == j {
if sum <= 0.0 {
sum = 1e-15;
}
l[i * n + j] = sum.sqrt();
} else {
let l_jj = l[j * n + j];
if l_jj.abs() < 1e-300 {
return Err(IntegrateError::ComputationError(
"Cholesky: near-zero diagonal element".to_string(),
));
}
l[i * n + j] = sum / l_jj;
}
}
}
let mut rng = Lcg::new(self.config.seed);
let z: Vec<f64> = (0..n).map(|_| rng.next_normal()).collect();
let xi_vec: Vec<f64> = (0..n)
.map(|i| {
let mut s = 0.0_f64;
for k in 0..=i {
s += l[i * n + k] * z[k];
}
s
})
.collect();
Ok(Array1::from_vec(xi_vec))
}
fn increments_davies_harte(&self) -> IntegrateResult<Array1<f64>> {
let n = self.config.n_steps;
if n < 2 {
return self.increments_hosking();
}
let h = self.config.hurst;
let dt = self.config.dt;
let gamma = |k: usize| -> f64 {
let kf = k as f64;
let h2 = 2.0 * h;
0.5 * dt.powf(h2)
* ((kf + 1.0).powf(h2) - 2.0 * kf.powf(h2) + (kf - 1.0).abs().powf(h2))
};
let m = 2 * n;
let mut circulant_row: Vec<Complex64> = Vec::with_capacity(m);
for k in 0..n {
circulant_row.push(Complex64::new(gamma(k), 0.0));
}
for k in (1..n).rev() {
circulant_row.push(Complex64::new(gamma(k), 0.0));
}
let fft_c = fft(&circulant_row, None).map_err(|e| {
IntegrateError::ComputationError(format!("Davies-Harte FFT failed: {e}"))
})?;
let lambda: Vec<f64> = fft_c.iter().map(|c| c.re.max(0.0)).collect();
let mut rng = Lcg::new(self.config.seed);
let mut w = vec![Complex64::new(0.0, 0.0); m];
let half = m / 2;
w[0] = Complex64::new(lambda[0].sqrt() * rng.next_normal(), 0.0);
w[half] = Complex64::new(lambda[half].sqrt() * rng.next_normal(), 0.0);
for k in 1..half {
let sigma = (lambda[k] / 2.0).sqrt();
let re = sigma * rng.next_normal();
let im = sigma * rng.next_normal();
w[k] = Complex64::new(re, im);
w[m - k] = Complex64::new(re, -im); }
let z_complex = ifft(&w, None).map_err(|e| {
IntegrateError::ComputationError(format!("Davies-Harte IFFT failed: {e}"))
})?;
let sqrt_m = (m as f64).sqrt();
let xi_vec: Vec<f64> = z_complex[..n].iter().map(|c| c.re * sqrt_m).collect();
Ok(Array1::from_vec(xi_vec))
}
pub fn estimate_hurst(path: &Array1<f64>) -> f64 {
let n_path = path.len();
if n_path < 21 {
return 0.5;
}
let increments: Vec<f64> = (1..n_path).map(|i| path[i] - path[i - 1]).collect();
let n = increments.len();
if n < 20 {
return 0.5;
}
let mut block_sizes: Vec<usize> = Vec::new();
let mut bs = 8_usize;
while bs <= n / 2 {
block_sizes.push(bs);
bs *= 2;
}
if block_sizes.is_empty() {
block_sizes.push(n / 2);
}
let mut log_rs_vals: Vec<f64> = Vec::with_capacity(block_sizes.len());
let mut log_n_vals: Vec<f64> = Vec::with_capacity(block_sizes.len());
for &blk in &block_sizes {
let n_blocks = n / blk;
if n_blocks == 0 {
continue;
}
let mut rs_sum = 0.0_f64;
let mut count = 0_usize;
for b in 0..n_blocks {
let start = b * blk;
let end = start + blk;
let slice = &increments[start..end];
let mean = slice.iter().sum::<f64>() / (blk as f64);
let mut cum = 0.0_f64;
let mut min_cum = 0.0_f64;
let mut max_cum = 0.0_f64;
let mut sum_sq = 0.0_f64;
for &v in slice.iter() {
let dev = v - mean;
cum += dev;
sum_sq += dev * dev;
if cum < min_cum {
min_cum = cum;
}
if cum > max_cum {
max_cum = cum;
}
}
let range = max_cum - min_cum;
let std_dev = (sum_sq / blk as f64).sqrt();
if std_dev > 1e-300 {
rs_sum += range / std_dev;
count += 1;
}
}
if count > 0 {
let rs_avg = rs_sum / count as f64;
if rs_avg > 0.0 {
log_rs_vals.push(rs_avg.ln());
log_n_vals.push((blk as f64).ln());
}
}
}
if log_rs_vals.len() < 2 {
return 0.5;
}
let k = log_rs_vals.len() as f64;
let sum_x: f64 = log_n_vals.iter().sum();
let sum_y: f64 = log_rs_vals.iter().sum();
let sum_xx: f64 = log_n_vals.iter().map(|&x| x * x).sum();
let sum_xy: f64 = log_n_vals
.iter()
.zip(&log_rs_vals)
.map(|(&x, &y)| x * y)
.sum();
let denom = k * sum_xx - sum_x * sum_x;
if denom.abs() < 1e-300 {
return 0.5;
}
let slope = (k * sum_xy - sum_x * sum_y) / denom;
slope.clamp(0.0, 1.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_cfg() -> FbmConfig {
FbmConfig::default()
}
fn hosking_cfg(h: f64, n: usize) -> FbmConfig {
FbmConfig {
hurst: h,
n_steps: n,
dt: 1.0 / n as f64,
seed: 1234,
method: FbmMethod::Hosking,
}
}
fn dh_cfg(h: f64, n: usize) -> FbmConfig {
FbmConfig {
hurst: h,
n_steps: n,
dt: 1.0 / n as f64,
seed: 5678,
method: FbmMethod::DaviesHarte,
}
}
#[test]
fn test_default_config() {
let fbm = FractionalBrownianMotion::new(default_cfg());
let path = fbm.sample_path().expect("sample_path should succeed");
assert_eq!(path.len(), 257); }
#[test]
fn test_hosking_path_starts_at_zero() {
let fbm = FractionalBrownianMotion::new(hosking_cfg(0.7, 64));
let path = fbm.sample_path().expect("sample_path should succeed");
assert!(
path[0].abs() < 1e-15,
"Hosking path[0] = {}, expected 0",
path[0]
);
}
#[test]
fn test_davies_harte_path_starts_at_zero() {
let fbm = FractionalBrownianMotion::new(dh_cfg(0.7, 128));
let path = fbm.sample_path().expect("sample_path should succeed");
assert!(
path[0].abs() < 1e-15,
"Davies-Harte path[0] = {}, expected 0",
path[0]
);
}
#[test]
fn test_path_length() {
for &n in &[32_usize, 64, 128, 256] {
let fbm = FractionalBrownianMotion::new(dh_cfg(0.6, n));
let path = fbm.sample_path().expect("sample_path should succeed");
assert_eq!(path.len(), n + 1, "Expected n+1 = {} path points", n + 1);
}
}
#[test]
fn test_increment_variance() {
let cfg = FbmConfig {
hurst: 0.7,
n_steps: 512,
dt: 1.0 / 512.0,
seed: 99,
method: FbmMethod::DaviesHarte,
};
let expected_var = cfg.dt.powf(2.0 * cfg.hurst);
let fbm = FractionalBrownianMotion::new(cfg);
let var = fbm.increment_variance();
let rel = (var - expected_var).abs() / expected_var;
assert!(rel < 1e-12, "increment_variance relative error = {}", rel);
}
#[test]
fn test_hosking_vs_davies_harte_statistical_properties() {
let n = 256;
let dt = 1.0 / n as f64;
let h = 0.7;
let h_cfg = FbmConfig {
hurst: h,
n_steps: n,
dt,
seed: 42,
method: FbmMethod::Hosking,
};
let dh_cfg_ = FbmConfig {
hurst: h,
n_steps: n,
dt,
seed: 42,
method: FbmMethod::DaviesHarte,
};
let xi_h = FractionalBrownianMotion::new(h_cfg)
.increments()
.expect("hosking increments");
let xi_dh = FractionalBrownianMotion::new(dh_cfg_)
.increments()
.expect("dh increments");
let mean_h: f64 = xi_h.sum() / n as f64;
let mean_dh: f64 = xi_dh.sum() / n as f64;
let expected_var = dt.powf(2.0 * h);
let var_h: f64 = xi_h.iter().map(|&v| v * v).sum::<f64>() / n as f64;
let var_dh: f64 = xi_dh.iter().map(|&v| v * v).sum::<f64>() / n as f64;
assert!(
mean_h.abs() < 5.0 * expected_var.sqrt(),
"Hosking mean {} too large",
mean_h
);
assert!(
mean_dh.abs() < 5.0 * expected_var.sqrt(),
"DH mean {} too large",
mean_dh
);
assert!(
var_h < 3.0 * expected_var,
"Hosking variance {} >> {}",
var_h,
expected_var
);
assert!(
var_dh < 3.0 * expected_var,
"DH variance {} >> {}",
var_dh,
expected_var
);
}
#[test]
fn test_h_half_linear_variance() {
let n = 512;
let dt = 1.0 / n as f64;
let cfg = FbmConfig {
hurst: 0.5,
n_steps: n,
dt,
seed: 7,
method: FbmMethod::DaviesHarte,
};
let fbm = FractionalBrownianMotion::new(cfg);
let n_paths = 200;
let mid = n / 2;
let mut sum_sq = 0.0_f64;
for seed in 0..n_paths_u64(n_paths) {
let cfg2 = FbmConfig {
hurst: 0.5,
n_steps: n,
dt,
seed,
method: FbmMethod::DaviesHarte,
};
let path = FractionalBrownianMotion::new(cfg2)
.sample_path()
.expect("sample_path");
sum_sq += path[mid] * path[mid];
}
let sample_var = sum_sq / n_paths as f64;
let theoretical = (mid as f64) * dt; let rel = (sample_var - theoretical).abs() / theoretical;
assert!(
rel < 0.3,
"H=0.5 variance at t=0.5: sample={:.4}, theory={:.4}, rel={:.4}",
sample_var,
theoretical,
rel
);
let _ = fbm.sample_path().expect("sample_path");
}
#[test]
fn test_estimate_hurst_within_tolerance() {
let h_true = 0.7_f64;
let n = 512;
let dt = 1.0 / n as f64;
let mut h_estimates = Vec::new();
for seed in 0..20_u64 {
let cfg = FbmConfig {
hurst: h_true,
n_steps: n,
dt,
seed,
method: FbmMethod::DaviesHarte,
};
let path = FractionalBrownianMotion::new(cfg)
.sample_path()
.expect("path");
h_estimates.push(FractionalBrownianMotion::estimate_hurst(&path));
}
let mean_h: f64 = h_estimates.iter().sum::<f64>() / h_estimates.len() as f64;
assert!(
(mean_h - h_true).abs() < 0.2,
"Hurst estimate {:.3} not within 0.2 of true {:.1}",
mean_h,
h_true
);
}
#[test]
fn test_long_range_dependence_h_gt_half() {
let n = 512;
let dt = 1.0 / n as f64;
let cfg = FbmConfig {
hurst: 0.8,
n_steps: n,
dt,
seed: 13,
method: FbmMethod::DaviesHarte,
};
let xi = FractionalBrownianMotion::new(cfg)
.increments()
.expect("increments");
let lag1_acf = compute_lag1_acf(&xi);
assert!(
lag1_acf > 0.0,
"H=0.8 lag-1 acf should be positive, got {}",
lag1_acf
);
}
#[test]
fn test_anti_persistent_h_lt_half() {
let n = 512;
let dt = 1.0 / n as f64;
let cfg = FbmConfig {
hurst: 0.2,
n_steps: n,
dt,
seed: 17,
method: FbmMethod::DaviesHarte,
};
let xi = FractionalBrownianMotion::new(cfg)
.increments()
.expect("increments");
let lag1_acf = compute_lag1_acf(&xi);
assert!(
lag1_acf < 0.0,
"H=0.2 lag-1 acf should be negative, got {}",
lag1_acf
);
}
#[test]
fn test_invalid_h_zero() {
let cfg = FbmConfig {
hurst: 0.0,
..Default::default()
};
let fbm = FractionalBrownianMotion::new(cfg);
assert!(fbm.sample_path().is_err(), "H=0 should be invalid");
}
#[test]
fn test_invalid_h_one() {
let cfg = FbmConfig {
hurst: 1.0,
..Default::default()
};
let fbm = FractionalBrownianMotion::new(cfg);
assert!(fbm.sample_path().is_err(), "H=1 should be invalid");
}
fn compute_lag1_acf(xi: &Array1<f64>) -> f64 {
let n = xi.len();
if n < 2 {
return 0.0;
}
let mean = xi.sum() / n as f64;
let var: f64 = xi.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / n as f64;
if var < 1e-300 {
return 0.0;
}
let cov: f64 = xi
.iter()
.zip(xi.iter().skip(1))
.map(|(&a, &b)| (a - mean) * (b - mean))
.sum::<f64>()
/ (n - 1) as f64;
cov / var
}
fn n_paths_u64(n: usize) -> u64 {
n as u64
}
}