use crate::errors::QlResult;
use crate::math::array::Array;
use crate::methods::finitedifferences::operators::FdmLinearOpComposite;
use crate::methods::finitedifferences::utilities::FdmBoundaryConditionSet;
use crate::shared::SharedMut;
use crate::types::{Real, Time};
use crate::{fail, require};
use super::boundaryconditionschemehelper::BoundaryConditionSchemeHelper;
use super::scheme::Scheme;
pub struct DouglasScheme {
dt: Option<Time>,
theta: Real,
map: SharedMut<dyn FdmLinearOpComposite>,
bc_set: BoundaryConditionSchemeHelper,
}
impl DouglasScheme {
pub fn new(
theta: Real,
map: SharedMut<dyn FdmLinearOpComposite>,
bc_set: FdmBoundaryConditionSet,
) -> Self {
DouglasScheme {
dt: None,
theta,
map,
bc_set: BoundaryConditionSchemeHelper::new(bc_set),
}
}
}
impl Scheme for DouglasScheme {
fn set_step(&mut self, dt: Time) {
self.dt = Some(dt);
}
#[allow(clippy::neg_cmp_op_on_partial_ord)]
fn step(&mut self, a: &mut Array, t: Time) -> QlResult<()> {
let Some(dt) = self.dt else {
fail!("the timestep is not set: call set_step before stepping");
};
require!(t - dt > -1e-8, "a step towards negative time given");
let start = (t - dt).max(0.0);
let mut y = {
let mut map = self.map.borrow_mut();
map.set_time(start, t)?;
self.bc_set.set_time(start);
self.bc_set.apply_before_applying(&mut *map);
let mut y = &*a + &(dt * &map.apply(a));
self.bc_set.apply_after_applying(&mut y);
for i in 0..map.size() {
let rhs = &y - &((self.theta * dt) * &map.apply_direction(i, a));
y = map.solve_splitting(i, &rhs, -self.theta * dt)?;
}
y
};
self.bc_set.apply_after_solving(&mut y);
*a = y;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::testops::{
GRID, WHOLE, assert_close, black_scholes_op, call_log, mesher, probe, scaled_composite,
};
use crate::methods::finitedifferences::operators::FdmLinearOp;
use crate::shared::shared_mut;
const THETA: Real = 0.5;
const DT: Time = 0.1;
const T: Time = 0.25;
const COEFFICIENTS: [Real; 2] = [0.3, -0.45];
fn douglas(
map: SharedMut<dyn FdmLinearOpComposite>,
bc_set: FdmBoundaryConditionSet,
) -> DouglasScheme {
let mut scheme = DouglasScheme::new(THETA, map, bc_set);
scheme.set_step(DT);
scheme
}
#[test]
fn a_step_replays_the_cpp_sequence_on_the_black_scholes_operator() {
let mesher = mesher();
let map: SharedMut<dyn FdmLinearOpComposite> = shared_mut(black_scholes_op(&mesher));
let mut scheme = douglas(map, Vec::new());
let u = probe(GRID);
let mut a = u.clone();
scheme.step(&mut a, T).unwrap();
let mut oracle = black_scholes_op(&mesher);
oracle.set_time(T - DT, T).unwrap();
let mut y = &u + &(DT * &oracle.apply(&u));
for i in 0..oracle.size() {
let rhs = &y - &((THETA * DT) * &oracle.apply_direction(i, &u));
y = oracle.solve_splitting(i, &rhs, -THETA * DT).unwrap();
}
assert_close(&a, &y);
}
#[test]
fn the_split_right_hand_side_can_tell_the_input_from_the_update() {
let mesher = mesher();
let mut oracle = black_scholes_op(&mesher);
oracle.set_time(T - DT, T).unwrap();
let u = probe(GRID);
let y = &u + &(DT * &oracle.apply(&u));
let from_input = oracle.apply_direction(0, &u);
let from_update = oracle.apply_direction(0, &y);
let gap = (0..u.size())
.map(|i| (THETA * DT * (from_input[i] - from_update[i])).abs())
.fold(0.0, Real::max);
assert!(gap > 1e-6, "the two right-hand sides are the same: {gap}");
}
#[test]
fn a_step_matches_the_closed_form_on_a_diagonal_operator() {
let mut scheme = douglas(scaled_composite(&COEFFICIENTS), Vec::new());
let u = probe(4);
let mut a = u.clone();
scheme.step(&mut a, T).unwrap();
let mut expected = &u * (1.0 + DT * WHOLE);
for c in COEFFICIENTS {
expected = &(&expected - &((THETA * DT * c) * &u)) / (1.0 - THETA * DT * c);
}
assert_close(&a, &expected);
}
#[test]
fn a_step_sets_the_operator_and_the_conditions_at_the_clamped_start() {
let raw = scaled_composite(&COEFFICIENTS[..1]);
let map: SharedMut<dyn FdmLinearOpComposite> = raw.clone();
let (log, bc_set) = call_log();
let mut scheme = douglas(map, bc_set);
let t = DT - 5e-9;
scheme.step(&mut probe(4), t).unwrap();
assert_eq!(raw.borrow().last_set_time, Some((0.0, t)));
assert_eq!(
*log.borrow(),
vec![
"set_time:0".to_string(),
"before_applying".to_string(),
"after_applying".to_string(),
"after_solving".to_string(),
]
);
}
#[test]
fn stepping_before_the_timestep_is_set_fails() {
let mut scheme = DouglasScheme::new(THETA, scaled_composite(&COEFFICIENTS), Vec::new());
assert!(scheme.step(&mut probe(4), T).is_err());
}
#[test]
fn a_step_towards_negative_time_fails() {
let mut scheme = douglas(scaled_composite(&COEFFICIENTS), Vec::new());
assert!(scheme.step(&mut probe(4), DT / 2.0).is_err());
}
}