1use super::*;
2
3impl CustomFamily for TransformationNormalFamily {
8 fn evaluate(&self, block_states: &[ParameterBlockState]) -> Result<FamilyEvaluation, String> {
9 crate::block_layout::block_count::validate_block_count::<TransformationNormalError>(
10 "TransformationNormalFamily",
11 1,
12 block_states.len(),
13 )?;
14 let evaluate_start = std::time::Instant::now();
15 let beta = &block_states[0].beta;
16 let row_q_start = std::time::Instant::now();
17 let row_quantities = self.row_quantities(beta)?;
18 log::info!(
19 "[STAGE] CTN row_quantities (h, h', 1/h', powers) n={} elapsed={:.3}s",
20 row_quantities.h.len(),
21 row_q_start.elapsed().as_secs_f64(),
22 );
23 let h = row_quantities.h.as_ref();
24 let n = h.len();
25
26 let log_likelihood = row_quantities.log_likelihood;
27 let grad_start = std::time::Instant::now();
31 let (grad, hessian) = self.scop_gradient_and_negative_hessian(beta, &row_quantities)?;
32 log::info!(
33 "[STAGE] CTN gradient terms n={} p={} elapsed={:.3}s",
34 n,
35 grad.len(),
36 grad_start.elapsed().as_secs_f64(),
37 );
38
39 let hess_start = std::time::Instant::now();
40 let p_dim = hessian.nrows() as u64;
41 let n_u64 = n as u64;
42 log::info!(
43 "[STAGE] CTN hessian terms (SCOP exact dense) n={} p={} flops~{} elapsed={:.3}s",
44 n,
45 p_dim,
46 n_u64.saturating_mul(p_dim).saturating_mul(p_dim),
47 hess_start.elapsed().as_secs_f64(),
48 );
49 log::info!(
50 "[STAGE] CTN evaluate end n={} p={} elapsed={:.3}s",
51 n,
52 p_dim,
53 evaluate_start.elapsed().as_secs_f64(),
54 );
55
56 Ok(FamilyEvaluation {
57 log_likelihood,
58 blockworking_sets: vec![BlockWorkingSet::ExactNewton {
59 gradient: grad,
60 hessian: SymmetricMatrix::Dense(hessian),
61 }],
62 })
63 }
64
65 fn log_likelihood_only(&self, block_states: &[ParameterBlockState]) -> Result<f64, String> {
66 crate::block_layout::block_count::validate_block_count::<TransformationNormalError>(
67 "TransformationNormalFamily",
68 1,
69 block_states.len(),
70 )?;
71 let row_quantities = match self.row_quantities(&block_states[0].beta) {
75 Ok(rq) => rq,
76 Err(_) => return Ok(f64::NEG_INFINITY),
77 };
78 Ok(row_quantities.log_likelihood)
79 }
80
81 fn log_likelihood_only_with_options(
82 &self,
83 block_states: &[ParameterBlockState],
84 options: &BlockwiseFitOptions,
85 ) -> Result<f64, String> {
86 match self.maybe_with_outer_subsample_from_options(options) {
93 Ok(Some(masked)) => masked.log_likelihood_only(block_states),
94 Ok(None) => self.log_likelihood_only(block_states),
95 Err(e) => Err(e.into()),
96 }
97 }
98
99 fn exact_newton_joint_gradient_evaluation(
115 &self,
116 block_states: &[ParameterBlockState],
117 _: &[ParameterBlockSpec],
118 ) -> Result<Option<ExactNewtonJointGradientEvaluation>, String> {
119 crate::block_layout::block_count::validate_block_count::<TransformationNormalError>(
120 "TransformationNormalFamily",
121 1,
122 block_states.len(),
123 )?;
124 let beta = &block_states[0].beta;
125 let row_quantities = self.row_quantities(beta)?;
126 let log_likelihood = row_quantities.log_likelihood;
127 let gradient = self.scop_gradient(beta, &row_quantities)?;
128 Ok(Some(ExactNewtonJointGradientEvaluation {
129 log_likelihood,
130 gradient,
131 }))
132 }
133
134 fn exact_newton_joint_hessian_beta_dependent(&self) -> bool {
135 true
137 }
138
139 fn joint_jeffreys_term_required(&self) -> bool {
140 false
159 }
160
161 fn coefficient_hessian_cost(&self, specs: &[ParameterBlockSpec]) -> u64 {
162 let n_usize = self.response_val_basis.nrows();
177 let p_resp = self.response_val_basis.ncols() as u64;
178 let p_cov = self.covariate_design.ncols() as u64;
179 let expected_p_total = p_resp.saturating_mul(p_cov);
180 let p_total = match specs {
192 [] => expected_p_total,
193 [spec] if spec.design.ncols() as u64 == expected_p_total => spec.design.ncols() as u64,
194 _ => return u64::MAX,
195 };
196 let n = n_usize as u64;
197 crate::coefficient_cost::operator_aware_hessian_cost(
204 p_total,
205 n,
206 n.saturating_mul(p_resp.saturating_add(p_cov)),
207 n.saturating_mul(p_total.saturating_mul(p_total)),
208 )
209 }
210
211 fn coefficient_gradient_cost(&self, specs: &[ParameterBlockSpec]) -> u64 {
212 self.coefficient_hessian_cost(specs) / 2
216 }
217
218 fn outer_derivative_policy(
219 &self,
220 specs: &[crate::custom_family::ParameterBlockSpec],
221 psi_dim: usize,
222 options: &crate::custom_family::BlockwiseFitOptions,
223 ) -> crate::custom_family::OuterDerivativePolicy {
224 let capability = self.exact_outer_derivative_order(specs, options);
237 let n = specs.first().map_or(0u128, |s| s.design.nrows() as u128);
238 let p_total: u128 = specs
239 .iter()
240 .map(|s| s.design.ncols() as u128)
241 .fold(0u128, |acc, x| acc.saturating_add(x));
242 let rho_dim: u128 = specs
243 .iter()
244 .map(|s| s.penalties.len() as u128)
245 .fold(0u128, |acc, x| acc.saturating_add(x));
246 let k = rho_dim.saturating_add(psi_dim as u128).max(1);
247 let p_eff = p_total.max(1);
248 let work_grad = n.saturating_mul(k).saturating_mul(p_eff);
250 let dense_hess = work_grad.saturating_mul(p_eff);
256 let mfree_hess = work_grad.saturating_mul(rho_dim.max(1));
257 let work_hess = dense_hess.min(mfree_hess);
258 crate::custom_family::OuterDerivativePolicy {
259 capability,
260 predicted_hessian_work: work_hess,
261 predicted_gradient_work: work_grad,
262 subsample_capable: true,
272 }
273 }
274
275 fn outer_seed_config(&self, n_params: usize) -> gam_solve::seeding::SeedConfig {
276 gam_solve::seeding::SeedConfig {
277 bounds: (-12.0, 12.0),
278 max_seeds: if n_params <= 8 { 1 } else { 2 },
279 seed_budget: 1,
280 screen_max_inner_iterations: 2,
281 risk_profile: gam_solve::seeding::SeedRiskProfile::Gaussian,
282 num_auxiliary_trailing: 0,
283 over_smoothing_probe_rho: None,
284 }
285 }
286
287 fn max_feasible_step_size(
288 &self,
289 block_states: &[ParameterBlockState],
290 block_index: usize,
291 delta: &Array1<f64>,
292 ) -> Result<Option<f64>, String> {
293 if block_index != 0 {
294 return Ok(None);
295 }
296 crate::block_layout::block_count::validate_block_count::<TransformationNormalError>(
297 "TransformationNormalFamily",
298 1,
299 block_states.len(),
300 )?;
301 if delta.len() != block_states[0].beta.len() {
302 return Err(TransformationNormalError::InvalidInput {
303 reason: format!(
304 "CTN line-search step length {} != beta length {}",
305 delta.len(),
306 block_states[0].beta.len()
307 ),
308 }
309 .into());
310 }
311 Ok(None)
317 }
318
319 fn block_linear_constraints(
320 &self,
321 _: &[ParameterBlockState],
322 block_index: usize,
323 block_spec: &ParameterBlockSpec,
324 ) -> Result<Option<LinearInequalityConstraints>, String> {
325 assert!(!block_spec.name.is_empty());
326 if block_index != 0 {
327 return Ok(None);
328 }
329 Ok(None)
333 }
334
335 fn exact_newton_hessian_directional_derivative(
336 &self,
337 block_states: &[ParameterBlockState],
338 block_index: usize,
339 d_beta: &Array1<f64>,
340 ) -> Result<Option<Array2<f64>>, String> {
341 if block_index != 0 {
342 return Ok(None);
343 }
344 let beta = &block_states[0].beta;
345 let row_quantities = self.row_quantities(beta)?;
346 let dd = self.scop_hessian_directional_derivative(beta, d_beta, &row_quantities)?;
347 Ok(Some(dd))
348 }
349
350 fn exact_newton_joint_hessian(
351 &self,
352 block_states: &[ParameterBlockState],
353 ) -> Result<Option<Array2<f64>>, String> {
354 let beta = &block_states[0].beta;
356 let row_quantities = self.row_quantities(beta)?;
357 let (_, hessian) = self.scop_gradient_and_negative_hessian(beta, &row_quantities)?;
358 Ok(Some(hessian))
359 }
360
361 fn exact_newton_joint_hessian_directional_derivative(
362 &self,
363 block_states: &[ParameterBlockState],
364 d_beta_flat: &Array1<f64>,
365 ) -> Result<Option<Array2<f64>>, String> {
366 self.exact_newton_hessian_directional_derivative(block_states, 0, d_beta_flat)
367 }
368
369 fn exact_newton_joint_hessiansecond_directional_derivative(
370 &self,
371 block_states: &[ParameterBlockState],
372 d_beta_u_flat: &Array1<f64>,
373 d_beta_v_flat: &Array1<f64>,
374 ) -> Result<Option<Array2<f64>>, String> {
375 let beta = &block_states[0].beta;
376 let row_quantities = self.row_quantities(beta)?;
377 let d2 = self.scop_hessian_second_directional_derivative(
378 beta,
379 d_beta_u_flat,
380 d_beta_v_flat,
381 &row_quantities,
382 )?;
383 Ok(Some(d2))
384 }
385
386 fn exact_newton_joint_psi_terms(
387 &self,
388 block_states: &[ParameterBlockState],
389 _: &[ParameterBlockSpec],
390 psi_derivs: &[Vec<CustomFamilyBlockPsiDerivative>],
391 psi_index: usize,
392 ) -> Result<Option<ExactNewtonJointPsiTerms>, String> {
393 if psi_derivs.is_empty() || psi_index >= psi_derivs[0].len() {
394 return Ok(None);
395 }
396 let psi_first_start = std::time::Instant::now();
397 let deriv = &psi_derivs[0][psi_index];
398 let beta = &block_states[0].beta;
399 let row = self.row_quantities(beta)?;
400 let op = deriv
401 .implicit_operator
402 .as_ref()
403 .and_then(|op| op.as_any().downcast_ref::<TensorKroneckerPsiOperator>())
404 .ok_or_else(|| {
405 "TransformationNormalFamily requires tensor psi derivatives to remain operator-backed"
406 .to_string()
407 })?;
408 let axis = deriv.implicit_axis;
409 let op_arc = Arc::clone(
410 deriv
411 .implicit_operator
412 .as_ref()
413 .expect("validated CTN psi derivative operator disappeared"),
414 );
415 let terms = self.scop_psi_terms(beta, &row, op, op_arc, axis)?;
416
417 log::info!(
418 "[STAGE] CTN psi first-order terms axis={} psi_index={} elapsed={:.3}s",
419 deriv.implicit_axis,
420 psi_index,
421 psi_first_start.elapsed().as_secs_f64(),
422 );
423
424 Ok(Some(terms))
425 }
426
427 fn exact_newton_joint_psisecond_order_terms(
428 &self,
429 block_states: &[ParameterBlockState],
430 _: &[ParameterBlockSpec],
431 psi_derivs: &[Vec<CustomFamilyBlockPsiDerivative>],
432 psi_i: usize,
433 psi_j: usize,
434 ) -> Result<Option<ExactNewtonJointPsiSecondOrderTerms>, String> {
435 if psi_derivs.is_empty() || psi_i >= psi_derivs[0].len() || psi_j >= psi_derivs[0].len() {
436 return Ok(None);
437 }
438 let psi_pair_start = std::time::Instant::now();
439 let deriv_i = &psi_derivs[0][psi_i];
440 let deriv_j = &psi_derivs[0][psi_j];
441 let beta = &block_states[0].beta;
442 let row = self.row_quantities(beta)?;
443 let p_resp = self.response_val_basis.ncols();
444 let p_cov = self.covariate_design.ncols();
445 let p_total = p_resp * p_cov;
446 if beta.len() != p_total {
447 return Err(TransformationNormalError::InvalidInput {
448 reason: format!(
449 "SCOP psi-psi terms beta length {} != p_resp({p_resp}) * p_cov({p_cov})",
450 beta.len()
451 ),
452 }
453 .into());
454 }
455
456 let op = deriv_i
457 .implicit_operator
458 .as_ref()
459 .and_then(|op| op.as_any().downcast_ref::<TensorKroneckerPsiOperator>())
460 .ok_or_else(|| {
461 "TransformationNormalFamily requires tensor psi derivatives to remain operator-backed"
462 .to_string()
463 })?;
464 let axis_i = deriv_i.implicit_axis;
465 let axis_j = deriv_j.implicit_axis;
466
467 let (objective_psi_psi, score_psi_psi, _) = self
468 .scop_psi_psi_value_score_hvp_from_operator(
469 beta,
470 op,
471 axis_i,
472 axis_j,
473 row.gamma.view(),
474 row.h.view(),
475 row.h_prime.view(),
476 row.endpoint_q.as_slice(),
477 None,
478 )?;
479 let hessian_psi_psi_operator: Box<dyn HyperOperator> =
480 Box::new(TransformationNormalPsiPsiHessianOperator::new(
481 Arc::new(self.clone()),
482 beta.clone(),
483 Arc::clone(
484 deriv_i
485 .implicit_operator
486 .as_ref()
487 .expect("validated CTN psi derivative has an implicit operator"),
488 ),
489 axis_i,
490 axis_j,
491 Arc::clone(&row.gamma),
492 Arc::clone(&row.h),
493 Arc::clone(&row.h_prime),
494 Arc::clone(&row.endpoint_q),
495 ));
496
497 if !objective_psi_psi.is_finite() || !score_psi_psi.iter().all(|v| v.is_finite()) {
503 return Err(TransformationNormalError::NonFinite {
504 reason: format!(
505 "TransformationNormalFamily exact ψ-ψ second-order terms produced \
506 non-finite values at psi_i={psi_i}, psi_j={psi_j}: \
507 obj_finite={}, score_all_finite={}. \
508 The outer evaluator should retreat from this trial point.",
509 objective_psi_psi.is_finite(),
510 score_psi_psi.iter().all(|v| v.is_finite()),
511 ),
512 }
513 .into());
514 }
515
516 log::info!(
517 "[STAGE] CTN psi-psi pair (psi_i={}, psi_j={}, axes={},{}) elapsed={:.3}s",
518 psi_i,
519 psi_j,
520 deriv_i.implicit_axis,
521 deriv_j.implicit_axis,
522 psi_pair_start.elapsed().as_secs_f64(),
523 );
524
525 Ok(Some(ExactNewtonJointPsiSecondOrderTerms {
526 objective_psi_psi,
527 score_psi_psi,
528 hessian_psi_psi: Array2::zeros((0, 0)),
529 hessian_psi_psi_operator: Some(hessian_psi_psi_operator),
530 }))
531 }
532
533 fn exact_newton_joint_psihessian_directional_derivative(
534 &self,
535 block_states: &[ParameterBlockState],
536 _: &[ParameterBlockSpec],
537 psi_derivs: &[Vec<CustomFamilyBlockPsiDerivative>],
538 psi_index: usize,
539 d_beta_flat: &Array1<f64>,
540 ) -> Result<Option<Array2<f64>>, String> {
541 if psi_derivs.is_empty() || psi_index >= psi_derivs[0].len() {
542 return Ok(None);
543 }
544 let deriv = &psi_derivs[0][psi_index];
545 let beta = &block_states[0].beta;
546 let op = deriv
547 .implicit_operator
548 .as_ref()
549 .and_then(|op| op.as_any().downcast_ref::<TensorKroneckerPsiOperator>())
550 .ok_or_else(|| {
551 "TransformationNormalFamily requires tensor psi derivatives to remain operator-backed"
552 .to_string()
553 })?;
554 let axis = deriv.implicit_axis;
555 let row = self.row_quantities(beta)?;
556 let hess =
557 self.scop_psi_hessian_directional_derivative(beta, d_beta_flat, &row, op, axis)?;
558 Ok(Some(hess))
559 }
560
561 fn exact_newton_joint_hessian_workspace(
562 &self,
563 block_states: &[ParameterBlockState],
564 specs: &[ParameterBlockSpec],
565 ) -> Result<Option<Arc<dyn ExactNewtonJointHessianWorkspace>>, String> {
566 crate::block_layout::block_count::validate_block_count::<TransformationNormalError>(
567 "TransformationNormalFamily",
568 1,
569 block_states.len(),
570 )?;
571 if !self.inner_coefficient_hessian_hvp_available(specs) {
572 return Err(TransformationNormalError::InvalidInput {
573 reason: "TransformationNormalFamily joint Hessian workspace received incompatible block specs"
574 .to_string(),
575 }
576 .into());
577 }
578 let beta = &block_states[0].beta;
579 let row_quantities = self.row_quantities(beta)?;
580 let workspace = TransformationNormalJointHessianWorkspace::new(
584 Arc::new(self.clone()),
585 beta.clone(),
586 row_quantities.clone(),
587 )?;
588 Ok(Some(
589 Arc::new(workspace) as Arc<dyn ExactNewtonJointHessianWorkspace>
590 ))
591 }
592
593 fn exact_newton_joint_psi_workspace(
594 &self,
595 block_states: &[ParameterBlockState],
596 specs: &[ParameterBlockSpec],
597 derivative_blocks: &[Vec<CustomFamilyBlockPsiDerivative>],
598 ) -> Result<Option<Arc<dyn ExactNewtonJointPsiWorkspace>>, String> {
599 if !self.inner_coefficient_hessian_hvp_available(specs) {
600 return Err(TransformationNormalError::InvalidInput {
601 reason: "TransformationNormalFamily joint psi workspace received incompatible block specs"
602 .to_string(),
603 }
604 .into());
605 }
606 Ok(Some(Arc::new(TransformationNormalPsiWorkspace::new(
607 self.clone(),
608 block_states.to_vec(),
609 derivative_blocks.to_vec(),
610 ))))
611 }
612
613 fn exact_newton_joint_hessian_workspace_with_options(
614 &self,
615 block_states: &[ParameterBlockState],
616 specs: &[ParameterBlockSpec],
617 options: &BlockwiseFitOptions,
618 ) -> Result<Option<Arc<dyn ExactNewtonJointHessianWorkspace>>, String> {
619 match self.maybe_with_outer_subsample_from_options(options)? {
627 Some(masked) => masked.exact_newton_joint_hessian_workspace(block_states, specs),
628 None => self.exact_newton_joint_hessian_workspace(block_states, specs),
629 }
630 }
631
632 fn exact_newton_joint_psi_workspace_with_options(
633 &self,
634 block_states: &[ParameterBlockState],
635 specs: &[ParameterBlockSpec],
636 derivative_blocks: &[Vec<CustomFamilyBlockPsiDerivative>],
637 options: &BlockwiseFitOptions,
638 ) -> Result<Option<Arc<dyn ExactNewtonJointPsiWorkspace>>, String> {
639 if !self.inner_coefficient_hessian_hvp_available(specs) {
640 return Err(TransformationNormalError::InvalidInput {
641 reason: "TransformationNormalFamily joint psi workspace received incompatible block specs"
642 .to_string(),
643 }
644 .into());
645 }
646 let family = match self.maybe_with_outer_subsample_from_options(options)? {
659 Some(masked) => masked,
660 None => self.clone(),
661 };
662 Ok(Some(Arc::new(TransformationNormalPsiWorkspace::new(
663 family,
664 block_states.to_vec(),
665 derivative_blocks.to_vec(),
666 ))))
667 }
668
669 fn exact_newton_joint_psi_workspace_for_first_order_terms(&self) -> bool {
670 true
676 }
677
678 fn inner_coefficient_hessian_hvp_available(&self, specs: &[ParameterBlockSpec]) -> bool {
679 matches!(specs, [spec] if spec.design.ncols()
682 == self.response_val_basis.ncols().saturating_mul(self.covariate_design.ncols()))
683 }
684
685 fn outer_hyper_hessian_hvp_available(&self, specs: &[ParameterBlockSpec]) -> bool {
686 self.inner_coefficient_hessian_hvp_available(specs)
687 }
688
689 fn outer_hyper_hessian_dense_available(&self, specs: &[ParameterBlockSpec]) -> bool {
690 self.inner_coefficient_hessian_hvp_available(specs)
694 }
695}