1use std::ops::Range;
2
3use crate::{
4 GlobalPenalty, ModelError, Objective, ParameterBlock, ParameterName, ParameterParts,
5 ParameterizedFamily, Penalty, PredictorBlock,
6};
7
8pub trait GamlssBlocks<F> {
10 fn nrows(&self) -> usize;
12 fn len(&self) -> usize;
14
15 fn is_empty(&self) -> bool {
17 self.len() == 0
18 }
19
20 fn validate(&self, y_len: usize) -> Result<(), ModelError>;
22 fn train_nll(&self, family: &F, y: &[f64], beta: &[f64]) -> f64;
24 fn penalty_value(&self, beta: &[f64]) -> f64;
26 fn value(&self, family: &F, y: &[f64], beta: &[f64]) -> f64 {
28 self.train_nll(family, y, beta) + self.penalty_value(beta)
29 }
30 fn gradient_into(&self, family: &F, y: &[f64], beta: &[f64], grad: &mut [f64]);
32 fn gradient_workspace(&self, y_len: usize) -> GradientWorkspace {
34 let mut workspace = GradientWorkspace::new();
35 let ranges = self.block_ranges();
36 workspace.prepare(ranges.len());
37 for (index, range) in ranges.iter().enumerate() {
38 workspace.prepare_score(index, y_len);
39 let _ = workspace.local_gradient_mut(index, range.len());
40 }
41 workspace
42 }
43 fn gradient_into_workspace(
45 &self,
46 family: &F,
47 y: &[f64],
48 beta: &[f64],
49 grad: &mut [f64],
50 _: &mut GradientWorkspace,
51 ) {
52 self.gradient_into(family, y, beta, grad);
53 }
54 fn block_ranges(&self) -> Vec<Range<usize>>;
56 fn parameter_layout(&self) -> ParameterLayout;
58}
59
60#[derive(Debug, Clone, Default, PartialEq)]
66pub struct GradientWorkspace {
67 scores: Vec<Vec<f64>>,
68 local_gradients: Vec<Vec<f64>>,
69}
70
71impl GradientWorkspace {
72 pub fn new() -> Self {
74 Self::default()
75 }
76
77 fn prepare(&mut self, block_count: usize) {
78 self.scores.resize_with(block_count, Vec::new);
79 self.local_gradients.resize_with(block_count, Vec::new);
80 }
81
82 fn prepare_score(&mut self, index: usize, len: usize) {
83 let score = &mut self.scores[index];
84 score.resize(len, 0.0);
85 }
86
87 fn set_score(&mut self, index: usize, row: usize, value: f64) {
88 debug_assert!(index < self.scores.len());
89 debug_assert!(row < self.scores[index].len());
90 self.scores[index][row] = value;
91 }
92
93 fn local_gradient_mut(&mut self, index: usize, len: usize) -> &mut [f64] {
94 let gradient = &mut self.local_gradients[index];
95 gradient.resize(len, 0.0);
96 gradient.fill(0.0);
97 gradient
98 }
99
100 fn score_and_local_gradient_mut(
101 &mut self,
102 index: usize,
103 local_gradient_len: usize,
104 ) -> (&[f64], &mut [f64]) {
105 let local_gradient = &mut self.local_gradients[index];
106 local_gradient.resize(local_gradient_len, 0.0);
107 local_gradient.fill(0.0);
108
109 (self.scores[index].as_slice(), local_gradient.as_mut_slice())
110 }
111}
112
113#[derive(Debug, Clone, PartialEq)]
118pub struct Gamlss<F, Blocks> {
119 pub family: F,
121 pub blocks: Blocks,
123 pub y: Vec<f64>,
125}
126
127#[derive(Debug, Clone, PartialEq)]
133pub struct CachedGamlss<F, Blocks> {
134 pub model: Gamlss<F, Blocks>,
136 pub workspace: GradientWorkspace,
138}
139
140#[derive(Debug, Clone, PartialEq)]
146pub struct WithGlobalPenalties<O, GP> {
147 pub objective: O,
149 pub penalties: GP,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct ParameterSlice {
159 pub name: &'static str,
161 pub range: Range<usize>,
163}
164
165#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct ParameterLayout {
171 slices: Vec<ParameterSlice>,
172}
173
174impl ParameterLayout {
175 pub fn new(slices: Vec<ParameterSlice>) -> Self {
177 Self { slices }
178 }
179
180 pub fn slices(&self) -> &[ParameterSlice] {
182 &self.slices
183 }
184
185 pub fn slice(&self, name: &str) -> Option<Range<usize>> {
187 self.slices
188 .iter()
189 .find(|slice| slice.name == name)
190 .map(|slice| slice.range.clone())
191 }
192}
193
194#[derive(Debug, Clone, PartialEq)]
199pub struct ParameterCoefficients {
200 pub name: &'static str,
202 pub coefficients: Vec<f64>,
204}
205
206#[derive(Debug, Clone, PartialEq)]
211pub struct UnpackedTheta {
212 pub blocks: Vec<ParameterCoefficients>,
214}
215
216impl UnpackedTheta {
217 pub fn block(&self, name: &str) -> Option<&ParameterCoefficients> {
219 self.blocks.iter().find(|block| block.name == name)
220 }
221
222 pub fn coefficients(&self, name: &str) -> Option<&[f64]> {
224 self.block(name).map(|block| block.coefficients.as_slice())
225 }
226}
227
228#[derive(Debug, Clone, Copy, PartialEq)]
233pub struct Diagnostics {
234 pub objective: f64,
236 pub train_nll: f64,
238 pub penalty: f64,
240 pub gradient_norm: f64,
242 pub nonfinite_gradient_count: usize,
244}
245
246impl<F, Blocks> Gamlss<F, Blocks> {
247 pub fn with_global_penalties<GP>(self, penalties: GP) -> WithGlobalPenalties<Self, GP> {
249 WithGlobalPenalties {
250 objective: self,
251 penalties,
252 }
253 }
254}
255
256impl<F, Blocks> Gamlss<F, Blocks>
257where
258 Blocks: GamlssBlocks<F>,
259{
260 pub fn try_new(family: F, blocks: Blocks, y: Vec<f64>) -> Result<Self, ModelError> {
262 if y.is_empty() {
263 return Err(ModelError::EmptyResponse);
264 }
265
266 blocks.validate(y.len())?;
267 Ok(Self { family, blocks, y })
268 }
269
270 pub fn nobs(&self) -> usize {
272 self.y.len()
273 }
274
275 pub fn nparams(&self) -> usize {
277 self.blocks.len()
278 }
279
280 pub fn initial_zeros(&self) -> Vec<f64> {
282 vec![0.0; self.nparams()]
283 }
284
285 pub fn initial_theta(&self) -> Result<Vec<f64>, ModelError> {
287 Ok(self.initial_zeros())
288 }
289
290 pub fn gradient_workspace(&self) -> GradientWorkspace {
292 self.blocks.gradient_workspace(self.y.len())
293 }
294
295 pub fn into_cached_objective(self) -> CachedGamlss<F, Blocks> {
297 let workspace = self.gradient_workspace();
298 CachedGamlss {
299 model: self,
300 workspace,
301 }
302 }
303
304 pub fn block_ranges(&self) -> Vec<Range<usize>> {
306 self.blocks.block_ranges()
307 }
308
309 pub fn parameter_layout(&self) -> ParameterLayout {
311 self.blocks.parameter_layout()
312 }
313
314 pub fn unpack_theta(&self, theta: &[f64]) -> Result<UnpackedTheta, ModelError> {
316 validate_len("theta", theta.len(), self.nparams())?;
317
318 let blocks = self
319 .parameter_layout()
320 .slices()
321 .iter()
322 .map(|slice| ParameterCoefficients {
323 name: slice.name,
324 coefficients: theta[slice.range.clone()].to_vec(),
325 })
326 .collect();
327
328 Ok(UnpackedTheta { blocks })
329 }
330
331 pub fn diagnostics(&self, theta: &[f64]) -> Result<Diagnostics, ModelError> {
333 validate_len("theta", theta.len(), self.nparams())?;
334
335 let train_nll = self.blocks.train_nll(&self.family, &self.y, theta);
336 let penalty = self.blocks.penalty_value(theta);
337 let mut grad = vec![0.0; self.nparams()];
338 self.try_gradient_into(theta, &mut grad)?;
339 let gradient_norm = grad
340 .iter()
341 .filter(|value| value.is_finite())
342 .map(|value| value * value)
343 .sum::<f64>()
344 .sqrt();
345 let nonfinite_gradient_count = grad.iter().filter(|value| !value.is_finite()).count();
346
347 Ok(Diagnostics {
348 objective: train_nll + penalty,
349 train_nll,
350 penalty,
351 gradient_norm,
352 nonfinite_gradient_count,
353 })
354 }
355
356 pub fn try_value(&self, beta: &[f64]) -> Result<f64, ModelError> {
358 let expected = self.nparams();
359 let actual = beta.len();
360 if actual != expected {
361 return Err(ModelError::BetaLength { expected, actual });
362 }
363
364 Ok(self.blocks.value(&self.family, &self.y, beta))
365 }
366
367 pub fn try_gradient_into(&self, beta: &[f64], grad: &mut [f64]) -> Result<(), ModelError> {
369 let mut workspace = self.gradient_workspace();
370 self.try_gradient_into_workspace(beta, grad, &mut workspace)
371 }
372
373 pub fn try_gradient_into_workspace(
375 &self,
376 beta: &[f64],
377 grad: &mut [f64],
378 workspace: &mut GradientWorkspace,
379 ) -> Result<(), ModelError> {
380 let expected = self.nparams();
381 let actual_beta = beta.len();
382 if actual_beta != expected {
383 return Err(ModelError::BetaLength {
384 expected,
385 actual: actual_beta,
386 });
387 }
388
389 let actual_grad = grad.len();
390 if actual_grad != expected {
391 return Err(ModelError::GradientLength {
392 expected,
393 actual: actual_grad,
394 });
395 }
396
397 grad.fill(0.0);
398 self.blocks
399 .gradient_into_workspace(&self.family, &self.y, beta, grad, workspace);
400 Ok(())
401 }
402}
403
404impl<F, Blocks> CachedGamlss<F, Blocks>
405where
406 Blocks: GamlssBlocks<F>,
407{
408 pub fn new(model: Gamlss<F, Blocks>) -> Self {
410 model.into_cached_objective()
411 }
412
413 pub fn model(&self) -> &Gamlss<F, Blocks> {
415 &self.model
416 }
417
418 pub fn model_mut(&mut self) -> &mut Gamlss<F, Blocks> {
420 &mut self.model
421 }
422
423 pub fn into_model(self) -> Gamlss<F, Blocks> {
425 self.model
426 }
427
428 pub fn with_global_penalties<GP>(self, penalties: GP) -> WithGlobalPenalties<Self, GP> {
430 WithGlobalPenalties {
431 objective: self,
432 penalties,
433 }
434 }
435}
436
437impl<F, Blocks> Objective for Gamlss<F, Blocks>
438where
439 Blocks: GamlssBlocks<F>,
440{
441 type Error = ModelError;
442
443 fn dim(&self) -> usize {
444 self.nparams()
445 }
446
447 fn value(&mut self, theta: &[f64]) -> Result<f64, Self::Error> {
448 self.try_value(theta)
449 }
450
451 fn gradient(&mut self, theta: &[f64], grad: &mut [f64]) -> Result<(), Self::Error> {
452 self.try_gradient_into(theta, grad)
453 }
454}
455
456impl<F, Blocks> Objective for CachedGamlss<F, Blocks>
457where
458 Blocks: GamlssBlocks<F>,
459{
460 type Error = ModelError;
461
462 fn dim(&self) -> usize {
463 self.model.nparams()
464 }
465
466 fn value(&mut self, theta: &[f64]) -> Result<f64, Self::Error> {
467 self.model.try_value(theta)
468 }
469
470 fn gradient(&mut self, theta: &[f64], grad: &mut [f64]) -> Result<(), Self::Error> {
471 self.model
472 .try_gradient_into_workspace(theta, grad, &mut self.workspace)
473 }
474}
475
476impl<O, GP> Objective for WithGlobalPenalties<O, GP>
477where
478 O: Objective,
479 GP: GlobalPenalty,
480{
481 type Error = O::Error;
482
483 fn dim(&self) -> usize {
484 self.objective.dim()
485 }
486
487 fn value(&mut self, theta: &[f64]) -> Result<f64, Self::Error> {
488 Ok(self.objective.value(theta)? + self.penalties.value(theta))
489 }
490
491 fn gradient(&mut self, theta: &[f64], grad: &mut [f64]) -> Result<(), Self::Error> {
492 self.objective.gradient(theta, grad)?;
493 self.penalties.add_gradient(theta, grad);
494 Ok(())
495 }
496}
497
498macro_rules! impl_gamlss_blocks {
505 (
506 $k:literal;
507 params = ($($param:ident),+);
508 links = ($($link:ident),+);
509 designs = ($($design:ident),+);
510 penalties = ($($penalty:ident),+);
511 blocks = ($($block:ident),+);
512 beta_blocks = ($($beta_block:ident),+);
513 scores = ($($score:ident),+);
514 local_grads = ($($local_grad:ident),+);
515 indices = ($($idx:tt),+)
516 ) => {
517 impl<F, $($param, $link, $design, $penalty,)+> GamlssBlocks<F>
518 for ($(ParameterBlock<$param, $link, $design, $penalty>,)+)
519 where
520 F: ParameterizedFamily<$k, Params = ($($param,)+), Links = ($($link,)+)>,
521 F::Eta: ParameterParts<$k>,
522 F::ScoreEta: ParameterParts<$k>,
523 $($param: ParameterName,)+
524 $($link: crate::Link<f64>,)+
525 $($design: PredictorBlock,)+
526 $($penalty: Penalty,)+
527 {
528 fn nrows(&self) -> usize {
529 PredictorBlock::nrows(&self.0.x)
530 }
531
532 fn len(&self) -> usize {
533 0$(.max(self.$idx.end()))+
534 }
535
536 fn validate(&self, y_len: usize) -> Result<(), ModelError> {
537 $(
538 self.$idx.x.validate()?;
539 validate_block_rows(
540 <$param as ParameterName>::NAME,
541 PredictorBlock::nrows(&self.$idx.x),
542 y_len,
543 )?;
544 )+
545
546 let ranges = [$((
547 <$param as ParameterName>::NAME,
548 self.$idx.range(),
549 ),)+];
550 for first in 0..ranges.len() {
551 for second in first + 1..ranges.len() {
552 if ranges_overlap(ranges[first].1.clone(), ranges[second].1.clone()) {
553 return Err(ModelError::BlockOverlap {
554 first: ranges[first].0,
555 second: ranges[second].0,
556 });
557 }
558 }
559 }
560
561 Ok(())
562 }
563
564 fn train_nll(&self, family: &F, y: &[f64], beta: &[f64]) -> f64 {
565 $(let $block = &self.$idx;)+
566 $(let $beta_block = &beta[$block.range()];)+
567 let mut loss = 0.0;
568
569 for (row, y_value) in y.iter().copied().enumerate() {
570 let eta = F::Eta::from_array([$($block.x.eta_row(row, $beta_block),)+]);
571 loss += family.nll_eta(y_value, eta);
572 }
573
574 loss
575 }
576
577 fn penalty_value(&self, beta: &[f64]) -> f64 {
578 $(let $block = &self.$idx;)+
579 $(let $beta_block = &beta[$block.range()];)+
580
581 0.0 $(+ $block.penalty.value($beta_block))+
582 }
583
584 fn gradient_into(&self, family: &F, y: &[f64], beta: &[f64], grad: &mut [f64]) {
585 let mut workspace = GradientWorkspace::new();
586 workspace.prepare($k);
587 $(
588 workspace.prepare_score($idx, y.len());
589 let _ = workspace.local_gradient_mut($idx, self.$idx.len());
590 )+
591 self.gradient_into_workspace(family, y, beta, grad, &mut workspace);
592 }
593
594 fn gradient_workspace(&self, y_len: usize) -> GradientWorkspace {
595 let mut workspace = GradientWorkspace::new();
596 workspace.prepare($k);
597 $(
598 workspace.prepare_score($idx, y_len);
599 let _ = workspace.local_gradient_mut($idx, self.$idx.len());
600 )+
601 workspace
602 }
603
604 fn gradient_into_workspace(
605 &self,
606 family: &F,
607 y: &[f64],
608 beta: &[f64],
609 grad: &mut [f64],
610 workspace: &mut GradientWorkspace,
611 ) {
612 $(let $block = &self.$idx;)+
613 $(let $beta_block = &beta[$block.range()];)+
614 workspace.prepare($k);
615 $(workspace.prepare_score($idx, y.len());)+
616
617 for (row, y_value) in y.iter().copied().enumerate() {
618 let eta = F::Eta::from_array([$($block.x.eta_row(row, $beta_block),)+]);
619 let (_, score) = family.nll_and_score_eta(y_value, eta);
620 $(workspace.set_score($idx, row, score.part($idx));)+
621 }
622
623 $(
624 let ($score, $local_grad) =
625 workspace.score_and_local_gradient_mut($idx, $block.len());
626 $block.x.add_gradient($score, $beta_block, $local_grad);
627 $block.penalty.add_gradient($beta_block, $local_grad);
628 add_into(&mut grad[$block.range()], $local_grad);
629 )+
630 }
631
632 fn block_ranges(&self) -> Vec<Range<usize>> {
633 vec![$(self.$idx.range(),)+]
634 }
635
636 fn parameter_layout(&self) -> ParameterLayout {
637 ParameterLayout::new(vec![$(
638 ParameterSlice {
639 name: <$param as ParameterName>::NAME,
640 range: self.$idx.range(),
641 },
642 )+])
643 }
644 }
645 };
646}
647
648impl_gamlss_blocks!(
649 1;
650 params = (P1);
651 links = (L1);
652 designs = (X1);
653 penalties = (Pen1);
654 blocks = (block1);
655 beta_blocks = (beta1);
656 scores = (score1);
657 local_grads = (grad1);
658 indices = (0)
659);
660
661impl_gamlss_blocks!(
662 2;
663 params = (P1, P2);
664 links = (L1, L2);
665 designs = (X1, X2);
666 penalties = (Pen1, Pen2);
667 blocks = (block1, block2);
668 beta_blocks = (beta1, beta2);
669 scores = (score1, score2);
670 local_grads = (grad1, grad2);
671 indices = (0, 1)
672);
673
674impl_gamlss_blocks!(
675 3;
676 params = (P1, P2, P3);
677 links = (L1, L2, L3);
678 designs = (X1, X2, X3);
679 penalties = (Pen1, Pen2, Pen3);
680 blocks = (block1, block2, block3);
681 beta_blocks = (beta1, beta2, beta3);
682 scores = (score1, score2, score3);
683 local_grads = (grad1, grad2, grad3);
684 indices = (0, 1, 2)
685);
686
687impl_gamlss_blocks!(
688 4;
689 params = (P1, P2, P3, P4);
690 links = (L1, L2, L3, L4);
691 designs = (X1, X2, X3, X4);
692 penalties = (Pen1, Pen2, Pen3, Pen4);
693 blocks = (block1, block2, block3, block4);
694 beta_blocks = (beta1, beta2, beta3, beta4);
695 scores = (score1, score2, score3, score4);
696 local_grads = (grad1, grad2, grad3, grad4);
697 indices = (0, 1, 2, 3)
698);
699
700fn validate_block_rows(
702 parameter: &'static str,
703 actual_rows: usize,
704 expected_rows: usize,
705) -> Result<(), ModelError> {
706 if actual_rows == expected_rows {
707 Ok(())
708 } else {
709 Err(ModelError::DesignRowMismatch {
710 parameter,
711 expected_rows,
712 actual_rows,
713 })
714 }
715}
716
717fn ranges_overlap(first: Range<usize>, second: Range<usize>) -> bool {
719 first.start < second.end && second.start < first.end
720}
721
722fn add_into(out: &mut [f64], values: &[f64]) {
726 debug_assert_eq!(out.len(), values.len());
727
728 for (out_value, value) in out.iter_mut().zip(values) {
729 *out_value += value;
730 }
731}
732
733fn validate_len(name: &'static str, actual: usize, expected: usize) -> Result<(), ModelError> {
735 if actual == expected {
736 Ok(())
737 } else if name == "gradient" {
738 Err(ModelError::GradientLength { expected, actual })
739 } else {
740 Err(ModelError::BetaLength { expected, actual })
741 }
742}
743
744#[cfg(test)]
745mod tests {
746 use approx::assert_relative_eq;
747
748 use crate::{
749 DenseDesign, Family, Gamlss, GlobalPenalty, Identity, Mu, NoPenalty, Nu, Objective,
750 ParameterBlock, ParameterizedFamily, PredictorBlock, RidgePenalty, Sigma, SumBlock,
751 };
752
753 #[derive(Debug, Clone, Copy)]
754 struct FixedSigmaNormal;
755
756 impl Family for FixedSigmaNormal {
757 type Eta = f64;
758 type Theta = f64;
759 type ScoreEta = f64;
760
761 fn theta(&self, eta: Self::Eta) -> Self::Theta {
762 eta
763 }
764
765 fn nll(&self, y: f64, theta: Self::Theta) -> f64 {
766 let residual = y - theta;
767 0.5 * residual * residual
768 }
769
770 fn nll_and_score_eta(&self, y: f64, eta: Self::Eta) -> (f64, Self::ScoreEta) {
771 (self.nll(y, self.theta(eta)), eta - y)
772 }
773 }
774
775 impl ParameterizedFamily<1> for FixedSigmaNormal {
776 type Params = (Mu,);
777 type Links = (Identity,);
778 }
779
780 #[test]
781 fn custom_one_parameter_family_uses_generic_family_contract() {
782 let y = vec![1.0, 2.0];
783 let x = DenseDesign::intercept(y.len());
784 let mu = ParameterBlock::<Mu, Identity, _, _>::linear(x, NoPenalty, 0);
785 let mut model = Gamlss::try_new(FixedSigmaNormal, (mu,), y).unwrap();
786 let beta = vec![1.5];
787 let mut grad = vec![0.0];
788
789 assert_relative_eq!(model.value(&beta).unwrap(), 0.25);
790
791 model.gradient(&beta, &mut grad).unwrap();
792
793 assert_relative_eq!(grad[0], 0.0);
794 }
795
796 #[test]
797 fn cached_objective_matches_model_gradient_on_repeated_calls() {
798 let y = vec![1.0, 2.0];
799 let x = DenseDesign::from_rows(&[[1.0, 0.0], [1.0, 1.0]]);
800 let mu = ParameterBlock::<Mu, Identity, _, _>::linear(x, NoPenalty, 0);
801 let model = Gamlss::try_new(FixedSigmaNormal, (mu,), y).unwrap();
802 let mut cached = model.clone().into_cached_objective();
803
804 for beta in [vec![1.0, 0.25], vec![1.5, -0.1]] {
805 let mut expected_grad = vec![0.0; beta.len()];
806 let mut cached_grad = vec![0.0; beta.len()];
807
808 model.try_gradient_into(&beta, &mut expected_grad).unwrap();
809 cached.gradient(&beta, &mut cached_grad).unwrap();
810
811 assert_relative_eq!(
812 cached.value(&beta).unwrap(),
813 model.try_value(&beta).unwrap()
814 );
815 for (actual, expected) in cached_grad.iter().zip(&expected_grad) {
816 assert_relative_eq!(actual, expected);
817 }
818 }
819 }
820
821 #[derive(Debug, Clone, Copy)]
822 struct SoftplusIntercept {
823 nrows: usize,
824 }
825
826 impl PredictorBlock for SoftplusIntercept {
827 fn nrows(&self) -> usize {
828 self.nrows
829 }
830
831 fn nparams(&self) -> usize {
832 1
833 }
834
835 fn eta_row(&self, _: usize, beta: &[f64]) -> f64 {
836 softplus(beta[0])
837 }
838
839 fn add_gradient(&self, scores: &[f64], beta: &[f64], grad: &mut [f64]) {
840 debug_assert_eq!(grad.len(), 1);
841 grad[0] += scores.iter().sum::<f64>() * sigmoid(beta[0]);
842 }
843 }
844
845 #[test]
846 fn sum_block_supports_user_defined_nonlinear_predictors() {
847 let y = vec![1.0, 2.0];
848 let linear = crate::LinearPredictorBlock::new(DenseDesign::intercept(y.len()));
849 let nonlinear = SoftplusIntercept { nrows: y.len() };
850 let predictor = SumBlock::new((linear, nonlinear));
851 let mu = ParameterBlock::<Mu, Identity, _, _>::new(predictor, NoPenalty, 0);
852 let mut model = Gamlss::try_new(FixedSigmaNormal, (mu,), y).unwrap();
853 let beta = vec![0.4, -0.2];
854 let eps = 1.0e-6;
855 let mut grad = vec![0.0; beta.len()];
856
857 model.gradient(&beta, &mut grad).unwrap();
858
859 for index in 0..beta.len() {
860 let mut plus = beta.clone();
861 plus[index] += eps;
862 let mut minus = beta.clone();
863 minus[index] -= eps;
864 let finite_difference =
865 (model.value(&plus).unwrap() - model.value(&minus).unwrap()) / (2.0 * eps);
866
867 assert_relative_eq!(grad[index], finite_difference, epsilon = 1.0e-6);
868 }
869 }
870
871 #[derive(Debug, Clone, Copy)]
872 struct StatefulLocation {
873 target_shift: f64,
874 }
875
876 impl Family for StatefulLocation {
877 type Eta = f64;
878 type Theta = f64;
879 type ScoreEta = f64;
880
881 fn theta(&self, eta: Self::Eta) -> Self::Theta {
882 eta + self.target_shift
883 }
884
885 fn nll(&self, y: f64, theta: Self::Theta) -> f64 {
886 let residual = y - theta;
887 0.5 * residual * residual
888 }
889
890 fn nll_and_score_eta(&self, y: f64, eta: Self::Eta) -> (f64, Self::ScoreEta) {
891 let theta = self.theta(eta);
892 (self.nll(y, theta), theta - y)
893 }
894 }
895
896 impl ParameterizedFamily<1> for StatefulLocation {
897 type Params = (Mu,);
898 type Links = (Identity,);
899 }
900
901 #[test]
902 fn family_instance_state_participates_in_objective() {
903 let y = vec![2.0];
904 let x = DenseDesign::intercept(y.len());
905 let mu = ParameterBlock::<Mu, Identity, _, _>::linear(x, NoPenalty, 0);
906 let mut model = Gamlss::try_new(StatefulLocation { target_shift: 1.0 }, (mu,), y).unwrap();
907 let beta = vec![0.5];
908 let mut grad = vec![0.0];
909
910 assert_relative_eq!(model.value(&beta).unwrap(), 0.125);
911
912 model.gradient(&beta, &mut grad).unwrap();
913
914 assert_relative_eq!(grad[0], -0.5);
915 }
916
917 #[derive(Debug, Clone, Copy)]
918 struct ThreeParameterMock;
919
920 impl Family for ThreeParameterMock {
921 type Eta = (f64, f64, f64);
922 type Theta = (f64, f64, f64);
923 type ScoreEta = (f64, f64, f64);
924
925 fn theta(&self, eta: Self::Eta) -> Self::Theta {
926 eta
927 }
928
929 fn nll(&self, y: f64, theta: Self::Theta) -> f64 {
930 let first = theta.0 - y;
931 let second = theta.1 - 1.0;
932 let third = theta.2 + 1.0;
933 0.5 * (first * first + second * second + third * third)
934 }
935
936 fn nll_and_score_eta(&self, y: f64, eta: Self::Eta) -> (f64, Self::ScoreEta) {
937 let score = (eta.0 - y, eta.1 - 1.0, eta.2 + 1.0);
938 (self.nll(y, eta), score)
939 }
940 }
941
942 impl ParameterizedFamily<3> for ThreeParameterMock {
943 type Params = (Mu, Sigma, Nu);
944 type Links = (Identity, Identity, Identity);
945 }
946
947 #[test]
948 fn custom_three_parameter_family_uses_generic_blocks() {
949 let y = vec![2.0];
950 let first = ParameterBlock::<Mu, Identity, _, _>::linear(
951 DenseDesign::intercept(y.len()),
952 NoPenalty,
953 0,
954 );
955 let second = ParameterBlock::<Sigma, Identity, _, _>::linear(
956 DenseDesign::intercept(y.len()),
957 NoPenalty,
958 1,
959 );
960 let third = ParameterBlock::<Nu, Identity, _, _>::linear(
961 DenseDesign::intercept(y.len()),
962 NoPenalty,
963 2,
964 );
965 let mut model = Gamlss::try_new(ThreeParameterMock, (first, second, third), y).unwrap();
966 let beta = vec![1.5, 0.5, -0.5];
967 let mut grad = vec![0.0; 3];
968
969 assert_relative_eq!(model.value(&beta).unwrap(), 0.375);
970
971 model.gradient(&beta, &mut grad).unwrap();
972
973 assert_relative_eq!(grad[0], -0.5);
974 assert_relative_eq!(grad[1], -0.5);
975 assert_relative_eq!(grad[2], 0.5);
976 }
977
978 #[test]
979 fn parameter_layout_and_unpack_use_distribution_parameter_names() {
980 let y = vec![2.0];
981 let first = ParameterBlock::<Mu, Identity, _, _>::linear(
982 DenseDesign::intercept(y.len()),
983 NoPenalty,
984 0,
985 );
986 let second = ParameterBlock::<Sigma, Identity, _, _>::linear(
987 DenseDesign::intercept(y.len()),
988 NoPenalty,
989 1,
990 );
991 let third = ParameterBlock::<Nu, Identity, _, _>::linear(
992 DenseDesign::intercept(y.len()),
993 NoPenalty,
994 2,
995 );
996 let model = Gamlss::try_new(ThreeParameterMock, (first, second, third), y).unwrap();
997 let theta = vec![1.5, 0.5, -0.5];
998 let layout = model.parameter_layout();
999 let unpacked = model.unpack_theta(&theta).unwrap();
1000
1001 assert_eq!(layout.slice("mu").unwrap(), 0..1);
1002 assert_eq!(layout.slice("sigma").unwrap(), 1..2);
1003 assert_eq!(layout.slice("nu").unwrap(), 2..3);
1004 assert_eq!(unpacked.coefficients("mu").unwrap(), &[1.5]);
1005 assert_eq!(unpacked.coefficients("sigma").unwrap(), &[0.5]);
1006 assert_eq!(unpacked.coefficients("nu").unwrap(), &[-0.5]);
1007 }
1008
1009 #[test]
1010 fn diagnostics_report_train_nll_penalty_and_gradient_norm() {
1011 let y = vec![1.0, 2.0];
1012 let x = DenseDesign::intercept(y.len());
1013 let mu = ParameterBlock::<Mu, Identity, _, _>::linear(x, RidgePenalty::new(0.5), 0);
1014 let model = Gamlss::try_new(FixedSigmaNormal, (mu,), y).unwrap();
1015 let theta = vec![1.5];
1016 let diagnostics = model.diagnostics(&theta).unwrap();
1017
1018 assert_relative_eq!(diagnostics.train_nll, 0.25);
1019 assert_relative_eq!(diagnostics.penalty, 1.125);
1020 assert_relative_eq!(diagnostics.objective, 1.375);
1021 assert_relative_eq!(diagnostics.gradient_norm, 1.5);
1022 assert_eq!(diagnostics.nonfinite_gradient_count, 0);
1023 }
1024
1025 #[derive(Debug, Clone, Copy)]
1026 struct DifferenceGlobalPenalty {
1027 lambda: f64,
1028 }
1029
1030 impl GlobalPenalty for DifferenceGlobalPenalty {
1031 fn value(&self, beta: &[f64]) -> f64 {
1032 let diff = beta[0] - beta[1];
1033 self.lambda * diff * diff
1034 }
1035
1036 fn add_gradient(&self, beta: &[f64], grad: &mut [f64]) {
1037 let diff = beta[0] - beta[1];
1038 let slope = 2.0 * self.lambda * diff;
1039 grad[0] += slope;
1040 grad[1] -= slope;
1041 }
1042 }
1043
1044 #[test]
1045 fn global_penalty_adds_value_and_gradient_to_full_objective() {
1046 let y = vec![0.0, 0.0];
1047 let x = DenseDesign::from_rows(&[[1.0, 0.0], [0.0, 1.0]]);
1048 let mu = ParameterBlock::<Mu, Identity, _, _>::linear(x, NoPenalty, 0);
1049 let mut model = Gamlss::try_new(FixedSigmaNormal, (mu,), y)
1050 .unwrap()
1051 .with_global_penalties(DifferenceGlobalPenalty { lambda: 1.0 });
1052 let beta = vec![1.0, -1.0];
1053 let mut grad = vec![0.0; beta.len()];
1054
1055 assert_relative_eq!(model.value(&beta).unwrap(), 5.0);
1056
1057 model.gradient(&beta, &mut grad).unwrap();
1058
1059 assert_relative_eq!(grad[0], 5.0);
1060 assert_relative_eq!(grad[1], -5.0);
1061 }
1062
1063 fn softplus(value: f64) -> f64 {
1064 if value > 30.0 {
1065 value
1066 } else if value < -30.0 {
1067 value.exp()
1068 } else {
1069 value.exp().ln_1p()
1070 }
1071 }
1072
1073 fn sigmoid(value: f64) -> f64 {
1074 if value >= 0.0 {
1075 1.0 / (1.0 + (-value).exp())
1076 } else {
1077 let exp_value = value.exp();
1078 exp_value / (1.0 + exp_value)
1079 }
1080 }
1081}