1use super::*;
2
3pub(crate) fn beta_bits_match(cached: &Array1<f64>, candidate: &Array1<f64>) -> bool {
4 cached.len() == candidate.len()
5 && cached
6 .iter()
7 .zip(candidate.iter())
8 .all(|(&left, &right)| left.to_bits() == right.to_bits())
9}
10
11#[derive(Clone, Debug)]
14pub struct TransformationWarmStart {
15 pub location: Array1<f64>,
17 pub scale: Array1<f64>,
19}
20
21#[derive(Clone)]
32pub struct TransformationNormalFamily {
33 pub(crate) x_val_kron: KroneckerDesign,
37 pub(crate) x_deriv_kron: KroneckerDesign,
39 pub(crate) response_val_basis: Array2<f64>,
42 pub(crate) response_lower_basis: Array1<f64>,
44 pub(crate) response_upper_basis: Array1<f64>,
46 pub(crate) response_deriv_basis: Array2<f64>,
48
49 pub(crate) covariate_design: DesignMatrix,
52 pub(crate) covariate_dense_cache: Arc<Mutex<Option<Arc<Array2<f64>>>>>,
59 pub(crate) weights: Arc<Array1<f64>>,
61 pub(crate) offset: Arc<Array1<f64>>,
63 pub(crate) tensor_penalties: Vec<PenaltyMatrix>,
65 pub(crate) tensor_penalty_layout: CtnTensorPenaltyLayout,
70
71 pub(crate) initial_beta: Array1<f64>,
73
74 pub(crate) block_name: String,
76
77 pub(crate) response_knots: Array1<f64>,
79 pub(crate) response_transform: Array2<f64>,
80 pub(crate) response_degree: usize,
81 pub(crate) response_median: f64,
82 pub(crate) response_floor_offset: Arc<Array1<f64>>,
83 pub(crate) response_lower_floor_offset: f64,
84 pub(crate) response_upper_floor_offset: f64,
85
86 pub(crate) row_quantity_cache: Arc<Mutex<Option<TransformationNormalRowQuantityCache>>>,
94 pub(crate) outer_subsample_weights: Option<Arc<Array1<f64>>>,
109}
110
111#[derive(Clone)]
112pub(crate) struct TransformationNormalRowQuantityCache {
113 pub(crate) beta: Arc<Array1<f64>>,
114 pub(crate) alpha: Arc<Array2<f64>>,
116 pub(crate) h: Arc<Array1<f64>>,
117 pub(crate) h_prime: Arc<Array1<f64>>,
118 pub(crate) h_lower: Arc<Array1<f64>>,
119 pub(crate) h_upper: Arc<Array1<f64>>,
120 pub(crate) endpoint_q: Arc<Vec<LogNormalCdfDiffDerivatives>>,
121 pub(crate) log_likelihood: f64,
122}
123
124#[derive(Debug)]
125pub(crate) struct TransformationNormalRowDerived {
126 pub(crate) log_likelihood: f64,
127 pub(crate) endpoint_q: Vec<LogNormalCdfDiffDerivatives>,
128}
129
130impl TransformationNormalRowQuantityCache {
131 pub(crate) fn matches_beta(&self, beta: &Array1<f64>) -> bool {
132 beta_bits_match(&self.beta, beta)
133 }
134}
135
136pub(crate) fn build_transformation_row_derived(
137 h: &Array1<f64>,
138 h_prime: &Array1<f64>,
139 h_lower: &Array1<f64>,
140 h_upper: &Array1<f64>,
141 weights: &Array1<f64>,
142) -> Result<TransformationNormalRowDerived, String> {
143 let n = h_prime.len();
144 assert_eq!(h.len(), n);
145 assert_eq!(h_lower.len(), n);
146 assert_eq!(h_upper.len(), n);
147 assert_eq!(weights.len(), n);
148
149 if let Some((i, value)) = h
150 .iter()
151 .copied()
152 .enumerate()
153 .find(|(_, value)| !value.is_finite())
154 {
155 return Err(TransformationNormalError::NonFinite {
156 reason: format!(
157 "TransformationNormalFamily row_quantities: h[{i}] = {value} is not finite"
158 ),
159 }
160 .into());
161 }
162 if let Some((i, value)) = weights
163 .iter()
164 .copied()
165 .enumerate()
166 .find(|(_, value)| !value.is_finite())
167 {
168 return Err(TransformationNormalError::NonFinite {
169 reason: format!(
170 "TransformationNormalFamily row_quantities: weight[{i}] = {value} is not finite"
171 ),
172 }
173 .into());
174 }
175
176 use rayon::iter::{IntoParallelIterator, ParallelIterator};
186 let rows: Vec<(f64, LogNormalCdfDiffDerivatives)> = (0..n)
187 .into_par_iter()
188 .map(|i| -> Result<(f64, LogNormalCdfDiffDerivatives), String> {
189 let hp = h_prime[i];
190 let inv_h_prime = 1.0 / hp;
191 let inv_h_prime_sq = inv_h_prime * inv_h_prime;
192 let inv_h_prime_cu = inv_h_prime_sq * inv_h_prime;
193 let inv_h_prime_qu = inv_h_prime_sq * inv_h_prime_sq;
194 let w_i = weights[i];
195 let h_i = h[i];
196 let weighted_h = w_i * h_i;
197 let weighted_inv_h_prime = w_i * inv_h_prime;
198 let weighted_inv_h_prime_sq = w_i * inv_h_prime_sq;
199 let q = log_normal_cdf_diff_derivatives(h_upper[i], h_lower[i]).map_err(|e| {
200 format!("TransformationNormalFamily row_quantities: row {i} invalid endpoint normalizer: {e}")
201 })?;
202 let log_z = q.log_z;
203 let row_ll = w_i
209 * (-0.5 * h_i * h_i - 0.5 * (2.0 * std::f64::consts::PI).ln() + hp.ln() - log_z);
210 if !(inv_h_prime.is_finite()
214 && inv_h_prime_sq.is_finite()
215 && inv_h_prime_cu.is_finite()
216 && inv_h_prime_qu.is_finite()
217 && weighted_h.is_finite()
218 && weighted_inv_h_prime.is_finite()
219 && weighted_inv_h_prime_sq.is_finite()
220 && log_z.is_finite())
221 {
222 let derived_values = [
223 ("1/h'", inv_h_prime),
224 ("1/h'^2", inv_h_prime_sq),
225 ("1/h'^3", inv_h_prime_cu),
226 ("1/h'^4", inv_h_prime_qu),
227 ("w*h", weighted_h),
228 ("w/h'", weighted_inv_h_prime),
229 ("w/h'^2", weighted_inv_h_prime_sq),
230 ("log normalizer", log_z),
231 ];
232 for (name, value) in derived_values {
233 if !value.is_finite() {
234 return Err(TransformationNormalError::NonFinite { reason: format!(
235 "TransformationNormalFamily row_quantities: {name} at row {i} is not finite ({value}); h'={hp} is outside the finite exact-derivative range",
236 ) }.into());
237 }
238 }
239 return Err(TransformationNormalError::NonFinite { reason: format!(
240 "TransformationNormalFamily row_quantities: row {i} entered non-finite branch but no named field was non-finite; h'={hp}",
241 ) }.into());
242 }
243 Ok((row_ll, q))
244 })
245 .collect::<Result<Vec<_>, _>>()?;
246
247 let mut log_likelihood = 0.0;
253 let mut endpoint_q = Vec::with_capacity(n);
254 for (row_ll, q) in rows {
255 log_likelihood += row_ll;
256 endpoint_q.push(q);
257 }
258 if !log_likelihood.is_finite() {
259 return Err(TransformationNormalError::NonFinite { reason: format!(
260 "TransformationNormalFamily row_quantities: log-likelihood is not finite ({log_likelihood})"
261 ) }.into());
262 }
263
264 Ok(TransformationNormalRowDerived {
265 log_likelihood,
266 endpoint_q,
267 })
268}
269
270impl TransformationNormalFamily {
271 pub fn new(
282 response: &Array1<f64>,
283 weights: &Array1<f64>,
284 offset: &Array1<f64>,
285 covariate_design: DesignMatrix,
286 covariate_penalties: Vec<PenaltyMatrix>,
287 config: &TransformationNormalConfig,
288 warm_start: Option<&TransformationWarmStart>,
289 ) -> Result<Self, String> {
290 let n = response.len();
291 if covariate_design.nrows() != n {
292 return Err(TransformationNormalError::InvalidInput {
293 reason: format!(
294 "response length {} != covariate design rows {}",
295 n,
296 covariate_design.nrows()
297 ),
298 }
299 .into());
300 }
301 let p_cov = covariate_design.ncols();
302 if p_cov == 0 {
303 return Err(TransformationNormalError::DesignDegenerate {
304 reason: "covariate design has zero columns".to_string(),
305 }
306 .into());
307 }
308 if weights.len() != n {
309 return Err(TransformationNormalError::InvalidInput {
310 reason: format!("response length {} != weights length {}", n, weights.len()),
311 }
312 .into());
313 }
314 if offset.len() != n {
315 return Err(TransformationNormalError::InvalidInput {
316 reason: format!("response length {} != offset length {}", n, offset.len()),
317 }
318 .into());
319 }
320 for (i, &weight) in weights.iter().enumerate() {
321 if !weight.is_finite() {
322 return Err(TransformationNormalError::NonFinite {
323 reason: format!("weights[{i}] is not finite: {weight}"),
324 }
325 .into());
326 }
327 if weight < 0.0 {
328 return Err(TransformationNormalError::InvalidInput {
329 reason: format!("weights[{i}] must be non-negative: {weight}"),
330 }
331 .into());
332 }
333 }
334 for (i, &value) in offset.iter().enumerate() {
335 if !value.is_finite() {
336 return Err(TransformationNormalError::NonFinite {
337 reason: format!("offset[{i}] is not finite: {value}"),
338 }
339 .into());
340 }
341 }
342 for (i, sp) in covariate_penalties.iter().enumerate() {
343 let (r, c) = sp.shape();
344 if r != p_cov || c != p_cov {
345 return Err(TransformationNormalError::InvalidInput {
346 reason: format!(
347 "covariate penalty {} has shape ({r}, {c}), expected ({p_cov}, {p_cov})",
348 i,
349 ),
350 }
351 .into());
352 }
353 }
354
355 let (resp_val, resp_deriv, resp_penalties, resp_knots, resp_transform) =
357 build_response_basis(response, config)?;
358 let p_resp = resp_val.ncols();
359 let (response_lower_basis, response_upper_basis) =
360 response_endpoint_value_bases(&resp_transform);
361
362 let x_val_kron = KroneckerDesign::new_khatri_rao(&resp_val, covariate_design.clone())?;
364 let x_deriv_kron = KroneckerDesign::new_khatri_rao(&resp_deriv, covariate_design.clone())?;
365 let p_total = p_resp * p_cov;
366 assert_eq!(x_val_kron.ncols(), p_total);
367 assert_eq!(x_deriv_kron.ncols(), p_total);
368
369 let initial_beta = compute_warm_start(
371 response,
372 weights,
373 offset,
374 &x_val_kron,
375 &x_deriv_kron,
376 &covariate_design,
377 &covariate_penalties,
378 p_resp,
379 p_cov,
380 warm_start,
381 )?;
382
383 let covariate_dense = covariate_design
385 .try_row_chunk(0..n)
386 .map_err(|e| format!("SCOP covariate dense materialization failed: {e}"))?;
387 let (tensor_penalties, tensor_penalty_layout) = build_tensor_penalties_kronecker(
388 &resp_penalties,
389 covariate_penalties,
390 resp_val.view(),
391 covariate_dense.view(),
392 weights.view(),
393 p_resp,
394 p_cov,
395 config,
396 )?;
397 let mut sorted_resp = response.to_vec();
399 sorted_resp.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
400 let resp_median = if sorted_resp.len() % 2 == 1 {
401 sorted_resp[sorted_resp.len() / 2]
402 } else {
403 0.5 * (sorted_resp[sorted_resp.len() / 2 - 1] + sorted_resp[sorted_resp.len() / 2])
404 };
405 let (response_floor_offset, response_lower_floor_offset, response_upper_floor_offset) =
406 response_floor_offsets(response, &resp_knots, resp_median);
407
408 Ok(Self {
409 x_val_kron,
410 x_deriv_kron,
411 response_val_basis: resp_val,
412 response_lower_basis,
413 response_upper_basis,
414 response_deriv_basis: resp_deriv,
415 covariate_design,
416 weights: Arc::new(weights.clone()),
417 offset: Arc::new(offset.clone()),
418 tensor_penalties,
419 tensor_penalty_layout,
420 initial_beta,
421 block_name: "transformation".to_string(),
422 response_knots: resp_knots,
423 response_transform: resp_transform,
424 response_degree: config.response_degree,
425 response_median: resp_median,
426 response_floor_offset: Arc::new(response_floor_offset),
427 response_lower_floor_offset,
428 response_upper_floor_offset,
429 covariate_dense_cache: Arc::new(Mutex::new(None)),
430 row_quantity_cache: Arc::new(Mutex::new(None)),
431 outer_subsample_weights: None,
432 })
433 }
434
435 pub(crate) fn from_prebuilt_response_basis(
440 response: &Array1<f64>,
441 response_val_basis: Array2<f64>,
442 response_deriv_basis: Array2<f64>,
443 response_penalties: Vec<Array2<f64>>,
444 response_knots: Array1<f64>,
445 response_degree: usize,
446 response_transform: Array2<f64>,
447 weights: &Array1<f64>,
448 offset: &Array1<f64>,
449 covariate_design: DesignMatrix,
450 covariate_penalties: Vec<PenaltyMatrix>,
451 config: &TransformationNormalConfig,
452 warm_start: Option<&TransformationWarmStart>,
453 ) -> Result<Self, String> {
454 let n = response_val_basis.nrows();
455 if n == 0 {
456 return Err(TransformationNormalError::InvalidInput {
457 reason: "response basis has zero rows".to_string(),
458 }
459 .into());
460 }
461 if response.len() != n {
462 return Err(TransformationNormalError::InvalidInput {
463 reason: format!(
464 "response length {} != response basis rows {}",
465 response.len(),
466 n
467 ),
468 }
469 .into());
470 }
471 if covariate_design.nrows() != n {
472 return Err(TransformationNormalError::InvalidInput {
473 reason: format!(
474 "response basis rows {} != covariate design rows {}",
475 n,
476 covariate_design.nrows()
477 ),
478 }
479 .into());
480 }
481 let p_cov = covariate_design.ncols();
482 if p_cov == 0 {
483 return Err(TransformationNormalError::DesignDegenerate {
484 reason: "covariate design has zero columns".to_string(),
485 }
486 .into());
487 }
488 if weights.len() != n {
489 return Err(TransformationNormalError::InvalidInput {
490 reason: format!(
491 "response basis rows {} != weights length {}",
492 n,
493 weights.len()
494 ),
495 }
496 .into());
497 }
498 if offset.len() != n {
499 return Err(TransformationNormalError::InvalidInput {
500 reason: format!(
501 "response basis rows {} != offset length {}",
502 n,
503 offset.len()
504 ),
505 }
506 .into());
507 }
508 for (i, &weight) in weights.iter().enumerate() {
509 if !weight.is_finite() {
510 return Err(TransformationNormalError::NonFinite {
511 reason: format!("weights[{i}] is not finite: {weight}"),
512 }
513 .into());
514 }
515 if weight < 0.0 {
516 return Err(TransformationNormalError::InvalidInput {
517 reason: format!("weights[{i}] must be non-negative: {weight}"),
518 }
519 .into());
520 }
521 }
522 for (i, &value) in offset.iter().enumerate() {
523 if !value.is_finite() {
524 return Err(TransformationNormalError::NonFinite {
525 reason: format!("offset[{i}] is not finite: {value}"),
526 }
527 .into());
528 }
529 }
530 for (i, sp) in covariate_penalties.iter().enumerate() {
531 let (r, c) = sp.shape();
532 if r != p_cov || c != p_cov {
533 return Err(TransformationNormalError::InvalidInput {
534 reason: format!(
535 "covariate penalty {} has shape ({r}, {c}), expected ({p_cov}, {p_cov})",
536 i,
537 ),
538 }
539 .into());
540 }
541 }
542
543 let p_resp = response_val_basis.ncols();
544 if response_transform.ncols() + 1 != p_resp {
545 return Err(TransformationNormalError::InvalidInput { reason: format!(
546 "response transform columns {} imply p_resp {}, but response value basis has {} columns",
547 response_transform.ncols(),
548 response_transform.ncols() + 1,
549 p_resp
550 ) }.into());
551 }
552 let (response_lower_basis, response_upper_basis) =
553 response_endpoint_value_bases(&response_transform);
554
555 let x_val_kron =
557 KroneckerDesign::new_khatri_rao(&response_val_basis, covariate_design.clone())?;
558 let x_deriv_kron =
559 KroneckerDesign::new_khatri_rao(&response_deriv_basis, covariate_design.clone())?;
560 let p_total = p_resp * p_cov;
561 assert_eq!(x_val_kron.ncols(), p_total);
562 assert_eq!(x_deriv_kron.ncols(), p_total);
563
564 let initial_beta = compute_warm_start(
565 response,
566 weights,
567 offset,
568 &x_val_kron,
569 &x_deriv_kron,
570 &covariate_design,
571 &covariate_penalties,
572 p_resp,
573 p_cov,
574 warm_start,
575 )?;
576
577 let covariate_dense = covariate_design
579 .try_row_chunk(0..n)
580 .map_err(|e| format!("SCOP covariate dense materialization failed: {e}"))?;
581 let (tensor_penalties, tensor_penalty_layout) = build_tensor_penalties_kronecker(
582 &response_penalties,
583 covariate_penalties,
584 response_val_basis.view(),
585 covariate_dense.view(),
586 weights.view(),
587 p_resp,
588 p_cov,
589 config,
590 )?;
591 let mut sorted_resp = response.to_vec();
593 sorted_resp.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
594 let resp_median = if sorted_resp.len() % 2 == 1 {
595 sorted_resp[sorted_resp.len() / 2]
596 } else {
597 0.5 * (sorted_resp[sorted_resp.len() / 2 - 1] + sorted_resp[sorted_resp.len() / 2])
598 };
599 let (response_floor_offset, response_lower_floor_offset, response_upper_floor_offset) =
600 response_floor_offsets(response, &response_knots, resp_median);
601
602 Ok(Self {
603 x_val_kron,
604 x_deriv_kron,
605 response_val_basis,
606 response_lower_basis,
607 response_upper_basis,
608 response_deriv_basis,
609 covariate_design,
610 weights: Arc::new(weights.clone()),
611 offset: Arc::new(offset.clone()),
612 tensor_penalties,
613 tensor_penalty_layout,
614 initial_beta,
615 block_name: "transformation".to_string(),
616 response_knots: response_knots.clone(),
617 response_transform: response_transform.clone(),
618 response_degree,
619 response_median: resp_median,
620 response_floor_offset: Arc::new(response_floor_offset),
621 response_lower_floor_offset,
622 response_upper_floor_offset,
623 covariate_dense_cache: Arc::new(Mutex::new(None)),
624 row_quantity_cache: Arc::new(Mutex::new(None)),
625 outer_subsample_weights: None,
626 })
627 }
628
629 pub fn response_knots(&self) -> &Array1<f64> {
631 &self.response_knots
632 }
633 pub fn response_transform(&self) -> &Array2<f64> {
634 &self.response_transform
635 }
636 pub fn response_degree(&self) -> usize {
637 self.response_degree
638 }
639 pub fn response_median(&self) -> f64 {
640 self.response_median
641 }
642
643 pub(crate) fn penalty_scale_log_lambdas(&self) -> Result<Array1<f64>, String> {
646 let policy = ResourcePolicy::default_library();
647 let likelihood_diagonal_mean = self
648 .x_val_kron
649 .weighted_gram_diagonal_mean(self.weights.as_ref(), &policy)?;
650 Ok(ctn_penalty_scale_log_lambdas(
651 &self.tensor_penalties,
652 likelihood_diagonal_mean,
653 ))
654 }
655
656 pub(crate) fn block_spec(
660 &self,
661 initial_log_lambdas: &Array1<f64>,
662 ) -> Result<ParameterBlockSpec, String> {
663 if initial_log_lambdas.len() != self.tensor_penalties.len() {
664 return Err(TransformationNormalError::InvalidInput {
665 reason: format!(
666 "transformation smoothing vector has length {}, expected {}",
667 initial_log_lambdas.len(),
668 self.tensor_penalties.len(),
669 ),
670 }
671 .into());
672 }
673 gam_problem::validate_log_strengths(initial_log_lambdas.iter().copied())
674 .map_err(|error| format!("invalid transformation smoothing strength: {error}"))?;
675 let offset = self.offset.as_ref() + self.response_floor_offset.as_ref();
676 Ok(ParameterBlockSpec {
677 name: self.block_name.clone(),
678 design: DesignMatrix::Dense(DenseDesignMatrix::from(Arc::new(self.x_val_kron.clone()))),
679 offset,
680 penalties: self.tensor_penalties.clone(),
681 nullspace_dims: vec![],
682 initial_log_lambdas: initial_log_lambdas.clone(),
683 initial_beta: Some(self.initial_beta.clone()),
684 gauge_priority: 100,
685 jacobian_callback: None,
686 stacked_design: None,
687 stacked_offset: None,
688 })
689 }
690
691 pub fn p_total(&self) -> usize {
693 self.x_val_kron.ncols()
694 }
695
696 pub fn n_obs(&self) -> usize {
698 self.x_val_kron.nrows()
699 }
700
701 pub(crate) fn p_resp(&self) -> usize {
703 self.response_val_basis.ncols()
704 }
705
706 pub(crate) fn p_cov(&self) -> usize {
708 self.covariate_design.ncols()
709 }
710
711 pub(crate) fn response_lower_basis(&self) -> &Array1<f64> {
714 &self.response_lower_basis
715 }
716
717 pub(crate) fn response_upper_basis(&self) -> &Array1<f64> {
720 &self.response_upper_basis
721 }
722
723 pub(crate) fn response_lower_floor_offset(&self) -> f64 {
726 self.response_lower_floor_offset
727 }
728
729 pub(crate) fn response_upper_floor_offset(&self) -> f64 {
732 self.response_upper_floor_offset
733 }
734
735 #[inline]
748 pub(crate) fn effective_weights(&self) -> &Array1<f64> {
749 match self.outer_subsample_weights.as_ref() {
750 Some(w) => w.as_ref(),
751 None => self.weights.as_ref(),
752 }
753 }
754
755 pub(crate) fn evaluate_response_value_basis(
765 &self,
766 response: ArrayView1<'_, f64>,
767 ) -> Result<Array2<f64>, String> {
768 let n = response.len();
769 for (i, &v) in response.iter().enumerate() {
770 if !v.is_finite() {
771 return Err(TransformationNormalError::NonFinite {
772 reason: format!(
773 "evaluate_response_value_basis: response[{i}] is not finite: {v}"
774 ),
775 }
776 .into());
777 }
778 }
779 let (i_val_basis, _) = create_basis::<Dense>(
780 response,
781 KnotSource::Provided(self.response_knots.view()),
782 self.response_degree,
783 BasisOptions::i_spline(),
784 )
785 .map_err(|e| format!("evaluate_response_value_basis: I-spline build failed: {e}"))?;
786 let shape_val = i_val_basis.as_ref();
787 let p_shape = shape_val.ncols();
788 let p_resp = self.response_val_basis.ncols();
789 if p_shape + 1 != p_resp {
790 return Err(TransformationNormalError::InvalidInput {
791 reason: format!(
792 "evaluate_response_value_basis: rebuilt shape columns {p_shape} imply p_resp {}, \
793 but fitted basis has {p_resp} columns",
794 p_shape + 1
795 ),
796 }
797 .into());
798 }
799 let mut resp_val = Array2::<f64>::zeros((n, p_resp));
800 resp_val.column_mut(0).fill(1.0);
801 resp_val.slice_mut(s![.., 1..]).assign(shape_val);
802 Ok(resp_val)
803 }
804
805 pub(crate) fn with_outer_subsample(
816 &self,
817 mask: &Array1<f64>,
818 ) -> Result<Self, TransformationNormalError> {
819 let n = self.weights.len();
820 if mask.len() != n {
821 bail_invalid_tnorm!(
822 "outer-score subsample mask length {} != n={}",
823 mask.len(),
824 n
825 );
826 }
827 let mut effective = Array1::<f64>::zeros(n);
828 for i in 0..n {
829 let m = mask[i];
830 if !m.is_finite() || m < 0.0 {
831 bail_invalid_tnorm!(
832 "outer-score subsample mask[{i}] = {m} is invalid (must be finite and >= 0)"
833 );
834 }
835 effective[i] = self.weights[i] * m;
836 }
837 Ok(Self {
838 x_val_kron: self.x_val_kron.clone(),
840 x_deriv_kron: self.x_deriv_kron.clone(),
841 response_val_basis: self.response_val_basis.clone(),
842 response_lower_basis: self.response_lower_basis.clone(),
843 response_upper_basis: self.response_upper_basis.clone(),
844 response_deriv_basis: self.response_deriv_basis.clone(),
845 covariate_design: self.covariate_design.clone(),
846 covariate_dense_cache: Arc::clone(&self.covariate_dense_cache),
847 weights: Arc::clone(&self.weights),
848 offset: Arc::clone(&self.offset),
849 tensor_penalties: self.tensor_penalties.clone(),
850 tensor_penalty_layout: self.tensor_penalty_layout,
851 initial_beta: self.initial_beta.clone(),
852 block_name: self.block_name.clone(),
853 response_knots: self.response_knots.clone(),
854 response_transform: self.response_transform.clone(),
855 response_degree: self.response_degree,
856 response_median: self.response_median,
857 response_floor_offset: Arc::clone(&self.response_floor_offset),
858 response_lower_floor_offset: self.response_lower_floor_offset,
859 response_upper_floor_offset: self.response_upper_floor_offset,
860 row_quantity_cache: Arc::new(Mutex::new(None)),
864 outer_subsample_weights: Some(Arc::new(effective)),
865 })
866 }
867
868 pub(crate) fn maybe_with_outer_subsample_from_options(
871 &self,
872 options: &BlockwiseFitOptions,
873 ) -> Result<Option<Self>, TransformationNormalError> {
874 let Some(sub) = options.outer_score_subsample.as_ref() else {
875 return Ok(None);
876 };
877 let n = self.weights.len();
878 let mut mask = Array1::<f64>::zeros(n);
879 for row in sub.rows.iter() {
880 if row.index < n {
881 mask[row.index] = row.weight;
882 }
883 }
884 Ok(Some(self.with_outer_subsample(&mask)?))
885 }
886
887 pub(crate) fn covariate_dense_arc(&self) -> Result<Arc<Array2<f64>>, String> {
890 let mut cache = self
891 .covariate_dense_cache
892 .lock()
893 .expect("CTN covariate dense cache mutex poisoned");
894 if let Some(cached) = cache.as_ref() {
895 return Ok(cached.clone());
896 }
897 let dense = Arc::new(
898 self.covariate_design
899 .try_row_chunk(0..self.response_val_basis.nrows())
900 .map_err(|e| format!("SCOP covariate dense materialization failed: {e}"))?,
901 );
902 *cache = Some(dense.clone());
903 Ok(dense)
904 }
905
906 pub(crate) fn row_quantities(
907 &self,
908 beta: &Array1<f64>,
909 ) -> Result<TransformationNormalRowQuantityCache, String> {
910 {
911 let cache = self
912 .row_quantity_cache
913 .lock()
914 .expect("CTN row quantity cache mutex poisoned");
915 if let Some(cached) = cache.as_ref().filter(|cached| cached.matches_beta(beta)) {
916 return Ok(cached.clone());
917 }
918 }
919
920 let p_resp = self.response_val_basis.ncols();
921 let p_cov = self.covariate_design.ncols();
922 let beta_mat = beta
923 .view()
924 .into_shape_with_order((p_resp, p_cov))
925 .map_err(|e| format!("SCOP endpoint beta reshape failed: {e}"))?;
926 let cov = self.covariate_dense_arc()?;
927
928 let alpha = fast_abt(cov.as_ref(), &beta_mat);
942 let n = alpha.nrows();
943 let mut h = Array1::<f64>::zeros(n);
944 let mut h_prime = Array1::<f64>::zeros(n);
945 let mut h_lower = Array1::<f64>::zeros(n);
946 let mut h_upper = Array1::<f64>::zeros(n);
947 ndarray::Zip::indexed(&mut h)
952 .and(&mut h_prime)
953 .and(&mut h_lower)
954 .and(&mut h_upper)
955 .par_for_each(|i, h_i, hp_i, lower_i, upper_i| {
956 let alpha_row = alpha.row(i);
957 let val_row = self.response_val_basis.row(i);
958 let deriv_row = self.response_deriv_basis.row(i);
959 let a0 = alpha_row[0];
960 let offset_i = self.offset[i];
961 let mut h_acc = val_row[0] * a0 + offset_i + self.response_floor_offset[i];
962 let mut hp_acc = deriv_row[0] * a0 + TRANSFORMATION_MONOTONICITY_EPS;
963 let mut lower_acc =
964 self.response_lower_basis[0] * a0 + offset_i + self.response_lower_floor_offset;
965 let mut upper_acc =
966 self.response_upper_basis[0] * a0 + offset_i + self.response_upper_floor_offset;
967 for k in 1..p_resp {
968 let a_k = alpha_row[k];
969 h_acc += val_row[k] * a_k;
970 hp_acc += deriv_row[k] * a_k;
971 lower_acc += self.response_lower_basis[k] * a_k;
972 upper_acc += self.response_upper_basis[k] * a_k;
973 }
974 *h_i = h_acc;
975 *hp_i = hp_acc;
976 *lower_i = lower_acc;
977 *upper_i = upper_acc;
978 });
979 for (i, &value) in h.iter().enumerate() {
980 if !value.is_finite() {
981 return Err(TransformationNormalError::NonFinite {
982 reason: format!(
983 "TransformationNormalFamily row_quantities: h[{i}] = {value} is not finite"
984 ),
985 }
986 .into());
987 }
988 if value.abs() > TRANSFORMATION_NORMAL_H_ABS_MAX {
989 return Err(TransformationNormalError::InvalidInput { reason: format!(
990 "TransformationNormalFamily row_quantities: h[{i}] = {value:.6e} exceeds the standard-normal domain bound ±{TRANSFORMATION_NORMAL_H_ABS_MAX}"
991 ) }.into());
992 }
993 }
994 let mut min_hp = f64::INFINITY;
1006 let mut nonfinite_idx: Option<usize> = None;
1007 for (i, &hp) in h_prime.iter().enumerate() {
1008 if !hp.is_finite() {
1009 nonfinite_idx = Some(i);
1010 break;
1011 }
1012 if hp < min_hp {
1013 min_hp = hp;
1014 }
1015 }
1016 if let Some(i) = nonfinite_idx {
1017 return Err(TransformationNormalError::NonFinite {
1018 reason: format!(
1019 "TransformationNormalFamily row_quantities: h'[{i}] = {} is not finite",
1020 h_prime[i]
1021 ),
1022 }
1023 .into());
1024 }
1025 if min_hp <= 0.0 {
1026 return Err(TransformationNormalError::MonotonicityViolated { reason: format!(
1027 "TransformationNormalFamily row_quantities: h' has non-positive values (min = {min_hp:.6e}). \
1028 Monotonicity constraint may be violated."
1029 ) }.into());
1030 }
1031 let derived = build_transformation_row_derived(
1036 &h,
1037 &h_prime,
1038 &h_lower,
1039 &h_upper,
1040 self.effective_weights(),
1041 )?;
1042 let row_quantities = TransformationNormalRowQuantityCache {
1043 beta: Arc::new(beta.clone()),
1044 alpha: Arc::new(alpha),
1045 h: Arc::new(h),
1046 h_prime: Arc::new(h_prime),
1047 h_lower: Arc::new(h_lower),
1048 h_upper: Arc::new(h_upper),
1049 endpoint_q: Arc::new(derived.endpoint_q),
1050 log_likelihood: derived.log_likelihood,
1051 };
1052
1053 let mut cache = self
1054 .row_quantity_cache
1055 .lock()
1056 .expect("CTN row quantity cache mutex poisoned");
1057 *cache = Some(row_quantities.clone());
1058 Ok(row_quantities)
1059 }
1060}