1use super::*;
2use serde::{Deserialize, Serialize};
3
4#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum SurvivalLocationScaleTimeParameterization {
10 MonotoneWarp,
12 ReducedParametricAft,
14}
15
16#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
19pub struct SurvivalCovariateTimeBasis {
20 pub degree: usize,
21 pub knots: Vec<f64>,
22}
23
24#[derive(Clone)]
29pub struct SurvivalCovariateReplayDesign {
30 pub design_exit: DesignMatrix,
31 pub design_entry: Option<DesignMatrix>,
32 pub design_derivative_exit: Option<DesignMatrix>,
33 pub offset: Array1<f64>,
34}
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub enum TimeBlockMonotonicity {
49 EnforcedByCoordinateCone,
54 EnforcedByRowConstraint,
61 StructuralISpline,
68}
69
70impl TimeBlockMonotonicity {
71 #[inline]
76 pub fn is_coordinate_cone(self) -> bool {
77 matches!(
78 self,
79 Self::EnforcedByCoordinateCone | Self::StructuralISpline
80 )
81 }
82
83 #[inline]
87 pub fn requires_row_constraints(self) -> bool {
88 matches!(self, Self::EnforcedByRowConstraint)
89 }
90}
91
92#[derive(Clone)]
93pub struct TimeBlockInput {
94 pub design_entry: DesignMatrix,
95 pub design_exit: DesignMatrix,
96 pub design_derivative_exit: DesignMatrix,
97 pub offset_entry: Array1<f64>,
98 pub offset_exit: Array1<f64>,
99 pub derivative_offset_exit: Array1<f64>,
100 pub time_monotonicity: TimeBlockMonotonicity,
104 pub penalties: Vec<Array2<f64>>,
105 pub nullspace_dims: Vec<usize>,
107 pub initial_log_lambdas: Option<Array1<f64>>,
108 pub initial_beta: Option<Array1<f64>>,
109}
110
111#[derive(Clone)]
123pub struct TimeDependentCovariateBlockInput {
124 pub design_covariates: DesignMatrix,
126 pub time_basis_entry: Array2<f64>,
128 pub time_basis_exit: Array2<f64>,
130 pub time_basis_derivative_exit: Array2<f64>,
132 pub penalties: Vec<PenaltyMatrix>,
134 pub initial_log_lambdas: Option<Array1<f64>>,
135 pub initial_beta: Option<Array1<f64>>,
136 pub offset: Array1<f64>,
137}
138
139#[derive(Clone)]
142pub enum CovariateBlockKind {
143 Static(ParameterBlockInput),
144 TimeVarying(TimeDependentCovariateBlockInput),
145}
146
147#[derive(Clone)]
148pub struct LinkWiggleBlockInput {
149 pub design: DesignMatrix,
150 pub knots: Array1<f64>,
151 pub degree: usize,
152 pub penalties: Vec<gam_terms::penalty_spec::PenaltySpec>,
153 pub nullspace_dims: Vec<usize>,
155 pub initial_log_lambdas: Option<Array1<f64>>,
156 pub initial_beta: Option<Array1<f64>>,
157}
158
159#[derive(Clone)]
160pub struct TimeWiggleBlockInput {
161 pub knots: Array1<f64>,
162 pub degree: usize,
163 pub ncols: usize,
164}
165
166#[derive(Clone)]
167pub(crate) struct SurvivalLocationScaleSpec {
168 pub age_entry: Array1<f64>,
169 pub age_exit: Array1<f64>,
170 pub event_target: Array1<f64>,
171 pub weights: Array1<f64>,
172 pub inverse_link: InverseLink,
173 pub derivative_guard: f64,
174 pub max_iter: usize,
175 pub tol: f64,
176 pub time_block: TimeBlockInput,
177 pub threshold_block: CovariateBlockKind,
178 pub log_sigma_block: CovariateBlockKind,
179 pub timewiggle_block: Option<TimeWiggleBlockInput>,
180 pub linkwiggle_block: Option<LinkWiggleBlockInput>,
181 pub cache_session: Option<std::sync::Arc<gam_runtime::warm_start::Session>>,
184 pub persistent_warm_start_store: Option<gam_runtime::warm_start::ConfiguredWarmStartStore>,
185 pub cache_mirror_sessions: Vec<std::sync::Arc<gam_runtime::warm_start::Session>>,
188}
189
190#[derive(Clone)]
191pub enum SurvivalCovariateTermBlockTemplate {
192 Static,
193 TimeVarying {
194 time_basis: SurvivalCovariateTimeBasis,
195 time_basis_entry: Array2<f64>,
196 time_basis_exit: Array2<f64>,
197 time_basis_derivative_exit: Array2<f64>,
198 time_penalties: Vec<Array2<f64>>,
199 },
200}
201
202impl SurvivalCovariateTermBlockTemplate {
203 pub fn resolved_time_basis(&self) -> Option<&SurvivalCovariateTimeBasis> {
204 match self {
205 Self::Static => None,
206 Self::TimeVarying { time_basis, .. } => Some(time_basis),
207 }
208 }
209}
210
211#[derive(Clone)]
212pub struct SurvivalLocationScaleTermSpec {
213 pub age_entry: Array1<f64>,
214 pub age_exit: Array1<f64>,
215 pub event_target: Array1<f64>,
216 pub weights: Array1<f64>,
217 pub inverse_link: InverseLink,
218 pub derivative_guard: f64,
221 pub max_iter: usize,
222 pub tol: f64,
223 pub time_block: TimeBlockInput,
224 pub thresholdspec: TermCollectionSpec,
225 pub log_sigmaspec: TermCollectionSpec,
226 pub threshold_offset: Array1<f64>,
227 pub log_sigma_offset: Array1<f64>,
228 pub threshold_template: SurvivalCovariateTermBlockTemplate,
229 pub log_sigma_template: SurvivalCovariateTermBlockTemplate,
230 pub timewiggle_block: Option<TimeWiggleBlockInput>,
231 pub linkwiggle_block: Option<LinkWiggleBlockInput>,
232 pub initial_threshold_log_lambdas: Option<Array1<f64>>,
238 pub initial_log_sigma_log_lambdas: Option<Array1<f64>>,
241 pub cache_session: Option<std::sync::Arc<gam_runtime::warm_start::Session>>,
244 pub persistent_warm_start_store: Option<gam_runtime::warm_start::ConfiguredWarmStartStore>,
245 pub cache_mirror_sessions: Vec<std::sync::Arc<gam_runtime::warm_start::Session>>,
248}
249
250pub const DEFAULT_SURVIVAL_LOCATION_SCALE_DERIVATIVE_GUARD: f64 = 1e-6;
251
252pub struct SurvivalLocationScaleTermFitResult {
253 pub fit: UnifiedFitResult,
254 pub time_parameterization: SurvivalLocationScaleTimeParameterization,
255 pub threshold_time_basis: Option<SurvivalCovariateTimeBasis>,
256 pub log_sigma_time_basis: Option<SurvivalCovariateTimeBasis>,
257 pub resolved_thresholdspec: TermCollectionSpec,
258 pub resolved_log_sigmaspec: TermCollectionSpec,
259 pub threshold_design: TermCollectionDesign,
260 pub log_sigma_design: TermCollectionDesign,
261 pub baseline_offset_residuals: OffsetChannelResiduals,
266 pub baseline_offset_curvatures: OffsetChannelCurvatures,
271 pub link_param_data_fit_gradient: Option<Array1<f64>>,
277}
278
279pub struct SurvivalLocationScaleFitResultParts {
282 pub training_sample_size: usize,
284 pub beta_time: Array1<f64>,
285 pub beta_threshold: Array1<f64>,
286 pub beta_log_sigma: Array1<f64>,
287 pub beta_link_wiggle: Option<Array1<f64>>,
288 pub link_wiggle_knots: Option<Array1<f64>>,
289 pub link_wiggle_degree: Option<usize>,
290 pub lambdas_time: Array1<f64>,
291 pub lambdas_threshold: Array1<f64>,
292 pub lambdas_log_sigma: Array1<f64>,
293 pub lambdas_linkwiggle: Option<Array1<f64>>,
294 pub log_likelihood: f64,
295 pub reml_score: Option<f64>,
300 pub stable_penalty_term: f64,
301 pub penalized_objective: Option<f64>,
303 pub used_device: bool,
307 pub outer_iterations: usize,
308 pub outer_gradient_norm: Option<f64>,
311 pub criterion_certificate:
318 Option<gam_solve::rho_optimizer::OuterCriterionCertificate>,
319 pub outer_converged: bool,
320 pub covariance_conditional: Option<Array2<f64>>,
321 pub covariance_corrected: Option<Array2<f64>>,
334 pub smoothing_correction:
338 Option<(Array2<f64>, gam_solve::model_types::SmoothingCorrectionMethod)>,
339 pub geometry: Option<FitGeometry>,
340 pub penalty_block_trace: Vec<f64>,
347 pub edf_by_block: Vec<f64>,
351}
352
353#[derive(Clone, Copy)]
354pub(crate) struct SurvivalLambdaLayout {
355 pub(crate) k_time: usize,
356 pub(crate) k_threshold: usize,
357 pub(crate) k_log_sigma: usize,
358 pub(crate) k_wiggle: usize,
359}
360
361impl SurvivalLambdaLayout {
362 pub(crate) fn new(
363 k_time: usize,
364 k_threshold: usize,
365 k_log_sigma: usize,
366 k_wiggle: usize,
367 ) -> Self {
368 Self {
369 k_time,
370 k_threshold,
371 k_log_sigma,
372 k_wiggle,
373 }
374 }
375
376 pub(crate) fn total(&self) -> usize {
377 self.k_time + self.k_threshold + self.k_log_sigma + self.k_wiggle
378 }
379
380 pub(crate) fn time_range(&self) -> std::ops::Range<usize> {
381 0..self.k_time
382 }
383
384 pub(crate) fn threshold_range(&self) -> std::ops::Range<usize> {
385 self.k_time..self.k_time + self.k_threshold
386 }
387
388 pub(crate) fn log_sigma_range(&self) -> std::ops::Range<usize> {
389 self.k_time + self.k_threshold..self.k_time + self.k_threshold + self.k_log_sigma
390 }
391
392 pub(crate) fn wiggle_range(&self) -> std::ops::Range<usize> {
393 self.k_time + self.k_threshold + self.k_log_sigma..self.total()
394 }
395
396 pub(crate) fn validate_rho(&self, rho: &Array1<f64>, label: &str) -> Result<(), String> {
397 if rho.len() != self.total() {
398 return Err(SurvivalLocationScaleError::DimensionMismatch {
399 reason: format!(
400 "{label} rho length mismatch: got {}, expected {}",
401 rho.len(),
402 self.total()
403 ),
404 }
405 .into());
406 }
407 Ok::<(), _>(())
408 }
409
410 pub(crate) fn time_from(&self, rho: &Array1<f64>) -> Array1<f64> {
411 let range = self.time_range();
412 rho.slice(s![range.start..range.end]).to_owned()
413 }
414
415 pub(crate) fn threshold_from(&self, rho: &Array1<f64>) -> Array1<f64> {
416 let range = self.threshold_range();
417 rho.slice(s![range.start..range.end]).to_owned()
418 }
419
420 pub(crate) fn log_sigma_from(&self, rho: &Array1<f64>) -> Array1<f64> {
421 let range = self.log_sigma_range();
422 rho.slice(s![range.start..range.end]).to_owned()
423 }
424
425 pub(crate) fn wiggle_from(&self, rho: &Array1<f64>) -> Option<Array1<f64>> {
426 if self.k_wiggle == 0 {
427 None
428 } else {
429 let range = self.wiggle_range();
430 Some(rho.slice(s![range.start..range.end]).to_owned())
431 }
432 }
433}
434
435pub fn survival_fit_from_parts(
437 parts: SurvivalLocationScaleFitResultParts,
438) -> Result<UnifiedFitResult, String> {
439 let SurvivalLocationScaleFitResultParts {
440 training_sample_size,
441 beta_time,
442 beta_threshold,
443 beta_log_sigma,
444 beta_link_wiggle,
445 link_wiggle_knots,
446 link_wiggle_degree,
447 lambdas_time,
448 lambdas_threshold,
449 lambdas_log_sigma,
450 lambdas_linkwiggle,
451 log_likelihood,
452 reml_score,
453 stable_penalty_term,
454 penalized_objective,
455 used_device,
456 outer_iterations,
457 outer_gradient_norm,
458 criterion_certificate,
459 outer_converged,
460 covariance_conditional,
461 covariance_corrected,
462 smoothing_correction,
463 geometry,
464 penalty_block_trace,
465 edf_by_block,
466 } = parts;
467
468 validate_all_finite_estimation("survival_fit.beta_time", beta_time.iter().copied())
470 .map_err(|e| e.to_string())?;
471 validate_all_finite_estimation(
472 "survival_fit.beta_threshold",
473 beta_threshold.iter().copied(),
474 )
475 .map_err(|e| e.to_string())?;
476 validate_all_finite_estimation(
477 "survival_fit.beta_log_sigma",
478 beta_log_sigma.iter().copied(),
479 )
480 .map_err(|e| e.to_string())?;
481 if let Some(beta_wiggle) = beta_link_wiggle.as_ref() {
482 validate_all_finite_estimation(
483 "survival_fit.beta_link_wiggle",
484 beta_wiggle.iter().copied(),
485 )
486 .map_err(|e| e.to_string())?;
487 let knots = link_wiggle_knots.as_ref().ok_or_else(|| {
488 "survival_fit.beta_link_wiggle requires link_wiggle_knots".to_string()
489 })?;
490 validate_all_finite_estimation("survival_fit.link_wiggle_knots", knots.iter().copied())
491 .map_err(|e| e.to_string())?;
492 if link_wiggle_degree.is_none() {
493 return Err(SurvivalLocationScaleError::InvalidConfiguration {
494 reason: "survival_fit.beta_link_wiggle requires link_wiggle_degree".to_string(),
495 }
496 .into());
497 }
498 } else if link_wiggle_knots.is_some() || link_wiggle_degree.is_some() {
499 return Err(SurvivalLocationScaleError::InvalidConfiguration {
500 reason: "survival_fit link-wiggle metadata requires beta_link_wiggle coefficients"
501 .to_string(),
502 }
503 .into());
504 }
505 validate_all_finite_estimation("survival_fit.lambdas_time", lambdas_time.iter().copied())
506 .map_err(|e| e.to_string())?;
507 validate_all_finite_estimation(
508 "survival_fit.lambdas_threshold",
509 lambdas_threshold.iter().copied(),
510 )
511 .map_err(|e| e.to_string())?;
512 validate_all_finite_estimation(
513 "survival_fit.lambdas_log_sigma",
514 lambdas_log_sigma.iter().copied(),
515 )
516 .map_err(|e| e.to_string())?;
517 if lambdas_time.len() > beta_time.len() {
525 return Err(SurvivalLocationScaleError::DimensionMismatch {
526 reason: format!(
527 "survival_fit.lambdas_time has {} entries but beta_time has only {} \
528 coefficients; each lambda corresponds to a penalty term on this block",
529 lambdas_time.len(),
530 beta_time.len()
531 ),
532 }
533 .into());
534 }
535 if lambdas_threshold.len() > beta_threshold.len() {
536 return Err(SurvivalLocationScaleError::DimensionMismatch {
537 reason: format!(
538 "survival_fit.lambdas_threshold has {} entries but beta_threshold has only {} \
539 coefficients; each lambda corresponds to a penalty term on this block",
540 lambdas_threshold.len(),
541 beta_threshold.len()
542 ),
543 }
544 .into());
545 }
546 if lambdas_log_sigma.len() > beta_log_sigma.len() {
547 return Err(SurvivalLocationScaleError::DimensionMismatch {
548 reason: format!(
549 "survival_fit.lambdas_log_sigma has {} entries but beta_log_sigma has only {} \
550 coefficients; each lambda corresponds to a penalty term on this block",
551 lambdas_log_sigma.len(),
552 beta_log_sigma.len()
553 ),
554 }
555 .into());
556 }
557 if let Some(lambdas_wiggle) = lambdas_linkwiggle.as_ref() {
558 if beta_link_wiggle.is_none() {
559 return Err(SurvivalLocationScaleError::InvalidConfiguration {
560 reason: "survival_fit.lambdas_linkwiggle requires beta_link_wiggle".to_string(),
561 }
562 .into());
563 }
564 validate_all_finite_estimation(
565 "survival_fit.lambdas_linkwiggle",
566 lambdas_wiggle.iter().copied(),
567 )
568 .map_err(|e| e.to_string())?;
569 let wiggle_len = beta_link_wiggle.as_ref().map_or(0, |beta| beta.len());
570 if lambdas_wiggle.len() > wiggle_len {
571 return Err(SurvivalLocationScaleError::DimensionMismatch {
572 reason: format!(
573 "survival_fit.lambdas_linkwiggle has {} entries but beta_link_wiggle has \
574 only {} coefficients; each lambda corresponds to a penalty term on this block",
575 lambdas_wiggle.len(),
576 wiggle_len
577 ),
578 }
579 .into());
580 }
581 }
582 ensure_finite_scalar_estimation("survival_fit.log_likelihood", log_likelihood)
583 .map_err(|e| e.to_string())?;
584 if let Some(reml_score) = reml_score {
585 ensure_finite_scalar_estimation("survival_fit.reml_score", reml_score)
586 .map_err(|e| e.to_string())?;
587 }
588 ensure_finite_scalar_estimation("survival_fit.stable_penalty_term", stable_penalty_term)
589 .map_err(|e| e.to_string())?;
590 if let Some(penalized_objective) = penalized_objective {
591 ensure_finite_scalar_estimation("survival_fit.penalized_objective", penalized_objective)
592 .map_err(|e| e.to_string())?;
593 }
594 if let Some(g) = outer_gradient_norm {
595 ensure_finite_scalar_estimation("survival_fit.outer_gradient_norm", g)
596 .map_err(|e| e.to_string())?;
597 }
598
599 let total_p = beta_time.len()
600 + beta_threshold.len()
601 + beta_log_sigma.len()
602 + beta_link_wiggle.as_ref().map_or(0, |beta| beta.len());
603 if let Some(cov) = covariance_conditional.as_ref() {
604 validate_all_finite_estimation("survival_fit.covariance_conditional", cov.iter().copied())
605 .map_err(|e| e.to_string())?;
606 let (rows, cols) = cov.dim();
607 if rows != total_p || cols != total_p {
608 return Err(SurvivalLocationScaleError::InvalidConfiguration {
609 reason: format!(
610 "survival_fit.covariance_conditional must be {}x{}, got {}x{}",
611 total_p, total_p, rows, cols
612 ),
613 }
614 .into());
615 }
616 }
617 if let Some(geom) = geometry.as_ref() {
618 geom.validate_numeric_finiteness()
619 .map_err(|e| e.to_string())?;
620 let mut saved_block_widths =
621 vec![beta_time.len(), beta_threshold.len(), beta_log_sigma.len()];
622 if let Some(beta) = beta_link_wiggle.as_ref() {
623 saved_block_widths.push(beta.len());
624 }
625 if geom.coefficient_gauge.raw_widths() != saved_block_widths {
626 return Err(SurvivalLocationScaleError::InvalidConfiguration {
627 reason: format!(
628 "survival_fit.geometry coefficient-gauge raw block widths {:?} do not match saved coefficient widths {:?}",
629 geom.coefficient_gauge.raw_widths(),
630 saved_block_widths,
631 ),
632 }
633 .into());
634 }
635 let active_p = geom.coefficient_gauge.reduced_total();
636 let (rows, cols) = geom.penalized_hessian.dim();
637 if rows != active_p || cols != active_p {
638 return Err(SurvivalLocationScaleError::InvalidConfiguration {
639 reason: format!(
640 "survival_fit.geometry active-coordinate penalized_hessian must be {}x{}, got {}x{}",
641 active_p, active_p, rows, cols
642 ),
643 }
644 .into());
645 }
646 }
647
648 let n_time = lambdas_time.len();
662 let n_threshold = lambdas_threshold.len();
663 let n_log_sigma = lambdas_log_sigma.len();
664 let n_wiggle = lambdas_linkwiggle.as_ref().map_or(0, |l| l.len());
665 let total_penalties = n_time + n_threshold + n_log_sigma + n_wiggle;
666 let traces_available = penalty_block_trace.len() == total_penalties;
669 let block_trace_sum = |offset: usize, count: usize| -> f64 {
670 if traces_available && count > 0 {
671 penalty_block_trace[offset..offset + count].iter().sum()
672 } else {
673 0.0
674 }
675 };
676 let effective_edf = |ncoef: usize, trace_sum: f64| -> f64 {
677 (ncoef as f64 - trace_sum).clamp(0.0, ncoef as f64)
678 };
679 let edf_time = effective_edf(beta_time.len(), block_trace_sum(0, n_time));
680 let edf_threshold = effective_edf(beta_threshold.len(), block_trace_sum(n_time, n_threshold));
681 let edf_log_sigma = effective_edf(
682 beta_log_sigma.len(),
683 block_trace_sum(n_time + n_threshold, n_log_sigma),
684 );
685 let wiggle_len = beta_link_wiggle.as_ref().map_or(0, |beta| beta.len());
686 let edf_link_wiggle = effective_edf(
687 wiggle_len,
688 block_trace_sum(n_time + n_threshold + n_log_sigma, n_wiggle),
689 );
690 let edf_total = edf_time + edf_threshold + edf_log_sigma + edf_link_wiggle;
691
692 use crate::model_types::{BlockRole, FittedBlock, FittedLinkState, UnifiedFitResultParts};
693 let mut blocks = vec![
694 FittedBlock {
695 beta: beta_time.clone(),
696 role: BlockRole::Time,
697 edf: edf_time,
698 lambdas: lambdas_time.clone(),
699 },
700 FittedBlock {
701 beta: beta_threshold.clone(),
702 role: BlockRole::Threshold,
703 edf: edf_threshold,
704 lambdas: lambdas_threshold.clone(),
705 },
706 FittedBlock {
707 beta: beta_log_sigma.clone(),
708 role: BlockRole::Scale,
709 edf: edf_log_sigma,
710 lambdas: lambdas_log_sigma.clone(),
711 },
712 ];
713 if let Some(ref bw) = beta_link_wiggle {
714 blocks.push(FittedBlock {
715 beta: bw.clone(),
716 role: BlockRole::LinkWiggle,
717 edf: edf_link_wiggle,
718 lambdas: lambdas_linkwiggle
719 .clone()
720 .unwrap_or_else(|| Array1::zeros(0)),
721 });
722 }
723 let all_lambdas: Vec<f64> = blocks
724 .iter()
725 .flat_map(|b| b.lambdas.iter().copied())
726 .collect();
727 let log_lambdas = Array1::from_vec(
728 all_lambdas
729 .iter()
730 .map(|&v| if v > 0.0 { v.ln() } else { f64::NEG_INFINITY })
731 .collect(),
732 );
733 let inference_penalty_block_trace = if penalty_block_trace.len() == all_lambdas.len() {
738 penalty_block_trace.clone()
739 } else {
740 Vec::new()
741 };
742 let inference_edf_by_block = if edf_by_block.len() == all_lambdas.len() {
743 edf_by_block.clone()
744 } else {
745 Vec::new()
746 };
747 let beta_standard_errors = covariance_conditional
753 .as_ref()
754 .map(gam_problem::se_from_covariance)
755 .transpose()
756 .map_err(|reason| {
757 format!("survival location-scale conditional standard errors are invalid: {reason}")
758 })?;
759 let lambda_is_fixed = outer_iterations == 0 && criterion_certificate.is_none();
780 let covariance_corrected = covariance_corrected.or_else(|| {
781 lambda_is_fixed
782 .then(|| covariance_conditional.clone())
783 .flatten()
784 });
785 let beta_standard_errors_corrected = covariance_corrected
789 .as_ref()
790 .map(gam_problem::se_from_covariance)
791 .transpose()
792 .map_err(|reason| {
793 format!("survival location-scale corrected standard errors are invalid: {reason}")
794 })?;
795 let (smoothing_correction_matrix, smoothing_correction_method) = match smoothing_correction {
796 Some((correction, method)) => (Some(correction), Some(method)),
797 None => (None, None),
798 };
799 let inference = geometry
800 .as_ref()
801 .map(|geom| gam_solve::estimate::FitInference {
802 edf_by_block: inference_edf_by_block.clone(),
803 penalty_block_trace: inference_penalty_block_trace.clone(),
804 edf_total,
805 smoothing_correction_first_order: smoothing_correction_matrix.clone(),
810 smoothing_correction_method_first_order: smoothing_correction_method,
811 smoothing_correction: smoothing_correction_matrix.clone(),
812 smoothing_correction_method,
813 penalized_hessian: geom.penalized_hessian.clone(),
814 reparam_qs: None,
815 dispersion: gam_solve::estimate::Dispersion::UNIT,
816 beta_covariance: covariance_conditional.clone().map(Into::into),
817 beta_standard_errors,
818 beta_covariance_corrected: covariance_corrected.clone(),
819 beta_standard_errors_corrected: beta_standard_errors_corrected.clone(),
820 beta_covariance_frequentist: None,
821 coefficient_influence: None,
822 weighted_gram: None,
823 bias_correction_beta: None,
824 bias_correction_jacobian: None,
825 });
826
827 let deviance = -2.0 * log_likelihood;
828 crate::model_types::UnifiedFitResult::try_from_parts(UnifiedFitResultParts {
829 blocks,
830 training_sample_size,
831 log_lambdas,
832 lambdas: Array1::from_vec(all_lambdas),
833 likelihood_family: None,
834 likelihood_scale: gam_problem::LikelihoodScaleMetadata::Unspecified,
835 log_likelihood_normalization: gam_problem::LogLikelihoodNormalization::UserProvided,
836 log_likelihood,
837 deviance,
838 reml_score,
839 stable_penalty_term,
840 penalized_objective,
841 used_device,
842 outer_iterations,
843 outer_converged,
844 outer_gradient_norm,
845 standard_deviation: 1.0,
846 covariance_conditional,
847 covariance_corrected,
848 inference,
849 fitted_link: FittedLinkState::Standard(None),
850 geometry,
851 block_states: Vec::new(),
852 pirls_status: gam_solve::pirls::PirlsStatus::Converged,
853 max_abs_eta: 0.0,
854 constraint_kkt: None,
855 artifacts: crate::model_types::FitArtifacts {
856 pirls: None,
857 null_space_logdet: None,
858 null_space_dim: None,
859 survival_link_wiggle_knots: link_wiggle_knots,
860 survival_link_wiggle_degree: link_wiggle_degree,
861 criterion_certificate,
862 rho_posterior_certificate: None,
863 rho_posterior_escalation: None,
864 rho_covariance: None,
865 joint_log_lambdas: None,
866 firth_bias_reduction: false,
870 covariance_declined: None,
873 },
874 inner_cycles: 0,
875 })
876 .map_err(|e| e.to_string())
877}
878
879#[derive(Clone)]
880pub struct SurvivalLocationScalePredictInput {
881 pub x_time_exit: Array2<f64>,
882 pub eta_time_offset_exit: Array1<f64>,
883 pub time_wiggle_knots: Option<Array1<f64>>,
884 pub time_wiggle_degree: Option<usize>,
885 pub time_wiggle_ncols: usize,
886 pub x_threshold: DesignMatrix,
887 pub eta_threshold_offset: Array1<f64>,
888 pub x_log_sigma: DesignMatrix,
889 pub eta_log_sigma_offset: Array1<f64>,
890 pub x_link_wiggle: Option<DesignMatrix>,
891 pub link_wiggle_knots: Option<Array1<f64>>,
892 pub link_wiggle_degree: Option<usize>,
893 pub inverse_link: InverseLink,
894}
895
896#[derive(Clone, Debug)]
897pub struct SurvivalLocationScalePredictResult {
898 pub eta: Array1<f64>,
899 pub survival_prob: Array1<f64>,
900}
901
902#[derive(Clone)]
903pub struct SurvivalLocationScalePredictUncertaintyResult {
904 pub eta: Array1<f64>,
905 pub survival_prob: Array1<f64>,
906 pub eta_standard_error: Array1<f64>,
907 pub response_standard_error: Option<Array1<f64>>,
908}
909
910pub(crate) fn initial_log_lambdas<T>(
911 penalties: &[T],
912 rho0: Option<Array1<f64>>,
913) -> Result<Array1<f64>, String> {
914 let k = penalties.len();
915 let rho = rho0.unwrap_or_else(|| Array1::zeros(k));
916 if rho.len() != k {
917 return Err(SurvivalLocationScaleError::DimensionMismatch {
918 reason: format!(
919 "initial_log_lambdas mismatch: got {}, expected {k}",
920 rho.len()
921 ),
922 }
923 .into());
924 }
925 Ok(rho)
926}