use crate::{CustomFamilyError, ParameterBlockSpec};
use ndarray::Array2;
use std::collections::BTreeMap;
pub const CUSTOM_FAMILY_RIDGE_FLOOR: f64 = 1e-12;
pub fn validate_blockspec_consistency(
specs: &[ParameterBlockSpec],
) -> Result<Vec<usize>, CustomFamilyError> {
let mut seen_names = BTreeMap::<String, usize>::new();
for (b, spec) in specs.iter().enumerate() {
if let Some(prev) = seen_names.insert(spec.name.clone(), b) {
return Err(CustomFamilyError::ConstraintViolation {
reason: format!(
"duplicate parameter block name '{}' at indices {prev} and {b}: block names must be unique so coefficient labels resolved by name are unambiguous",
spec.name
),
});
}
}
let mut penalty_counts = Vec::with_capacity(specs.len());
for (b, spec) in specs.iter().enumerate() {
let n = spec.design.nrows();
if spec.offset.len() != n {
return Err(CustomFamilyError::DimensionMismatch {
reason: format!(
"block {b} offset length mismatch: got {}, expected {}",
spec.offset.len(),
n
),
});
}
match (&spec.stacked_design, &spec.stacked_offset) {
(Some(sd), Some(so)) => {
if sd.nrows() != so.len() {
return Err(CustomFamilyError::DimensionMismatch {
reason: format!(
"block {b} stacked_design/stacked_offset row mismatch: \
stacked_design.nrows()={}, stacked_offset.len()={}",
sd.nrows(),
so.len(),
),
});
}
if sd.ncols() != spec.design.ncols() {
return Err(CustomFamilyError::DimensionMismatch {
reason: format!(
"block {b} stacked_design column count {} disagrees with \
design column count {}",
sd.ncols(),
spec.design.ncols(),
),
});
}
}
(None, None) => {}
(Some(_), None) | (None, Some(_)) => {
return Err(CustomFamilyError::ConstraintViolation {
reason: format!(
"block {b} stacked_design and stacked_offset must be Some together \
or both None"
),
});
}
}
let p = spec.design.ncols();
if let Some(beta0) = &spec.initial_beta
&& beta0.len() != p
{
return Err(CustomFamilyError::DimensionMismatch {
reason: format!(
"block {b} initial_beta length mismatch: got {}, expected {p}",
beta0.len()
),
});
}
if spec.initial_log_lambdas.len() != spec.penalties.len() {
return Err(CustomFamilyError::DimensionMismatch {
reason: format!(
"block {b} initial_log_lambdas length {} does not match penalties {}",
spec.initial_log_lambdas.len(),
spec.penalties.len()
),
});
}
for (k, &log_lambda) in spec.initial_log_lambdas.iter().enumerate() {
if let Err(error) = crate::validate_log_strength(log_lambda) {
return Err(CustomFamilyError::ConstraintViolation {
reason: format!("block {b} initial log-precision {k}: {error}"),
});
}
}
for (k, s) in spec.penalties.iter().enumerate() {
let (r, c) = s.shape();
if r != p || c != p {
return Err(CustomFamilyError::DimensionMismatch {
reason: format!("block {b} penalty {k} must be {p}x{p}, got {r}x{c}"),
});
}
if let Err(reason) = s.validate(p) {
return Err(CustomFamilyError::ConstraintViolation {
reason: format!("block {b} penalty {k}: {reason}"),
});
}
}
penalty_counts.push(spec.penalties.len());
}
Ok(penalty_counts)
}
pub struct ExactNewtonOuterCurvature {
pub hessian: Array2<f64>,
pub rho_curvature_scale: f64,
pub hessian_logdet_correction: f64,
}
#[cfg(test)]
mod validate_blockspec_tests {
use crate::{CustomFamilyError, ParameterBlockSpec};
use gam_linalg::matrix::DesignMatrix;
use ndarray::{Array1, array};
fn block(name: &str) -> ParameterBlockSpec {
ParameterBlockSpec {
name: name.to_string(),
design: DesignMatrix::Dense(gam_linalg::matrix::DenseDesignMatrix::from(array![
[1.0],
[1.0]
])),
offset: array![0.0, 0.0],
penalties: Vec::new(),
nullspace_dims: Vec::new(),
initial_log_lambdas: Array1::zeros(0),
initial_beta: None,
gauge_priority: 100,
jacobian_callback: None,
stacked_design: None,
stacked_offset: None,
}
}
#[test]
fn a_duplicate_block_name_refuses_with_a_structural_variant_2667() {
let error = super::validate_blockspec_consistency(&[block("dup"), block("dup")])
.expect_err("duplicate block names must be refused");
assert!(
matches!(error, CustomFamilyError::ConstraintViolation { .. }),
"a duplicate block name is structural, got: {error:?}"
);
assert!(
!error.is_trial_point_infeasible(),
"a refusal true at every theta must not be graded rho-local: {error}"
);
assert!(
error.to_string().contains("duplicate parameter block name"),
"the reason must survive the typed return: {error}"
);
}
#[test]
fn a_dimension_mismatch_refuses_with_a_structural_variant_2667() {
let mut spec = block("b0");
spec.offset = array![0.0, 0.0, 0.0];
let error = super::validate_blockspec_consistency(&[spec])
.expect_err("an offset/design row mismatch must be refused");
assert!(
matches!(error, CustomFamilyError::DimensionMismatch { .. }),
"an offset length mismatch is structural, got: {error:?}"
);
assert!(!error.is_trial_point_infeasible(), "{error}");
}
#[test]
fn consistent_specs_still_report_their_penalty_counts() {
let counts = super::validate_blockspec_consistency(&[block("a"), block("b")])
.expect("distinct, dimensionally consistent blocks must validate");
assert_eq!(counts, vec![0, 0]);
}
}