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 penalized_deviance_scale(&self) -> f64 {
51 1.0
52 }
53}
54
55#[derive(Debug, Clone)]
58pub struct CandidateScreen {
59 pub deviance: f64,
60 pub penalty_term: f64,
61 pub arithmetic_finite: bool,
62}
63
64pub enum CandidateEvaluation {
68 Screen(CandidateScreen),
69 Full(WorkingState),
70}
71
72impl CandidateEvaluation {
73 #[inline]
79 pub(crate) fn penalized_objective(&self, firth_bias_reduction: bool, dev_scale: f64) -> f64 {
80 match self {
81 Self::Screen(s) => dev_scale * s.deviance + s.penalty_term,
82 Self::Full(state) => {
83 let mut value = dev_scale * state.deviance + state.penalty_term;
84 if firth_bias_reduction && let Some(j) = state.jeffreys_logdet() {
85 value -= 2.0 * j;
86 }
87 value
88 }
89 }
90 }
91
92 #[inline]
93 pub(crate) fn arithmetic_finite(&self) -> bool {
94 match self {
95 Self::Screen(s) => s.arithmetic_finite,
96 Self::Full(state) => state.gradient.iter().all(|g| g.is_finite()),
97 }
98 }
99
100 #[inline]
101 pub(crate) fn into_full(self) -> Option<WorkingState> {
102 match self {
103 Self::Full(state) => Some(state),
104 Self::Screen(_) => None,
105 }
106 }
107}
108
109#[derive(Clone, Debug, PartialEq, Eq)]
110pub(super) struct PirlsAcceptedStateCacheKey {
111 curvature: HessianCurvatureKind,
112 firth_active: bool,
113 beta_bits: Vec<u64>,
114 arrow_latent_bits: Option<Vec<u64>>,
115}
116
117impl PirlsAcceptedStateCacheKey {
118 pub(crate) fn requested(
119 beta: &Coefficients,
120 curvature: HessianCurvatureKind,
121 options: &WorkingModelPirlsOptions,
122 ) -> Self {
123 Self::new(beta, curvature, options.firth_bias_reduction, options)
124 }
125
126 pub(crate) fn accepted(
127 beta: &Coefficients,
128 state: &WorkingState,
129 options: &WorkingModelPirlsOptions,
130 ) -> Self {
131 Self::new(
132 beta,
133 state.hessian_curvature,
134 matches!(state.firth, FirthDiagnostics::Active { .. }),
135 options,
136 )
137 }
138
139 pub(crate) fn new(
140 beta: &Coefficients,
141 curvature: HessianCurvatureKind,
142 firth_active: bool,
143 options: &WorkingModelPirlsOptions,
144 ) -> Self {
145 let arrow_latent_bits = options.arrow_schur.as_ref().map(|arrow_cfg| {
146 arrow_cfg.snapshot_t.as_ref()()
147 .iter()
148 .map(|value| value.to_bits())
149 .collect()
150 });
151 Self {
152 curvature,
153 firth_active,
154 beta_bits: beta.as_ref().iter().map(|value| value.to_bits()).collect(),
155 arrow_latent_bits,
156 }
157 }
158}
159
160#[derive(Clone, Copy)]
162pub(crate) struct IntegratedWorkingInput<'a> {
163 pub quadctx: &'a crate::quadrature::QuadratureContext,
164 pub se: ArrayView1<'a, f64>,
165 pub mixture_link_state: Option<&'a MixtureLinkState>,
166 pub sas_link_state: Option<&'a SasLinkState>,
167}
168
169pub struct WorkingDerivativeBuffersMut<'a> {
170 pub(crate) c: &'a mut Array1<f64>,
171 pub(crate) d: &'a mut Array1<f64>,
172 pub(crate) dmu_deta: &'a mut Array1<f64>,
173 pub(crate) d2mu_deta2: &'a mut Array1<f64>,
174 pub(crate) d3mu_deta3: &'a mut Array1<f64>,
175}
176
177pub(super) struct WorkingSlices<'a> {
180 pub mu: &'a mut [f64],
181 pub weights: &'a mut [f64],
182 pub z: &'a mut [f64],
183}
184
185pub(super) struct WorkingDerivSlices<'a> {
188 pub c: &'a mut [f64],
189 pub d: &'a mut [f64],
190 pub dmu: &'a mut [f64],
191 pub d2: &'a mut [f64],
192 pub d3: &'a mut [f64],
193}
194
195#[inline]
200pub(super) fn working_slices<'a>(
201 mu: &'a mut Array1<f64>,
202 weights: &'a mut Array1<f64>,
203 z: &'a mut Array1<f64>,
204) -> WorkingSlices<'a> {
205 WorkingSlices {
206 mu: mu.as_slice_mut().expect("mu must be contiguous"),
207 weights: weights.as_slice_mut().expect("weights must be contiguous"),
208 z: z.as_slice_mut().expect("z must be contiguous"),
209 }
210}
211
212#[inline]
218pub(super) fn working_deriv_slices<'a>(
219 derivs: &'a mut WorkingDerivativeBuffersMut<'_>,
220) -> WorkingDerivSlices<'a> {
221 WorkingDerivSlices {
222 c: derivs.c.as_slice_mut().expect("c must be contiguous"),
223 d: derivs.d.as_slice_mut().expect("d must be contiguous"),
224 dmu: derivs
225 .dmu_deta
226 .as_slice_mut()
227 .expect("dmu_deta must be contiguous"),
228 d2: derivs
229 .d2mu_deta2
230 .as_slice_mut()
231 .expect("d2mu_deta2 must be contiguous"),
232 d3: derivs
233 .d3mu_deta3
234 .as_slice_mut()
235 .expect("d3mu_deta3 must be contiguous"),
236 }
237}
238
239#[derive(Clone, Copy)]
240pub(crate) struct WorkingBernoulliGeometry {
241 pub(crate) mu: f64,
242 pub(crate) weight: f64,
243 pub(crate) z: f64,
244 pub(crate) c: f64,
245 pub(crate) d: f64,
246}
247
248pub(crate) trait WorkingLikelihood {
254 fn irls_update(
255 &self,
256 y: ArrayView1<f64>,
257 eta: &Array1<f64>,
258 priorweights: ArrayView1<f64>,
259 mu: &mut Array1<f64>,
260 weights: &mut Array1<f64>,
261 z: &mut Array1<f64>,
262 integrated: Option<IntegratedWorkingInput<'_>>,
263 derivatives: Option<WorkingDerivativeBuffersMut<'_>>,
264 ) -> Result<(), EstimationError>;
265
266 fn loglik_deviance(
267 &self,
268 y: ArrayView1<f64>,
269 mu: &Array1<f64>,
270 priorweights: ArrayView1<f64>,
271 ) -> Result<f64, EstimationError>;
272}
273
274impl WorkingLikelihood for GlmLikelihoodSpec {
275 fn irls_update(
276 &self,
277 y: ArrayView1<f64>,
278 eta: &Array1<f64>,
279 priorweights: ArrayView1<f64>,
280 mu: &mut Array1<f64>,
281 weights: &mut Array1<f64>,
282 z: &mut Array1<f64>,
283 integrated: Option<IntegratedWorkingInput<'_>>,
284 derivatives: Option<WorkingDerivativeBuffersMut<'_>>,
285 ) -> Result<(), EstimationError> {
286 match (&self.spec.response, &self.spec.link, integrated.is_some()) {
287 (ResponseFamily::Binomial, _, true) => {
288 let integ = integrated.unwrap();
289 update_glmvectors_integrated_by_family(
290 integ.quadctx,
291 y,
292 eta,
293 integ.se,
294 &self.spec,
295 priorweights,
296 mu,
297 weights,
298 z,
299 derivatives,
300 integ.mixture_link_state,
301 integ.sas_link_state,
302 )?;
303 Ok(())
304 }
305 (ResponseFamily::Binomial, link, false) => {
306 if matches!(link, InverseLink::Mixture(_)) {
307 crate::bail_invalid_estim!(
308 "BinomialMixture IRLS update requires explicit mixture link state"
309 .to_string(),
310 );
311 }
312 update_glmvectors(
313 y,
314 eta,
315 &self.spec.link,
316 priorweights,
317 mu,
318 weights,
319 z,
320 derivatives,
321 )?;
322 Ok(())
323 }
324 (ResponseFamily::Gaussian, _, _) => {
325 update_glmvectors(
326 y,
327 eta,
328 &InverseLink::Standard(StandardLink::Identity),
329 priorweights,
330 mu,
331 weights,
332 z,
333 None,
334 )?;
335 if let Some(phi) = self.scale.fixed_phi() {
345 if !(phi.is_finite() && phi > 0.0) {
346 crate::bail_invalid_estim!(
347 "Gaussian fixed dispersion phi must be finite and positive (got {})",
348 phi
349 );
350 }
351 if phi != 1.0 {
352 let inv_phi = 1.0 / phi;
353 weights.mapv_inplace(|w| w * inv_phi);
354 }
355 }
356 Ok(())
357 }
358 (ResponseFamily::Poisson, _, _) => {
359 write_poisson_log_working_state(y, eta, priorweights, mu, weights, z, derivatives);
360 Ok(())
361 }
362 (ResponseFamily::Tweedie { p }, _, _) => {
363 let p = *p;
364 write_tweedie_log_working_state(
365 y,
366 eta,
367 priorweights,
368 p,
369 fixed_glm_dispersion(self),
370 mu,
371 weights,
372 z,
373 derivatives,
374 )?;
375 Ok(())
376 }
377 (ResponseFamily::NegativeBinomial { theta, .. }, _, _) => {
378 let theta = *theta;
379 write_negative_binomial_log_working_state(
380 y,
381 eta,
382 priorweights,
383 theta,
384 mu,
385 weights,
386 z,
387 derivatives,
388 )?;
389 Ok(())
390 }
391 (ResponseFamily::Beta { phi }, _, _) => {
392 let phi = *phi;
393 write_beta_logit_working_state(
394 y,
395 eta,
396 priorweights,
397 phi,
398 mu,
399 weights,
400 z,
401 derivatives,
402 )?;
403 Ok(())
404 }
405 (ResponseFamily::Gamma, _, _) => {
406 write_gamma_log_working_state(
407 y,
408 eta,
409 priorweights,
410 self.gamma_shape().unwrap_or(1.0),
411 mu,
412 weights,
413 z,
414 derivatives,
415 );
416 Ok(())
417 }
418 (ResponseFamily::RoystonParmar, _, _) => Err(EstimationError::InvalidInput(
419 "RoystonParmar is survival-specific and not a GLM IRLS family".to_string(),
420 )),
421 }
422 }
423
424 fn loglik_deviance(
425 &self,
426 y: ArrayView1<f64>,
427 mu: &Array1<f64>,
428 priorweights: ArrayView1<f64>,
429 ) -> Result<f64, EstimationError> {
430 if matches!(self.spec.response, ResponseFamily::Tweedie { .. }) {
431 validate_tweedie_responses(&y, &priorweights)?;
432 }
433 Ok(calculate_deviance(y, mu, self, priorweights))
434 }
435}