use crate::errors::QlResult;
use crate::math::array::Array;
use crate::methods::finitedifferences::StepCondition;
use crate::methods::finitedifferences::schemes::Scheme;
use crate::require;
use crate::types::{Real, Size, Time};
pub struct FiniteDifferenceModel<S> {
evolver: S,
stopping_times: Vec<Time>,
}
impl<S: Scheme> FiniteDifferenceModel<S> {
pub fn new(evolver: S, stopping_times: &[Time]) -> Self {
let mut stopping_times = stopping_times.to_vec();
stopping_times.sort_by(Real::total_cmp);
stopping_times.dedup();
FiniteDifferenceModel {
evolver,
stopping_times,
}
}
#[allow(clippy::neg_cmp_op_on_partial_ord)]
pub fn rollback(
&mut self,
a: &mut Array,
from: Time,
to: Time,
steps: Size,
condition: Option<&dyn StepCondition>,
) -> QlResult<()> {
require!(from >= to, "trying to roll back from {from} to {to}");
let dt = (from - to) / steps as Real;
let mut t = from;
self.evolver.set_step(dt);
if self.stopping_times.last() == Some(&from)
&& let Some(condition) = condition
{
condition.apply_to(a, from);
}
for i in 0..steps {
let mut now = t;
let mut next = if i < steps - 1 { t - dt } else { to };
if (to - next).abs() < Real::EPSILON.sqrt() {
next = to;
}
let mut hit = false;
for &stopping_time in self.stopping_times.iter().rev() {
if next <= stopping_time && stopping_time < now {
hit = true;
self.evolver.set_step(now - stopping_time);
self.evolver.step(a, now)?;
if let Some(condition) = condition {
condition.apply_to(a, stopping_time);
}
now = stopping_time;
}
}
if hit {
if now > next {
self.evolver.set_step(now - next);
self.evolver.step(a, now)?;
if let Some(condition) = condition {
condition.apply_to(a, next);
}
}
self.evolver.set_step(dt);
} else {
self.evolver.step(a, now)?;
if let Some(condition) = condition {
condition.apply_to(a, next);
}
}
t -= dt;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use super::*;
use crate::fail;
use crate::methods::finitedifferences::schemes::testops::{
WHOLE, assert_close, probe, scaled_composite,
};
use crate::methods::finitedifferences::schemes::{DouglasScheme, ImplicitEulerScheme};
use crate::methods::finitedifferences::stepconditions::FdmSnapshotCondition;
use crate::shared::{Shared, shared};
const THETA: Real = 0.5;
const COEFFICIENT: Real = 0.4;
const COEFFICIENTS: [Real; 2] = [0.3, -0.45];
const SIZE: Size = 4;
struct LogScheme {
dt: Option<Time>,
failing: bool,
log: Shared<RefCell<Vec<String>>>,
}
impl Scheme for LogScheme {
fn set_step(&mut self, dt: Time) {
self.dt = Some(dt);
}
fn step(&mut self, _a: &mut Array, t: Time) -> QlResult<()> {
let dt = self.dt.expect("the rollback sets the step before stepping");
self.log
.borrow_mut()
.push(format!("step dt={dt:.6} t={t:.6}"));
if self.failing {
fail!("the scheme was asked to fail");
}
Ok(())
}
}
struct LogCondition {
log: Shared<RefCell<Vec<String>>>,
}
impl StepCondition for LogCondition {
fn apply_to(&self, _a: &mut Array, t: Time) {
self.log.borrow_mut().push(format!("condition t={t:.6}"));
}
}
fn log_model(
failing: bool,
stopping_times: &[Time],
) -> (
Shared<RefCell<Vec<String>>>,
FiniteDifferenceModel<LogScheme>,
) {
let log = shared(RefCell::new(Vec::new()));
let scheme = LogScheme {
dt: None,
failing,
log: Shared::clone(&log),
};
(
Shared::clone(&log),
FiniteDifferenceModel::new(scheme, stopping_times),
)
}
#[test]
fn implicit_euler_steps_compound_to_the_closed_form() {
let steps = 4;
let dt = 0.25;
let mut model = FiniteDifferenceModel::new(
ImplicitEulerScheme::new(scaled_composite(&[COEFFICIENT]), Vec::new()),
&[],
);
let u = probe(SIZE);
let mut a = u.clone();
model.rollback(&mut a, 1.0, 0.0, steps, None).unwrap();
let expected = &u / (1.0 - dt * COEFFICIENT).powi(steps as i32);
assert_close(&a, &expected);
}
#[test]
fn a_douglas_rollback_chains_the_one_step_map() {
let dt = 0.2;
let mut model = FiniteDifferenceModel::new(
DouglasScheme::new(THETA, scaled_composite(&COEFFICIENTS), Vec::new()),
&[],
);
let mut a = probe(SIZE);
model.rollback(&mut a, 0.9, 0.3, 3, None).unwrap();
let mut expected = probe(SIZE);
for _ in 0..3 {
let u = expected.clone();
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 the_step_and_condition_times_follow_the_cpp_bookkeeping() {
let (log, mut model) = log_model(false, &[]);
let condition = LogCondition {
log: Shared::clone(&log),
};
model
.rollback(&mut probe(SIZE), 1.0, 0.0, 4, Some(&condition))
.unwrap();
assert_eq!(
*log.borrow(),
vec![
"step dt=0.250000 t=1.000000",
"condition t=0.750000",
"step dt=0.250000 t=0.750000",
"condition t=0.500000",
"step dt=0.250000 t=0.500000",
"condition t=0.250000",
"step dt=0.250000 t=0.250000",
"condition t=0.000000",
]
);
}
#[test]
fn rolling_forward_fails() {
let (_, mut model) = log_model(false, &[]);
assert!(model.rollback(&mut probe(SIZE), 0.0, 1.0, 4, None).is_err());
}
#[test]
fn a_failing_step_stops_the_rollback() {
let (log, mut model) = log_model(true, &[]);
assert!(model.rollback(&mut probe(SIZE), 1.0, 0.0, 4, None).is_err());
assert_eq!(log.borrow().len(), 1);
}
#[test]
fn a_stopping_time_at_from_applies_the_condition_first() {
let (_, mut model) = log_model(false, &[1.0]);
let snapshot = FdmSnapshotCondition::new(1.0);
let u = probe(SIZE);
model
.rollback(&mut u.clone(), 1.0, 0.0, 4, Some(&snapshot))
.unwrap();
assert_eq!(snapshot.values(), u);
}
#[test]
fn a_step_spanning_stopping_times_is_cut_at_each_from_the_latest_down() {
let (log, mut model) = log_model(false, &[0.7, 0.9, 0.7]);
let condition = LogCondition {
log: Shared::clone(&log),
};
model
.rollback(&mut probe(SIZE), 1.0, 0.0, 2, Some(&condition))
.unwrap();
assert_eq!(
*log.borrow(),
vec![
"step dt=0.100000 t=1.000000",
"condition t=0.900000",
"step dt=0.200000 t=0.900000",
"condition t=0.700000",
"step dt=0.200000 t=0.700000",
"condition t=0.500000",
"step dt=0.500000 t=0.500000",
"condition t=0.000000",
]
);
}
#[test]
fn a_stopping_time_at_to_is_hit_on_the_last_step() {
let (_, mut model) = log_model(false, &[4.0]);
let snapshot = FdmSnapshotCondition::new(4.0);
model
.rollback(&mut probe(SIZE), 4.625, 4.0, 38, Some(&snapshot))
.unwrap();
assert_eq!(snapshot.values(), probe(SIZE));
}
#[test]
fn a_condition_at_a_stopping_time_sees_the_grid_at_that_time() {
let mut model = FiniteDifferenceModel::new(
ImplicitEulerScheme::new(scaled_composite(&[COEFFICIENT]), Vec::new()),
&[0.9],
);
let snapshot = FdmSnapshotCondition::new(0.9);
let u = probe(SIZE);
let mut a = u.clone();
model
.rollback(&mut a, 1.0, 0.0, 2, Some(&snapshot))
.unwrap();
let expected = &u / (1.0 - 0.1 * COEFFICIENT);
assert_close(&snapshot.values(), &expected);
}
}