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 current_eta: &LinearPredictor,
32 curvature: HessianCurvatureKind,
33 ) -> Result<CandidateEvaluation, EstimationError> {
34 assert!(arr.iter().all(|v| !v.is_nan()));
35 if !current_eta.as_ref().iter().all(|v| v.is_finite()) {
40 crate::bail_invalid_estim!(
41 "PIRLS candidate screen requires a finite current linear predictor"
42 );
43 }
44 self.update_candidate(beta, curvature)
45 .map(CandidateEvaluation::Full)
46 }
47
48 fn supports_observed_information_curvature(&self) -> bool {
49 false
50 }
51
52 fn solve_unconstrained_direction(
57 &mut self,
58 beta: &Coefficients,
59 state: &WorkingState,
60 loop_lambda: f64,
61 lm_d2: &Array1<f64>,
62 regularized_hessian: &Array2<f64>,
63 direction_out: &mut Array1<f64>,
64 ) -> Result<(), EstimationError> {
65 if beta.as_ref().len() != state.gradient.len() {
66 crate::bail_invalid_estim!(
67 "PIRLS coefficient length {} does not match gradient length {}",
68 beta.as_ref().len(),
69 state.gradient.len()
70 );
71 }
72 if !(loop_lambda.is_finite() && loop_lambda >= 0.0) {
73 crate::bail_invalid_estim!(
74 "PIRLS LM damping must be finite and nonnegative, got {loop_lambda}"
75 );
76 }
77 if lm_d2.len() != state.gradient.len() {
78 crate::bail_invalid_estim!(
79 "PIRLS LM diagonal length {} does not match gradient length {}",
80 lm_d2.len(),
81 state.gradient.len()
82 );
83 }
84 let curvature = objective_curvature_for_direction(
85 regularized_hessian,
86 self.objective_hessian_matrix_correction(),
87 )?;
88 solve_newton_direction_dense(curvature.as_ref(), &state.gradient, direction_out)?;
89 Ok(())
90 }
91
92 fn exact_unconstrained_decrement_sq(
100 &mut self,
101 beta: &Coefficients,
102 state: &WorkingState,
103 ) -> Result<Option<f64>, EstimationError> {
104 if beta.as_ref().len() != state.gradient.len() {
105 crate::bail_invalid_estim!(
106 "PIRLS coefficient length {} does not match gradient length {}",
107 beta.as_ref().len(),
108 state.gradient.len()
109 );
110 }
111 Ok(None)
112 }
113
114 fn objective_hessian_quadratic_correction(
123 &self,
124 direction: &Array1<f64>,
125 ) -> Result<f64, EstimationError> {
126 assert!(array_is_finite(direction));
127 Ok(0.0)
128 }
129
130 fn objective_hessian_matrix_correction(&self) -> Option<&Array2<f64>> {
145 None
146 }
147
148 fn penalized_deviance_scale(&self) -> Result<f64, EstimationError> {
157 Ok(1.0)
158 }
159}
160
161#[derive(Debug, Clone)]
164pub struct CandidateScreen {
165 pub deviance: f64,
166 pub penalty_term: f64,
167 pub arithmetic_finite: bool,
168}
169
170pub enum CandidateEvaluation {
174 Screen(CandidateScreen),
175 Full(WorkingState),
176}
177
178impl CandidateEvaluation {
179 #[inline]
185 pub(crate) fn penalized_objective(&self, firth_bias_reduction: bool, dev_scale: f64) -> f64 {
186 match self {
187 Self::Screen(s) => 0.5 * (dev_scale * s.deviance + s.penalty_term),
188 Self::Full(state) => {
189 let mut value = 0.5 * (dev_scale * state.deviance + state.penalty_term);
190 if firth_bias_reduction && let Some(j) = state.jeffreys_logdet() {
191 value -= j;
192 }
193 value
194 }
195 }
196 }
197
198 #[inline]
199 pub(crate) fn arithmetic_finite(&self) -> bool {
200 match self {
201 Self::Screen(s) => s.arithmetic_finite,
202 Self::Full(state) => state.gradient.iter().all(|g| g.is_finite()),
203 }
204 }
205
206 #[inline]
207 pub(crate) fn into_full(self) -> Option<WorkingState> {
208 match self {
209 Self::Full(state) => Some(state),
210 Self::Screen(_) => None,
211 }
212 }
213}
214
215#[derive(Clone, Debug, PartialEq, Eq)]
216pub(super) struct PirlsAcceptedStateCacheKey {
217 curvature: HessianCurvatureKind,
218 firth_active: bool,
219 beta_bits: Vec<u64>,
220 arrow_latent_bits: Option<Vec<u64>>,
221}
222
223impl PirlsAcceptedStateCacheKey {
224 pub(crate) fn requested(
225 beta: &Coefficients,
226 curvature: HessianCurvatureKind,
227 options: &WorkingModelPirlsOptions,
228 ) -> Self {
229 Self::new(beta, curvature, options.firth_bias_reduction, options)
230 }
231
232 pub(crate) fn accepted(
233 beta: &Coefficients,
234 state: &WorkingState,
235 options: &WorkingModelPirlsOptions,
236 ) -> Self {
237 Self::new(
238 beta,
239 state.hessian_curvature,
240 matches!(state.firth, FirthDiagnostics::Active { .. }),
241 options,
242 )
243 }
244
245 pub(crate) fn new(
246 beta: &Coefficients,
247 curvature: HessianCurvatureKind,
248 firth_active: bool,
249 options: &WorkingModelPirlsOptions,
250 ) -> Self {
251 let arrow_latent_bits = options.arrow_schur.as_ref().map(|arrow_cfg| {
252 arrow_cfg.snapshot_t.as_ref()()
253 .iter()
254 .map(|value| value.to_bits())
255 .collect()
256 });
257 Self {
258 curvature,
259 firth_active,
260 beta_bits: beta.as_ref().iter().map(|value| value.to_bits()).collect(),
261 arrow_latent_bits,
262 }
263 }
264}
265
266#[derive(Clone, Copy)]
268pub(crate) struct IntegratedWorkingInput<'a> {
269 pub quadctx: &'a crate::quadrature::QuadratureContext,
270 pub se: ArrayView1<'a, f64>,
271 pub mixture_link_state: Option<&'a MixtureLinkState>,
272 pub sas_link_state: Option<&'a SasLinkState>,
273}
274
275pub struct WorkingDerivativeBuffersMut<'a> {
276 pub(crate) c: &'a mut Array1<f64>,
277 pub(crate) d: &'a mut Array1<f64>,
278 pub(crate) dmu_deta: &'a mut Array1<f64>,
279 pub(crate) d2mu_deta2: &'a mut Array1<f64>,
280 pub(crate) d3mu_deta3: &'a mut Array1<f64>,
281}
282
283pub(super) struct WorkingSlices<'a> {
286 pub mu: &'a mut [f64],
287 pub weights: &'a mut [f64],
288 pub z: &'a mut [f64],
289}
290
291pub(super) struct WorkingDerivSlices<'a> {
294 pub c: &'a mut [f64],
295 pub d: &'a mut [f64],
296 pub dmu: &'a mut [f64],
297 pub d2: &'a mut [f64],
298 pub d3: &'a mut [f64],
299}
300
301#[inline]
306pub(super) fn working_slices<'a>(
307 mu: &'a mut Array1<f64>,
308 weights: &'a mut Array1<f64>,
309 z: &'a mut Array1<f64>,
310) -> WorkingSlices<'a> {
311 WorkingSlices {
312 mu: mu.as_slice_mut().expect("mu must be contiguous"),
313 weights: weights.as_slice_mut().expect("weights must be contiguous"),
314 z: z.as_slice_mut().expect("z must be contiguous"),
315 }
316}
317
318#[inline]
324pub(super) fn working_deriv_slices<'a>(
325 derivs: &'a mut WorkingDerivativeBuffersMut<'_>,
326) -> WorkingDerivSlices<'a> {
327 WorkingDerivSlices {
328 c: derivs.c.as_slice_mut().expect("c must be contiguous"),
329 d: derivs.d.as_slice_mut().expect("d must be contiguous"),
330 dmu: derivs
331 .dmu_deta
332 .as_slice_mut()
333 .expect("dmu_deta must be contiguous"),
334 d2: derivs
335 .d2mu_deta2
336 .as_slice_mut()
337 .expect("d2mu_deta2 must be contiguous"),
338 d3: derivs
339 .d3mu_deta3
340 .as_slice_mut()
341 .expect("d3mu_deta3 must be contiguous"),
342 }
343}
344
345#[derive(Clone, Copy)]
346pub(crate) struct WorkingBernoulliGeometry {
347 pub(crate) mu: f64,
348 pub(crate) weight: f64,
349 pub(crate) z: f64,
350 pub(crate) c: f64,
351 pub(crate) d: f64,
352}
353
354pub(crate) trait WorkingLikelihood {
360 fn irls_update(
361 &self,
362 y: ArrayView1<f64>,
363 eta: &Array1<f64>,
364 priorweights: ArrayView1<f64>,
365 mu: &mut Array1<f64>,
366 weights: &mut Array1<f64>,
367 z: &mut Array1<f64>,
368 integrated: Option<IntegratedWorkingInput<'_>>,
369 derivatives: Option<WorkingDerivativeBuffersMut<'_>>,
370 ) -> Result<(), EstimationError>;
371
372 fn loglik_deviance(
373 &self,
374 y: ArrayView1<f64>,
375 eta: &Array1<f64>,
376 inverse_link: &InverseLink,
377 priorweights: ArrayView1<f64>,
378 ) -> Result<f64, EstimationError>;
379}
380
381impl WorkingLikelihood for GlmLikelihoodSpec {
382 fn irls_update(
383 &self,
384 y: ArrayView1<f64>,
385 eta: &Array1<f64>,
386 priorweights: ArrayView1<f64>,
387 mu: &mut Array1<f64>,
388 weights: &mut Array1<f64>,
389 z: &mut Array1<f64>,
390 integrated: Option<IntegratedWorkingInput<'_>>,
391 derivatives: Option<WorkingDerivativeBuffersMut<'_>>,
392 ) -> Result<(), EstimationError> {
393 match (&self.spec.response, &self.spec.link, integrated) {
394 (ResponseFamily::Binomial, _, Some(integ)) => {
395 update_glmvectors_integrated_by_family(
396 integ.quadctx,
397 y,
398 eta,
399 integ.se,
400 &self.spec,
401 priorweights,
402 mu,
403 weights,
404 z,
405 derivatives,
406 integ.mixture_link_state,
407 integ.sas_link_state,
408 )?;
409 Ok(())
410 }
411 (ResponseFamily::Binomial, link, None) => {
412 if matches!(link, InverseLink::Mixture(_)) {
413 crate::bail_invalid_estim!(
414 "BinomialMixture IRLS update requires explicit mixture link state"
415 .to_string(),
416 );
417 }
418 update_glmvectors(
419 y,
420 eta,
421 &self.spec.link,
422 priorweights,
423 mu,
424 weights,
425 z,
426 derivatives,
427 )?;
428 Ok(())
429 }
430 (ResponseFamily::Gaussian, _, _) => {
431 let resolved_scale = self
432 .resolved_scale()
433 .map_err(|error| EstimationError::InvalidInput(error.to_string()))?;
434 update_glmvectors(
435 y,
436 eta,
437 &InverseLink::Standard(StandardLink::Identity),
438 priorweights,
439 mu,
440 weights,
441 z,
442 None,
443 )?;
444 if let gam_problem::ResolvedLikelihoodScale::FixedGaussian { phi } = resolved_scale
454 {
455 let phi = phi.value();
456 if phi != 1.0 {
457 let inv_phi = 1.0 / phi;
458 if !(inv_phi.is_finite() && inv_phi > 0.0) {
459 crate::bail_invalid_estim!(
460 "Gaussian reciprocal dispersion is not representable for phi={phi}: {inv_phi:?}"
461 );
462 }
463 weights.mapv_inplace(|w| w * inv_phi);
464 }
465 }
466 Ok(())
467 }
468 (ResponseFamily::Poisson, _, _) => {
469 write_poisson_log_working_state(y, eta, priorweights, mu, weights, z, derivatives)
470 }
471 (ResponseFamily::Tweedie { p }, _, _) => {
472 let p = *p;
473 write_tweedie_log_working_state(
474 y,
475 eta,
476 priorweights,
477 p,
478 self.resolved_tweedie_phi()
479 .map_err(|error| EstimationError::InvalidInput(error.to_string()))?,
480 mu,
481 weights,
482 z,
483 derivatives,
484 )?;
485 Ok(())
486 }
487 (ResponseFamily::NegativeBinomial { .. }, _, _) => {
488 let theta = self
489 .resolved_negbin_theta()
490 .map_err(|error| EstimationError::InvalidInput(error.to_string()))?;
491 write_negative_binomial_log_working_state(
492 y,
493 eta,
494 priorweights,
495 theta,
496 mu,
497 weights,
498 z,
499 derivatives,
500 )?;
501 Ok(())
502 }
503 (ResponseFamily::Beta { .. }, _, _) => {
504 let phi = self
505 .resolved_beta_precision()
506 .map_err(|error| EstimationError::InvalidInput(error.to_string()))?;
507 write_beta_logit_working_state(
508 y,
509 eta,
510 priorweights,
511 phi,
512 mu,
513 weights,
514 z,
515 derivatives,
516 )?;
517 Ok(())
518 }
519 (ResponseFamily::Gamma, _, _) => {
520 let shape = self
521 .resolved_gamma_shape()
522 .map_err(|error| EstimationError::InvalidInput(error.to_string()))?;
523 write_gamma_log_working_state(
524 y,
525 eta,
526 priorweights,
527 shape,
528 mu,
529 weights,
530 z,
531 derivatives,
532 )
533 }
534 (ResponseFamily::RoystonParmar, _, _) => Err(EstimationError::InvalidInput(
535 "RoystonParmar is survival-specific and not a GLM IRLS family".to_string(),
536 )),
537 }
538 }
539
540 fn loglik_deviance(
541 &self,
542 y: ArrayView1<f64>,
543 eta: &Array1<f64>,
544 inverse_link: &InverseLink,
545 priorweights: ArrayView1<f64>,
546 ) -> Result<f64, EstimationError> {
547 if matches!(self.spec.response, ResponseFamily::Tweedie { .. }) {
548 validate_tweedie_responses(&y, &priorweights)?;
549 }
550 calculate_deviance_from_eta(y, eta, self, inverse_link, priorweights)
551 }
552}