use crate::consts::{REALMAX, SYMTOL};
use crate::mat::Mat;
use crate::math;
pub(crate) fn inprod(x: &[f64], y: &[f64]) -> f64 {
debug_assert_eq!(x.len(), y.len());
let mut z = 0.0;
for i in 0..x.len() {
z += x[i] * y[i];
}
z
}
#[expect(clippy::needless_range_loop)] pub(crate) fn matprod12_into(x: &[f64], y: &Mat, z: &mut [f64]) {
debug_assert_eq!(x.len(), y.nrows());
debug_assert_eq!(z.len(), y.ncols());
for j in 0..y.ncols() {
z[j] = inprod(x, y.col(j));
}
}
#[cfg(test)]
pub(crate) fn matprod12(x: &[f64], y: &Mat) -> Vec<f64> {
let mut z = vec![0.0; y.ncols()];
matprod12_into(x, y, &mut z);
z
}
#[expect(clippy::needless_range_loop)] pub(crate) fn matprod21_into(x: &Mat, y: &[f64], z: &mut [f64]) {
debug_assert_eq!(x.ncols(), y.len());
debug_assert_eq!(z.len(), x.nrows());
z.fill(0.0);
for j in 0..x.ncols() {
let xj = x.col(j);
for i in 0..z.len() {
z[i] += xj[i] * y[j];
}
}
}
#[cfg(test)]
pub(crate) fn matprod21(x: &Mat, y: &[f64]) -> Vec<f64> {
let mut z = vec![0.0; x.nrows()];
matprod21_into(x, y, &mut z);
z
}
pub(crate) fn matprod22_into(x: &Mat, y: &Mat, z: &mut Mat) {
debug_assert_eq!(x.ncols(), y.nrows());
debug_assert_eq!((z.nrows(), z.ncols()), (x.nrows(), y.ncols()));
z.fill(0.0);
for j in 0..y.ncols() {
for i in 0..x.ncols() {
let xi = x.col(i);
let yij = y[[i, j]];
let zj = z.col_mut(j);
for r in 0..zj.len() {
zj[r] += xi[r] * yij;
}
}
}
}
#[cfg(test)]
pub(crate) fn matprod22(x: &Mat, y: &Mat) -> Mat {
let mut z = Mat::zeros(x.nrows(), y.ncols());
matprod22_into(x, y, &mut z);
z
}
#[expect(clippy::needless_range_loop)] pub(crate) fn outprod_into(x: &[f64], y: &[f64], z: &mut Mat) {
debug_assert_eq!((z.nrows(), z.ncols()), (x.len(), y.len()));
for i in 0..y.len() {
let zi = z.col_mut(i);
for r in 0..x.len() {
zi[r] = x[r] * y[i];
}
}
}
#[cfg(test)]
pub(crate) fn outprod(x: &[f64], y: &[f64]) -> Mat {
let mut z = Mat::zeros(x.len(), y.len());
outprod_into(x, y, &mut z);
z
}
pub(crate) fn norm(x: &[f64]) -> f64 {
let mut maxabs = 0.0_f64;
for &v in x {
maxabs = maxabs.max(math::abs(v));
}
if x.is_empty() {
0.0
} else if !x.iter().all(|v| v.is_finite()) {
let mut y = 0.0;
for &v in x {
y += math::abs(v);
}
y
} else if maxabs <= 0.0 {
0.0
} else {
let mut sumsq = 0.0;
for &v in x {
sumsq += v * v;
}
let mut y = math::sqrt(sumsq);
if (y.is_infinite() && y.is_sign_positive()) || y <= 0.0 {
#[expect(clippy::unnecessary_min_or_max)]
let scalmin = f64::from(f64::RADIX).powi((f64::MIN_EXP - 1).max(1 - f64::MAX_EXP));
#[expect(clippy::unnecessary_min_or_max)]
let scalmax = f64::from(f64::RADIX).powi((f64::MAX_EXP - 1).min(1 - f64::MIN_EXP));
let scaling = scalmax.min(scalmin.max(maxabs));
let mut scaled_sumsq = 0.0;
for &v in x {
let s = v / scaling;
scaled_sumsq += s * s;
}
y = scaling * math::sqrt(scaled_sumsq);
}
y
}
}
#[allow(dead_code)] pub(crate) fn diag(a: &Mat) -> Vec<f64> {
let dlen = a.nrows().min(a.ncols());
let mut d = vec![0.0; dlen];
for i in 0..dlen {
d[i] = a[[i, i]];
}
d
}
#[allow(dead_code)] pub(crate) fn issymmetric(a: &Mat) -> bool {
if a.nrows() != a.ncols() {
return false;
}
let mut maxabs = 0.0_f64;
for j in 0..a.ncols() {
for &v in a.col(j) {
maxabs = maxabs.max(math::abs(v));
}
}
let tol = SYMTOL * maxabs.max(1.0);
for j in 0..a.ncols() {
for i in 0..a.nrows() {
if math::abs(a[[i, j]] - a[[j, i]]) > tol {
return false;
}
if a[[i, j]].is_nan() != a[[j, i]].is_nan() {
return false;
}
}
}
true
}
#[allow(dead_code)] pub(crate) fn trueloc(x: &[bool]) -> Vec<usize> {
let mut loc = Vec::with_capacity(x.len());
for (i, &v) in x.iter().enumerate() {
if v {
loc.push(i);
}
}
loc
}
#[expect(clippy::many_single_char_names)] pub(crate) fn planerot(x: [f64; 2]) -> [[f64; 2]; 2] {
let eps = f64::EPSILON;
let (c, s);
if x[0].is_nan() || x[1].is_nan() {
c = 1.0;
s = 0.0;
} else if x[0].is_infinite() && x[1].is_infinite() {
c = (1.0 / math::sqrt(2.0)).copysign(x[0]);
s = (1.0 / math::sqrt(2.0)).copysign(x[1]);
} else if math::abs(x[0]) <= 0.0 && math::abs(x[1]) <= 0.0 {
c = 1.0;
s = 0.0;
} else if math::abs(x[1]) <= eps * math::abs(x[0]) {
c = 1.0_f64.copysign(x[0]);
s = 0.0;
} else if math::abs(x[0]) <= eps * math::abs(x[1]) {
c = 0.0;
s = 1.0_f64.copysign(x[1]);
} else {
let lo = math::sqrt(f64::MIN_POSITIVE);
let hi = math::sqrt(REALMAX / 2.1);
if x.iter().all(|&v| math::abs(v) > lo && math::abs(v) < hi) {
let r = norm(&x);
c = x[0] / r;
s = x[1] / r;
} else if math::abs(x[0]) > math::abs(x[1]) {
let t = x[1] / x[0];
let u = 1.0_f64
.max(math::abs(t))
.max(math::sqrt(1.0 + t * t))
.copysign(x[0]);
c = 1.0 / u;
s = t / u;
} else {
let t = x[0] / x[1];
let u = 1.0_f64
.max(math::abs(t))
.max(math::sqrt(1.0 + t * t))
.copysign(x[1]);
c = t / u;
s = 1.0 / u;
}
}
[[c, s], [-s, c]]
}
pub(crate) fn symmetrize(a: &mut Mat) {
debug_assert_eq!(a.nrows(), a.ncols());
for j in 0..a.nrows() {
for i in 0..j {
a[[i, j]] = a[[j, i]];
}
}
}
pub(crate) fn r1update(a: &mut Mat, alpha: f64, x: &[f64]) {
let n = x.len();
debug_assert_eq!((a.nrows(), a.ncols()), (n, n));
for j in 0..n {
for i in j..n {
a[[i, j]] += alpha * x[i] * x[j];
}
}
symmetrize(a);
}
pub(crate) fn r2update(a: &mut Mat, alpha: f64, x: &[f64], y: &[f64]) {
let n = x.len();
debug_assert_eq!(y.len(), n);
debug_assert_eq!((a.nrows(), a.ncols()), (n, n));
for j in 0..n {
for i in j..n {
a[[i, j]] = (a[[i, j]] + alpha * x[i] * y[j]) + alpha * y[i] * x[j];
}
}
symmetrize(a);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mat::Mat;
#[test]
fn inprod_is_the_dot_product_in_order() {
assert_eq!(inprod(&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0]), 32.0);
}
#[test]
fn matprod21_multiplies_matrix_by_column() {
let a = Mat::from_col_major(2, 2, vec![1.0, 2.0, 3.0, 4.0]);
assert_eq!(matprod21(&a, &[5.0, 6.0]), vec![23.0, 34.0]);
}
#[test]
fn matprod12_multiplies_row_by_matrix() {
let a = Mat::from_col_major(2, 2, vec![1.0, 2.0, 3.0, 4.0]);
assert_eq!(matprod12(&[5.0, 6.0], &a), vec![17.0, 39.0]);
}
#[test]
fn matprod22_multiplies_two_matrices() {
let a = Mat::from_col_major(2, 2, vec![1.0, 2.0, 3.0, 4.0]);
let b = Mat::from_col_major(2, 2, vec![5.0, 6.0, 7.0, 8.0]);
assert_eq!(matprod22(&a, &b).data(), &[23.0, 34.0, 31.0, 46.0]);
}
#[test]
fn outprod_builds_x_y_transposed() {
let m = outprod(&[1.0, 2.0], &[3.0, 4.0]);
assert_eq!(m.data(), &[3.0, 6.0, 4.0, 8.0]);
}
#[test]
fn norm_is_the_euclidean_norm_with_overflow_rescue() {
assert_eq!(norm(&[3.0, 4.0]), 5.0);
assert_eq!(norm(&[]), 0.0);
assert_eq!(norm(&[0.0, 0.0]), 0.0);
let big = norm(&[1.0e300, 1.0e300]); assert!((big / 1.0e300 - core::f64::consts::SQRT_2).abs() < 1e-15);
assert!((norm(&[2.0e300, 1.0e300]) / 1.0e300 - 5.0_f64.sqrt()).abs() < 1e-14);
assert_eq!(norm(&[f64::INFINITY, 1.0]), f64::INFINITY);
assert!(norm(&[f64::NAN, 1.0]).is_nan());
}
#[test]
fn diag_extracts_the_main_diagonal() {
let a = Mat::from_col_major(2, 2, vec![1.0, 2.0, 3.0, 4.0]);
assert_eq!(diag(&a), vec![1.0, 4.0]);
}
#[test]
fn trueloc_returns_zero_based_positions() {
assert_eq!(trueloc(&[false, true, false, true]), vec![1, 3]);
}
#[test]
fn planerot_zeroes_the_second_component() {
let g = planerot([3.0, 4.0]);
let y2 = g[1][0] * 3.0 + g[1][1] * 4.0;
let y1 = g[0][0] * 3.0 + g[0][1] * 4.0;
assert!(y2.abs() < 1e-15);
assert!((y1 - 5.0).abs() < 1e-15);
}
#[test]
fn planerot_handles_the_nan_inf_zero_and_near_axis_branches() {
let r = 0.707_106_781_186_547_5_f64; assert_eq!(planerot([f64::NAN, 1.0]), [[1.0, 0.0], [0.0, 1.0]]);
assert_eq!(planerot([1.0, f64::NAN]), [[1.0, 0.0], [0.0, 1.0]]);
assert_eq!(planerot([f64::INFINITY, f64::INFINITY]), [[r, r], [-r, r]]);
assert_eq!(
planerot([f64::INFINITY, f64::NEG_INFINITY]),
[[r, -r], [r, r]]
);
assert_eq!(planerot([0.0, 0.0]), [[1.0, 0.0], [0.0, 1.0]]);
assert_eq!(planerot([-2.0, 1e-300]), [[-1.0, 0.0], [0.0, -1.0]]);
assert_eq!(planerot([1e-300, -3.0]), [[0.0, -1.0], [1.0, 0.0]]);
assert_eq!(planerot([f64::INFINITY, 1.0]), [[1.0, 0.0], [0.0, 1.0]]);
assert_eq!(
planerot([1.0, f64::NEG_INFINITY]),
[[0.0, -1.0], [1.0, 0.0]]
);
for x in [
[1.0e-200, 1.0e-210],
[1.0e-210, -1.0e-200],
[1.0e-200, 5.0e-201],
[5.0e-201, 1.0e-200],
] {
let g = planerot(x);
let y0 = g[0][0] * x[0] + g[0][1] * x[1];
let y1 = g[1][0] * x[0] + g[1][1] * x[1];
let nx = norm(&x);
assert!((y0 - nx).abs() <= 1e-14 * nx, "G*x[0] != norm(x) for {x:?}");
assert!(y1.abs() <= 1e-14 * nx, "G*x[1] != 0 for {x:?}");
assert!(
(g[0][0] * g[0][0] + g[0][1] * g[0][1] - 1.0).abs() < 1e-14,
"G not orthogonal for {x:?}"
);
}
}
#[test]
fn r1update_adds_alpha_x_x_transposed() {
let mut a = Mat::zeros(2, 2);
r1update(&mut a, 2.0, &[1.0, 3.0]);
assert_eq!(a.data(), &[2.0, 6.0, 6.0, 18.0]);
}
#[test]
fn r2update_adds_the_symmetric_cross_terms() {
let mut a = Mat::zeros(2, 2);
r2update(&mut a, 1.0, &[1.0, 0.0], &[0.0, 1.0]);
assert_eq!(a.data(), &[0.0, 1.0, 1.0, 0.0]);
let mut b = Mat::zeros(2, 2);
r2update(&mut b, 1.0, &[1.0, 1.0], &[1.0, 2.0]);
assert_eq!(b.data(), &[2.0, 3.0, 3.0, 4.0]);
}
#[test]
fn issymmetric_and_symmetrize_agree() {
let mut a = Mat::from_col_major(2, 2, vec![1.0, 2.0, 2.0 + 1.0e-16, 4.0]);
assert!(issymmetric(&a));
a[[0, 1]] = 9.0;
assert!(!issymmetric(&a));
symmetrize(&mut a);
assert!(issymmetric(&a));
}
}