1use super::*;
2
3pub(crate) fn sas_log_deltaridgeweight() -> f64 {
4 1e-4
7}
8
9#[inline]
10pub(crate) fn sas_log_delta_edge_barrierweight() -> f64 {
11 1e-2
14}
15
16#[inline]
17pub(crate) fn sas_log_delta_bound() -> f64 {
18 crate::mixture_link::SAS_LOG_DELTA_BOUND
19}
20
21#[inline]
22pub(crate) fn sas_log_delta_edge_barriercostgrad(raw_log_delta: f64) -> (f64, f64) {
23 let w = sas_log_delta_edge_barrierweight();
24 if w <= 0.0 || !raw_log_delta.is_finite() {
25 return (0.0, 0.0);
26 }
27 let b = sas_log_delta_bound().max(f64::EPSILON);
28 let u = raw_log_delta / b;
29 let t = u.tanh();
30 let cost = 2.0 * w * ln_cosh(u);
34 let grad = (2.0 * w / b) * t;
36 (cost, grad)
37}
38
39#[inline]
43fn ln_cosh(u: f64) -> f64 {
44 let a = u.abs();
45 a + (-2.0 * a).exp().ln_1p() - std::f64::consts::LN_2
46}
47
48#[inline]
49pub(crate) fn sas_epsilon_bound() -> f64 {
50 8.0
52}
53
54#[inline]
55pub(crate) fn sas_effective_epsilon(raw_epsilon: f64) -> (f64, f64) {
56 let bound = sas_epsilon_bound().max(f64::EPSILON);
57 let t = (raw_epsilon / bound).tanh();
58 let epsilon = bound * t;
59 let d_epsilon_d_raw = 1.0 - t * t;
60 (epsilon, d_epsilon_d_raw)
61}
62
63#[inline]
64pub(crate) fn sas_effective_epsilon_second(raw_epsilon: f64) -> (f64, f64, f64) {
65 let bound = sas_epsilon_bound().max(f64::EPSILON);
66 let t = (raw_epsilon / bound).tanh();
67 let first = 1.0 - t * t;
68 let second = -2.0 * t * first / bound;
69 (bound * t, first, second)
70}
71
72#[inline]
73pub(crate) fn sas_log_delta_edge_barriercostgradhess(raw_log_delta: f64) -> (f64, f64, f64) {
74 let w = sas_log_delta_edge_barrierweight();
75 if w <= 0.0 || !raw_log_delta.is_finite() {
76 return (0.0, 0.0, 0.0);
77 }
78 let b = sas_log_delta_bound().max(f64::EPSILON);
79 let u = raw_log_delta / b;
80 let t = u.tanh();
81 let ln_cosh_u = ln_cosh(u);
82 let cost = 2.0 * w * ln_cosh_u;
83 let grad = (2.0 * w / b) * t;
84 let one_minus_t2 = (-2.0 * ln_cosh_u).exp();
87 let hess = (2.0 * w / (b * b)) * one_minus_t2;
88 (cost, grad, hess)
89}
90
91pub(crate) fn materialize_link_outer_hessian(
92 hessian: gam_problem::HessianValue,
93 theta_dim: usize,
94) -> Result<Array2<f64>, EstimationError> {
95 match hessian.materialize_dense() {
96 Ok(Some(h)) => {
97 if h.nrows() != theta_dim || h.ncols() != theta_dim {
98 crate::bail_invalid_estim!(
99 "unified evaluator Hessian shape {}x{} != theta_dim {}",
100 h.nrows(),
101 h.ncols(),
102 theta_dim
103 );
104 }
105 Ok(h)
106 }
107 Ok(None) => Err(EstimationError::InvalidInput(
108 "unified evaluator returned no analytic Hessian in ValueGradientHessian mode"
109 .to_string(),
110 )),
111 Err(err) => Err(EstimationError::InvalidInput(format!(
112 "failed to materialize analytic link Hessian: {err}"
113 ))),
114 }
115}
116
117pub fn evaluate_externalgradient<X>(
119 y: ArrayView1<'_, f64>,
120 w: ArrayView1<'_, f64>,
121 x: X,
122 offset: ArrayView1<'_, f64>,
123 s_list: &[BlockwisePenalty],
124 opts: &ExternalOptimOptions,
125 rho: &Array1<f64>,
126) -> Result<Array1<f64>, EstimationError>
127where
128 X: Into<DesignMatrix>,
129{
130 let specs: Vec<PenaltySpec> = s_list.iter().map(PenaltySpec::from_blockwise_ref).collect();
131 let x = x.into();
132 if let Some(message) = row_mismatch_message(y.len(), w.len(), x.nrows(), offset.len()) {
133 crate::bail_invalid_estim!("{}", message);
134 }
135
136 let p = x.ncols();
137 validate_penalty_specs(&specs, p, "evaluate_externalgradient")?;
138 let (canonical, active_nullspace_dims) = gam_terms::construction::canonicalize_penalty_specs(
139 &specs,
140 &opts.nullspace_dims,
141 p,
142 "evaluate_externalgradient",
143 )?;
144 if rho.len() != active_nullspace_dims.len() {
145 crate::bail_invalid_estim!(
146 "rho dimension mismatch: rho_dim={}, active_penalties={}",
147 rho.len(),
148 active_nullspace_dims.len()
149 );
150 }
151
152 let (cfg, _) = resolved_external_config(opts)?;
153
154 let y_o = y.to_owned();
155 let w_o = w.to_owned();
156 let offset_o = offset.to_owned();
157 let conditioning = ParametricColumnConditioning::infer_from_penalty_specs(&x, &specs);
158 let x_fit = conditioning.apply_to_design(&x);
159 let fit_linear_constraints =
160 conditioning.transform_linear_constraints_to_internal(opts.linear_constraints.clone());
161
162 let mut reml_state = RemlState::newwith_offset(
163 y_o.view(),
164 x_fit,
165 w_o.view(),
166 offset_o.view(),
167 canonical,
168 p,
169 &cfg,
170 Some(active_nullspace_dims),
171 None,
172 fit_linear_constraints,
173 )?;
174 reml_state.set_rho_prior(opts.rho_prior.clone());
175 reml_state.set_link_states(
176 cfg.link_kind.mixture_state().cloned(),
177 cfg.link_kind.sas_state().copied(),
178 );
179
180 reml_state.compute_gradient(rho)
181}
182
183pub fn evaluate_externalcost_andridge<X>(
186 y: ArrayView1<'_, f64>,
187 w: ArrayView1<'_, f64>,
188 x: X,
189 offset: ArrayView1<'_, f64>,
190 s_list: &[BlockwisePenalty],
191 opts: &ExternalOptimOptions,
192 rho: &Array1<f64>,
193) -> Result<(f64, f64), EstimationError>
194where
195 X: Into<DesignMatrix>,
196{
197 let specs: Vec<PenaltySpec> = s_list.iter().map(PenaltySpec::from_blockwise_ref).collect();
198 let x = x.into();
199 if let Some(message) = row_mismatch_message(y.len(), w.len(), x.nrows(), offset.len()) {
200 crate::bail_invalid_estim!("{}", message);
201 }
202
203 let p = x.ncols();
204 validate_penalty_specs(&specs, p, "evaluate_externalcost_andridge")?;
205 let (canonical, active_nullspace_dims) = gam_terms::construction::canonicalize_penalty_specs(
206 &specs,
207 &opts.nullspace_dims,
208 p,
209 "evaluate_externalcost_andridge",
210 )?;
211 if rho.len() != active_nullspace_dims.len() {
212 crate::bail_invalid_estim!(
213 "rho dimension mismatch: rho_dim={}, active_penalties={}",
214 rho.len(),
215 active_nullspace_dims.len()
216 );
217 }
218
219 let (cfg, _) = resolved_external_config(opts)?;
220
221 let y_o = y.to_owned();
222 let w_o = w.to_owned();
223 let offset_o = offset.to_owned();
224 let conditioning = ParametricColumnConditioning::infer_from_penalty_specs(&x, &specs);
225 let x_fit = conditioning.apply_to_design(&x);
226 let fit_linear_constraints =
227 conditioning.transform_linear_constraints_to_internal(opts.linear_constraints.clone());
228
229 let mut reml_state = RemlState::newwith_offset(
230 y_o.view(),
231 x_fit,
232 w_o.view(),
233 offset_o.view(),
234 canonical,
235 p,
236 &cfg,
237 Some(active_nullspace_dims),
238 None,
239 fit_linear_constraints,
240 )?;
241 reml_state.set_rho_prior(opts.rho_prior.clone());
242 reml_state.set_link_states(
243 cfg.link_kind.mixture_state().cloned(),
244 cfg.link_kind.sas_state().copied(),
245 );
246
247 let cost = reml_state.compute_cost(rho)?;
248 let ridge = reml_state.last_ridge_used().unwrap_or(0.0);
249 Ok((cost, ridge))
250}