use super::{
build_design_matrix, cholesky_solve, compute_fitted, compute_r_squared, recover_beta_t,
resolve_ncomp, validate_fregre_inputs, FregreRobustResult,
};
use crate::error::FdarError;
use crate::matrix::FdMatrix;
use crate::regression::fdata_to_pc_1d;
const MAX_ITER: usize = 100;
const CONV_TOL: f64 = 1e-6;
const L1_EPS: f64 = 1e-6;
fn compute_xtwx(x: &FdMatrix, w: &[f64]) -> Vec<f64> {
let (n, p) = x.shape();
let mut xtwx = vec![0.0; p * p];
for k in 0..p {
for j in k..p {
let mut s = 0.0;
for i in 0..n {
s += w[i] * x[(i, k)] * x[(i, j)];
}
xtwx[k * p + j] = s;
xtwx[j * p + k] = s;
}
}
xtwx
}
fn compute_xtwy(x: &FdMatrix, y: &[f64], w: &[f64]) -> Vec<f64> {
let (n, p) = x.shape();
(0..p)
.map(|k| {
let mut s = 0.0;
for i in 0..n {
s += w[i] * x[(i, k)] * y[i];
}
s
})
.collect()
}
fn wls_solve(x: &FdMatrix, y: &[f64], w: &[f64]) -> Result<Vec<f64>, FdarError> {
let p = x.ncols();
let xtwx = compute_xtwx(x, w);
let xtwy = compute_xtwy(x, y, w);
cholesky_solve(&xtwx, &xtwy, p)
}
fn ols_init(x: &FdMatrix, y: &[f64]) -> Result<Vec<f64>, FdarError> {
let n = x.nrows();
let w = vec![1.0; n];
wls_solve(x, y, &w)
}
fn relative_change(beta_new: &[f64], beta_old: &[f64]) -> f64 {
let diff_norm: f64 = beta_new
.iter()
.zip(beta_old)
.map(|(&a, &b)| (a - b).powi(2))
.sum::<f64>()
.sqrt();
let old_norm: f64 = beta_old.iter().map(|&b| b * b).sum::<f64>().sqrt();
diff_norm / (old_norm + 1e-12)
}
#[must_use = "expensive computation whose result should not be discarded"]
pub fn fregre_l1(
data: &FdMatrix,
y: &[f64],
scalar_covariates: Option<&FdMatrix>,
ncomp: usize,
) -> Result<FregreRobustResult, FdarError> {
fregre_robust_irls(data, y, scalar_covariates, ncomp, RobustType::L1)
}
#[must_use = "expensive computation whose result should not be discarded"]
pub fn fregre_huber(
data: &FdMatrix,
y: &[f64],
scalar_covariates: Option<&FdMatrix>,
ncomp: usize,
k: f64,
) -> Result<FregreRobustResult, FdarError> {
if k <= 0.0 {
return Err(FdarError::InvalidParameter {
parameter: "k",
message: format!("Huber tuning constant must be positive, got {k}"),
});
}
fregre_robust_irls(data, y, scalar_covariates, ncomp, RobustType::Huber(k))
}
enum RobustType {
L1,
Huber(f64),
}
fn compute_weights(residuals: &[f64], robust_type: &RobustType) -> Vec<f64> {
match robust_type {
RobustType::L1 => residuals
.iter()
.map(|&r| 1.0 / r.abs().max(L1_EPS))
.collect(),
RobustType::Huber(k) => residuals
.iter()
.map(|&r| {
let abs_r = r.abs();
if abs_r <= *k {
1.0
} else {
*k / abs_r
}
})
.collect(),
}
}
fn fregre_robust_irls(
data: &FdMatrix,
y: &[f64],
scalar_covariates: Option<&FdMatrix>,
ncomp: usize,
robust_type: RobustType,
) -> Result<FregreRobustResult, FdarError> {
let (n, m) = data.shape();
validate_fregre_inputs(n, m, y, scalar_covariates)?;
let ncomp = resolve_ncomp(ncomp, data, y, scalar_covariates, n, m)?;
let argvals: Vec<f64> = (0..m).map(|j| j as f64 / (m - 1).max(1) as f64).collect();
let fpca = fdata_to_pc_1d(data, ncomp, &argvals)?;
let design = build_design_matrix(&fpca.scores, ncomp, scalar_covariates, n);
let p_total = design.ncols();
let mut coeffs = ols_init(&design, y)?;
let mut iterations = 0;
let mut converged = false;
let mut weights = vec![1.0; n];
for iter in 0..MAX_ITER {
let fitted = compute_fitted(&design, &coeffs);
let residuals: Vec<f64> = y.iter().zip(&fitted).map(|(&yi, &yh)| yi - yh).collect();
weights = compute_weights(&residuals, &robust_type);
let new_coeffs = wls_solve(&design, y, &weights)?;
let rel_change = relative_change(&new_coeffs, &coeffs);
coeffs = new_coeffs;
iterations = iter + 1;
if rel_change < CONV_TOL {
converged = true;
break;
}
}
let fitted_values = compute_fitted(&design, &coeffs);
let residuals: Vec<f64> = y
.iter()
.zip(&fitted_values)
.map(|(&yi, &yh)| yi - yh)
.collect();
weights = compute_weights(&residuals, &robust_type);
let (r_squared, _) = compute_r_squared(y, &residuals, p_total);
let beta_t = recover_beta_t(&coeffs[1..=ncomp], &fpca.rotation, m);
Ok(FregreRobustResult {
intercept: coeffs[0],
beta_t,
fitted_values,
residuals,
coefficients: coeffs,
ncomp,
fpca,
iterations,
converged,
weights,
r_squared,
})
}
pub fn predict_fregre_robust(
fit: &FregreRobustResult,
new_data: &FdMatrix,
new_scalar: Option<&FdMatrix>,
) -> Vec<f64> {
let (n_new, m) = new_data.shape();
let ncomp = fit.ncomp;
let p_scalar = fit.coefficients.len() - 1 - ncomp;
let mut predictions = vec![0.0; n_new];
for i in 0..n_new {
let mut yhat = fit.intercept;
for k in 0..ncomp {
let mut s = 0.0;
for j in 0..m {
s += (new_data[(i, j)] - fit.fpca.mean[j])
* fit.fpca.rotation[(j, k)]
* fit.fpca.weights[j];
}
yhat += fit.coefficients[1 + k] * s;
}
if let Some(sc) = new_scalar {
for j in 0..p_scalar {
yhat += fit.coefficients[1 + ncomp + j] * sc[(i, j)];
}
}
predictions[i] = yhat;
}
predictions
}