use alloc::{vec, vec::Vec};
use core::convert::TryFrom;
use core::fmt;
use core::ops::{Index, IndexMut};
use slatec_sys::{FortranInteger, pde::fishpack as raw};
use crate::runtime::lock_native;
#[derive(Clone, Debug, PartialEq)]
pub struct Grid3 {
values: Vec<f32>,
nx: usize,
ny: usize,
nz: usize,
}
impl Grid3 {
pub fn new(nx: usize, ny: usize, nz: usize, values: Vec<f32>) -> Result<Self, Pois3dError> {
let expected = checked_volume(nx, ny, nz)?;
if values.len() != expected {
return Err(Pois3dError::InvalidStorageLength {
expected,
actual: values.len(),
});
}
Ok(Self { values, nx, ny, nz })
}
pub fn zeros(nx: usize, ny: usize, nz: usize) -> Result<Self, Pois3dError> {
let len = checked_volume(nx, ny, nz)?;
Ok(Self {
values: allocate_zeroed(len)?,
nx,
ny,
nz,
})
}
#[must_use]
pub fn nx(&self) -> usize {
self.nx
}
#[must_use]
pub fn ny(&self) -> usize {
self.ny
}
#[must_use]
pub fn nz(&self) -> usize {
self.nz
}
#[must_use]
pub fn values(&self) -> &[f32] {
&self.values
}
#[must_use]
pub fn values_mut(&mut self) -> &mut [f32] {
&mut self.values
}
#[must_use]
pub fn get(&self, x: usize, y: usize, z: usize) -> Option<&f32> {
self.offset(x, y, z).map(|index| &self.values[index])
}
pub fn get_mut(&mut self, x: usize, y: usize, z: usize) -> Option<&mut f32> {
self.offset(x, y, z).map(|index| &mut self.values[index])
}
fn offset(&self, x: usize, y: usize, z: usize) -> Option<usize> {
(x < self.nx && y < self.ny && z < self.nz)
.then_some(z * self.nx * self.ny + y * self.nx + x)
}
}
impl Index<(usize, usize, usize)> for Grid3 {
type Output = f32;
fn index(&self, index: (usize, usize, usize)) -> &Self::Output {
self.get(index.0, index.1, index.2)
.expect("Grid3 index is within the documented dimensions")
}
}
impl IndexMut<(usize, usize, usize)> for Grid3 {
fn index_mut(&mut self, index: (usize, usize, usize)) -> &mut Self::Output {
self.get_mut(index.0, index.1, index.2)
.expect("Grid3 index is within the documented dimensions")
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TransverseBoundary {
Periodic,
ZeroBoth,
ZeroLowerReflectUpper,
ReflectBoth,
ReflectLowerZeroUpper,
}
impl TransverseBoundary {
fn native_code(self) -> FortranInteger {
match self {
Self::Periodic => 0,
Self::ZeroBoth => 1,
Self::ZeroLowerReflectUpper => 2,
Self::ReflectBoth => 3,
Self::ReflectLowerZeroUpper => 4,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CyclicAxisCoefficients {
off_diagonal: f32,
diagonal: f32,
}
impl CyclicAxisCoefficients {
pub fn new(off_diagonal: f32, diagonal: f32) -> Result<Self, Pois3dError> {
if !off_diagonal.is_finite() || !diagonal.is_finite() {
return Err(Pois3dError::NonFiniteInput {
field: "cyclic third-axis coefficient",
});
}
if off_diagonal == 0.0 {
return Err(Pois3dError::CyclicCouplingMustBeNonzero);
}
Ok(Self {
off_diagonal,
diagonal,
})
}
#[must_use]
pub fn off_diagonal(self) -> f32 {
self.off_diagonal
}
#[must_use]
pub fn diagonal(self) -> f32 {
self.diagonal
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct TridiagonalAxisCoefficients {
lower: Vec<f32>,
diagonal: Vec<f32>,
upper: Vec<f32>,
}
impl TridiagonalAxisCoefficients {
pub fn new(lower: Vec<f32>, diagonal: Vec<f32>, upper: Vec<f32>) -> Result<Self, Pois3dError> {
if lower.is_empty() {
return Err(Pois3dError::CoefficientVectorsEmpty);
}
if lower.len() != diagonal.len() || lower.len() != upper.len() {
return Err(Pois3dError::CoefficientLengthMismatch {
lower: lower.len(),
diagonal: diagonal.len(),
upper: upper.len(),
});
}
if lower
.iter()
.chain(diagonal.iter())
.chain(upper.iter())
.any(|value| !value.is_finite())
{
return Err(Pois3dError::NonFiniteInput {
field: "tridiagonal third-axis coefficient",
});
}
if lower[0] != 0.0 {
return Err(Pois3dError::NoncyclicLowerEndpointMustBeZero { value: lower[0] });
}
if upper[upper.len() - 1] != 0.0 {
return Err(Pois3dError::NoncyclicUpperEndpointMustBeZero {
value: upper[upper.len() - 1],
});
}
Ok(Self {
lower,
diagonal,
upper,
})
}
#[must_use]
pub fn lower(&self) -> &[f32] {
&self.lower
}
#[must_use]
pub fn diagonal(&self) -> &[f32] {
&self.diagonal
}
#[must_use]
pub fn upper(&self) -> &[f32] {
&self.upper
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum ThirdAxisOperator {
Cyclic(CyclicAxisCoefficients),
Tridiagonal(TridiagonalAxisCoefficients),
}
impl ThirdAxisOperator {
fn validate_for_length(&self, n: usize) -> Result<(), Pois3dError> {
if let Self::Tridiagonal(coefficients) = self {
if coefficients.lower.len() != n {
return Err(Pois3dError::ThirdAxisLengthMismatch {
expected: n,
actual: coefficients.lower.len(),
});
}
}
Ok(())
}
fn into_native_coefficients(self, n: usize) -> (FortranInteger, Vec<f32>, Vec<f32>, Vec<f32>) {
match self {
Self::Cyclic(coefficients) => (
0,
vec![coefficients.off_diagonal; n],
vec![coefficients.diagonal; n],
vec![coefficients.off_diagonal; n],
),
Self::Tridiagonal(coefficients) => (
1,
coefficients.lower,
coefficients.diagonal,
coefficients.upper,
),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Pois3dProblem {
l_boundary: TransverseBoundary,
m_boundary: TransverseBoundary,
c1: f32,
c2: f32,
third_axis: ThirdAxisOperator,
rhs: Grid3,
}
impl Pois3dProblem {
pub fn new(
l_boundary: TransverseBoundary,
m_boundary: TransverseBoundary,
c1: f32,
c2: f32,
third_axis: ThirdAxisOperator,
rhs: Grid3,
) -> Result<Self, Pois3dError> {
validate_dimension("first", rhs.nx)?;
validate_dimension("second", rhs.ny)?;
validate_dimension("third", rhs.nz)?;
if !c1.is_finite() || !c2.is_finite() {
return Err(Pois3dError::NonFiniteInput {
field: "transverse coefficient",
});
}
if rhs.values.iter().any(|value| !value.is_finite()) {
return Err(Pois3dError::NonFiniteInput {
field: "right-hand side",
});
}
third_axis.validate_for_length(rhs.nz)?;
Ok(Self {
l_boundary,
m_boundary,
c1,
c2,
third_axis,
rhs,
})
}
pub fn solve(self) -> Result<Grid3, Pois3dError> {
let l = checked_fortran_dimension(self.rhs.nx)?;
let m = checked_fortran_dimension(self.rhs.ny)?;
let n = checked_fortran_dimension(self.rhs.nz)?;
let ldimf = l;
let mdimf = m;
let workspace_len = workspace_len(self.rhs.nx, self.rhs.ny, self.rhs.nz)?;
let mut workspace = allocate_zeroed(workspace_len)?;
let mut values = self.rhs.values;
let (nperod, mut lower, mut diagonal, mut upper) =
self.third_axis.into_native_coefficients(self.rhs.nz);
let lperod = self.l_boundary.native_code();
let mperod = self.m_boundary.native_code();
let mut native_code = 0;
let _native = lock_native();
unsafe {
raw::pois3d(
&lperod,
&l,
&self.c1,
&mperod,
&m,
&self.c2,
&nperod,
&n,
lower.as_mut_ptr(),
diagonal.as_mut_ptr(),
upper.as_mut_ptr(),
&ldimf,
&mdimf,
values.as_mut_ptr(),
&mut native_code,
workspace.as_mut_ptr(),
);
}
if native_code != 0 {
return Err(Pois3dError::NativeFailure { code: native_code });
}
Ok(Grid3 {
values,
nx: self.rhs.nx,
ny: self.rhs.ny,
nz: self.rhs.nz,
})
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Pois3dError {
GridTooSmall {
axis: &'static str,
count: usize,
minimum: usize,
},
DimensionOverflow,
InvalidStorageLength {
expected: usize,
actual: usize,
},
NonFiniteInput {
field: &'static str,
},
CoefficientLengthMismatch {
lower: usize,
diagonal: usize,
upper: usize,
},
CoefficientVectorsEmpty,
ThirdAxisLengthMismatch {
expected: usize,
actual: usize,
},
NoncyclicLowerEndpointMustBeZero {
value: f32,
},
NoncyclicUpperEndpointMustBeZero {
value: f32,
},
CyclicCouplingMustBeNonzero,
NativeFailure {
code: i32,
},
AllocationFailed,
}
impl fmt::Display for Pois3dError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::GridTooSmall {
axis,
count,
minimum,
} => write!(
formatter,
"{axis} dimension has {count} unknowns; POIS3D requires at least {minimum}"
),
Self::DimensionOverflow => {
formatter.write_str("POIS3D dimension or workspace arithmetic overflowed")
}
Self::InvalidStorageLength { expected, actual } => {
write!(formatter, "Grid3 has {actual} values; expected {expected}")
}
Self::NonFiniteInput { field } => write!(formatter, "{field} must be finite"),
Self::CoefficientLengthMismatch {
lower,
diagonal,
upper,
} => write!(
formatter,
"third-axis coefficient lengths differ: lower={lower}, diagonal={diagonal}, upper={upper}"
),
Self::CoefficientVectorsEmpty => {
formatter.write_str("third-axis coefficient vectors cannot be empty")
}
Self::ThirdAxisLengthMismatch { expected, actual } => write!(
formatter,
"third-axis coefficient length is {actual}; expected grid extent {expected}"
),
Self::NoncyclicLowerEndpointMustBeZero { value } => {
write!(formatter, "noncyclic lower[0] must be zero, got {value}")
}
Self::NoncyclicUpperEndpointMustBeZero { value } => {
write!(formatter, "noncyclic upper[N-1] must be zero, got {value}")
}
Self::CyclicCouplingMustBeNonzero => {
formatter.write_str("cyclic third-axis off-diagonal coupling must be nonzero")
}
Self::NativeFailure { code } => {
write!(
formatter,
"POIS3D returned unexpected native error code {code}"
)
}
Self::AllocationFailed => formatter.write_str("POIS3D workspace allocation failed"),
}
}
}
impl std::error::Error for Pois3dError {}
fn validate_dimension(axis: &'static str, count: usize) -> Result<(), Pois3dError> {
if count < 3 {
return Err(Pois3dError::GridTooSmall {
axis,
count,
minimum: 3,
});
}
Ok(())
}
fn checked_fortran_dimension(value: usize) -> Result<FortranInteger, Pois3dError> {
FortranInteger::try_from(value).map_err(|_| Pois3dError::DimensionOverflow)
}
fn checked_volume(nx: usize, ny: usize, nz: usize) -> Result<usize, Pois3dError> {
nx.checked_mul(ny)
.and_then(|area| area.checked_mul(nz))
.ok_or(Pois3dError::DimensionOverflow)
}
fn allocate_zeroed(length: usize) -> Result<Vec<f32>, Pois3dError> {
let mut values = Vec::new();
values
.try_reserve_exact(length)
.map_err(|_| Pois3dError::AllocationFailed)?;
values.resize(length, 0.0);
Ok(values)
}
fn workspace_len(l: usize, m: usize, n: usize) -> Result<usize, Pois3dError> {
let half_sum = l
.checked_add(1)
.map(|value| value / 2)
.and_then(|left| {
m.checked_add(1)
.map(|value| value / 2)
.and_then(|right| left.checked_add(right))
})
.ok_or(Pois3dError::DimensionOverflow)?;
30_usize
.checked_add(l)
.and_then(|value| value.checked_add(m))
.and_then(|value| n.checked_mul(2).and_then(|twice| value.checked_add(twice)))
.and_then(|value| value.checked_add(l.max(m).max(n)))
.and_then(|value| {
half_sum
.checked_mul(7)
.and_then(|tail| value.checked_add(tail))
})
.ok_or(Pois3dError::DimensionOverflow)
}
#[cfg(test)]
mod tests {
use alloc::vec;
use super::{
CyclicAxisCoefficients, Grid3, Pois3dError, Pois3dProblem, ThirdAxisOperator,
TransverseBoundary, TridiagonalAxisCoefficients, checked_fortran_dimension, workspace_len,
};
#[test]
fn grid_is_x_fast_and_three_dimensional() {
let grid = Grid3::new(2, 3, 4, (0..24).map(|value| value as f32).collect()).unwrap();
assert_eq!(grid[(1, 0, 0)], 1.0);
assert_eq!(grid[(0, 1, 0)], 2.0);
assert_eq!(grid[(0, 0, 1)], 6.0);
assert_eq!(grid[(1, 2, 3)], 23.0);
}
#[test]
fn validates_grid_and_dimension_overflow() {
assert!(matches!(
Grid3::new(2, 3, 4, vec![0.0; 23]),
Err(Pois3dError::InvalidStorageLength { .. })
));
assert!(matches!(
Grid3::zeros(usize::MAX, 2, 2),
Err(Pois3dError::DimensionOverflow)
));
assert!(matches!(
workspace_len(usize::MAX, 3, 3),
Err(Pois3dError::DimensionOverflow)
));
assert!(matches!(
checked_fortran_dimension(usize::MAX),
Err(Pois3dError::DimensionOverflow)
));
}
#[test]
fn validates_structured_operator_models() {
assert!(matches!(
CyclicAxisCoefficients::new(0.0, 1.0),
Err(Pois3dError::CyclicCouplingMustBeNonzero)
));
assert!(matches!(
TridiagonalAxisCoefficients::new(vec![0.0, 1.0], vec![1.0], vec![1.0, 0.0]),
Err(Pois3dError::CoefficientLengthMismatch { .. })
));
assert!(matches!(
TridiagonalAxisCoefficients::new(
vec![1.0, 1.0, 1.0],
vec![1.0; 3],
vec![1.0, 1.0, 0.0]
),
Err(Pois3dError::NoncyclicLowerEndpointMustBeZero { .. })
));
assert!(matches!(
TridiagonalAxisCoefficients::new(
vec![0.0, 1.0, 1.0],
vec![1.0; 3],
vec![1.0, 1.0, 1.0]
),
Err(Pois3dError::NoncyclicUpperEndpointMustBeZero { .. })
));
}
#[test]
fn problem_requires_native_minimums_and_matching_third_axis() {
let cyclic = ThirdAxisOperator::Cyclic(CyclicAxisCoefficients::new(1.0, -2.0).unwrap());
assert!(matches!(
Pois3dProblem::new(
TransverseBoundary::Periodic,
TransverseBoundary::ZeroBoth,
1.0,
1.0,
cyclic,
Grid3::zeros(2, 3, 3).unwrap(),
),
Err(Pois3dError::GridTooSmall { axis: "first", .. })
));
let tridiagonal = ThirdAxisOperator::Tridiagonal(
TridiagonalAxisCoefficients::new(
vec![0.0, 1.0, 1.0],
vec![1.0; 3],
vec![1.0, 1.0, 0.0],
)
.unwrap(),
);
assert!(matches!(
Pois3dProblem::new(
TransverseBoundary::ReflectBoth,
TransverseBoundary::ZeroLowerReflectUpper,
1.0,
1.0,
tridiagonal,
Grid3::zeros(3, 4, 4).unwrap(),
),
Err(Pois3dError::ThirdAxisLengthMismatch { .. })
));
}
}