use crate::families::custom_family::family_trait::{CustomFamily, OuterEvalContext};
use crate::families::custom_family::psi_design::{
CustomFamilyHyperLayout, ExactNewtonJointHessianWorkspace,
};
use gam_linalg::RidgePolicy;
use gam_problem::{ParameterBlockSpec, ParameterBlockState};
use ndarray::Array1;
use std::ops::Range;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
pub use gam_problem::{ExactNewtonOuterObjective, ExactOuterDerivativeOrder};
pub use gam_problem::validate_blockspec_consistency;
pub(crate) fn assert_valid_blockspecs(specs: &[ParameterBlockSpec], context: &str) {
assert!(
validate_blockspec_consistency(specs).is_ok(),
"{context}: inconsistent parameter block specs"
);
}
pub(crate) fn assert_valid_options(options: &BlockwiseFitOptions, context: &str) {
assert!(
options.inner_tol.is_finite() && options.inner_tol >= 0.0,
"{context}: inner_tol must be finite and non-negative"
);
assert!(
options.outer_tol.is_finite() && options.outer_tol >= 0.0,
"{context}: outer_tol must be finite and non-negative"
);
assert!(
options.ridge_floor.is_finite() && options.ridge_floor >= 0.0,
"{context}: ridge_floor must be finite and non-negative"
);
if let Some(threshold) = options.early_exit_threshold {
assert!(
threshold.is_finite(),
"{context}: early_exit_threshold must be finite"
);
}
}
pub(crate) fn assert_states_match_specs(
states: &[ParameterBlockState],
specs: &[ParameterBlockSpec],
context: &str,
) {
assert_eq!(
states.len(),
specs.len(),
"{context}: state/spec block count mismatch"
);
for (block, (state, spec)) in states.iter().zip(specs).enumerate() {
assert_eq!(
state.beta.len(),
spec.design.ncols(),
"{context}: beta length mismatch in block {block}"
);
assert_eq!(
state.eta.len(),
spec.solver_design().nrows(),
"{context}: eta length mismatch in block {block}"
);
}
}
pub(crate) fn assert_blockstates_are_a_point(states: &[ParameterBlockState], context: &str) {
for (block, state) in states.iter().enumerate() {
assert!(
state.beta.iter().all(|v| !v.is_nan()),
"{context}: NaN beta in block {block}"
);
assert!(
state.eta.iter().all(|v| !v.is_nan()),
"{context}: NaN eta in block {block}"
);
}
}
pub(crate) fn assert_block_index_matches_spec(
states: &[ParameterBlockState],
block_index: usize,
spec: &ParameterBlockSpec,
context: &str,
) {
assert!(
block_index < states.len(),
"{context}: block index {block_index} out of range for {} blocks",
states.len()
);
assert_eq!(
states[block_index].beta.len(),
spec.design.ncols(),
"{context}: spec does not describe block {block_index}"
);
}
pub(crate) fn assert_block_local_beta_direction(
states: &[ParameterBlockState],
block_index: usize,
direction: &Array1<f64>,
context: &str,
) {
assert!(
block_index < states.len(),
"{context}: block index {block_index} out of range for {} blocks",
states.len()
);
assert_eq!(
direction.len(),
states[block_index].beta.len(),
"{context}: direction is not in block {block_index}'s coefficient space"
);
assert!(
direction.iter().all(|v| !v.is_nan()),
"{context}: NaN entry in block {block_index} coefficient direction"
);
}
pub(crate) fn assert_block_local_eta_direction(
states: &[ParameterBlockState],
block_index: usize,
d_eta: &Array1<f64>,
context: &str,
) {
assert!(
block_index < states.len(),
"{context}: block index {block_index} out of range for {} blocks",
states.len()
);
assert_eq!(
d_eta.len(),
states[block_index].eta.len(),
"{context}: direction is not in block {block_index}'s predictor space"
);
assert!(
d_eta.iter().all(|v| !v.is_nan()),
"{context}: NaN entry in block {block_index} predictor direction"
);
}
pub(crate) fn assert_psi_index_in_layout(
hyper_layout: &CustomFamilyHyperLayout,
psi_index: usize,
context: &str,
) {
assert!(
hyper_layout.axis(psi_index).is_some(),
"{context}: psi index {psi_index} is not an axis of a layout with {} coordinates",
hyper_layout.len()
);
}
pub(crate) fn assert_hyper_layout_matches_specs(
hyper_layout: &CustomFamilyHyperLayout,
specs: &[ParameterBlockSpec],
context: &str,
) {
assert_eq!(
hyper_layout.block_count(),
specs.len(),
"{context}: hyper-layout/spec block count mismatch"
);
}
pub(crate) fn assert_rho_matches_specs(
rho: &Array1<f64>,
specs: &[ParameterBlockSpec],
context: &str,
) {
let expected = specs.iter().map(|spec| spec.penalties.len()).sum::<usize>();
assert_eq!(
rho.len(),
expected,
"{context}: rho length does not match penalty count"
);
}
pub(crate) fn validate_hessian_workspace_ready(
hessian_workspace: &Option<Arc<dyn ExactNewtonJointHessianWorkspace>>,
context: &str,
eval_mode: gam_problem::EvalMode,
) -> Result<(), String> {
if let Some(workspace) = hessian_workspace.as_ref() {
workspace
.warm_up_outer_caches_for_mode(eval_mode)
.map_err(|err| format!("{context}: failed to warm Hessian workspace caches: {err}"))?;
}
Ok(())
}
pub fn exact_outer_order_from_capability(
specs: &[ParameterBlockSpec],
coefficient_cost: u64,
) -> ExactOuterDerivativeOrder {
assert_valid_blockspecs(specs, "exact outer derivative order");
match coefficient_cost {
0 => ExactOuterDerivativeOrder::Second,
_ => ExactOuterDerivativeOrder::Second,
}
}
pub fn exact_outer_order_with_outer_hvp(
specs: &[ParameterBlockSpec],
coefficient_cost: u64,
outer_hyper_hessian_hvp_available: bool,
) -> ExactOuterDerivativeOrder {
if outer_hyper_hessian_hvp_available {
assert_valid_blockspecs(specs, "exact outer derivative order with HVP");
match coefficient_cost {
0 => ExactOuterDerivativeOrder::Second,
_ => ExactOuterDerivativeOrder::Second,
}
} else {
exact_outer_order_from_capability(specs, coefficient_cost)
}
}
#[derive(Clone, Copy, Debug)]
pub struct OuterDerivativePolicy {
pub capability: ExactOuterDerivativeOrder,
pub predicted_hessian_work: u128,
pub predicted_gradient_work: u128,
pub subsample_capable: bool,
}
impl OuterDerivativePolicy {
pub const OUTER_GRADIENT_WORK_BUDGET: u128 = 50_000_000_000;
pub const STAGED_KAPPA_TRIGGER_N: usize = 30_000;
pub fn order_for_evaluation(&self, requested: crate::OuterEvalOrder) -> crate::OuterEvalOrder {
use crate::OuterEvalOrder;
match requested {
OuterEvalOrder::Value => OuterEvalOrder::Value,
OuterEvalOrder::ValueAndGradient => OuterEvalOrder::ValueAndGradient,
OuterEvalOrder::ValueGradientHessian => {
if matches!(
self.declared_hessian_form(),
gam_problem::DeclaredHessianForm::Unavailable
) {
OuterEvalOrder::ValueAndGradient
} else {
OuterEvalOrder::ValueGradientHessian
}
}
}
}
pub fn declared_hessian_form(&self) -> gam_problem::DeclaredHessianForm {
use gam_problem::DeclaredHessianForm;
if !self.capability.has_hessian() {
return DeclaredHessianForm::Unavailable;
}
DeclaredHessianForm::Either
}
pub fn should_use_staged_kappa(&self, n: usize) -> bool {
if !self.subsample_capable {
return false;
}
n >= Self::STAGED_KAPPA_TRIGGER_N
|| self.predicted_gradient_work > Self::OUTER_GRADIENT_WORK_BUDGET
}
}
#[inline]
pub(crate) fn outer_coord_dim_for_policy(specs: &[ParameterBlockSpec], psi_dim: usize) -> u128 {
let rho_total: u128 = specs
.iter()
.map(|s| s.penalties.len() as u128)
.fold(0u128, |acc, k| acc.saturating_add(k));
rho_total.saturating_add(psi_dim as u128)
}
pub fn default_outer_derivative_policy_costs(
specs: &[ParameterBlockSpec],
psi_dim: usize,
grad_cost: u64,
hess_cost: u64,
) -> (u128, u128) {
let k = outer_coord_dim_for_policy(specs, psi_dim);
let grad = (grad_cost as u128).saturating_mul(k.max(1));
let hess = (hess_cost as u128).saturating_mul(k.max(1));
(grad, hess)
}
pub fn default_coefficient_hessian_cost(specs: &[ParameterBlockSpec]) -> u64 {
specs
.iter()
.map(|s| {
let n = s.design.nrows() as u64;
let p = s.design.ncols() as u64;
n.saturating_mul(p.saturating_mul(p))
})
.fold(0u64, |acc, c| acc.saturating_add(c))
}
pub fn joint_coupled_coefficient_hessian_cost(n: u64, specs: &[ParameterBlockSpec]) -> u64 {
let p_total: u64 = specs
.iter()
.map(|s| s.design.ncols() as u64)
.fold(0u64, |acc, p| acc.saturating_add(p));
n.saturating_mul(p_total.saturating_mul(p_total))
}
pub fn block_offsets_from_specs(specs: &[ParameterBlockSpec]) -> Arc<[Range<usize>]> {
let mut ranges: Vec<Range<usize>> = Vec::with_capacity(specs.len());
let mut cursor = 0usize;
for spec in specs {
let p = spec.design.ncols();
ranges.push(cursor..cursor + p);
cursor += p;
}
Arc::from(ranges.into_boxed_slice())
}
pub const FIRST_ORDER_BFGS_LOGLAMBDA_STEP_CAP: f64 = 5.0;
pub fn exact_newton_outer_geometry_supports_second_order_solver<F: CustomFamily + ?Sized>(
family: &F,
) -> bool {
family.exact_newton_outerobjective() == ExactNewtonOuterObjective::StrictPseudoLaplace
}
#[derive(Clone)]
pub struct BlockwiseFitOptions {
pub inner_max_cycles: usize,
pub inner_tol: f64,
pub outer_max_iter: usize,
pub outer_tol: f64,
pub outer_rel_cost_tol: Option<f64>,
pub rho_lower_bound: f64,
pub ridge_floor: f64,
pub ridge_policy: RidgePolicy,
pub use_remlobjective: bool,
pub use_outer_hessian: bool,
pub compute_covariance: bool,
pub screening_max_inner_iterations: Option<Arc<AtomicUsize>>,
pub outer_inner_max_iterations: Option<Arc<AtomicUsize>>,
pub early_exit_threshold: Option<f64>,
pub outer_score_subsample: Option<Arc<crate::OuterScoreSubsample>>,
pub auto_outer_subsample: bool,
pub outer_eval_context: Option<OuterEvalContext>,
pub cache_session: Option<Arc<gam_runtime::warm_start::Session>>,
pub persistent_warm_start_store: Option<gam_runtime::warm_start::ConfiguredWarmStartStore>,
pub cache_mirror_sessions: Vec<Arc<gam_runtime::warm_start::Session>>,
pub joint_penalties: Option<Arc<crate::JointPenaltyBundle>>,
pub independent_prior_factor_labels: Vec<String>,
pub screen_initial_rho: bool,
pub seed_screening: bool,
}
pub const DEFAULT_CUSTOM_FAMILY_INNER_MAX_CYCLES: usize = 1200;
impl Default for BlockwiseFitOptions {
fn default() -> Self {
Self {
inner_max_cycles: DEFAULT_CUSTOM_FAMILY_INNER_MAX_CYCLES,
inner_tol: 1e-6,
outer_max_iter: 60,
outer_tol: 1e-5,
outer_rel_cost_tol: None,
rho_lower_bound: -10.0,
ridge_floor: 0.0,
ridge_policy: RidgePolicy::solver_only(),
use_remlobjective: true,
use_outer_hessian: true,
compute_covariance: false,
screening_max_inner_iterations: None,
outer_inner_max_iterations: None,
seed_screening: false,
early_exit_threshold: None,
outer_score_subsample: None,
auto_outer_subsample: true,
outer_eval_context: None,
cache_session: None,
persistent_warm_start_store: None,
cache_mirror_sessions: Vec::new(),
joint_penalties: None,
independent_prior_factor_labels: Vec::new(),
screen_initial_rho: true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use gam_linalg::matrix::DesignMatrix;
use ndarray::Array2;
fn make_spec(nrows: usize, ncols: usize) -> ParameterBlockSpec {
ParameterBlockSpec {
design: DesignMatrix::from(Array2::<f64>::zeros((nrows, ncols))),
..ParameterBlockSpec::defaults()
}
}
#[test]
fn hessian_cost_empty_specs_is_zero() {
assert_eq!(default_coefficient_hessian_cost(&[]), 0);
}
#[test]
fn hessian_cost_single_block() {
let spec = make_spec(10, 3);
assert_eq!(default_coefficient_hessian_cost(&[spec]), 90);
}
#[test]
fn hessian_cost_two_blocks_sum() {
let specs = [make_spec(10, 3), make_spec(5, 4)];
assert_eq!(default_coefficient_hessian_cost(&specs), 170);
}
#[test]
fn joint_coupled_cost_empty_specs_is_zero() {
assert_eq!(joint_coupled_coefficient_hessian_cost(100, &[]), 0);
}
#[test]
fn joint_coupled_cost_two_blocks() {
let specs = [make_spec(99, 3), make_spec(99, 4)];
assert_eq!(joint_coupled_coefficient_hessian_cost(10, &specs), 490);
}
#[test]
fn block_offsets_empty_is_empty() {
let offsets = block_offsets_from_specs(&[]);
assert_eq!(offsets.len(), 0);
}
#[test]
fn block_offsets_three_blocks() {
let specs = [make_spec(1, 2), make_spec(1, 3), make_spec(1, 1)];
let offsets = block_offsets_from_specs(&specs);
assert_eq!(&offsets[0], &(0..2));
assert_eq!(&offsets[1], &(2..5));
assert_eq!(&offsets[2], &(5..6));
}
#[test]
fn block_offsets_zero_width_block() {
let specs = [make_spec(1, 2), make_spec(1, 0), make_spec(1, 1)];
let offsets = block_offsets_from_specs(&specs);
assert_eq!(&offsets[0], &(0..2));
assert_eq!(&offsets[1], &(2..2));
assert_eq!(&offsets[2], &(2..3));
}
#[test]
fn default_custom_family_objective_is_coefficient_ridge_free() {
let options = BlockwiseFitOptions::default();
assert_eq!(options.ridge_floor, 0.0);
assert!(!options.ridge_policy.accounts_for_objective());
}
}