Skip to content

Commit 8b87a34

Browse files
committed
feat: add fraction brownian sheet
1 parent fa9197f commit 8b87a34

File tree

3 files changed

+188
-0
lines changed

3 files changed

+188
-0
lines changed

src/stochastic.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ pub mod malliavin;
5353
pub mod noise;
5454
pub mod process;
5555
pub mod sde;
56+
pub mod sheet;
5657
pub mod volatility;
5758

5859
use std::sync::{Arc, Mutex};

src/stochastic/sheet.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod fbs;

src/stochastic/sheet/fbs.rs

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
use impl_new_derive::ImplNew;
2+
use ndarray::{linalg::kron, s, Array1, Array2, Axis};
3+
use ndarray_rand::RandomExt;
4+
use ndrustfft::{ndfft, ndfft_par, FftHandler};
5+
use num_complex::{Complex64, ComplexDistribution};
6+
use rand_distr::StandardNormal;
7+
8+
#[derive(ImplNew)]
9+
pub struct FBS {
10+
pub hurst: f64,
11+
pub m: usize,
12+
pub n: usize,
13+
pub R: f64,
14+
}
15+
16+
impl FBS {
17+
pub fn sample(&self) -> Array2<f64> {
18+
let (m, n, H, R) = (self.m, self.n, self.hurst, self.R);
19+
let alpha = 2.0 * H;
20+
21+
let tx = Array1::linspace(R / n as f64, R, n);
22+
let ty = Array1::linspace(R / m as f64, R, m);
23+
24+
let mut cov = Array2::<f64>::zeros((m, n));
25+
for i in 0..n {
26+
for j in 0..m {
27+
cov[[j, i]] = Self::rho((tx[i], ty[j]), (tx[0], ty[0]), R, alpha).0;
28+
}
29+
}
30+
31+
// 3. Construct block circulant matrix
32+
let big_m = 2 * (m - 1);
33+
let big_n = 2 * (n - 1);
34+
let mut blk = Array2::<f64>::zeros((big_m, big_n));
35+
// top-left block
36+
blk.slice_mut(s![..m, ..n]).assign(&cov);
37+
38+
// top-right: flip columns except first
39+
blk
40+
.slice_mut(s![..m, n..])
41+
.assign(&cov.slice(s![.., 1..n - 1;-1]));
42+
43+
// bottom-left: flip rows except first
44+
blk
45+
.slice_mut(s![m.., ..n])
46+
.assign(&cov.slice(s![1..m - 1;-1, ..]));
47+
48+
// bottom-right: flip both rows and cols except first
49+
blk
50+
.slice_mut(s![m.., n..])
51+
.assign(&cov.slice(s![1..m - 1, 1..n - 1]).slice(s![..;-1, ..;-1]));
52+
53+
// 4. Compute eigenvalues via FFT
54+
let scale = 4.0 * (m - 1) as f64 * (n - 1) as f64;
55+
let mut fft_handler0 = FftHandler::<f64>::new(big_m);
56+
let mut fft_handler1 = FftHandler::<f64>::new(big_n);
57+
58+
let blk_c = blk.mapv(Complex64::from);
59+
let mut fft_tmp = Array2::<Complex64>::zeros((big_m, big_n));
60+
ndfft(&blk_c, &mut fft_tmp, &mut fft_handler0, 0);
61+
let mut fft_freq = Array2::<Complex64>::zeros((big_m, big_n));
62+
ndfft(&fft_tmp, &mut fft_freq, &mut fft_handler1, 1);
63+
64+
let lam = fft_freq.mapv(|c| (c.re / scale).max(0.0).sqrt());
65+
66+
let z = Array2::random(
67+
(big_m, big_n),
68+
ComplexDistribution::new(StandardNormal, StandardNormal),
69+
);
70+
71+
let mut Z = lam.mapv(Complex64::from) * z;
72+
let mut inv_tmp = Array2::<Complex64>::zeros((big_m, big_n));
73+
ndfft_par(&Z, &mut inv_tmp, &mut fft_handler0, 0);
74+
ndfft_par(&inv_tmp, &mut Z, &mut fft_handler1, 1);
75+
76+
let mut field = Array2::<f64>::zeros((m, n));
77+
for i in 0..m {
78+
for j in 0..n {
79+
field[[i, j]] = Z[[i, j]].re;
80+
}
81+
}
82+
83+
let (_, _, c2) = Self::rho((0.0, 0.0), (0.0, 0.0), R, alpha);
84+
85+
let shift = field[[0, 0]];
86+
field.mapv_inplace(|v| v - shift);
87+
88+
let rand_x = Array1::<f64>::random(n, StandardNormal);
89+
let rand_y = Array1::<f64>::random(m, StandardNormal);
90+
91+
let ty_rand = &ty * &rand_y;
92+
let tx_rand = &tx * &rand_x;
93+
let ty_mat = ty_rand.insert_axis(Axis(1)); // (m, 1)
94+
let tx_mat = tx_rand.insert_axis(Axis(0)); // (1, n)
95+
96+
let correction = kron(&ty_mat, &tx_mat) * (2.0 * c2).sqrt();
97+
field = &field + &correction;
98+
99+
field
100+
}
101+
102+
fn rho(x: (f64, f64), y: (f64, f64), R: f64, alpha: f64) -> (f64, f64, f64) {
103+
let (beta, c2, c0) = if alpha <= 1.5 {
104+
let c2 = alpha / 2.0;
105+
let c0 = 1.0 - alpha / 2.0;
106+
(0.0, c2, c0)
107+
} else {
108+
let beta = alpha * (2.0 - alpha) / (3.0 * R * (R * R - 1.0));
109+
let c2 = (alpha - beta * (R - 1.0).powi(2) * (R + 2.0)) / 2.0;
110+
let c0 = beta * (R - 1.0).powi(3) + 1.0 - c2;
111+
(beta, c2, c0)
112+
};
113+
114+
let dx = x.0 - y.0;
115+
let dy = x.1 - y.1;
116+
let r = (dx * dx + dy * dy).sqrt();
117+
let out = if r <= 1.0 {
118+
c0 - r.powf(alpha) + c2 * r * r
119+
} else if r <= R {
120+
beta * (R - r).powi(3) / r
121+
} else {
122+
0.0
123+
};
124+
(out, c0, c2)
125+
}
126+
}
127+
128+
#[cfg(test)]
129+
mod tests {
130+
use super::FBS;
131+
use ndarray::s;
132+
use plotly::{surface::PlaneContours, Layout, Plot, Surface};
133+
134+
#[test]
135+
fn test_fbm_plot_matlab_like() {
136+
let m = 100;
137+
let n = 100;
138+
let hurst = 0.8;
139+
let R = 2.0;
140+
141+
let half_m = m / 2;
142+
let half_n = n / 2;
143+
144+
let fbs = FBS::new(hurst, m, n, R);
145+
let sheet = fbs.sample();
146+
147+
let x: Vec<f64> = (1..=half_n).map(|i| i as f64 * R / n as f64).collect();
148+
let y: Vec<f64> = (1..=half_m).map(|j| j as f64 * R / m as f64).collect();
149+
150+
let mut masked_half = sheet.slice(s![..half_m, ..half_n]).to_owned();
151+
152+
for (i, yi) in y.iter().enumerate() {
153+
for (j, xj) in x.iter().enumerate() {
154+
if xj.powi(2) + yi.powi(2) > 1.0 {
155+
masked_half[[i, j]] = f64::NAN; // NaN works as mask
156+
}
157+
}
158+
}
159+
160+
let z: Vec<Vec<f64>> = masked_half.outer_iter().map(|row| row.to_vec()).collect();
161+
162+
let surface = Surface::new(z)
163+
.x(x.clone())
164+
.y(y.clone())
165+
.name(&format!("FBS H={}", hurst))
166+
.color_scale(plotly::common::ColorScale::Palette(
167+
plotly::common::ColorScalePalette::Hot,
168+
))
169+
.show_scale(true)
170+
.contours(plotly::surface::SurfaceContours::new().z(PlaneContours::new()));
171+
172+
let mut plot = Plot::new();
173+
plot.add_trace(surface);
174+
plot.set_layout(
175+
Layout::new()
176+
.width(800)
177+
.height(800)
178+
.title("Fractional Gaussian Field")
179+
.x_axis(plotly::layout::Axis::new().title("X"))
180+
.y_axis(plotly::layout::Axis::new().title("Y")),
181+
);
182+
plot.show();
183+
184+
assert_eq!(sheet.shape(), &[m, n]);
185+
}
186+
}

0 commit comments

Comments
 (0)