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 cache_mirror_sessions: Vec<std::sync::Arc<gam_runtime::warm_start::Session>>,
187}
188
189#[derive(Clone)]
190pub enum SurvivalCovariateTermBlockTemplate {
191 Static,
192 TimeVarying {
193 time_basis: SurvivalCovariateTimeBasis,
194 time_basis_entry: Array2<f64>,
195 time_basis_exit: Array2<f64>,
196 time_basis_derivative_exit: Array2<f64>,
197 time_penalties: Vec<Array2<f64>>,
198 },
199}
200
201impl SurvivalCovariateTermBlockTemplate {
202 pub fn resolved_time_basis(&self) -> Option<&SurvivalCovariateTimeBasis> {
203 match self {
204 Self::Static => None,
205 Self::TimeVarying { time_basis, .. } => Some(time_basis),
206 }
207 }
208}
209
210#[derive(Clone)]
211pub struct SurvivalLocationScaleTermSpec {
212 pub age_entry: Array1<f64>,
213 pub age_exit: Array1<f64>,
214 pub event_target: Array1<f64>,
215 pub weights: Array1<f64>,
216 pub inverse_link: InverseLink,
217 pub derivative_guard: f64,
220 pub max_iter: usize,
221 pub tol: f64,
222 pub time_block: TimeBlockInput,
223 pub thresholdspec: TermCollectionSpec,
224 pub log_sigmaspec: TermCollectionSpec,
225 pub threshold_offset: Array1<f64>,
226 pub log_sigma_offset: Array1<f64>,
227 pub threshold_template: SurvivalCovariateTermBlockTemplate,
228 pub log_sigma_template: SurvivalCovariateTermBlockTemplate,
229 pub timewiggle_block: Option<TimeWiggleBlockInput>,
230 pub linkwiggle_block: Option<LinkWiggleBlockInput>,
231 pub initial_threshold_log_lambdas: Option<Array1<f64>>,
237 pub initial_log_sigma_log_lambdas: Option<Array1<f64>>,
240 pub cache_session: Option<std::sync::Arc<gam_runtime::warm_start::Session>>,
243 pub cache_mirror_sessions: Vec<std::sync::Arc<gam_runtime::warm_start::Session>>,
246}
247
248pub const DEFAULT_SURVIVAL_LOCATION_SCALE_DERIVATIVE_GUARD: f64 = 1e-6;
249
250pub struct SurvivalLocationScaleTermFitResult {
251 pub fit: UnifiedFitResult,
252 pub time_parameterization: SurvivalLocationScaleTimeParameterization,
253 pub threshold_time_basis: Option<SurvivalCovariateTimeBasis>,
254 pub log_sigma_time_basis: Option<SurvivalCovariateTimeBasis>,
255 pub resolved_thresholdspec: TermCollectionSpec,
256 pub resolved_log_sigmaspec: TermCollectionSpec,
257 pub threshold_design: TermCollectionDesign,
258 pub log_sigma_design: TermCollectionDesign,
259 pub baseline_offset_residuals: OffsetChannelResiduals,
264 pub baseline_offset_curvatures: OffsetChannelCurvatures,
269 pub link_param_data_fit_gradient: Option<Array1<f64>>,
275}
276
277pub struct SurvivalLocationScaleFitResultParts {
280 pub beta_time: Array1<f64>,
281 pub beta_threshold: Array1<f64>,
282 pub beta_log_sigma: Array1<f64>,
283 pub beta_link_wiggle: Option<Array1<f64>>,
284 pub link_wiggle_knots: Option<Array1<f64>>,
285 pub link_wiggle_degree: Option<usize>,
286 pub lambdas_time: Array1<f64>,
287 pub lambdas_threshold: Array1<f64>,
288 pub lambdas_log_sigma: Array1<f64>,
289 pub lambdas_linkwiggle: Option<Array1<f64>>,
290 pub log_likelihood: f64,
291 pub reml_score: f64,
292 pub stable_penalty_term: f64,
293 pub penalized_objective: f64,
294 pub used_device: bool,
298 pub outer_iterations: usize,
299 pub outer_gradient_norm: Option<f64>,
302 pub criterion_certificate:
309 Option<gam_solve::rho_optimizer::OuterCriterionCertificate>,
310 pub outer_converged: bool,
311 pub covariance_conditional: Option<Array2<f64>>,
312 pub geometry: Option<FitGeometry>,
313 pub penalty_block_trace: Vec<f64>,
320 pub edf_by_block: Vec<f64>,
324}
325
326#[derive(Clone, Copy)]
327pub(crate) struct SurvivalLambdaLayout {
328 pub(crate) k_time: usize,
329 pub(crate) k_threshold: usize,
330 pub(crate) k_log_sigma: usize,
331 pub(crate) k_wiggle: usize,
332}
333
334impl SurvivalLambdaLayout {
335 pub(crate) fn new(
336 k_time: usize,
337 k_threshold: usize,
338 k_log_sigma: usize,
339 k_wiggle: usize,
340 ) -> Self {
341 Self {
342 k_time,
343 k_threshold,
344 k_log_sigma,
345 k_wiggle,
346 }
347 }
348
349 pub(crate) fn total(&self) -> usize {
350 self.k_time + self.k_threshold + self.k_log_sigma + self.k_wiggle
351 }
352
353 pub(crate) fn time_range(&self) -> std::ops::Range<usize> {
354 0..self.k_time
355 }
356
357 pub(crate) fn threshold_range(&self) -> std::ops::Range<usize> {
358 self.k_time..self.k_time + self.k_threshold
359 }
360
361 pub(crate) fn log_sigma_range(&self) -> std::ops::Range<usize> {
362 self.k_time + self.k_threshold..self.k_time + self.k_threshold + self.k_log_sigma
363 }
364
365 pub(crate) fn wiggle_range(&self) -> std::ops::Range<usize> {
366 self.k_time + self.k_threshold + self.k_log_sigma..self.total()
367 }
368
369 pub(crate) fn validate_rho(&self, rho: &Array1<f64>, label: &str) -> Result<(), String> {
370 if rho.len() != self.total() {
371 return Err(SurvivalLocationScaleError::DimensionMismatch {
372 reason: format!(
373 "{label} rho length mismatch: got {}, expected {}",
374 rho.len(),
375 self.total()
376 ),
377 }
378 .into());
379 }
380 Ok::<(), _>(())
381 }
382
383 pub(crate) fn time_from(&self, rho: &Array1<f64>) -> Array1<f64> {
384 let range = self.time_range();
385 rho.slice(s![range.start..range.end]).to_owned()
386 }
387
388 pub(crate) fn threshold_from(&self, rho: &Array1<f64>) -> Array1<f64> {
389 let range = self.threshold_range();
390 rho.slice(s![range.start..range.end]).to_owned()
391 }
392
393 pub(crate) fn log_sigma_from(&self, rho: &Array1<f64>) -> Array1<f64> {
394 let range = self.log_sigma_range();
395 rho.slice(s![range.start..range.end]).to_owned()
396 }
397
398 pub(crate) fn wiggle_from(&self, rho: &Array1<f64>) -> Option<Array1<f64>> {
399 if self.k_wiggle == 0 {
400 None
401 } else {
402 let range = self.wiggle_range();
403 Some(rho.slice(s![range.start..range.end]).to_owned())
404 }
405 }
406}
407
408pub fn survival_fit_from_parts(
410 parts: SurvivalLocationScaleFitResultParts,
411) -> Result<UnifiedFitResult, String> {
412 let SurvivalLocationScaleFitResultParts {
413 beta_time,
414 beta_threshold,
415 beta_log_sigma,
416 beta_link_wiggle,
417 link_wiggle_knots,
418 link_wiggle_degree,
419 lambdas_time,
420 lambdas_threshold,
421 lambdas_log_sigma,
422 lambdas_linkwiggle,
423 log_likelihood,
424 reml_score,
425 stable_penalty_term,
426 penalized_objective,
427 used_device,
428 outer_iterations,
429 outer_gradient_norm,
430 criterion_certificate,
431 outer_converged,
432 covariance_conditional,
433 geometry,
434 penalty_block_trace,
435 edf_by_block,
436 } = parts;
437
438 validate_all_finite_estimation("survival_fit.beta_time", beta_time.iter().copied())
440 .map_err(|e| e.to_string())?;
441 validate_all_finite_estimation(
442 "survival_fit.beta_threshold",
443 beta_threshold.iter().copied(),
444 )
445 .map_err(|e| e.to_string())?;
446 validate_all_finite_estimation(
447 "survival_fit.beta_log_sigma",
448 beta_log_sigma.iter().copied(),
449 )
450 .map_err(|e| e.to_string())?;
451 if let Some(beta_wiggle) = beta_link_wiggle.as_ref() {
452 validate_all_finite_estimation(
453 "survival_fit.beta_link_wiggle",
454 beta_wiggle.iter().copied(),
455 )
456 .map_err(|e| e.to_string())?;
457 let knots = link_wiggle_knots.as_ref().ok_or_else(|| {
458 "survival_fit.beta_link_wiggle requires link_wiggle_knots".to_string()
459 })?;
460 validate_all_finite_estimation("survival_fit.link_wiggle_knots", knots.iter().copied())
461 .map_err(|e| e.to_string())?;
462 if link_wiggle_degree.is_none() {
463 return Err(SurvivalLocationScaleError::InvalidConfiguration {
464 reason: "survival_fit.beta_link_wiggle requires link_wiggle_degree".to_string(),
465 }
466 .into());
467 }
468 } else if link_wiggle_knots.is_some() || link_wiggle_degree.is_some() {
469 return Err(SurvivalLocationScaleError::InvalidConfiguration {
470 reason: "survival_fit link-wiggle metadata requires beta_link_wiggle coefficients"
471 .to_string(),
472 }
473 .into());
474 }
475 validate_all_finite_estimation("survival_fit.lambdas_time", lambdas_time.iter().copied())
476 .map_err(|e| e.to_string())?;
477 validate_all_finite_estimation(
478 "survival_fit.lambdas_threshold",
479 lambdas_threshold.iter().copied(),
480 )
481 .map_err(|e| e.to_string())?;
482 validate_all_finite_estimation(
483 "survival_fit.lambdas_log_sigma",
484 lambdas_log_sigma.iter().copied(),
485 )
486 .map_err(|e| e.to_string())?;
487 if lambdas_time.len() > beta_time.len() {
495 return Err(SurvivalLocationScaleError::DimensionMismatch {
496 reason: format!(
497 "survival_fit.lambdas_time has {} entries but beta_time has only {} \
498 coefficients; each lambda corresponds to a penalty term on this block",
499 lambdas_time.len(),
500 beta_time.len()
501 ),
502 }
503 .into());
504 }
505 if lambdas_threshold.len() > beta_threshold.len() {
506 return Err(SurvivalLocationScaleError::DimensionMismatch {
507 reason: format!(
508 "survival_fit.lambdas_threshold has {} entries but beta_threshold has only {} \
509 coefficients; each lambda corresponds to a penalty term on this block",
510 lambdas_threshold.len(),
511 beta_threshold.len()
512 ),
513 }
514 .into());
515 }
516 if lambdas_log_sigma.len() > beta_log_sigma.len() {
517 return Err(SurvivalLocationScaleError::DimensionMismatch {
518 reason: format!(
519 "survival_fit.lambdas_log_sigma has {} entries but beta_log_sigma has only {} \
520 coefficients; each lambda corresponds to a penalty term on this block",
521 lambdas_log_sigma.len(),
522 beta_log_sigma.len()
523 ),
524 }
525 .into());
526 }
527 if let Some(lambdas_wiggle) = lambdas_linkwiggle.as_ref() {
528 if beta_link_wiggle.is_none() {
529 return Err(SurvivalLocationScaleError::InvalidConfiguration {
530 reason: "survival_fit.lambdas_linkwiggle requires beta_link_wiggle".to_string(),
531 }
532 .into());
533 }
534 validate_all_finite_estimation(
535 "survival_fit.lambdas_linkwiggle",
536 lambdas_wiggle.iter().copied(),
537 )
538 .map_err(|e| e.to_string())?;
539 let wiggle_len = beta_link_wiggle.as_ref().map_or(0, |beta| beta.len());
540 if lambdas_wiggle.len() > wiggle_len {
541 return Err(SurvivalLocationScaleError::DimensionMismatch {
542 reason: format!(
543 "survival_fit.lambdas_linkwiggle has {} entries but beta_link_wiggle has \
544 only {} coefficients; each lambda corresponds to a penalty term on this block",
545 lambdas_wiggle.len(),
546 wiggle_len
547 ),
548 }
549 .into());
550 }
551 }
552 ensure_finite_scalar_estimation("survival_fit.log_likelihood", log_likelihood)
553 .map_err(|e| e.to_string())?;
554 ensure_finite_scalar_estimation("survival_fit.reml_score", reml_score)
555 .map_err(|e| e.to_string())?;
556 ensure_finite_scalar_estimation("survival_fit.stable_penalty_term", stable_penalty_term)
557 .map_err(|e| e.to_string())?;
558 ensure_finite_scalar_estimation("survival_fit.penalized_objective", penalized_objective)
559 .map_err(|e| e.to_string())?;
560 if let Some(g) = outer_gradient_norm {
561 ensure_finite_scalar_estimation("survival_fit.outer_gradient_norm", g)
562 .map_err(|e| e.to_string())?;
563 }
564
565 let total_p = beta_time.len()
566 + beta_threshold.len()
567 + beta_log_sigma.len()
568 + beta_link_wiggle.as_ref().map_or(0, |beta| beta.len());
569 if let Some(cov) = covariance_conditional.as_ref() {
570 validate_all_finite_estimation("survival_fit.covariance_conditional", cov.iter().copied())
571 .map_err(|e| e.to_string())?;
572 let (rows, cols) = cov.dim();
573 if rows != total_p || cols != total_p {
574 return Err(SurvivalLocationScaleError::InvalidConfiguration {
575 reason: format!(
576 "survival_fit.covariance_conditional must be {}x{}, got {}x{}",
577 total_p, total_p, rows, cols
578 ),
579 }
580 .into());
581 }
582 }
583 if let Some(geom) = geometry.as_ref() {
584 geom.validate_numeric_finiteness()
585 .map_err(|e| e.to_string())?;
586 let mut saved_block_widths =
587 vec![beta_time.len(), beta_threshold.len(), beta_log_sigma.len()];
588 if let Some(beta) = beta_link_wiggle.as_ref() {
589 saved_block_widths.push(beta.len());
590 }
591 if geom.coefficient_gauge.raw_widths() != saved_block_widths {
592 return Err(SurvivalLocationScaleError::InvalidConfiguration {
593 reason: format!(
594 "survival_fit.geometry coefficient-gauge raw block widths {:?} do not match saved coefficient widths {:?}",
595 geom.coefficient_gauge.raw_widths(),
596 saved_block_widths,
597 ),
598 }
599 .into());
600 }
601 let active_p = geom.coefficient_gauge.reduced_total();
602 let (rows, cols) = geom.penalized_hessian.dim();
603 if rows != active_p || cols != active_p {
604 return Err(SurvivalLocationScaleError::InvalidConfiguration {
605 reason: format!(
606 "survival_fit.geometry active-coordinate penalized_hessian must be {}x{}, got {}x{}",
607 active_p, active_p, rows, cols
608 ),
609 }
610 .into());
611 }
612 }
613
614 let n_time = lambdas_time.len();
628 let n_threshold = lambdas_threshold.len();
629 let n_log_sigma = lambdas_log_sigma.len();
630 let n_wiggle = lambdas_linkwiggle.as_ref().map_or(0, |l| l.len());
631 let total_penalties = n_time + n_threshold + n_log_sigma + n_wiggle;
632 let traces_available = penalty_block_trace.len() == total_penalties;
635 let block_trace_sum = |offset: usize, count: usize| -> f64 {
636 if traces_available && count > 0 {
637 penalty_block_trace[offset..offset + count].iter().sum()
638 } else {
639 0.0
640 }
641 };
642 let effective_edf = |ncoef: usize, trace_sum: f64| -> f64 {
643 (ncoef as f64 - trace_sum).clamp(0.0, ncoef as f64)
644 };
645 let edf_time = effective_edf(beta_time.len(), block_trace_sum(0, n_time));
646 let edf_threshold = effective_edf(beta_threshold.len(), block_trace_sum(n_time, n_threshold));
647 let edf_log_sigma = effective_edf(
648 beta_log_sigma.len(),
649 block_trace_sum(n_time + n_threshold, n_log_sigma),
650 );
651 let wiggle_len = beta_link_wiggle.as_ref().map_or(0, |beta| beta.len());
652 let edf_link_wiggle = effective_edf(
653 wiggle_len,
654 block_trace_sum(n_time + n_threshold + n_log_sigma, n_wiggle),
655 );
656 let edf_total = edf_time + edf_threshold + edf_log_sigma + edf_link_wiggle;
657
658 use crate::model_types::{BlockRole, FittedBlock, FittedLinkState, UnifiedFitResultParts};
659 let mut blocks = vec![
660 FittedBlock {
661 beta: beta_time.clone(),
662 role: BlockRole::Time,
663 edf: edf_time,
664 lambdas: lambdas_time.clone(),
665 },
666 FittedBlock {
667 beta: beta_threshold.clone(),
668 role: BlockRole::Threshold,
669 edf: edf_threshold,
670 lambdas: lambdas_threshold.clone(),
671 },
672 FittedBlock {
673 beta: beta_log_sigma.clone(),
674 role: BlockRole::Scale,
675 edf: edf_log_sigma,
676 lambdas: lambdas_log_sigma.clone(),
677 },
678 ];
679 if let Some(ref bw) = beta_link_wiggle {
680 blocks.push(FittedBlock {
681 beta: bw.clone(),
682 role: BlockRole::LinkWiggle,
683 edf: edf_link_wiggle,
684 lambdas: lambdas_linkwiggle
685 .clone()
686 .unwrap_or_else(|| Array1::zeros(0)),
687 });
688 }
689 let all_lambdas: Vec<f64> = blocks
690 .iter()
691 .flat_map(|b| b.lambdas.iter().copied())
692 .collect();
693 let log_lambdas = Array1::from_vec(
694 all_lambdas
695 .iter()
696 .map(|&v| if v > 0.0 { v.ln() } else { f64::NEG_INFINITY })
697 .collect(),
698 );
699 let inference_penalty_block_trace = if penalty_block_trace.len() == all_lambdas.len() {
704 penalty_block_trace.clone()
705 } else {
706 Vec::new()
707 };
708 let inference_edf_by_block = if edf_by_block.len() == all_lambdas.len() {
709 edf_by_block.clone()
710 } else {
711 Vec::new()
712 };
713 let inference = geometry
714 .as_ref()
715 .map(|geom| gam_solve::estimate::FitInference {
716 edf_by_block: inference_edf_by_block.clone(),
717 penalty_block_trace: inference_penalty_block_trace.clone(),
718 edf_total,
719 smoothing_correction: None,
720 smoothing_correction_method: None,
721 smoothing_correction_first_order: None,
722 smoothing_correction_method_first_order: None,
723 penalized_hessian: geom.penalized_hessian.clone(),
724 reparam_qs: None,
725 dispersion: gam_solve::estimate::Dispersion::UNIT,
726 beta_covariance: covariance_conditional.clone().map(Into::into),
727 beta_standard_errors: covariance_conditional
728 .as_ref()
729 .map(|cov| Array1::from_iter(cov.diag().iter().map(|&v| v.max(0.0).sqrt()))),
730 beta_covariance_corrected: None,
731 beta_standard_errors_corrected: None,
732 beta_covariance_frequentist: None,
733 coefficient_influence: None,
734 weighted_gram: None,
735 bias_correction_beta: None,
736 bias_correction_jacobian: None,
737 });
738
739 let deviance = -2.0 * log_likelihood;
740 crate::model_types::UnifiedFitResult::try_from_parts(UnifiedFitResultParts {
741 blocks,
742 log_lambdas,
743 lambdas: Array1::from_vec(all_lambdas),
744 likelihood_family: None,
745 likelihood_scale: gam_problem::LikelihoodScaleMetadata::Unspecified,
746 log_likelihood_normalization: gam_problem::LogLikelihoodNormalization::UserProvided,
747 log_likelihood,
748 deviance,
749 reml_score,
750 stable_penalty_term,
751 penalized_objective,
752 used_device,
753 outer_iterations,
754 outer_converged,
755 outer_gradient_norm,
756 standard_deviation: 1.0,
757 covariance_conditional,
758 covariance_corrected: None,
759 inference,
760 fitted_link: FittedLinkState::Standard(None),
761 geometry,
762 block_states: Vec::new(),
763 pirls_status: gam_solve::pirls::PirlsStatus::Converged,
764 max_abs_eta: 0.0,
765 constraint_kkt: None,
766 artifacts: crate::model_types::FitArtifacts {
767 pirls: None,
768 null_space_logdet: None,
769 null_space_dim: None,
770 survival_link_wiggle_knots: link_wiggle_knots,
771 survival_link_wiggle_degree: link_wiggle_degree,
772 criterion_certificate,
773 rho_posterior_certificate: None,
774 rho_posterior_escalation: None,
775 rho_covariance: None,
776 joint_log_lambdas: None,
777 firth_bias_reduction: false,
781 },
782 inner_cycles: 0,
783 })
784 .map_err(|e| e.to_string())
785}
786
787#[derive(Clone)]
788pub struct SurvivalLocationScalePredictInput {
789 pub x_time_exit: Array2<f64>,
790 pub eta_time_offset_exit: Array1<f64>,
791 pub time_wiggle_knots: Option<Array1<f64>>,
792 pub time_wiggle_degree: Option<usize>,
793 pub time_wiggle_ncols: usize,
794 pub x_threshold: DesignMatrix,
795 pub eta_threshold_offset: Array1<f64>,
796 pub x_log_sigma: DesignMatrix,
797 pub eta_log_sigma_offset: Array1<f64>,
798 pub x_link_wiggle: Option<DesignMatrix>,
799 pub link_wiggle_knots: Option<Array1<f64>>,
800 pub link_wiggle_degree: Option<usize>,
801 pub inverse_link: InverseLink,
802}
803
804#[derive(Clone, Debug)]
805pub struct SurvivalLocationScalePredictResult {
806 pub eta: Array1<f64>,
807 pub survival_prob: Array1<f64>,
808}
809
810#[derive(Clone)]
811pub struct SurvivalLocationScalePredictUncertaintyResult {
812 pub eta: Array1<f64>,
813 pub survival_prob: Array1<f64>,
814 pub eta_standard_error: Array1<f64>,
815 pub response_standard_error: Option<Array1<f64>>,
816}
817
818pub(crate) fn initial_log_lambdas<T>(
819 penalties: &[T],
820 rho0: Option<Array1<f64>>,
821) -> Result<Array1<f64>, String> {
822 let k = penalties.len();
823 let rho = rho0.unwrap_or_else(|| Array1::zeros(k));
824 if rho.len() != k {
825 return Err(SurvivalLocationScaleError::DimensionMismatch {
826 reason: format!(
827 "initial_log_lambdas mismatch: got {}, expected {k}",
828 rho.len()
829 ),
830 }
831 .into());
832 }
833 Ok(rho)
834}