use super::tridiagonal::thomas_algorithm;
#[derive(Debug, Clone)]
pub struct TensorGrid {
dims: Vec<usize>,
}
impl TensorGrid {
pub fn new(dims: &[usize]) -> Self {
assert!(!dims.is_empty() && dims.len() <= 3, "1 to 3 axes supported");
assert!(dims.iter().all(|&n| n >= 1), "every axis needs at least one node");
Self { dims: dims.to_vec() }
}
pub fn len(&self) -> usize {
self.dims.iter().product()
}
pub fn is_empty(&self) -> bool {
false }
pub fn ndim(&self) -> usize {
self.dims.len()
}
pub fn dims(&self) -> &[usize] {
&self.dims
}
pub fn strides(&self) -> Vec<usize> {
let mut s = vec![1; self.dims.len()];
for k in (0..self.dims.len().saturating_sub(1)).rev() {
s[k] = s[k + 1] * self.dims[k + 1];
}
s
}
pub fn index(&self, idx: &[usize]) -> usize {
assert_eq!(idx.len(), self.dims.len());
idx.iter().zip(self.strides()).map(|(i, s)| i * s).sum()
}
fn line_starts(&self, axis: usize) -> Vec<usize> {
let stride = self.strides()[axis];
let n = self.dims[axis];
(0..self.len()).filter(|i| (i / stride) % n == 0).collect()
}
}
#[derive(Debug, Clone)]
pub struct AxisOperator {
pub axis: usize,
pub sub: Vec<f64>,
pub diag: Vec<f64>,
pub sup: Vec<f64>,
}
impl AxisOperator {
pub fn zero(grid: &TensorGrid, axis: usize) -> Self {
assert!(axis < grid.ndim());
let n = grid.len();
Self { axis, sub: vec![0.0; n], diag: vec![0.0; n], sup: vec![0.0; n] }
}
pub fn apply(&self, grid: &TensorGrid, u: &[f64]) -> Vec<f64> {
let stride = grid.strides()[self.axis];
let n = grid.dims()[self.axis];
assert_eq!(u.len(), grid.len());
(0..grid.len())
.map(|i| {
let j = (i / stride) % n;
let mut v = self.diag[i] * u[i];
if j > 0 {
v += self.sub[i] * u[i - stride];
}
if j < n - 1 {
v += self.sup[i] * u[i + stride];
}
v
})
.collect()
}
pub fn solve_shifted(&self, grid: &TensorGrid, c: f64, rhs: &[f64]) -> Vec<f64> {
let stride = grid.strides()[self.axis];
let n = grid.dims()[self.axis];
assert_eq!(rhs.len(), grid.len());
let mut x = rhs.to_vec();
if n == 1 {
for (xi, &r) in x.iter_mut().zip(rhs) {
*xi = r / (1.0 - c * self.diag[0]);
}
return x;
}
let mut a = vec![0.0; n - 1];
let mut b = vec![0.0; n];
let mut cc = vec![0.0; n - 1];
let mut d = vec![0.0; n];
for start in grid.line_starts(self.axis) {
for j in 0..n {
let i = start + j * stride;
b[j] = 1.0 - c * self.diag[i];
d[j] = rhs[i];
if j > 0 {
a[j - 1] = -c * self.sub[i];
}
if j < n - 1 {
cc[j] = -c * self.sup[i];
}
}
let line = thomas_algorithm(&a, &b, &cc, &d);
for (j, v) in line.into_iter().enumerate() {
x[start + j * stride] = v;
}
}
x
}
}
#[cfg(test)]
mod tests {
use super::*;
pub fn laplacian_1d(grid: &TensorGrid, axis: usize, h: f64) -> AxisOperator {
let mut op = AxisOperator::zero(grid, axis);
let stride = grid.strides()[axis];
let n = grid.dims()[axis];
for i in 0..grid.len() {
let j = (i / stride) % n;
if j > 0 && j < n - 1 {
op.sub[i] = 1.0 / (h * h);
op.diag[i] = -2.0 / (h * h);
op.sup[i] = 1.0 / (h * h);
}
}
op
}
#[test]
fn strides_and_indexing_are_row_major() {
let g = TensorGrid::new(&[3, 4, 5]);
assert_eq!(g.strides(), vec![20, 5, 1]);
assert_eq!(g.index(&[1, 2, 3]), 33);
assert_eq!(g.len(), 60);
}
#[test]
fn apply_matches_dense_stencil_2d() {
let g = TensorGrid::new(&[2, 4]);
let h = 1.0;
let op = laplacian_1d(&g, 1, h);
let u: Vec<f64> = (0..8).map(|i| (i * i) as f64).collect();
let au = op.apply(&g, &u);
assert_eq!(&au[0..4], &[0.0, 2.0, 2.0, 0.0]);
assert_eq!(&au[4..8], &[0.0, 2.0, 2.0, 0.0]);
}
#[test]
fn solve_shifted_inverts_apply() {
let g = TensorGrid::new(&[3, 5, 4]);
let op = laplacian_1d(&g, 1, 0.25);
let rhs: Vec<f64> = (0..g.len()).map(|i| ((i % 7) as f64) - 3.0).collect();
let c = 0.37;
let x = op.solve_shifted(&g, c, &rhs);
let ax = op.apply(&g, &x);
for i in 0..g.len() {
let back = x[i] - c * ax[i];
assert!((back - rhs[i]).abs() < 1e-10, "node {i}: {back} vs {}", rhs[i]);
}
}
}