use crate::DType;
use numr::error::Result;
use numr::ops::{ScalarOps, TensorOps};
use numr::runtime::{Runtime, RuntimeClient};
use numr::tensor::Tensor;
#[derive(Debug, Clone)]
pub struct DenseOutputStep<R: Runtime<DType = DType>> {
pub t_old: f64,
pub t_new: f64,
pub y_old: Tensor<R>,
pub y_new: Tensor<R>,
pub f_old: Tensor<R>,
pub f_new: Tensor<R>,
}
impl<R: Runtime<DType = DType>> DenseOutputStep<R> {
pub fn new(
t_old: f64,
t_new: f64,
y_old: Tensor<R>,
y_new: Tensor<R>,
f_old: Tensor<R>,
f_new: Tensor<R>,
) -> Self {
Self {
t_old,
t_new,
y_old,
y_new,
f_old,
f_new,
}
}
pub fn h(&self) -> f64 {
self.t_new - self.t_old
}
pub fn contains(&self, t: f64) -> bool {
t >= self.t_old && t <= self.t_new
}
pub fn theta(&self, t: f64) -> f64 {
if self.h().abs() < 1e-15 {
0.0
} else {
(t - self.t_old) / self.h()
}
}
}
pub fn dense_eval<R, C>(client: &C, step: &DenseOutputStep<R>, t: f64) -> Result<Tensor<R>>
where
R: Runtime<DType = DType>,
C: TensorOps<R> + ScalarOps<R> + RuntimeClient<R>,
{
let h = step.h();
let theta = step.theta(t);
if theta <= 0.0 {
return Ok(step.y_old.clone());
}
if theta >= 1.0 {
return Ok(step.y_new.clone());
}
let dy = client.sub(&step.y_new, &step.y_old)?;
let h_f_old = client.mul_scalar(&step.f_old, h)?;
let h_f_new = client.mul_scalar(&step.f_new, h)?;
let three_dy = client.mul_scalar(&dy, 3.0)?;
let two_h_f_old = client.mul_scalar(&h_f_old, 2.0)?;
let a = client.sub(&client.sub(&three_dy, &two_h_f_old)?, &h_f_new)?;
let neg_two_dy = client.mul_scalar(&dy, -2.0)?;
let b = client.add(&client.add(&neg_two_dy, &h_f_old)?, &h_f_new)?;
let theta_sq = theta * theta;
let theta_cu = theta_sq * theta;
let term1 = client.mul_scalar(&h_f_old, theta)?;
let term2 = client.mul_scalar(&a, theta_sq)?;
let term3 = client.mul_scalar(&b, theta_cu)?;
let result = client.add(&step.y_old, &term1)?;
let result = client.add(&result, &term2)?;
client.add(&result, &term3)
}
pub fn dense_eval_derivative<R, C>(
client: &C,
step: &DenseOutputStep<R>,
t: f64,
) -> Result<Tensor<R>>
where
R: Runtime<DType = DType>,
C: TensorOps<R> + ScalarOps<R> + RuntimeClient<R>,
{
let h = step.h();
let theta = step.theta(t);
if theta <= 0.0 {
return Ok(step.f_old.clone());
}
if theta >= 1.0 {
return Ok(step.f_new.clone());
}
let dy = client.sub(&step.y_new, &step.y_old)?;
let h_f_old = client.mul_scalar(&step.f_old, h)?;
let h_f_new = client.mul_scalar(&step.f_new, h)?;
let three_dy = client.mul_scalar(&dy, 3.0)?;
let two_h_f_old = client.mul_scalar(&h_f_old, 2.0)?;
let a = client.sub(&client.sub(&three_dy, &two_h_f_old)?, &h_f_new)?;
let neg_two_dy = client.mul_scalar(&dy, -2.0)?;
let b = client.add(&client.add(&neg_two_dy, &h_f_old)?, &h_f_new)?;
let theta_sq = theta * theta;
let term1 = client.mul_scalar(&a, 2.0 * theta / h)?;
let term2 = client.mul_scalar(&b, 3.0 * theta_sq / h)?;
let result = client.add(&step.f_old, &term1)?;
client.add(&result, &term2)
}
#[cfg(test)]
mod tests {
use super::*;
use numr::runtime::cpu::{CpuClient, CpuDevice, CpuRuntime};
fn setup() -> (CpuDevice, CpuClient) {
let device = CpuDevice::new();
let client = CpuClient::new(device.clone());
(device, client)
}
#[test]
fn test_dense_eval_endpoints() {
let (device, client) = setup();
let y_old = Tensor::<CpuRuntime>::from_slice(&[0.0], &[1], &device);
let y_new = Tensor::<CpuRuntime>::from_slice(&[1.0], &[1], &device);
let f_old = Tensor::<CpuRuntime>::from_slice(&[1.0], &[1], &device);
let f_new = Tensor::<CpuRuntime>::from_slice(&[1.0], &[1], &device);
let step = DenseOutputStep::new(0.0, 1.0, y_old, y_new, f_old, f_new);
let y_at_start = dense_eval(&client, &step, 0.0).unwrap();
assert!((y_at_start.to_vec::<f64>()[0] - 0.0).abs() < 1e-10);
let y_at_end = dense_eval(&client, &step, 1.0).unwrap();
assert!((y_at_end.to_vec::<f64>()[0] - 1.0).abs() < 1e-10);
}
#[test]
fn test_dense_eval_linear() {
let (device, client) = setup();
let y_old = Tensor::<CpuRuntime>::from_slice(&[0.0], &[1], &device);
let y_new = Tensor::<CpuRuntime>::from_slice(&[1.0], &[1], &device);
let f_old = Tensor::<CpuRuntime>::from_slice(&[1.0], &[1], &device);
let f_new = Tensor::<CpuRuntime>::from_slice(&[1.0], &[1], &device);
let step = DenseOutputStep::new(0.0, 1.0, y_old, y_new, f_old, f_new);
for t in [0.25, 0.5, 0.75] {
let y_interp = dense_eval(&client, &step, t).unwrap();
assert!(
(y_interp.to_vec::<f64>()[0] - t).abs() < 1e-10,
"Failed at t = {}: got {}",
t,
y_interp.to_vec::<f64>()[0]
);
}
}
#[test]
fn test_dense_eval_quadratic() {
let (device, client) = setup();
let y_old = Tensor::<CpuRuntime>::from_slice(&[0.0], &[1], &device);
let y_new = Tensor::<CpuRuntime>::from_slice(&[1.0], &[1], &device);
let f_old = Tensor::<CpuRuntime>::from_slice(&[0.0], &[1], &device); let f_new = Tensor::<CpuRuntime>::from_slice(&[2.0], &[1], &device);
let step = DenseOutputStep::new(0.0, 1.0, y_old, y_new, f_old, f_new);
let y_mid = dense_eval(&client, &step, 0.5).unwrap();
assert!(
(y_mid.to_vec::<f64>()[0] - 0.25).abs() < 1e-10,
"Expected 0.25, got {}",
y_mid.to_vec::<f64>()[0]
);
for t in [0.25, 0.5, 0.75] {
let y_interp = dense_eval(&client, &step, t).unwrap();
let exact = t * t;
assert!(
(y_interp.to_vec::<f64>()[0] - exact).abs() < 1e-10,
"Failed at t = {}: got {}, expected {}",
t,
y_interp.to_vec::<f64>()[0],
exact
);
}
}
#[test]
fn test_dense_eval_derivative() {
let (device, client) = setup();
let y_old = Tensor::<CpuRuntime>::from_slice(&[0.0], &[1], &device);
let y_new = Tensor::<CpuRuntime>::from_slice(&[1.0], &[1], &device);
let f_old = Tensor::<CpuRuntime>::from_slice(&[0.0], &[1], &device);
let f_new = Tensor::<CpuRuntime>::from_slice(&[2.0], &[1], &device);
let step = DenseOutputStep::new(0.0, 1.0, y_old, y_new, f_old, f_new);
let f_at_start = dense_eval_derivative(&client, &step, 0.0).unwrap();
assert!((f_at_start.to_vec::<f64>()[0] - 0.0).abs() < 1e-10);
let f_at_end = dense_eval_derivative(&client, &step, 1.0).unwrap();
assert!((f_at_end.to_vec::<f64>()[0] - 2.0).abs() < 1e-10);
let f_mid = dense_eval_derivative(&client, &step, 0.5).unwrap();
assert!(
(f_mid.to_vec::<f64>()[0] - 1.0).abs() < 1e-10,
"Expected 1.0, got {}",
f_mid.to_vec::<f64>()[0]
);
}
#[test]
fn test_dense_output_step_methods() {
let (device, _client) = setup();
let y_old = Tensor::<CpuRuntime>::from_slice(&[0.0], &[1], &device);
let y_new = Tensor::<CpuRuntime>::from_slice(&[1.0], &[1], &device);
let f_old = Tensor::<CpuRuntime>::from_slice(&[1.0], &[1], &device);
let f_new = Tensor::<CpuRuntime>::from_slice(&[1.0], &[1], &device);
let step = DenseOutputStep::new(0.0, 2.0, y_old, y_new, f_old, f_new);
assert!((step.h() - 2.0).abs() < 1e-10);
assert!(step.contains(1.0));
assert!(step.contains(0.0));
assert!(step.contains(2.0));
assert!(!step.contains(-0.1));
assert!(!step.contains(2.1));
assert!((step.theta(1.0) - 0.5).abs() < 1e-10);
}
}