1use super::*;
2
3#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum TimeBlockMonotonicity {
16 EnforcedByCoordinateCone,
21 EnforcedByRowConstraint,
28 StructuralISpline,
35}
36
37impl TimeBlockMonotonicity {
38 #[inline]
43 pub fn is_coordinate_cone(self) -> bool {
44 matches!(
45 self,
46 Self::EnforcedByCoordinateCone | Self::StructuralISpline
47 )
48 }
49
50 #[inline]
54 pub fn requires_row_constraints(self) -> bool {
55 matches!(self, Self::EnforcedByRowConstraint)
56 }
57}
58
59#[derive(Clone)]
60pub struct TimeBlockInput {
61 pub design_entry: DesignMatrix,
62 pub design_exit: DesignMatrix,
63 pub design_derivative_exit: DesignMatrix,
64 pub offset_entry: Array1<f64>,
65 pub offset_exit: Array1<f64>,
66 pub derivative_offset_exit: Array1<f64>,
67 pub time_monotonicity: TimeBlockMonotonicity,
71 pub penalties: Vec<Array2<f64>>,
72 pub nullspace_dims: Vec<usize>,
74 pub initial_log_lambdas: Option<Array1<f64>>,
75 pub initial_beta: Option<Array1<f64>>,
76}
77
78#[derive(Clone)]
90pub struct TimeDependentCovariateBlockInput {
91 pub design_covariates: DesignMatrix,
93 pub time_basis_entry: Array2<f64>,
95 pub time_basis_exit: Array2<f64>,
97 pub time_basis_derivative_exit: Array2<f64>,
99 pub penalties: Vec<PenaltyMatrix>,
101 pub initial_log_lambdas: Option<Array1<f64>>,
102 pub initial_beta: Option<Array1<f64>>,
103 pub offset: Array1<f64>,
104}
105
106#[derive(Clone)]
109pub enum CovariateBlockKind {
110 Static(ParameterBlockInput),
111 TimeVarying(TimeDependentCovariateBlockInput),
112}
113
114#[derive(Clone)]
115pub struct LinkWiggleBlockInput {
116 pub design: DesignMatrix,
117 pub knots: Array1<f64>,
118 pub degree: usize,
119 pub penalties: Vec<gam_terms::penalty_spec::PenaltySpec>,
120 pub nullspace_dims: Vec<usize>,
122 pub initial_log_lambdas: Option<Array1<f64>>,
123 pub initial_beta: Option<Array1<f64>>,
124}
125
126#[derive(Clone)]
127pub struct TimeWiggleBlockInput {
128 pub knots: Array1<f64>,
129 pub degree: usize,
130 pub ncols: usize,
131}
132
133#[derive(Clone)]
134pub(crate) struct SurvivalLocationScaleSpec {
135 pub age_entry: Array1<f64>,
136 pub age_exit: Array1<f64>,
137 pub event_target: Array1<f64>,
138 pub weights: Array1<f64>,
139 pub inverse_link: InverseLink,
140 pub derivative_guard: f64,
141 pub max_iter: usize,
142 pub tol: f64,
143 pub time_block: TimeBlockInput,
144 pub threshold_block: CovariateBlockKind,
145 pub log_sigma_block: CovariateBlockKind,
146 pub timewiggle_block: Option<TimeWiggleBlockInput>,
147 pub linkwiggle_block: Option<LinkWiggleBlockInput>,
148 pub cache_session: Option<std::sync::Arc<gam_runtime::warm_start::Session>>,
151 pub cache_mirror_sessions: Vec<std::sync::Arc<gam_runtime::warm_start::Session>>,
154}
155
156#[derive(Clone)]
157pub enum SurvivalCovariateTermBlockTemplate {
158 Static,
159 TimeVarying {
160 time_basis_entry: Array2<f64>,
161 time_basis_exit: Array2<f64>,
162 time_basis_derivative_exit: Array2<f64>,
163 time_penalties: Vec<Array2<f64>>,
164 },
165}
166
167#[derive(Clone)]
168pub struct SurvivalLocationScaleTermSpec {
169 pub age_entry: Array1<f64>,
170 pub age_exit: Array1<f64>,
171 pub event_target: Array1<f64>,
172 pub weights: Array1<f64>,
173 pub inverse_link: InverseLink,
174 pub derivative_guard: f64,
177 pub max_iter: usize,
178 pub tol: f64,
179 pub time_block: TimeBlockInput,
180 pub thresholdspec: TermCollectionSpec,
181 pub log_sigmaspec: TermCollectionSpec,
182 pub threshold_offset: Array1<f64>,
183 pub log_sigma_offset: Array1<f64>,
184 pub threshold_template: SurvivalCovariateTermBlockTemplate,
185 pub log_sigma_template: SurvivalCovariateTermBlockTemplate,
186 pub timewiggle_block: Option<TimeWiggleBlockInput>,
187 pub linkwiggle_block: Option<LinkWiggleBlockInput>,
188 pub initial_threshold_log_lambdas: Option<Array1<f64>>,
194 pub initial_log_sigma_log_lambdas: Option<Array1<f64>>,
197 pub cache_session: Option<std::sync::Arc<gam_runtime::warm_start::Session>>,
200 pub cache_mirror_sessions: Vec<std::sync::Arc<gam_runtime::warm_start::Session>>,
203}
204
205pub const DEFAULT_SURVIVAL_LOCATION_SCALE_DERIVATIVE_GUARD: f64 = 1e-6;
206
207pub struct SurvivalLocationScaleTermFitResult {
208 pub fit: UnifiedFitResult,
209 pub resolved_thresholdspec: TermCollectionSpec,
210 pub resolved_log_sigmaspec: TermCollectionSpec,
211 pub threshold_design: TermCollectionDesign,
212 pub log_sigma_design: TermCollectionDesign,
213 pub baseline_offset_residuals: OffsetChannelResiduals,
218 pub baseline_offset_curvatures: OffsetChannelCurvatures,
223 pub link_param_data_fit_gradient: Option<Array1<f64>>,
229}
230
231pub struct SurvivalLocationScaleFitResultParts {
234 pub beta_time: Array1<f64>,
235 pub beta_threshold: Array1<f64>,
236 pub beta_log_sigma: Array1<f64>,
237 pub beta_link_wiggle: Option<Array1<f64>>,
238 pub link_wiggle_knots: Option<Array1<f64>>,
239 pub link_wiggle_degree: Option<usize>,
240 pub lambdas_time: Array1<f64>,
241 pub lambdas_threshold: Array1<f64>,
242 pub lambdas_log_sigma: Array1<f64>,
243 pub lambdas_linkwiggle: Option<Array1<f64>>,
244 pub log_likelihood: f64,
245 pub reml_score: f64,
246 pub stable_penalty_term: f64,
247 pub penalized_objective: f64,
248 pub used_device: bool,
252 pub outer_iterations: usize,
253 pub outer_gradient_norm: Option<f64>,
256 pub outer_converged: bool,
257 pub covariance_conditional: Option<Array2<f64>>,
258 pub geometry: Option<FitGeometry>,
259 pub penalty_block_trace: Vec<f64>,
266 pub edf_by_block: Vec<f64>,
270}
271
272#[derive(Clone, Copy)]
273pub(crate) struct SurvivalLambdaLayout {
274 pub(crate) k_time: usize,
275 pub(crate) k_threshold: usize,
276 pub(crate) k_log_sigma: usize,
277 pub(crate) k_wiggle: usize,
278}
279
280impl SurvivalLambdaLayout {
281 pub(crate) fn new(
282 k_time: usize,
283 k_threshold: usize,
284 k_log_sigma: usize,
285 k_wiggle: usize,
286 ) -> Self {
287 Self {
288 k_time,
289 k_threshold,
290 k_log_sigma,
291 k_wiggle,
292 }
293 }
294
295 pub(crate) fn total(&self) -> usize {
296 self.k_time + self.k_threshold + self.k_log_sigma + self.k_wiggle
297 }
298
299 pub(crate) fn time_range(&self) -> std::ops::Range<usize> {
300 0..self.k_time
301 }
302
303 pub(crate) fn threshold_range(&self) -> std::ops::Range<usize> {
304 self.k_time..self.k_time + self.k_threshold
305 }
306
307 pub(crate) fn log_sigma_range(&self) -> std::ops::Range<usize> {
308 self.k_time + self.k_threshold..self.k_time + self.k_threshold + self.k_log_sigma
309 }
310
311 pub(crate) fn wiggle_range(&self) -> std::ops::Range<usize> {
312 self.k_time + self.k_threshold + self.k_log_sigma..self.total()
313 }
314
315 pub(crate) fn validate_rho(&self, rho: &Array1<f64>, label: &str) -> Result<(), String> {
316 if rho.len() != self.total() {
317 return Err(SurvivalLocationScaleError::DimensionMismatch {
318 reason: format!(
319 "{label} rho length mismatch: got {}, expected {}",
320 rho.len(),
321 self.total()
322 ),
323 }
324 .into());
325 }
326 Ok::<(), _>(())
327 }
328
329 pub(crate) fn time_from(&self, rho: &Array1<f64>) -> Array1<f64> {
330 let range = self.time_range();
331 rho.slice(s![range.start..range.end]).to_owned()
332 }
333
334 pub(crate) fn threshold_from(&self, rho: &Array1<f64>) -> Array1<f64> {
335 let range = self.threshold_range();
336 rho.slice(s![range.start..range.end]).to_owned()
337 }
338
339 pub(crate) fn log_sigma_from(&self, rho: &Array1<f64>) -> Array1<f64> {
340 let range = self.log_sigma_range();
341 rho.slice(s![range.start..range.end]).to_owned()
342 }
343
344 pub(crate) fn wiggle_from(&self, rho: &Array1<f64>) -> Option<Array1<f64>> {
345 if self.k_wiggle == 0 {
346 None
347 } else {
348 let range = self.wiggle_range();
349 Some(rho.slice(s![range.start..range.end]).to_owned())
350 }
351 }
352}
353
354pub fn survival_fit_from_parts(
356 parts: SurvivalLocationScaleFitResultParts,
357) -> Result<UnifiedFitResult, String> {
358 let SurvivalLocationScaleFitResultParts {
359 beta_time,
360 beta_threshold,
361 beta_log_sigma,
362 beta_link_wiggle,
363 link_wiggle_knots,
364 link_wiggle_degree,
365 lambdas_time,
366 lambdas_threshold,
367 lambdas_log_sigma,
368 lambdas_linkwiggle,
369 log_likelihood,
370 reml_score,
371 stable_penalty_term,
372 penalized_objective,
373 used_device,
374 outer_iterations,
375 outer_gradient_norm,
376 outer_converged,
377 covariance_conditional,
378 geometry,
379 penalty_block_trace,
380 edf_by_block,
381 } = parts;
382
383 validate_all_finite_estimation("survival_fit.beta_time", beta_time.iter().copied())
385 .map_err(|e| e.to_string())?;
386 validate_all_finite_estimation(
387 "survival_fit.beta_threshold",
388 beta_threshold.iter().copied(),
389 )
390 .map_err(|e| e.to_string())?;
391 validate_all_finite_estimation(
392 "survival_fit.beta_log_sigma",
393 beta_log_sigma.iter().copied(),
394 )
395 .map_err(|e| e.to_string())?;
396 if let Some(beta_wiggle) = beta_link_wiggle.as_ref() {
397 validate_all_finite_estimation(
398 "survival_fit.beta_link_wiggle",
399 beta_wiggle.iter().copied(),
400 )
401 .map_err(|e| e.to_string())?;
402 let knots = link_wiggle_knots.as_ref().ok_or_else(|| {
403 "survival_fit.beta_link_wiggle requires link_wiggle_knots".to_string()
404 })?;
405 validate_all_finite_estimation("survival_fit.link_wiggle_knots", knots.iter().copied())
406 .map_err(|e| e.to_string())?;
407 if link_wiggle_degree.is_none() {
408 return Err(SurvivalLocationScaleError::InvalidConfiguration {
409 reason: "survival_fit.beta_link_wiggle requires link_wiggle_degree".to_string(),
410 }
411 .into());
412 }
413 } else if link_wiggle_knots.is_some() || link_wiggle_degree.is_some() {
414 return Err(SurvivalLocationScaleError::InvalidConfiguration {
415 reason: "survival_fit link-wiggle metadata requires beta_link_wiggle coefficients"
416 .to_string(),
417 }
418 .into());
419 }
420 validate_all_finite_estimation("survival_fit.lambdas_time", lambdas_time.iter().copied())
421 .map_err(|e| e.to_string())?;
422 validate_all_finite_estimation(
423 "survival_fit.lambdas_threshold",
424 lambdas_threshold.iter().copied(),
425 )
426 .map_err(|e| e.to_string())?;
427 validate_all_finite_estimation(
428 "survival_fit.lambdas_log_sigma",
429 lambdas_log_sigma.iter().copied(),
430 )
431 .map_err(|e| e.to_string())?;
432 if lambdas_time.len() > beta_time.len() {
440 return Err(SurvivalLocationScaleError::DimensionMismatch {
441 reason: format!(
442 "survival_fit.lambdas_time has {} entries but beta_time has only {} \
443 coefficients; each lambda corresponds to a penalty term on this block",
444 lambdas_time.len(),
445 beta_time.len()
446 ),
447 }
448 .into());
449 }
450 if lambdas_threshold.len() > beta_threshold.len() {
451 return Err(SurvivalLocationScaleError::DimensionMismatch {
452 reason: format!(
453 "survival_fit.lambdas_threshold has {} entries but beta_threshold has only {} \
454 coefficients; each lambda corresponds to a penalty term on this block",
455 lambdas_threshold.len(),
456 beta_threshold.len()
457 ),
458 }
459 .into());
460 }
461 if lambdas_log_sigma.len() > beta_log_sigma.len() {
462 return Err(SurvivalLocationScaleError::DimensionMismatch {
463 reason: format!(
464 "survival_fit.lambdas_log_sigma has {} entries but beta_log_sigma has only {} \
465 coefficients; each lambda corresponds to a penalty term on this block",
466 lambdas_log_sigma.len(),
467 beta_log_sigma.len()
468 ),
469 }
470 .into());
471 }
472 if let Some(lambdas_wiggle) = lambdas_linkwiggle.as_ref() {
473 if beta_link_wiggle.is_none() {
474 return Err(SurvivalLocationScaleError::InvalidConfiguration {
475 reason: "survival_fit.lambdas_linkwiggle requires beta_link_wiggle".to_string(),
476 }
477 .into());
478 }
479 validate_all_finite_estimation(
480 "survival_fit.lambdas_linkwiggle",
481 lambdas_wiggle.iter().copied(),
482 )
483 .map_err(|e| e.to_string())?;
484 let wiggle_len = beta_link_wiggle.as_ref().map_or(0, |beta| beta.len());
485 if lambdas_wiggle.len() > wiggle_len {
486 return Err(SurvivalLocationScaleError::DimensionMismatch {
487 reason: format!(
488 "survival_fit.lambdas_linkwiggle has {} entries but beta_link_wiggle has \
489 only {} coefficients; each lambda corresponds to a penalty term on this block",
490 lambdas_wiggle.len(),
491 wiggle_len
492 ),
493 }
494 .into());
495 }
496 }
497 ensure_finite_scalar_estimation("survival_fit.log_likelihood", log_likelihood)
498 .map_err(|e| e.to_string())?;
499 ensure_finite_scalar_estimation("survival_fit.reml_score", reml_score)
500 .map_err(|e| e.to_string())?;
501 ensure_finite_scalar_estimation("survival_fit.stable_penalty_term", stable_penalty_term)
502 .map_err(|e| e.to_string())?;
503 ensure_finite_scalar_estimation("survival_fit.penalized_objective", penalized_objective)
504 .map_err(|e| e.to_string())?;
505 if let Some(g) = outer_gradient_norm {
506 ensure_finite_scalar_estimation("survival_fit.outer_gradient_norm", g)
507 .map_err(|e| e.to_string())?;
508 }
509
510 let total_p = beta_time.len()
511 + beta_threshold.len()
512 + beta_log_sigma.len()
513 + beta_link_wiggle.as_ref().map_or(0, |beta| beta.len());
514 if let Some(cov) = covariance_conditional.as_ref() {
515 validate_all_finite_estimation("survival_fit.covariance_conditional", cov.iter().copied())
516 .map_err(|e| e.to_string())?;
517 let (rows, cols) = cov.dim();
518 if rows != total_p || cols != total_p {
519 return Err(SurvivalLocationScaleError::InvalidConfiguration {
520 reason: format!(
521 "survival_fit.covariance_conditional must be {}x{}, got {}x{}",
522 total_p, total_p, rows, cols
523 ),
524 }
525 .into());
526 }
527 }
528 if let Some(geom) = geometry.as_ref() {
529 geom.validate_numeric_finiteness()
530 .map_err(|e| e.to_string())?;
531 let (rows, cols) = geom.penalized_hessian.dim();
532 if rows != total_p || cols != total_p {
533 return Err(SurvivalLocationScaleError::InvalidConfiguration {
534 reason: format!(
535 "survival_fit.geometry.penalized_hessian must be {}x{}, got {}x{}",
536 total_p, total_p, rows, cols
537 ),
538 }
539 .into());
540 }
541 if geom.working_weights.len() != geom.working_response.len() {
542 return Err(SurvivalLocationScaleError::DimensionMismatch {
543 reason: format!(
544 "survival_fit.geometry working length mismatch: weights={}, response={}",
545 geom.working_weights.len(),
546 geom.working_response.len()
547 ),
548 }
549 .into());
550 }
551 }
552
553 let n_time = lambdas_time.len();
567 let n_threshold = lambdas_threshold.len();
568 let n_log_sigma = lambdas_log_sigma.len();
569 let n_wiggle = lambdas_linkwiggle.as_ref().map_or(0, |l| l.len());
570 let total_penalties = n_time + n_threshold + n_log_sigma + n_wiggle;
571 let traces_available = penalty_block_trace.len() == total_penalties;
574 let block_trace_sum = |offset: usize, count: usize| -> f64 {
575 if traces_available && count > 0 {
576 penalty_block_trace[offset..offset + count].iter().sum()
577 } else {
578 0.0
579 }
580 };
581 let effective_edf = |ncoef: usize, trace_sum: f64| -> f64 {
582 (ncoef as f64 - trace_sum).clamp(0.0, ncoef as f64)
583 };
584 let edf_time = effective_edf(beta_time.len(), block_trace_sum(0, n_time));
585 let edf_threshold = effective_edf(beta_threshold.len(), block_trace_sum(n_time, n_threshold));
586 let edf_log_sigma = effective_edf(
587 beta_log_sigma.len(),
588 block_trace_sum(n_time + n_threshold, n_log_sigma),
589 );
590 let wiggle_len = beta_link_wiggle.as_ref().map_or(0, |beta| beta.len());
591 let edf_link_wiggle = effective_edf(
592 wiggle_len,
593 block_trace_sum(n_time + n_threshold + n_log_sigma, n_wiggle),
594 );
595 let edf_total = edf_time + edf_threshold + edf_log_sigma + edf_link_wiggle;
596
597 use crate::model_types::{BlockRole, FittedBlock, FittedLinkState, UnifiedFitResultParts};
598 let mut blocks = vec![
599 FittedBlock {
600 beta: beta_time.clone(),
601 role: BlockRole::Time,
602 edf: edf_time,
603 lambdas: lambdas_time.clone(),
604 },
605 FittedBlock {
606 beta: beta_threshold.clone(),
607 role: BlockRole::Threshold,
608 edf: edf_threshold,
609 lambdas: lambdas_threshold.clone(),
610 },
611 FittedBlock {
612 beta: beta_log_sigma.clone(),
613 role: BlockRole::Scale,
614 edf: edf_log_sigma,
615 lambdas: lambdas_log_sigma.clone(),
616 },
617 ];
618 if let Some(ref bw) = beta_link_wiggle {
619 blocks.push(FittedBlock {
620 beta: bw.clone(),
621 role: BlockRole::LinkWiggle,
622 edf: edf_link_wiggle,
623 lambdas: lambdas_linkwiggle
624 .clone()
625 .unwrap_or_else(|| Array1::zeros(0)),
626 });
627 }
628 let all_lambdas: Vec<f64> = blocks
629 .iter()
630 .flat_map(|b| b.lambdas.iter().copied())
631 .collect();
632 let log_lambdas = Array1::from_vec(
633 all_lambdas
634 .iter()
635 .map(|&v| if v > 0.0 { v.ln() } else { f64::NEG_INFINITY })
636 .collect(),
637 );
638 let inference_penalty_block_trace = if penalty_block_trace.len() == all_lambdas.len() {
643 penalty_block_trace.clone()
644 } else {
645 Vec::new()
646 };
647 let inference_edf_by_block = if edf_by_block.len() == all_lambdas.len() {
648 edf_by_block.clone()
649 } else {
650 Vec::new()
651 };
652 let inference = geometry
653 .as_ref()
654 .map(|geom| gam_solve::estimate::FitInference {
655 edf_by_block: inference_edf_by_block.clone(),
656 penalty_block_trace: inference_penalty_block_trace.clone(),
657 edf_total,
658 smoothing_correction: None,
659 penalized_hessian: geom.penalized_hessian.clone(),
660 working_weights: geom.working_weights.clone(),
661 working_response: geom.working_response.clone(),
662 reparam_qs: None,
663 dispersion: gam_solve::estimate::Dispersion::Known(1.0),
664 beta_covariance: covariance_conditional.clone().map(Into::into),
665 beta_standard_errors: covariance_conditional
666 .as_ref()
667 .map(|cov| Array1::from_iter(cov.diag().iter().map(|&v| v.max(0.0).sqrt()))),
668 beta_covariance_corrected: None,
669 beta_standard_errors_corrected: None,
670 beta_covariance_frequentist: None,
671 coefficient_influence: None,
672 weighted_gram: None,
673 bias_correction_beta: None,
674 bias_correction_jacobian: None,
675 });
676
677 let deviance = -2.0 * log_likelihood;
678 crate::model_types::UnifiedFitResult::try_from_parts(UnifiedFitResultParts {
679 blocks,
680 log_lambdas,
681 lambdas: Array1::from_vec(all_lambdas),
682 likelihood_family: None,
683 likelihood_scale: gam_problem::LikelihoodScaleMetadata::Unspecified,
684 log_likelihood_normalization: gam_problem::LogLikelihoodNormalization::UserProvided,
685 log_likelihood,
686 deviance,
687 reml_score,
688 stable_penalty_term,
689 penalized_objective,
690 used_device,
691 outer_iterations,
692 outer_converged,
693 outer_gradient_norm,
694 standard_deviation: 1.0,
695 covariance_conditional,
696 covariance_corrected: None,
697 inference,
698 fitted_link: FittedLinkState::Standard(None),
699 geometry,
700 block_states: Vec::new(),
701 pirls_status: gam_solve::pirls::PirlsStatus::Converged,
702 max_abs_eta: 0.0,
703 constraint_kkt: None,
704 artifacts: crate::model_types::FitArtifacts {
705 pirls: None,
706 null_space_logdet: None,
707 null_space_dim: None,
708 survival_link_wiggle_knots: link_wiggle_knots,
709 survival_link_wiggle_degree: link_wiggle_degree,
710 criterion_certificate: None,
711 rho_posterior_certificate: None,
712 rho_posterior_escalation: None,
713 rho_covariance: None,
714 joint_log_lambdas: None,
715 firth_bias_reduction: false,
719 },
720 inner_cycles: 0,
721 })
722 .map_err(|e| e.to_string())
723}
724
725#[derive(Clone)]
726pub struct SurvivalLocationScalePredictInput {
727 pub x_time_exit: Array2<f64>,
728 pub eta_time_offset_exit: Array1<f64>,
729 pub time_wiggle_knots: Option<Array1<f64>>,
730 pub time_wiggle_degree: Option<usize>,
731 pub time_wiggle_ncols: usize,
732 pub x_threshold: DesignMatrix,
733 pub eta_threshold_offset: Array1<f64>,
734 pub x_log_sigma: DesignMatrix,
735 pub eta_log_sigma_offset: Array1<f64>,
736 pub x_link_wiggle: Option<DesignMatrix>,
737 pub link_wiggle_knots: Option<Array1<f64>>,
738 pub link_wiggle_degree: Option<usize>,
739 pub inverse_link: InverseLink,
740}
741
742#[derive(Clone, Debug)]
743pub struct SurvivalLocationScalePredictResult {
744 pub eta: Array1<f64>,
745 pub survival_prob: Array1<f64>,
746}
747
748#[derive(Clone)]
749pub struct SurvivalLocationScalePredictUncertaintyResult {
750 pub eta: Array1<f64>,
751 pub survival_prob: Array1<f64>,
752 pub eta_standard_error: Array1<f64>,
753 pub response_standard_error: Option<Array1<f64>>,
754}
755
756pub(crate) fn initial_log_lambdas<T>(
757 penalties: &[T],
758 rho0: Option<Array1<f64>>,
759) -> Result<Array1<f64>, String> {
760 let k = penalties.len();
761 let rho = rho0.unwrap_or_else(|| Array1::zeros(k));
762 if rho.len() != k {
763 return Err(SurvivalLocationScaleError::DimensionMismatch {
764 reason: format!(
765 "initial_log_lambdas mismatch: got {}, expected {k}",
766 rho.len()
767 ),
768 }
769 .into());
770 }
771 Ok(rho)
772}