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(
586 beta_threshold.len(),
587 block_trace_sum(n_time, n_threshold),
588 );
589 let edf_log_sigma = effective_edf(
590 beta_log_sigma.len(),
591 block_trace_sum(n_time + n_threshold, n_log_sigma),
592 );
593 let wiggle_len = beta_link_wiggle.as_ref().map_or(0, |beta| beta.len());
594 let edf_link_wiggle = effective_edf(
595 wiggle_len,
596 block_trace_sum(n_time + n_threshold + n_log_sigma, n_wiggle),
597 );
598 let edf_total = edf_time + edf_threshold + edf_log_sigma + edf_link_wiggle;
599
600 use crate::model_types::{BlockRole, FittedBlock, FittedLinkState, UnifiedFitResultParts};
601 let mut blocks = vec![
602 FittedBlock {
603 beta: beta_time.clone(),
604 role: BlockRole::Time,
605 edf: edf_time,
606 lambdas: lambdas_time.clone(),
607 },
608 FittedBlock {
609 beta: beta_threshold.clone(),
610 role: BlockRole::Threshold,
611 edf: edf_threshold,
612 lambdas: lambdas_threshold.clone(),
613 },
614 FittedBlock {
615 beta: beta_log_sigma.clone(),
616 role: BlockRole::Scale,
617 edf: edf_log_sigma,
618 lambdas: lambdas_log_sigma.clone(),
619 },
620 ];
621 if let Some(ref bw) = beta_link_wiggle {
622 blocks.push(FittedBlock {
623 beta: bw.clone(),
624 role: BlockRole::LinkWiggle,
625 edf: edf_link_wiggle,
626 lambdas: lambdas_linkwiggle
627 .clone()
628 .unwrap_or_else(|| Array1::zeros(0)),
629 });
630 }
631 let all_lambdas: Vec<f64> = blocks
632 .iter()
633 .flat_map(|b| b.lambdas.iter().copied())
634 .collect();
635 let log_lambdas = Array1::from_vec(
636 all_lambdas
637 .iter()
638 .map(|&v| if v > 0.0 { v.ln() } else { f64::NEG_INFINITY })
639 .collect(),
640 );
641 let inference_penalty_block_trace = if penalty_block_trace.len() == all_lambdas.len() {
646 penalty_block_trace.clone()
647 } else {
648 Vec::new()
649 };
650 let inference_edf_by_block = if edf_by_block.len() == all_lambdas.len() {
651 edf_by_block.clone()
652 } else {
653 Vec::new()
654 };
655 let inference = geometry.as_ref().map(|geom| gam_solve::estimate::FitInference {
656 edf_by_block: inference_edf_by_block.clone(),
657 penalty_block_trace: inference_penalty_block_trace.clone(),
658 edf_total,
659 smoothing_correction: None,
660 penalized_hessian: geom.penalized_hessian.clone(),
661 working_weights: geom.working_weights.clone(),
662 working_response: geom.working_response.clone(),
663 reparam_qs: None,
664 dispersion: gam_solve::estimate::Dispersion::Known(1.0),
665 beta_covariance: covariance_conditional.clone().map(Into::into),
666 beta_standard_errors: covariance_conditional.as_ref().map(|cov| {
667 Array1::from_iter(cov.diag().iter().map(|&v| v.max(0.0).sqrt()))
668 }),
669 beta_covariance_corrected: None,
670 beta_standard_errors_corrected: None,
671 beta_covariance_frequentist: None,
672 coefficient_influence: None,
673 weighted_gram: None,
674 bias_correction_beta: 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 },
716 inner_cycles: 0,
717 })
718 .map_err(|e| e.to_string())
719}
720
721#[derive(Clone)]
722pub struct SurvivalLocationScalePredictInput {
723 pub x_time_exit: Array2<f64>,
724 pub eta_time_offset_exit: Array1<f64>,
725 pub time_wiggle_knots: Option<Array1<f64>>,
726 pub time_wiggle_degree: Option<usize>,
727 pub time_wiggle_ncols: usize,
728 pub x_threshold: DesignMatrix,
729 pub eta_threshold_offset: Array1<f64>,
730 pub x_log_sigma: DesignMatrix,
731 pub eta_log_sigma_offset: Array1<f64>,
732 pub x_link_wiggle: Option<DesignMatrix>,
733 pub link_wiggle_knots: Option<Array1<f64>>,
734 pub link_wiggle_degree: Option<usize>,
735 pub inverse_link: InverseLink,
736}
737
738#[derive(Clone, Debug)]
739pub struct SurvivalLocationScalePredictResult {
740 pub eta: Array1<f64>,
741 pub survival_prob: Array1<f64>,
742}
743
744#[derive(Clone)]
745pub struct SurvivalLocationScalePredictUncertaintyResult {
746 pub eta: Array1<f64>,
747 pub survival_prob: Array1<f64>,
748 pub eta_standard_error: Array1<f64>,
749 pub response_standard_error: Option<Array1<f64>>,
750}
751
752pub(crate) fn initial_log_lambdas<T>(
753 penalties: &[T],
754 rho0: Option<Array1<f64>>,
755) -> Result<Array1<f64>, String> {
756 let k = penalties.len();
757 let rho = rho0.unwrap_or_else(|| Array1::zeros(k));
758 if rho.len() != k {
759 return Err(SurvivalLocationScaleError::DimensionMismatch {
760 reason: format!(
761 "initial_log_lambdas mismatch: got {}, expected {k}",
762 rho.len()
763 ),
764 }
765 .into());
766 }
767 Ok(rho)
768}