1use super::*;
7
8pub trait WorkingModel {
9 fn update(&mut self, beta: &Coefficients) -> Result<WorkingState, EstimationError>;
10
11 fn update_with_curvature(
12 &mut self,
13 beta: &Coefficients,
14 _: HessianCurvatureKind,
15 ) -> Result<WorkingState, EstimationError> {
16 self.update(beta)
17 }
18
19 fn update_candidate(
20 &mut self,
21 beta: &Coefficients,
22 curvature: HessianCurvatureKind,
23 ) -> Result<WorkingState, EstimationError> {
24 self.update_with_curvature(beta, curvature)
25 }
26
27 fn screen_candidate(
28 &mut self,
29 beta: &Coefficients,
30 arr: &Array1<f64>,
31 _: &LinearPredictor,
32 curvature: HessianCurvatureKind,
33 ) -> Result<CandidateEvaluation, EstimationError> {
34 assert!(arr.iter().all(|v| !v.is_nan()));
35 self.update_candidate(beta, curvature)
36 .map(CandidateEvaluation::Full)
37 }
38
39 fn supports_observed_information_curvature(&self) -> bool {
40 false
41 }
42
43 fn solve_unconstrained_direction(
48 &mut self,
49 beta: &Coefficients,
50 state: &WorkingState,
51 loop_lambda: f64,
52 lm_d2: &Array1<f64>,
53 regularized_hessian: &Array2<f64>,
54 direction_out: &mut Array1<f64>,
55 ) -> Result<(), EstimationError> {
56 if beta.as_ref().len() != state.gradient.len() {
57 crate::bail_invalid_estim!(
58 "PIRLS coefficient length {} does not match gradient length {}",
59 beta.as_ref().len(),
60 state.gradient.len()
61 );
62 }
63 if !(loop_lambda.is_finite() && loop_lambda >= 0.0) {
64 crate::bail_invalid_estim!(
65 "PIRLS LM damping must be finite and nonnegative, got {loop_lambda}"
66 );
67 }
68 if lm_d2.len() != state.gradient.len() {
69 crate::bail_invalid_estim!(
70 "PIRLS LM diagonal length {} does not match gradient length {}",
71 lm_d2.len(),
72 state.gradient.len()
73 );
74 }
75 solve_newton_direction_dense(regularized_hessian, &state.gradient, direction_out)?;
76 Ok(())
77 }
78
79 fn exact_unconstrained_decrement_sq(
87 &mut self,
88 beta: &Coefficients,
89 state: &WorkingState,
90 ) -> Result<Option<f64>, EstimationError> {
91 if beta.as_ref().len() != state.gradient.len() {
92 crate::bail_invalid_estim!(
93 "PIRLS coefficient length {} does not match gradient length {}",
94 beta.as_ref().len(),
95 state.gradient.len()
96 );
97 }
98 Ok(None)
99 }
100
101 fn objective_hessian_quadratic_correction(
110 &self,
111 direction: &Array1<f64>,
112 ) -> Result<f64, EstimationError> {
113 assert!(array_is_finite(direction));
114 Ok(0.0)
115 }
116
117 fn penalized_deviance_scale(&self) -> Result<f64, EstimationError> {
126 Ok(1.0)
127 }
128}
129
130#[derive(Debug, Clone)]
133pub struct CandidateScreen {
134 pub deviance: f64,
135 pub penalty_term: f64,
136 pub arithmetic_finite: bool,
137}
138
139pub enum CandidateEvaluation {
143 Screen(CandidateScreen),
144 Full(WorkingState),
145}
146
147impl CandidateEvaluation {
148 #[inline]
154 pub(crate) fn penalized_objective(&self, firth_bias_reduction: bool, dev_scale: f64) -> f64 {
155 match self {
156 Self::Screen(s) => 0.5 * (dev_scale * s.deviance + s.penalty_term),
157 Self::Full(state) => {
158 let mut value = 0.5 * (dev_scale * state.deviance + state.penalty_term);
159 if firth_bias_reduction && let Some(j) = state.jeffreys_logdet() {
160 value -= j;
161 }
162 value
163 }
164 }
165 }
166
167 #[inline]
168 pub(crate) fn arithmetic_finite(&self) -> bool {
169 match self {
170 Self::Screen(s) => s.arithmetic_finite,
171 Self::Full(state) => state.gradient.iter().all(|g| g.is_finite()),
172 }
173 }
174
175 #[inline]
176 pub(crate) fn into_full(self) -> Option<WorkingState> {
177 match self {
178 Self::Full(state) => Some(state),
179 Self::Screen(_) => None,
180 }
181 }
182}
183
184#[derive(Clone, Debug, PartialEq, Eq)]
185pub(super) struct PirlsAcceptedStateCacheKey {
186 curvature: HessianCurvatureKind,
187 firth_active: bool,
188 beta_bits: Vec<u64>,
189 arrow_latent_bits: Option<Vec<u64>>,
190}
191
192impl PirlsAcceptedStateCacheKey {
193 pub(crate) fn requested(
194 beta: &Coefficients,
195 curvature: HessianCurvatureKind,
196 options: &WorkingModelPirlsOptions,
197 ) -> Self {
198 Self::new(beta, curvature, options.firth_bias_reduction, options)
199 }
200
201 pub(crate) fn accepted(
202 beta: &Coefficients,
203 state: &WorkingState,
204 options: &WorkingModelPirlsOptions,
205 ) -> Self {
206 Self::new(
207 beta,
208 state.hessian_curvature,
209 matches!(state.firth, FirthDiagnostics::Active { .. }),
210 options,
211 )
212 }
213
214 pub(crate) fn new(
215 beta: &Coefficients,
216 curvature: HessianCurvatureKind,
217 firth_active: bool,
218 options: &WorkingModelPirlsOptions,
219 ) -> Self {
220 let arrow_latent_bits = options.arrow_schur.as_ref().map(|arrow_cfg| {
221 arrow_cfg.snapshot_t.as_ref()()
222 .iter()
223 .map(|value| value.to_bits())
224 .collect()
225 });
226 Self {
227 curvature,
228 firth_active,
229 beta_bits: beta.as_ref().iter().map(|value| value.to_bits()).collect(),
230 arrow_latent_bits,
231 }
232 }
233}
234
235#[derive(Clone, Copy)]
237pub(crate) struct IntegratedWorkingInput<'a> {
238 pub quadctx: &'a crate::quadrature::QuadratureContext,
239 pub se: ArrayView1<'a, f64>,
240 pub mixture_link_state: Option<&'a MixtureLinkState>,
241 pub sas_link_state: Option<&'a SasLinkState>,
242}
243
244pub struct WorkingDerivativeBuffersMut<'a> {
245 pub(crate) c: &'a mut Array1<f64>,
246 pub(crate) d: &'a mut Array1<f64>,
247 pub(crate) dmu_deta: &'a mut Array1<f64>,
248 pub(crate) d2mu_deta2: &'a mut Array1<f64>,
249 pub(crate) d3mu_deta3: &'a mut Array1<f64>,
250}
251
252pub(super) struct WorkingSlices<'a> {
255 pub mu: &'a mut [f64],
256 pub weights: &'a mut [f64],
257 pub z: &'a mut [f64],
258}
259
260pub(super) struct WorkingDerivSlices<'a> {
263 pub c: &'a mut [f64],
264 pub d: &'a mut [f64],
265 pub dmu: &'a mut [f64],
266 pub d2: &'a mut [f64],
267 pub d3: &'a mut [f64],
268}
269
270#[inline]
275pub(super) fn working_slices<'a>(
276 mu: &'a mut Array1<f64>,
277 weights: &'a mut Array1<f64>,
278 z: &'a mut Array1<f64>,
279) -> WorkingSlices<'a> {
280 WorkingSlices {
281 mu: mu.as_slice_mut().expect("mu must be contiguous"),
282 weights: weights.as_slice_mut().expect("weights must be contiguous"),
283 z: z.as_slice_mut().expect("z must be contiguous"),
284 }
285}
286
287#[inline]
293pub(super) fn working_deriv_slices<'a>(
294 derivs: &'a mut WorkingDerivativeBuffersMut<'_>,
295) -> WorkingDerivSlices<'a> {
296 WorkingDerivSlices {
297 c: derivs.c.as_slice_mut().expect("c must be contiguous"),
298 d: derivs.d.as_slice_mut().expect("d must be contiguous"),
299 dmu: derivs
300 .dmu_deta
301 .as_slice_mut()
302 .expect("dmu_deta must be contiguous"),
303 d2: derivs
304 .d2mu_deta2
305 .as_slice_mut()
306 .expect("d2mu_deta2 must be contiguous"),
307 d3: derivs
308 .d3mu_deta3
309 .as_slice_mut()
310 .expect("d3mu_deta3 must be contiguous"),
311 }
312}
313
314#[derive(Clone, Copy)]
315pub(crate) struct WorkingBernoulliGeometry {
316 pub(crate) mu: f64,
317 pub(crate) weight: f64,
318 pub(crate) z: f64,
319 pub(crate) c: f64,
320 pub(crate) d: f64,
321}
322
323pub(crate) trait WorkingLikelihood {
329 fn irls_update(
330 &self,
331 y: ArrayView1<f64>,
332 eta: &Array1<f64>,
333 priorweights: ArrayView1<f64>,
334 mu: &mut Array1<f64>,
335 weights: &mut Array1<f64>,
336 z: &mut Array1<f64>,
337 integrated: Option<IntegratedWorkingInput<'_>>,
338 derivatives: Option<WorkingDerivativeBuffersMut<'_>>,
339 ) -> Result<(), EstimationError>;
340
341 fn loglik_deviance(
342 &self,
343 y: ArrayView1<f64>,
344 eta: &Array1<f64>,
345 inverse_link: &InverseLink,
346 priorweights: ArrayView1<f64>,
347 ) -> Result<f64, EstimationError>;
348}
349
350impl WorkingLikelihood for GlmLikelihoodSpec {
351 fn irls_update(
352 &self,
353 y: ArrayView1<f64>,
354 eta: &Array1<f64>,
355 priorweights: ArrayView1<f64>,
356 mu: &mut Array1<f64>,
357 weights: &mut Array1<f64>,
358 z: &mut Array1<f64>,
359 integrated: Option<IntegratedWorkingInput<'_>>,
360 derivatives: Option<WorkingDerivativeBuffersMut<'_>>,
361 ) -> Result<(), EstimationError> {
362 match (&self.spec.response, &self.spec.link, integrated.is_some()) {
363 (ResponseFamily::Binomial, _, true) => {
364 let integ = integrated.unwrap();
365 update_glmvectors_integrated_by_family(
366 integ.quadctx,
367 y,
368 eta,
369 integ.se,
370 &self.spec,
371 priorweights,
372 mu,
373 weights,
374 z,
375 derivatives,
376 integ.mixture_link_state,
377 integ.sas_link_state,
378 )?;
379 Ok(())
380 }
381 (ResponseFamily::Binomial, link, false) => {
382 if matches!(link, InverseLink::Mixture(_)) {
383 crate::bail_invalid_estim!(
384 "BinomialMixture IRLS update requires explicit mixture link state"
385 .to_string(),
386 );
387 }
388 update_glmvectors(
389 y,
390 eta,
391 &self.spec.link,
392 priorweights,
393 mu,
394 weights,
395 z,
396 derivatives,
397 )?;
398 Ok(())
399 }
400 (ResponseFamily::Gaussian, _, _) => {
401 let resolved_scale = self
402 .resolved_scale()
403 .map_err(|error| EstimationError::InvalidInput(error.to_string()))?;
404 update_glmvectors(
405 y,
406 eta,
407 &InverseLink::Standard(StandardLink::Identity),
408 priorweights,
409 mu,
410 weights,
411 z,
412 None,
413 )?;
414 if let gam_problem::ResolvedLikelihoodScale::FixedGaussian { phi } = resolved_scale
424 {
425 let phi = phi.value();
426 if phi != 1.0 {
427 let inv_phi = 1.0 / phi;
428 if !(inv_phi.is_finite() && inv_phi > 0.0) {
429 crate::bail_invalid_estim!(
430 "Gaussian reciprocal dispersion is not representable for phi={phi}: {inv_phi:?}"
431 );
432 }
433 weights.mapv_inplace(|w| w * inv_phi);
434 }
435 }
436 Ok(())
437 }
438 (ResponseFamily::Poisson, _, _) => {
439 write_poisson_log_working_state(y, eta, priorweights, mu, weights, z, derivatives)
440 }
441 (ResponseFamily::Tweedie { p }, _, _) => {
442 let p = *p;
443 write_tweedie_log_working_state(
444 y,
445 eta,
446 priorweights,
447 p,
448 self.resolved_tweedie_phi()
449 .map_err(|error| EstimationError::InvalidInput(error.to_string()))?,
450 mu,
451 weights,
452 z,
453 derivatives,
454 )?;
455 Ok(())
456 }
457 (ResponseFamily::NegativeBinomial { .. }, _, _) => {
458 let theta = self
459 .resolved_negbin_theta()
460 .map_err(|error| EstimationError::InvalidInput(error.to_string()))?;
461 write_negative_binomial_log_working_state(
462 y,
463 eta,
464 priorweights,
465 theta,
466 mu,
467 weights,
468 z,
469 derivatives,
470 )?;
471 Ok(())
472 }
473 (ResponseFamily::Beta { .. }, _, _) => {
474 let phi = self
475 .resolved_beta_precision()
476 .map_err(|error| EstimationError::InvalidInput(error.to_string()))?;
477 write_beta_logit_working_state(
478 y,
479 eta,
480 priorweights,
481 phi,
482 mu,
483 weights,
484 z,
485 derivatives,
486 )?;
487 Ok(())
488 }
489 (ResponseFamily::Gamma, _, _) => {
490 let shape = self
491 .resolved_gamma_shape()
492 .map_err(|error| EstimationError::InvalidInput(error.to_string()))?;
493 write_gamma_log_working_state(
494 y,
495 eta,
496 priorweights,
497 shape,
498 mu,
499 weights,
500 z,
501 derivatives,
502 )
503 }
504 (ResponseFamily::RoystonParmar, _, _) => Err(EstimationError::InvalidInput(
505 "RoystonParmar is survival-specific and not a GLM IRLS family".to_string(),
506 )),
507 }
508 }
509
510 fn loglik_deviance(
511 &self,
512 y: ArrayView1<f64>,
513 eta: &Array1<f64>,
514 inverse_link: &InverseLink,
515 priorweights: ArrayView1<f64>,
516 ) -> Result<f64, EstimationError> {
517 if matches!(self.spec.response, ResponseFamily::Tweedie { .. }) {
518 validate_tweedie_responses(&y, &priorweights)?;
519 }
520 calculate_deviance_from_eta(y, eta, self, inverse_link, priorweights)
521 }
522}