use crate::errors::QlResult;
use crate::math::array::Array;
use crate::math::randomnumbers::rngtraits::SequenceGenerator;
use crate::math::timegrid::TimeGrid;
use crate::methods::montecarlo::{Path, PathGen, Sample};
use crate::shared::Shared;
use crate::stochasticprocess::StochasticProcess1D;
use crate::types::{Size, Time};
use crate::{fail, require};
pub struct PathGenerator<GSG> {
generator: GSG,
dimension: Size,
time_grid: TimeGrid,
process: Shared<dyn StochasticProcess1D>,
}
impl<GSG: SequenceGenerator> PathGenerator<GSG> {
pub fn new(
process: Shared<dyn StochasticProcess1D>,
length: Time,
time_steps: Size,
generator: GSG,
brownian_bridge: bool,
) -> QlResult<Self> {
let time_grid = TimeGrid::new(length, time_steps)?;
Self::assemble(process, time_grid, generator, brownian_bridge)
}
pub fn from_time_grid(
process: Shared<dyn StochasticProcess1D>,
time_grid: TimeGrid,
generator: GSG,
brownian_bridge: bool,
) -> QlResult<Self> {
Self::assemble(process, time_grid, generator, brownian_bridge)
}
fn assemble(
process: Shared<dyn StochasticProcess1D>,
time_grid: TimeGrid,
generator: GSG,
brownian_bridge: bool,
) -> QlResult<Self> {
require!(
!brownian_bridge,
"brownian bridge path generation is not yet ported; only the \
direct-copy variant is available"
);
let dimension = generator.dimension();
let time_steps = time_grid.size() - 1;
require!(
dimension == time_steps,
"sequence generator dimensionality ({dimension}) != timeSteps ({time_steps})"
);
Ok(PathGenerator {
generator,
dimension,
time_grid,
process,
})
}
pub fn size(&self) -> Size {
self.dimension
}
pub fn time_grid(&self) -> &TimeGrid {
&self.time_grid
}
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> QlResult<Sample<Path>> {
let (weight, draws) = {
let sequence = self.generator.next_sequence();
(sequence.weight, sequence.value.clone())
};
let mut path = Path::new(self.time_grid.clone(), Array::new())?;
*path.front_mut() = self.process.x0()?;
for i in 1..path.length() {
let t = self.time_grid[i - 1];
let dt = self.time_grid.dt(i - 1);
path[i] = self.process.evolve(t, path[i - 1], dt, draws[i - 1])?;
}
Ok(Sample::new(path, weight))
}
}
impl<GSG: SequenceGenerator> PathGen for PathGenerator<GSG> {
type PathType = Path;
fn next(&mut self) -> QlResult<Sample<Path>> {
PathGenerator::next(self)
}
fn antithetic(&mut self) -> QlResult<Sample<Path>> {
fail!(
"antithetic single-factor path generation is not yet ported; only \
the forward draw is available"
)
}
fn dimension(&self) -> Size {
self.dimension
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::handle::{Handle, RelinkableHandle};
use crate::interestrate::Compounding;
use crate::math::randomnumbers::rngtraits::{McRngTraits, PseudoRandom};
use crate::processes::BlackScholesMertonProcess;
use crate::quotes::make_quote_handle;
use crate::shared::shared;
use crate::termstructures::volatility::{BlackConstantVol, BlackVolTermStructure};
use crate::termstructures::yields::FlatForward;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::calendars::target::Target;
use crate::time::date::{Date, Month};
use crate::time::daycounters::actual360::Actual360;
use crate::time::frequency::Frequency;
use crate::types::{Rate, Real, Volatility};
const SPOT: Real = 100.0;
const R: Rate = 0.05;
const Q: Rate = 0.02;
const VOL: Volatility = 0.20;
fn reference() -> Date {
Date::new(15, Month::June, 2026)
}
fn flat_yield(rate: Rate) -> Handle<dyn YieldTermStructure> {
Handle::new(shared(FlatForward::with_rate(
reference(),
rate,
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>)
}
fn gbs_process() -> Shared<dyn StochasticProcess1D> {
let spot = make_quote_handle(SPOT);
let vol = RelinkableHandle::new(shared(BlackConstantVol::new(
reference(),
Some(Target::new()),
VOL,
Actual360::new(),
)) as Shared<dyn BlackVolTermStructure>);
shared(BlackScholesMertonProcess::new(
spot.handle(),
flat_yield(Q),
flat_yield(R),
vol.handle(),
)) as Shared<dyn StochasticProcess1D>
}
fn generator(dimension: usize, seed: u32) -> <PseudoRandom as McRngTraits>::RsgType {
PseudoRandom::make_sequence_generator(dimension, seed).unwrap()
}
#[test]
fn path_invariants_hold() {
let mut pg = PathGenerator::new(gbs_process(), 1.0, 12, generator(12, 42), false).unwrap();
assert_eq!(pg.size(), 12);
let grid = pg.time_grid().clone();
let sample = pg.next().unwrap();
let path = &sample.value;
assert_eq!(path.length(), grid.size());
assert_eq!(path[0], SPOT);
assert_eq!(path.front(), SPOT);
assert_eq!(path[0], path.front());
for i in 0..path.length() {
assert_eq!(path.time(i), grid[i]);
}
}
#[test]
fn same_seed_produces_identical_paths() {
let mut a = PathGenerator::new(gbs_process(), 1.0, 12, generator(12, 42), false).unwrap();
let mut b = PathGenerator::new(gbs_process(), 1.0, 12, generator(12, 42), false).unwrap();
let pa = a.next().unwrap();
let pb = b.next().unwrap();
assert_eq!(pa.value.values(), pb.value.values());
}
#[test]
fn dimension_mismatch_is_rejected() {
match PathGenerator::new(gbs_process(), 1.0, 12, generator(10, 42), false) {
Err(e) => assert!(e.message().contains("!= timeSteps")),
Ok(_) => panic!("dimensionality 10 != 12 timeSteps must be rejected"),
}
let grid = TimeGrid::new(1.0, 5).unwrap();
assert!(
PathGenerator::from_time_grid(gbs_process(), grid, generator(10, 42), false).is_err()
);
}
#[test]
fn brownian_bridge_is_rejected_as_deferred() {
match PathGenerator::new(gbs_process(), 1.0, 12, generator(12, 42), true) {
Err(e) => assert!(e.message().contains("brownian bridge")),
Ok(_) => panic!("brownian_bridge = true must be rejected as deferred"),
}
}
#[test]
fn pathgen_antithetic_is_rejected_as_deferred() {
let mut pg = PathGenerator::new(gbs_process(), 1.0, 12, generator(12, 42), false).unwrap();
match PathGen::antithetic(&mut pg) {
Err(e) => assert!(e.message().contains("antithetic")),
Ok(_) => panic!("single-factor antithetic must be rejected as deferred"),
}
}
#[test]
fn terminal_moments_match_the_gbm_law() {
const N: usize = 50_000;
const STEPS: usize = 12;
const T: Time = 1.0;
let mut pg =
PathGenerator::new(gbs_process(), T, STEPS, generator(STEPS, 42), false).unwrap();
let mut logs = Vec::with_capacity(N);
for _ in 0..N {
let path = pg.next().unwrap().value;
logs.push((path.back() / path.front()).ln());
}
let mean = logs.iter().sum::<Real>() / N as Real;
let variance = logs.iter().map(|x| (x - mean).powi(2)).sum::<Real>() / (N as Real - 1.0);
let mean_target = (R - Q - 0.5 * VOL * VOL) * T;
let var_target = VOL * VOL * T;
let se = VOL * (T / N as Real).sqrt();
let se_var = 2.0_f64.sqrt() * var_target / (N as Real - 1.0).sqrt();
assert!(
(mean - mean_target).abs() < 4.0 * se,
"mean {mean} vs target {mean_target}: {:.2} se (bound 4.0)",
(mean - mean_target).abs() / se
);
assert!(
(variance - var_target).abs() < 5.0 * se_var,
"variance {variance} vs target {var_target}: {:.2} se_var (bound 5.0)",
(variance - var_target).abs() / se_var
);
}
}