use std::ops::Range;
use std::sync::Arc;
use ndarray::{Array1, Array2};
use crate::PenaltyMatrix;
use gam_linalg::matrix::{DesignMatrix, SymmetricMatrix};
pub trait FamilyChannelHessian: Send + Sync {
fn n_outputs(&self) -> usize;
fn n_subjects(&self) -> usize;
fn fill_subject(&self, i: usize, out: &mut [f64]);
fn evaluate_full(&self) -> ndarray::Array3<f64> {
let n = self.n_subjects();
let k = self.n_outputs();
let mut out = ndarray::Array3::<f64>::zeros((n, k, k));
let mut buf = vec![0.0_f64; k * k];
for i in 0..n {
self.fill_subject(i, &mut buf);
for a in 0..k {
for b in 0..k {
out[[i, a, b]] = buf[a * k + b];
}
}
}
out
}
fn channel_hessian_at(
&self,
beta: &[f64],
family_scalars: Option<&std::sync::Arc<dyn std::any::Any + Send + Sync>>,
) -> Result<Arc<dyn FamilyChannelHessian>, String> {
if beta.iter().any(|v| v.is_nan()) {
return Err("channel_hessian_at: beta contains NaN".to_string());
}
if family_scalars.is_some() && beta.is_empty() {
return Err(
"channel_hessian_at: family_scalars supplied but beta is empty".to_string(),
);
}
let tensor = self.evaluate_full();
Ok(Arc::new(TensorChannelHessian { h: tensor }))
}
}
pub struct TensorChannelHessian {
pub h: ndarray::Array3<f64>,
}
impl FamilyChannelHessian for TensorChannelHessian {
fn n_outputs(&self) -> usize {
self.h.shape()[1]
}
fn n_subjects(&self) -> usize {
self.h.shape()[0]
}
fn fill_subject(&self, i: usize, out: &mut [f64]) {
let k = self.h.shape()[1];
assert_eq!(out.len(), k * k);
for a in 0..k {
for b in 0..k {
out[a * k + b] = self.h[[i, a, b]];
}
}
}
fn evaluate_full(&self) -> ndarray::Array3<f64> {
self.h.clone()
}
}
pub struct FamilyLinearizationState<'a> {
pub beta: &'a [f64],
pub family_scalars: Option<Arc<dyn std::any::Any + Send + Sync>>,
pub channel_hessian: Option<Arc<dyn FamilyChannelHessian>>,
pub probit_frailty_scale: f64,
}
pub trait BlockEffectiveJacobian: Send + Sync {
fn effective_jacobian_rows(
&self,
state: &FamilyLinearizationState<'_>,
rows: Range<usize>,
) -> Result<Array2<f64>, String>;
fn effective_jacobian_at(
&self,
state: &FamilyLinearizationState<'_>,
) -> Result<Array2<f64>, String> {
let full = self.effective_jacobian_rows(state, 0..usize::MAX)?;
Ok(full)
}
fn n_outputs(&self) -> usize {
1
}
fn eta_row_scaling_for_skewness(&self) -> Option<Arc<[f64]>> {
None
}
}
pub struct AdditiveBlockJacobian {
pub design: Array2<f64>,
pub own_output: usize,
pub n_family_outputs: usize,
}
impl BlockEffectiveJacobian for AdditiveBlockJacobian {
fn effective_jacobian_rows(
&self,
state: &FamilyLinearizationState<'_>,
rows: Range<usize>,
) -> Result<Array2<f64>, String> {
let n = self.design.nrows();
let p = self.design.ncols();
let rows = clamp_jacobian_rows(rows, n);
if !state.beta.is_empty() && state.beta.iter().any(|v| v.is_nan()) {
return Err(
"AdditiveBlockJacobian::effective_jacobian_at: beta contains NaN".to_string(),
);
}
let chunk = rows.end - rows.start;
let total_rows = self.n_family_outputs * chunk;
let mut jac = Array2::<f64>::zeros((total_rows, p));
let row_start = self.own_output * chunk;
jac.slice_mut(ndarray::s![row_start..row_start + chunk, ..])
.assign(&self.design.slice(ndarray::s![rows.start..rows.end, ..]));
Ok(jac)
}
fn n_outputs(&self) -> usize {
self.n_family_outputs
}
}
pub struct RowScaledJacobian {
pub design: Arc<Array2<f64>>,
pub eta_scaling: Arc<[f64]>,
}
impl BlockEffectiveJacobian for RowScaledJacobian {
fn effective_jacobian_rows(
&self,
state: &FamilyLinearizationState<'_>,
rows: Range<usize>,
) -> Result<Array2<f64>, String> {
let n = self.design.nrows();
let rows = clamp_jacobian_rows(rows, n);
if self.eta_scaling.len() != n {
return Err(format!(
"RowScaledJacobian: eta_scaling length {} != design nrows {}",
self.eta_scaling.len(),
n,
));
}
if !state.beta.is_empty() && state.beta.iter().any(|v| v.is_nan()) {
return Err(
"RowScaledJacobian::effective_jacobian_at: state.beta contains NaN".to_string(),
);
}
let mut scaled = self
.design
.slice(ndarray::s![rows.start..rows.end, ..])
.to_owned();
for local_i in 0..scaled.nrows() {
let s = self.eta_scaling[rows.start + local_i];
for j in 0..scaled.ncols() {
scaled[[local_i, j]] *= s;
}
}
Ok(scaled)
}
fn eta_row_scaling_for_skewness(&self) -> Option<Arc<[f64]>> {
Some(Arc::clone(&self.eta_scaling))
}
}
pub(crate) fn clamp_jacobian_rows(rows: Range<usize>, n: usize) -> Range<usize> {
let start = rows.start.min(n);
let end = rows.end.min(n);
start..end.max(start)
}
pub struct GaugeComposedJacobian {
inner: Arc<dyn BlockEffectiveJacobian>,
t_block: Arc<Array2<f64>>,
}
impl GaugeComposedJacobian {
pub fn new(inner: Arc<dyn BlockEffectiveJacobian>, t_block: Arc<Array2<f64>>) -> Self {
Self { inner, t_block }
}
}
impl BlockEffectiveJacobian for GaugeComposedJacobian {
fn effective_jacobian_rows(
&self,
state: &FamilyLinearizationState<'_>,
rows: Range<usize>,
) -> Result<Array2<f64>, String> {
let raw_width = self.t_block.nrows();
let reduced_width = self.t_block.ncols();
let lifted_beta;
let lifted_state;
let zero_raw_beta;
let delegate_state = if state.beta.len() == raw_width {
state
} else if state.beta.len() == reduced_width {
lifted_beta = self.t_block.dot(&ndarray::ArrayView1::from(state.beta));
lifted_state = FamilyLinearizationState {
beta: lifted_beta
.as_slice()
.expect("GaugeComposedJacobian lifted beta is contiguous"),
family_scalars: state.family_scalars.clone(),
channel_hessian: state.channel_hessian.clone(),
probit_frailty_scale: state.probit_frailty_scale,
};
&lifted_state
} else if state.beta.is_empty() {
zero_raw_beta = ndarray::Array1::<f64>::zeros(raw_width);
lifted_state = FamilyLinearizationState {
beta: zero_raw_beta
.as_slice()
.expect("GaugeComposedJacobian zero raw beta is contiguous"),
family_scalars: state.family_scalars.clone(),
channel_hessian: state.channel_hessian.clone(),
probit_frailty_scale: state.probit_frailty_scale,
};
&lifted_state
} else {
return Err(format!(
"GaugeComposedJacobian: beta has length {}, expected raw width {} \
or reduced width {}; this wrapper cannot infer a block slice from a joint \
coefficient vector",
state.beta.len(),
raw_width,
reduced_width,
));
};
let j_raw = self.inner.effective_jacobian_rows(delegate_state, rows)?;
if j_raw.ncols() != self.t_block.nrows() {
return Err(format!(
"GaugeComposedJacobian: inner Jacobian has {} columns but T_b has {} rows",
j_raw.ncols(),
self.t_block.nrows(),
));
}
Ok(j_raw.dot(self.t_block.as_ref()))
}
fn n_outputs(&self) -> usize {
self.inner.n_outputs()
}
fn eta_row_scaling_for_skewness(&self) -> Option<Arc<[f64]>> {
self.inner.eta_row_scaling_for_skewness()
}
}
#[cfg(test)]
mod gauge_composed_jacobian_tests {
use super::*;
use ndarray::array;
struct BetaScaledJacobian {
design: Array2<f64>,
}
impl BlockEffectiveJacobian for BetaScaledJacobian {
fn effective_jacobian_rows(
&self,
state: &FamilyLinearizationState<'_>,
rows: Range<usize>,
) -> Result<Array2<f64>, String> {
let n = self.design.nrows();
let rows = rows.start.min(n)..rows.end.min(n);
let mut out = self.design.slice(ndarray::s![rows, ..]).to_owned();
for col in 0..out.ncols() {
let scale = 1.0 + state.beta.get(col).copied().unwrap_or(0.0);
out.column_mut(col).mapv_inplace(|v| v * scale);
}
Ok(out)
}
fn n_outputs(&self) -> usize {
1
}
}
#[test]
fn gauge_composed_jacobian_lifts_reduced_block_beta_before_delegating() {
let inner: Arc<dyn BlockEffectiveJacobian> = Arc::new(BetaScaledJacobian {
design: array![[2.0, 3.0], [5.0, 7.0]],
});
let t_block = Arc::new(array![[0.0], [1.0]]);
let wrapped = GaugeComposedJacobian::new(inner, Arc::clone(&t_block));
let theta = [4.0];
let reduced_state = FamilyLinearizationState {
beta: &theta,
family_scalars: None,
channel_hessian: None,
probit_frailty_scale: 1.0,
};
let reduced = wrapped
.effective_jacobian_rows(&reduced_state, 0..2)
.expect("reduced beta should be lifted through T before inner callback");
let raw_beta = [0.0, 4.0];
let raw_state = FamilyLinearizationState {
beta: &raw_beta,
family_scalars: None,
channel_hessian: None,
probit_frailty_scale: 1.0,
};
let raw = wrapped
.effective_jacobian_rows(&raw_state, 0..2)
.expect("raw beta state remains valid");
assert_eq!(reduced, raw);
assert_eq!(reduced, array![[15.0], [35.0]]);
}
struct StrictRawWidthJacobian {
design: Array2<f64>,
}
impl BlockEffectiveJacobian for StrictRawWidthJacobian {
fn effective_jacobian_rows(
&self,
state: &FamilyLinearizationState<'_>,
rows: Range<usize>,
) -> Result<Array2<f64>, String> {
if state.beta.len() != self.design.ncols() {
return Err(format!(
"StrictRawWidthJacobian expected raw beta len {}, got {}",
self.design.ncols(),
state.beta.len(),
));
}
Ok(self.design.slice(ndarray::s![rows, ..]).to_owned())
}
}
#[test]
fn gauge_composed_jacobian_lifts_zero_reduced_beta_before_delegating() {
let inner: Arc<dyn BlockEffectiveJacobian> = Arc::new(StrictRawWidthJacobian {
design: array![[2.0, 3.0], [5.0, 7.0]],
});
let wrapped = GaugeComposedJacobian::new(inner, Arc::new(array![[0.0], [1.0]]));
let theta = [0.0];
let reduced_state = FamilyLinearizationState {
beta: &theta,
family_scalars: None,
channel_hessian: None,
probit_frailty_scale: 1.0,
};
let reduced = wrapped
.effective_jacobian_rows(&reduced_state, 0..2)
.expect("zero reduced beta must still be lifted to raw width");
assert_eq!(reduced, array![[3.0], [7.0]]);
}
#[test]
fn gauge_composed_jacobian_rejects_nonzero_unknown_beta_layout() {
let inner: Arc<dyn BlockEffectiveJacobian> = Arc::new(BetaScaledJacobian {
design: array![[2.0, 3.0]],
});
let wrapped = GaugeComposedJacobian::new(inner, Arc::new(array![[0.0], [1.0]]));
let joint_like_beta = [1.0, 0.0, 0.0];
let state = FamilyLinearizationState {
beta: &joint_like_beta,
family_scalars: None,
channel_hessian: None,
probit_frailty_scale: 1.0,
};
let err = wrapped
.effective_jacobian_rows(&state, 0..1)
.expect_err("nonzero joint-layout beta cannot be inferred from one block T");
assert!(
err.contains("cannot infer a block slice"),
"unexpected error: {err}"
);
}
}
#[derive(Clone)]
pub struct ParameterBlockSpec {
pub name: String,
pub design: DesignMatrix,
pub offset: Array1<f64>,
pub penalties: Vec<PenaltyMatrix>,
pub nullspace_dims: Vec<usize>,
pub initial_log_lambdas: Array1<f64>,
pub initial_beta: Option<Array1<f64>>,
pub gauge_priority: u8,
pub jacobian_callback: Option<Arc<dyn BlockEffectiveJacobian>>,
pub stacked_design: Option<DesignMatrix>,
pub stacked_offset: Option<Array1<f64>>,
}
impl std::fmt::Debug for ParameterBlockSpec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ParameterBlockSpec")
.field("name", &self.name)
.field("design", &self.design)
.field("offset", &self.offset)
.field("penalties", &self.penalties)
.field("nullspace_dims", &self.nullspace_dims)
.field("initial_log_lambdas", &self.initial_log_lambdas)
.field("initial_beta", &self.initial_beta)
.field("gauge_priority", &self.gauge_priority)
.field(
"jacobian_callback",
&self
.jacobian_callback
.as_ref()
.map(|_| "<BlockEffectiveJacobian>"),
)
.finish()
}
}
impl ParameterBlockSpec {
pub fn defaults() -> Self {
Self {
name: String::new(),
design: DesignMatrix::Dense(gam_linalg::matrix::DenseDesignMatrix::from(
ndarray::Array2::<f64>::zeros((0, 0)),
)),
offset: ndarray::Array1::<f64>::zeros(0),
penalties: Vec::new(),
nullspace_dims: Vec::new(),
initial_log_lambdas: ndarray::Array1::<f64>::zeros(0),
initial_beta: None,
gauge_priority: 100,
jacobian_callback: None,
stacked_design: None,
stacked_offset: None,
}
}
pub fn solver_design(&self) -> &DesignMatrix {
self.stacked_design.as_ref().unwrap_or(&self.design)
}
pub fn solver_offset(&self) -> &Array1<f64> {
self.stacked_offset.as_ref().unwrap_or(&self.offset)
}
pub fn effective_design(&self, caller: &str) -> Result<ndarray::Array2<f64>, String> {
let p = self.design.ncols();
let zeros = vec![0.0f64; p];
let state = FamilyLinearizationState {
beta: &zeros,
family_scalars: None,
channel_hessian: None,
probit_frailty_scale: 1.0,
};
self.effective_jacobian_at(caller, &state)
}
pub fn effective_jacobian_at(
&self,
caller: &str,
state: &FamilyLinearizationState<'_>,
) -> Result<ndarray::Array2<f64>, String> {
if let Some(cb) = self.jacobian_callback.as_ref() {
return cb.effective_jacobian_at(state);
}
self.design
.try_to_dense_arc(&format!(
"{caller}::effective_jacobian_at block '{}'",
self.name
))
.map(|arc| arc.as_ref().clone())
}
}
#[derive(Clone, Debug)]
pub struct ParameterBlockState {
pub beta: Array1<f64>,
pub eta: Array1<f64>,
}
#[derive(Clone)]
pub struct BlockGeometryDirectionalDerivative {
pub d_design: Option<Array2<f64>>,
pub d_offset: Array1<f64>,
}
#[derive(Clone, Debug)]
pub enum BlockWorkingSet {
Diagonal {
working_response: Array1<f64>,
working_weights: Array1<f64>,
},
ExactNewton {
gradient: Array1<f64>,
hessian: SymmetricMatrix,
},
}
impl BlockWorkingSet {
#[inline]
pub fn diagonal_checked(
working_response: Array1<f64>,
working_weights: Array1<f64>,
) -> Result<Self, String> {
if working_response.len() != working_weights.len() {
return Err(format!(
"BlockWorkingSet::Diagonal length mismatch: working_response={}, working_weights={}",
working_response.len(),
working_weights.len(),
));
}
Ok(Self::Diagonal {
working_response,
working_weights,
})
}
}