1use std::ops::Range;
2
3use crate::{
4 BlockObjective, Family, GlobalPenalty, ModelError, Objective, ParameterBlock, ParameterName,
5 ParameterParts, ParameterizedFamily, Penalty, PredictorBlock,
6};
7
8mod layout;
9mod observation;
10mod workspace;
11
12pub use layout::{
13 ParameterCoefficients, ParameterLayout, ParameterSlice, TrainingDiagnostics, UnpackedTheta,
14};
15pub use observation::ObservationView;
16pub use workspace::GradientWorkspace;
17
18pub trait GamlssBlocks<F>
26where
27 F: Family,
28{
29 fn nrows(&self) -> usize;
31 fn len(&self) -> usize;
33
34 fn is_empty(&self) -> bool {
36 self.len() == 0
37 }
38
39 fn validate(&self, nobs: usize) -> Result<(), ModelError>;
41 fn train_nll<'obs, Obs>(&self, family: &F, obs: &'obs Obs, beta: &[f64]) -> f64
47 where
48 Obs: ObservationView<'obs, Observation = F::Observation<'obs>> + 'obs;
49 fn eta_row(&self, beta: &[f64], row: usize) -> F::Eta
51 where
52 F: Family;
53 fn penalty_value(&self, beta: &[f64]) -> f64;
55 fn add_penalty_gradient(&self, _beta: &[f64], _grad: &mut [f64]) {}
63 fn value<'obs, Obs>(&self, family: &F, obs: &'obs Obs, beta: &[f64]) -> f64
65 where
66 Obs: ObservationView<'obs, Observation = F::Observation<'obs>> + 'obs,
67 {
68 self.train_nll(family, obs, beta) + self.penalty_value(beta)
69 }
70 fn gradient_workspace(&self, nobs: usize) -> GradientWorkspace {
72 let mut workspace = GradientWorkspace::new();
73 let ranges = self.block_ranges();
74 workspace.prepare(ranges.len());
75 for (index, range) in ranges.iter().enumerate() {
76 workspace.prepare_row_gradient(index, nobs);
77 let _ = workspace.local_gradient_mut(index, range.len());
78 }
79 workspace
80 }
81 fn gradient_into_workspace<'obs, Obs>(
86 &self,
87 family: &F,
88 obs: &'obs Obs,
89 beta: &[f64],
90 grad: &mut [f64],
91 workspace: &mut GradientWorkspace,
92 ) where
93 Obs: ObservationView<'obs, Observation = F::Observation<'obs>> + 'obs,
94 {
95 let _ = self.value_gradient_into_workspace(family, obs, beta, grad, workspace);
96 }
97
98 fn value_gradient_into_workspace<'obs, Obs>(
100 &self,
101 family: &F,
102 obs: &'obs Obs,
103 beta: &[f64],
104 grad: &mut [f64],
105 workspace: &mut GradientWorkspace,
106 ) -> f64
107 where
108 Obs: ObservationView<'obs, Observation = F::Observation<'obs>> + 'obs;
109 fn block_ranges(&self) -> Vec<Range<usize>>;
111 fn parameter_layout(&self) -> ParameterLayout;
113
114 fn visit_block_ranges<V>(&self, mut visit: V)
116 where
117 V: FnMut(usize, Range<usize>),
118 {
119 for (index, range) in self.block_ranges().into_iter().enumerate() {
120 visit(index, range);
121 }
122 }
123
124 fn parameter_slice_count(&self) -> usize {
125 self.parameter_layout().slices().len()
126 }
127
128 #[doc(hidden)]
129 fn parameter_slice_matches(
130 &self,
131 index: usize,
132 name: &'static str,
133 range: Range<usize>,
134 ) -> bool {
135 self.parameter_layout()
136 .slices()
137 .get(index)
138 .is_some_and(|slice| slice.name == name && slice.range == range)
139 }
140
141 fn visit_parameter_slices<V>(&self, mut visit: V)
143 where
144 V: FnMut(usize, &'static str, Range<usize>),
145 {
146 for (index, slice) in self.parameter_layout().slices().iter().enumerate() {
147 visit(index, slice.name, slice.range.clone());
148 }
149 }
150
151 #[doc(hidden)]
152 fn parameter_slice_of<P>(&self) -> Option<Range<usize>>
153 where
154 P: ParameterName,
155 {
156 let mut found = None;
157 self.visit_parameter_slices(|_, name, range| {
158 if name == P::NAME && found.is_none() {
159 found = Some(range);
160 }
161 });
162 found
163 }
164
165 #[doc(hidden)]
166 fn has_same_parameter_layout<Other>(&self, other: &Other) -> bool
167 where
168 Other: GamlssBlocks<F>,
169 {
170 let mut got_count = 0;
171 let mut matches = true;
172
173 other.visit_parameter_slices(|index, name, range| {
174 got_count += 1;
175 matches &= self.parameter_slice_matches(index, name, range);
176 });
177
178 matches && got_count == self.parameter_slice_count()
179 }
180}
181
182#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
187pub enum ObjectiveScale {
188 #[default]
190 Sum,
191 Mean,
197}
198
199impl ObjectiveScale {
200 #[inline(always)]
201 fn likelihood_multiplier(self, weight_sum: f64) -> f64 {
202 match self {
203 Self::Sum => 1.0,
204 Self::Mean if weight_sum > 0.0 => 1.0 / weight_sum,
205 Self::Mean => 1.0,
206 }
207 }
208}
209
210#[derive(Debug, Clone, PartialEq)]
220pub struct Gamlss<F, Blocks, Obs> {
221 pub family: F,
223 pub blocks: Blocks,
225 pub obs: Obs,
227 pub objective_scale: ObjectiveScale,
229}
230
231#[derive(Debug, Clone, PartialEq)]
237pub struct WorkspaceGamlss<F, Blocks, Obs> {
238 pub model: Gamlss<F, Blocks, Obs>,
240 pub workspace: GradientWorkspace,
242}
243
244#[derive(Debug, Clone, PartialEq)]
250pub struct WithGlobalPenalties<O, GP> {
251 pub objective: O,
253 pub penalties: GP,
255}
256
257impl<F, Blocks, Obs> Gamlss<F, Blocks, Obs> {
258 pub fn with_global_penalties<GP>(self, penalties: GP) -> WithGlobalPenalties<Self, GP> {
260 WithGlobalPenalties {
261 objective: self,
262 penalties,
263 }
264 }
265}
266
267impl<F, Blocks, Obs> Gamlss<F, Blocks, Obs>
268where
269 F: Family,
270 Blocks: GamlssBlocks<F>,
271 for<'row> Obs: ObservationView<'row, Observation = F::Observation<'row>>,
272{
273 pub fn try_new_with_observations(
279 family: F,
280 blocks: Blocks,
281 obs: Obs,
282 ) -> Result<Self, ModelError> {
283 if obs.is_empty() {
284 return Err(ModelError::EmptyResponse);
285 }
286
287 obs.validate()?;
288 blocks.validate(obs.len())?;
289 Ok(Self {
290 family,
291 blocks,
292 obs,
293 objective_scale: ObjectiveScale::Sum,
294 })
295 }
296
297 pub fn nobs(&self) -> usize {
299 self.obs.len()
300 }
301
302 pub fn nparams(&self) -> usize {
304 self.blocks.len()
305 }
306
307 pub fn objective_scale(&self) -> ObjectiveScale {
309 self.objective_scale
310 }
311
312 #[must_use]
314 pub fn with_objective_scale(mut self, objective_scale: ObjectiveScale) -> Self {
315 self.objective_scale = objective_scale;
316 self
317 }
318
319 pub fn set_objective_scale(&mut self, objective_scale: ObjectiveScale) {
321 self.objective_scale = objective_scale;
322 }
323
324 fn likelihood_multiplier(&self) -> f64 {
325 self.objective_scale
326 .likelihood_multiplier(observation_weight_sum(&self.obs))
327 }
328
329 pub fn initial_zeros(&self) -> Vec<f64> {
331 vec![0.0; self.nparams()]
332 }
333
334 pub fn initial_theta(&self) -> Result<Vec<f64>, ModelError> {
336 Ok(self.initial_zeros())
337 }
338
339 pub fn gradient_workspace(&self) -> GradientWorkspace {
341 self.blocks.gradient_workspace(self.obs.len())
342 }
343
344 pub fn into_workspace_objective(self) -> WorkspaceGamlss<F, Blocks, Obs> {
346 let workspace = self.gradient_workspace();
347 WorkspaceGamlss {
348 model: self,
349 workspace,
350 }
351 }
352
353 pub fn block_ranges(&self) -> Vec<Range<usize>> {
355 self.blocks.block_ranges()
356 }
357
358 pub fn visit_block_ranges<V>(&self, visit: V)
360 where
361 V: FnMut(usize, Range<usize>),
362 {
363 self.blocks.visit_block_ranges(visit);
364 }
365
366 pub fn parameter_layout(&self) -> ParameterLayout {
368 self.blocks.parameter_layout()
369 }
370
371 pub fn visit_parameter_slices<V>(&self, visit: V)
373 where
374 V: FnMut(usize, &'static str, Range<usize>),
375 {
376 self.blocks.visit_parameter_slices(visit);
377 }
378
379 pub fn block_objective_for<P>(
393 &mut self,
394 full_beta: Vec<f64>,
395 ) -> Result<BlockObjective<'_, Self>, ModelError>
396 where
397 P: ParameterName,
398 {
399 validate_len("theta", full_beta.len(), self.nparams())?;
400 let range = self
401 .blocks
402 .parameter_slice_of::<P>()
403 .ok_or(ModelError::UnknownParameter { name: P::NAME })?;
404 Ok(BlockObjective::new(self, full_beta, range))
405 }
406
407 pub fn unpack_theta(&self, theta: &[f64]) -> Result<UnpackedTheta, ModelError> {
409 validate_len("theta", theta.len(), self.nparams())?;
410
411 let blocks = self
412 .parameter_layout()
413 .slices()
414 .iter()
415 .map(|slice| ParameterCoefficients {
416 name: slice.name,
417 coefficients: theta[slice.range.clone()].to_vec(),
418 })
419 .collect();
420
421 Ok(UnpackedTheta { blocks })
422 }
423
424 pub fn training_diagnostics(&self, theta: &[f64]) -> Result<TrainingDiagnostics, ModelError> {
426 validate_len("theta", theta.len(), self.nparams())?;
427
428 let likelihood_multiplier = self.likelihood_multiplier();
429 let train_nll =
430 likelihood_multiplier * self.blocks.train_nll(&self.family, &self.obs, theta);
431 let penalty = self.blocks.penalty_value(theta);
432 let mut grad = vec![0.0; self.nparams()];
433 self.try_gradient_into(theta, &mut grad)?;
434 let (finite_gradient_sum_squares, nonfinite_gradient_count) =
435 grad.iter().fold((0.0, 0), |(sum_squares, count), value| {
436 if value.is_finite() {
437 (sum_squares + value * value, count)
438 } else {
439 (sum_squares, count + 1)
440 }
441 });
442 let gradient_norm = finite_gradient_sum_squares.sqrt();
443
444 Ok(TrainingDiagnostics {
445 objective: train_nll + penalty,
446 train_nll,
447 penalty,
448 gradient_norm,
449 nonfinite_gradient_count,
450 })
451 }
452
453 pub fn predict_eta_row(&self, theta: &[f64], row: usize) -> Result<F::Eta, ModelError>
455 where
456 F: Family,
457 {
458 validate_len("theta", theta.len(), self.nparams())?;
459 validate_row(row, self.nobs())?;
460 Ok(self.blocks.eta_row(theta, row))
461 }
462
463 pub fn predict_theta_row(&self, theta: &[f64], row: usize) -> Result<F::Theta, ModelError>
465 where
466 F: Family,
467 {
468 Ok(self.family.theta(self.predict_eta_row(theta, row)?))
469 }
470
471 pub fn predict_eta(&self, theta: &[f64]) -> Result<Vec<F::Eta>, ModelError>
473 where
474 F: Family,
475 {
476 validate_len("theta", theta.len(), self.nparams())?;
477 Ok((0..self.nobs())
478 .map(|row| self.blocks.eta_row(theta, row))
479 .collect())
480 }
481
482 pub fn predict_theta(&self, theta: &[f64]) -> Result<Vec<F::Theta>, ModelError>
484 where
485 F: Family,
486 {
487 validate_len("theta", theta.len(), self.nparams())?;
488 Ok((0..self.nobs())
489 .map(|row| self.family.theta(self.blocks.eta_row(theta, row)))
490 .collect())
491 }
492
493 pub fn predict_eta_row_with_blocks<PBlocks>(
501 &self,
502 theta: &[f64],
503 blocks: &PBlocks,
504 row: usize,
505 ) -> Result<F::Eta, ModelError>
506 where
507 F: Family,
508 PBlocks: GamlssBlocks<F>,
509 {
510 validate_len("theta", theta.len(), self.nparams())?;
511 validate_prediction_blocks(&self.blocks, blocks)?;
512 validate_row(row, blocks.nrows())?;
513 Ok(blocks.eta_row(theta, row))
514 }
515
516 pub fn predict_theta_row_with_blocks<PBlocks>(
524 &self,
525 theta: &[f64],
526 blocks: &PBlocks,
527 row: usize,
528 ) -> Result<F::Theta, ModelError>
529 where
530 F: Family,
531 PBlocks: GamlssBlocks<F>,
532 {
533 Ok(self
534 .family
535 .theta(self.predict_eta_row_with_blocks(theta, blocks, row)?))
536 }
537
538 pub fn predict_eta_with_blocks<PBlocks>(
545 &self,
546 theta: &[f64],
547 blocks: &PBlocks,
548 ) -> Result<Vec<F::Eta>, ModelError>
549 where
550 F: Family,
551 PBlocks: GamlssBlocks<F>,
552 {
553 validate_len("theta", theta.len(), self.nparams())?;
554 validate_prediction_blocks(&self.blocks, blocks)?;
555 Ok((0..blocks.nrows())
556 .map(|row| blocks.eta_row(theta, row))
557 .collect())
558 }
559
560 pub fn predict_theta_with_blocks<PBlocks>(
567 &self,
568 theta: &[f64],
569 blocks: &PBlocks,
570 ) -> Result<Vec<F::Theta>, ModelError>
571 where
572 F: Family,
573 PBlocks: GamlssBlocks<F>,
574 {
575 validate_len("theta", theta.len(), self.nparams())?;
576 validate_prediction_blocks(&self.blocks, blocks)?;
577 Ok((0..blocks.nrows())
578 .map(|row| self.family.theta(blocks.eta_row(theta, row)))
579 .collect())
580 }
581
582 pub fn try_value(&self, beta: &[f64]) -> Result<f64, ModelError> {
584 validate_len("theta", beta.len(), self.nparams())?;
585
586 let train_nll = self.blocks.train_nll(&self.family, &self.obs, beta);
587 let penalty = self.blocks.penalty_value(beta);
588 Ok(self.likelihood_multiplier() * train_nll + penalty)
589 }
590
591 pub fn try_gradient_into(&self, beta: &[f64], grad: &mut [f64]) -> Result<(), ModelError> {
593 let mut workspace = self.gradient_workspace();
594 self.try_gradient_into_workspace(beta, grad, &mut workspace)
595 }
596
597 pub fn try_gradient_into_workspace(
599 &self,
600 beta: &[f64],
601 grad: &mut [f64],
602 workspace: &mut GradientWorkspace,
603 ) -> Result<(), ModelError> {
604 validate_beta_and_gradient_len(self.nparams(), beta, grad)?;
605
606 grad.fill(0.0);
607 let value = self.blocks.value_gradient_into_workspace(
608 &self.family,
609 &self.obs,
610 beta,
611 grad,
612 workspace,
613 );
614 self.scale_value_gradient(beta, grad, workspace, value);
615 Ok(())
616 }
617
618 pub fn try_value_gradient_into(
620 &self,
621 beta: &[f64],
622 grad: &mut [f64],
623 ) -> Result<f64, ModelError> {
624 let mut workspace = self.gradient_workspace();
625 self.try_value_gradient_into_workspace(beta, grad, &mut workspace)
626 }
627
628 pub fn try_value_gradient_into_workspace(
630 &self,
631 beta: &[f64],
632 grad: &mut [f64],
633 workspace: &mut GradientWorkspace,
634 ) -> Result<f64, ModelError> {
635 validate_beta_and_gradient_len(self.nparams(), beta, grad)?;
636
637 grad.fill(0.0);
638 let value = self.blocks.value_gradient_into_workspace(
639 &self.family,
640 &self.obs,
641 beta,
642 grad,
643 workspace,
644 );
645 Ok(self.scale_value_gradient(beta, grad, workspace, value))
646 }
647
648 fn scale_value_gradient(
649 &self,
650 beta: &[f64],
651 grad: &mut [f64],
652 workspace: &mut GradientWorkspace,
653 unscaled_value: f64,
654 ) -> f64 {
655 let likelihood_multiplier = self.likelihood_multiplier();
656 if likelihood_multiplier == 1.0 {
657 return unscaled_value;
658 }
659
660 let penalty = self.blocks.penalty_value(beta);
661 let penalty_grad = workspace.penalty_gradient_mut(self.nparams());
662 self.blocks.add_penalty_gradient(beta, penalty_grad);
663
664 for (grad_value, penalty_grad_value) in grad.iter_mut().zip(penalty_grad.iter().copied()) {
665 *grad_value = (*grad_value - penalty_grad_value)
666 .mul_add(likelihood_multiplier, penalty_grad_value);
667 }
668
669 (unscaled_value - penalty).mul_add(likelihood_multiplier, penalty)
670 }
671}
672
673impl<'a, F, Blocks> Gamlss<F, Blocks, &'a [f64]>
674where
675 F: for<'obs> Family<Observation<'obs> = f64>,
676 Blocks: GamlssBlocks<F>,
677{
678 pub fn try_new(family: F, blocks: Blocks, y: &'a [f64]) -> Result<Self, ModelError> {
680 Self::try_new_with_observations(family, blocks, y)
681 }
682}
683
684impl<'a, F, Blocks> Gamlss<F, Blocks, (&'a [f64], &'a [f64])>
685where
686 F: for<'obs> Family<Observation<'obs> = f64>,
687 Blocks: GamlssBlocks<F>,
688{
689 pub fn try_new_weighted(
695 family: F,
696 blocks: Blocks,
697 y: &'a [f64],
698 weights: &'a [f64],
699 ) -> Result<Self, ModelError> {
700 Self::try_new_with_observations(family, blocks, (y, weights))
701 }
702}
703
704impl<F, Blocks, Obs> WorkspaceGamlss<F, Blocks, Obs>
705where
706 F: Family,
707 Blocks: GamlssBlocks<F>,
708 for<'row> Obs: ObservationView<'row, Observation = F::Observation<'row>>,
709{
710 pub fn new(model: Gamlss<F, Blocks, Obs>) -> Self {
712 model.into_workspace_objective()
713 }
714
715 pub fn model(&self) -> &Gamlss<F, Blocks, Obs> {
717 &self.model
718 }
719
720 pub fn model_mut(&mut self) -> &mut Gamlss<F, Blocks, Obs> {
722 &mut self.model
723 }
724
725 pub fn into_model(self) -> Gamlss<F, Blocks, Obs> {
727 self.model
728 }
729
730 pub fn objective_scale(&self) -> ObjectiveScale {
732 self.model.objective_scale()
733 }
734
735 #[must_use]
737 pub fn with_objective_scale(mut self, objective_scale: ObjectiveScale) -> Self {
738 self.model.set_objective_scale(objective_scale);
739 self
740 }
741
742 pub fn set_objective_scale(&mut self, objective_scale: ObjectiveScale) {
744 self.model.set_objective_scale(objective_scale);
745 }
746
747 pub fn block_objective_for<P>(
758 &mut self,
759 full_beta: Vec<f64>,
760 ) -> Result<BlockObjective<'_, Self>, ModelError>
761 where
762 P: ParameterName,
763 {
764 validate_len("theta", full_beta.len(), self.dim())?;
765 let range = self
766 .model
767 .blocks
768 .parameter_slice_of::<P>()
769 .ok_or(ModelError::UnknownParameter { name: P::NAME })?;
770 Ok(BlockObjective::new(self, full_beta, range))
771 }
772
773 pub fn with_global_penalties<GP>(self, penalties: GP) -> WithGlobalPenalties<Self, GP> {
775 WithGlobalPenalties {
776 objective: self,
777 penalties,
778 }
779 }
780}
781
782impl<F, Blocks, Obs> Objective for Gamlss<F, Blocks, Obs>
783where
784 F: Family,
785 Blocks: GamlssBlocks<F>,
786 for<'row> Obs: ObservationView<'row, Observation = F::Observation<'row>>,
787{
788 type Error = ModelError;
789
790 fn dim(&self) -> usize {
791 self.nparams()
792 }
793
794 fn value(&mut self, theta: &[f64]) -> Result<f64, Self::Error> {
795 self.try_value(theta)
796 }
797
798 fn gradient(&mut self, theta: &[f64], grad: &mut [f64]) -> Result<(), Self::Error> {
799 self.try_value_gradient_into(theta, grad).map(|_| ())
800 }
801
802 fn value_gradient(&mut self, theta: &[f64], grad: &mut [f64]) -> Result<f64, Self::Error> {
803 self.try_value_gradient_into(theta, grad)
804 }
805}
806
807impl<F, Blocks, Obs> Objective for WorkspaceGamlss<F, Blocks, Obs>
808where
809 F: Family,
810 Blocks: GamlssBlocks<F>,
811 for<'row> Obs: ObservationView<'row, Observation = F::Observation<'row>>,
812{
813 type Error = ModelError;
814
815 fn dim(&self) -> usize {
816 self.model.nparams()
817 }
818
819 fn value(&mut self, theta: &[f64]) -> Result<f64, Self::Error> {
820 self.model.try_value(theta)
821 }
822
823 fn gradient(&mut self, theta: &[f64], grad: &mut [f64]) -> Result<(), Self::Error> {
824 self.model
825 .try_value_gradient_into_workspace(theta, grad, &mut self.workspace)
826 .map(|_| ())
827 }
828
829 fn value_gradient(&mut self, theta: &[f64], grad: &mut [f64]) -> Result<f64, Self::Error> {
830 self.model
831 .try_value_gradient_into_workspace(theta, grad, &mut self.workspace)
832 }
833}
834
835impl<O, GP> Objective for WithGlobalPenalties<O, GP>
836where
837 O: Objective,
838 GP: GlobalPenalty,
839{
840 type Error = O::Error;
841
842 fn dim(&self) -> usize {
843 self.objective.dim()
844 }
845
846 fn value(&mut self, theta: &[f64]) -> Result<f64, Self::Error> {
847 Ok(self.objective.value(theta)? + self.penalties.value(theta))
848 }
849
850 fn gradient(&mut self, theta: &[f64], grad: &mut [f64]) -> Result<(), Self::Error> {
851 self.value_gradient(theta, grad).map(|_| ())
852 }
853
854 fn value_gradient(&mut self, theta: &[f64], grad: &mut [f64]) -> Result<f64, Self::Error> {
855 let mut value = self.objective.value_gradient(theta, grad)?;
856 value += self.penalties.value(theta);
857 self.penalties.add_gradient(theta, grad);
858 Ok(value)
859 }
860}
861
862macro_rules! impl_gamlss_blocks {
869 (
870 $k:literal;
871 params = ($($param:ident),+);
872 links = ($($link:ident),+);
873 designs = ($($design:ident),+);
874 penalties = ($($penalty:ident),+);
875 blocks = ($($block:ident),+);
876 beta_blocks = ($($beta_block:ident),+);
877 row_gradients = ($($row_gradient:ident),+);
878 local_grads = ($($local_grad:ident),+);
879 indices = ($($idx:tt),+)
880 ) => {
881 impl<F, $($param, $link, $design, $penalty,)+> GamlssBlocks<F>
882 for ($(ParameterBlock<$param, $link, $design, $penalty>,)+)
883 where
884 F: ParameterizedFamily<$k, Params = ($($param,)+), Links = ($($link,)+)>,
885 F::Eta: ParameterParts<$k>,
886 F::NllGradientEta: ParameterParts<$k>,
887 $($param: ParameterName,)+
888 $($link: crate::Link<f64>,)+
889 $($design: PredictorBlock,)+
890 $($penalty: Penalty,)+
891 {
892 fn nrows(&self) -> usize {
893 PredictorBlock::nrows(&self.0.x)
894 }
895
896 fn len(&self) -> usize {
897 0$(.max(self.$idx.offset.saturating_add(self.$idx.len)))+
898 }
899
900 fn validate(&self, y_len: usize) -> Result<(), ModelError> {
901 $(
902 self.$idx.x.validate()?;
903 validate_block_rows(
904 <$param as ParameterName>::NAME,
905 PredictorBlock::nrows(&self.$idx.x),
906 y_len,
907 )?;
908 )+
909
910 let ranges = [$((
911 <$param as ParameterName>::NAME,
912 self.$idx.try_range()?,
913 ),)+];
914 for (first_index, first) in ranges.iter().enumerate() {
915 for second in ranges.iter().skip(first_index + 1) {
916 if ranges_overlap(first.1.clone(), second.1.clone()) {
917 return Err(ModelError::BlockOverlap {
918 first: first.0,
919 second: second.0,
920 });
921 }
922 }
923 }
924
925 Ok(())
926 }
927
928 fn train_nll<'obs, Obs>(
929 &self,
930 family: &F,
931 obs: &'obs Obs,
932 beta: &[f64],
933 ) -> f64
934 where
935 Obs: ObservationView<'obs, Observation = F::Observation<'obs>> + 'obs,
936 {
937 $(let $block = &self.$idx;)+
938 $(let $beta_block = &beta[$block.range()];)+
939 let mut loss = 0.0;
940
941 for row in 0..obs.len() {
942 let weight = obs.weight_at(row);
943 if weight == 0.0 {
944 continue;
945 }
946 let observation = obs.observation_at(row);
947 let eta = F::Eta::from_array([$($block.x.eta_row(row, $beta_block),)+]);
948 loss += weight * family.nll_eta(observation, eta);
949 }
950
951 loss
952 }
953
954 fn eta_row(&self, beta: &[f64], row: usize) -> F::Eta
955 where
956 F: Family,
957 {
958 $(let $block = &self.$idx;)+
959 $(let $beta_block = &beta[$block.range()];)+
960 F::Eta::from_array([$($block.x.eta_row(row, $beta_block),)+])
961 }
962
963 fn penalty_value(&self, beta: &[f64]) -> f64 {
964 $(let $block = &self.$idx;)+
965 $(let $beta_block = &beta[$block.range()];)+
966
967 0.0 $(+ $block.penalty.value($beta_block))+
968 }
969
970 fn add_penalty_gradient(&self, beta: &[f64], grad: &mut [f64]) {
971 $(let $block = &self.$idx;)+
972 $(let $beta_block = &beta[$block.range()];)+
973
974 $(
975 $block.penalty.add_gradient(
976 $beta_block,
977 &mut grad[$block.offset..$block.offset + $block.len],
978 );
979 )+
980 }
981
982 fn gradient_workspace(&self, y_len: usize) -> GradientWorkspace {
983 let mut workspace = GradientWorkspace::new();
984 workspace.prepare($k);
985 $(
986 workspace.prepare_row_gradient($idx, y_len);
987 let _ = workspace.local_gradient_mut($idx, self.$idx.len());
988 )+
989 workspace
990 }
991
992 fn value_gradient_into_workspace<'obs, Obs>(
993 &self,
994 family: &F,
995 obs: &'obs Obs,
996 beta: &[f64],
997 grad: &mut [f64],
998 workspace: &mut GradientWorkspace,
999 ) -> f64
1000 where
1001 Obs: ObservationView<'obs, Observation = F::Observation<'obs>> + 'obs,
1002 {
1003 $(let $block = &self.$idx;)+
1004 $(let $beta_block = &beta[$block.range()];)+
1005 workspace.prepare($k);
1006 $(workspace.prepare_row_gradient($idx, obs.len());)+
1007 let mut loss = 0.0;
1008
1009 for row in 0..obs.len() {
1010 let weight = obs.weight_at(row);
1011 if weight == 0.0 {
1012 $(workspace.set_row_gradient($idx, row, 0.0);)+
1013 continue;
1014 }
1015 let observation = obs.observation_at(row);
1016 let eta = F::Eta::from_array([$($block.x.eta_row(row, $beta_block),)+]);
1017 let (nll, gradient) = family.nll_and_gradient_eta(observation, eta);
1018 loss += weight * nll;
1019 $(workspace.set_row_gradient($idx, row, weight * gradient.part($idx));)+
1020 }
1021
1022 $(
1023 loss += $block.penalty.value($beta_block);
1024 let ($row_gradient, $local_grad) =
1025 workspace.row_gradient_and_local_gradient_mut($idx, $block.len());
1026 $block.x.add_gradient($row_gradient, $beta_block, $local_grad);
1027 $block.penalty.add_gradient($beta_block, $local_grad);
1028 add_into(&mut grad[$block.offset..$block.offset + $block.len], $local_grad);
1029 )+
1030
1031 loss
1032 }
1033
1034 fn block_ranges(&self) -> Vec<Range<usize>> {
1035 vec![$(self.$idx.range(),)+]
1036 }
1037
1038 fn visit_block_ranges<V>(&self, mut visit: V)
1039 where
1040 V: FnMut(usize, Range<usize>),
1041 {
1042 $(
1043 visit($idx, self.$idx.range());
1044 )+
1045 }
1046
1047 fn parameter_layout(&self) -> ParameterLayout {
1048 ParameterLayout::new(vec![$(
1049 ParameterSlice {
1050 name: <$param as ParameterName>::NAME,
1051 range: self.$idx.range(),
1052 },
1053 )+])
1054 }
1055
1056 #[doc(hidden)]
1057 fn parameter_slice_count(&self) -> usize {
1058 $k
1059 }
1060
1061 #[doc(hidden)]
1062 fn parameter_slice_matches(
1063 &self,
1064 index: usize,
1065 name: &'static str,
1066 range: Range<usize>,
1067 ) -> bool {
1068 match index {
1069 $(
1070 $idx => name == <$param as ParameterName>::NAME
1071 && range == self.$idx.range(),
1072 )+
1073 _ => false,
1074 }
1075 }
1076
1077 #[doc(hidden)]
1078 fn visit_parameter_slices<V>(&self, mut visit: V)
1079 where
1080 V: FnMut(usize, &'static str, Range<usize>),
1081 {
1082 $(
1083 visit($idx, <$param as ParameterName>::NAME, self.$idx.range());
1084 )+
1085 }
1086
1087 #[doc(hidden)]
1088 fn has_same_parameter_layout<Other>(&self, other: &Other) -> bool
1089 where
1090 Other: GamlssBlocks<F>,
1091 {
1092 other.parameter_slice_count() == $k
1093 $(&& other.parameter_slice_matches(
1094 $idx,
1095 <$param as ParameterName>::NAME,
1096 self.$idx.range(),
1097 ))+
1098 }
1099 }
1100 };
1101}
1102
1103impl_gamlss_blocks!(
1104 1;
1105 params = (P1);
1106 links = (L1);
1107 designs = (X1);
1108 penalties = (Pen1);
1109 blocks = (block1);
1110 beta_blocks = (beta1);
1111 row_gradients = (row_gradient1);
1112 local_grads = (grad1);
1113 indices = (0)
1114);
1115
1116impl_gamlss_blocks!(
1117 2;
1118 params = (P1, P2);
1119 links = (L1, L2);
1120 designs = (X1, X2);
1121 penalties = (Pen1, Pen2);
1122 blocks = (block1, block2);
1123 beta_blocks = (beta1, beta2);
1124 row_gradients = (row_gradient1, row_gradient2);
1125 local_grads = (grad1, grad2);
1126 indices = (0, 1)
1127);
1128
1129impl_gamlss_blocks!(
1130 3;
1131 params = (P1, P2, P3);
1132 links = (L1, L2, L3);
1133 designs = (X1, X2, X3);
1134 penalties = (Pen1, Pen2, Pen3);
1135 blocks = (block1, block2, block3);
1136 beta_blocks = (beta1, beta2, beta3);
1137 row_gradients = (row_gradient1, row_gradient2, row_gradient3);
1138 local_grads = (grad1, grad2, grad3);
1139 indices = (0, 1, 2)
1140);
1141
1142impl_gamlss_blocks!(
1143 4;
1144 params = (P1, P2, P3, P4);
1145 links = (L1, L2, L3, L4);
1146 designs = (X1, X2, X3, X4);
1147 penalties = (Pen1, Pen2, Pen3, Pen4);
1148 blocks = (block1, block2, block3, block4);
1149 beta_blocks = (beta1, beta2, beta3, beta4);
1150 row_gradients = (row_gradient1, row_gradient2, row_gradient3, row_gradient4);
1151 local_grads = (grad1, grad2, grad3, grad4);
1152 indices = (0, 1, 2, 3)
1153);
1154
1155impl_gamlss_blocks!(
1156 5;
1157 params = (P1, P2, P3, P4, P5);
1158 links = (L1, L2, L3, L4, L5);
1159 designs = (X1, X2, X3, X4, X5);
1160 penalties = (Pen1, Pen2, Pen3, Pen4, Pen5);
1161 blocks = (block1, block2, block3, block4, block5);
1162 beta_blocks = (beta1, beta2, beta3, beta4, beta5);
1163 row_gradients = (
1164 row_gradient1,
1165 row_gradient2,
1166 row_gradient3,
1167 row_gradient4,
1168 row_gradient5
1169 );
1170 local_grads = (grad1, grad2, grad3, grad4, grad5);
1171 indices = (0, 1, 2, 3, 4)
1172);
1173
1174impl_gamlss_blocks!(
1175 6;
1176 params = (P1, P2, P3, P4, P5, P6);
1177 links = (L1, L2, L3, L4, L5, L6);
1178 designs = (X1, X2, X3, X4, X5, X6);
1179 penalties = (Pen1, Pen2, Pen3, Pen4, Pen5, Pen6);
1180 blocks = (block1, block2, block3, block4, block5, block6);
1181 beta_blocks = (beta1, beta2, beta3, beta4, beta5, beta6);
1182 row_gradients = (
1183 row_gradient1,
1184 row_gradient2,
1185 row_gradient3,
1186 row_gradient4,
1187 row_gradient5,
1188 row_gradient6
1189 );
1190 local_grads = (grad1, grad2, grad3, grad4, grad5, grad6);
1191 indices = (0, 1, 2, 3, 4, 5)
1192);
1193
1194impl_gamlss_blocks!(
1195 7;
1196 params = (P1, P2, P3, P4, P5, P6, P7);
1197 links = (L1, L2, L3, L4, L5, L6, L7);
1198 designs = (X1, X2, X3, X4, X5, X6, X7);
1199 penalties = (Pen1, Pen2, Pen3, Pen4, Pen5, Pen6, Pen7);
1200 blocks = (block1, block2, block3, block4, block5, block6, block7);
1201 beta_blocks = (beta1, beta2, beta3, beta4, beta5, beta6, beta7);
1202 row_gradients = (
1203 row_gradient1,
1204 row_gradient2,
1205 row_gradient3,
1206 row_gradient4,
1207 row_gradient5,
1208 row_gradient6,
1209 row_gradient7
1210 );
1211 local_grads = (grad1, grad2, grad3, grad4, grad5, grad6, grad7);
1212 indices = (0, 1, 2, 3, 4, 5, 6)
1213);
1214
1215impl_gamlss_blocks!(
1216 8;
1217 params = (P1, P2, P3, P4, P5, P6, P7, P8);
1218 links = (L1, L2, L3, L4, L5, L6, L7, L8);
1219 designs = (X1, X2, X3, X4, X5, X6, X7, X8);
1220 penalties = (Pen1, Pen2, Pen3, Pen4, Pen5, Pen6, Pen7, Pen8);
1221 blocks = (block1, block2, block3, block4, block5, block6, block7, block8);
1222 beta_blocks = (beta1, beta2, beta3, beta4, beta5, beta6, beta7, beta8);
1223 row_gradients = (
1224 row_gradient1,
1225 row_gradient2,
1226 row_gradient3,
1227 row_gradient4,
1228 row_gradient5,
1229 row_gradient6,
1230 row_gradient7,
1231 row_gradient8
1232 );
1233 local_grads = (grad1, grad2, grad3, grad4, grad5, grad6, grad7, grad8);
1234 indices = (0, 1, 2, 3, 4, 5, 6, 7)
1235);
1236
1237fn validate_block_rows(
1239 parameter: &'static str,
1240 actual_rows: usize,
1241 expected_rows: usize,
1242) -> Result<(), ModelError> {
1243 if actual_rows == expected_rows {
1244 Ok(())
1245 } else {
1246 Err(ModelError::DesignRowMismatch {
1247 parameter,
1248 expected_rows,
1249 actual_rows,
1250 })
1251 }
1252}
1253
1254fn ranges_overlap(first: Range<usize>, second: Range<usize>) -> bool {
1256 first.start < second.end && second.start < first.end
1257}
1258
1259fn add_into(out: &mut [f64], values: &[f64]) {
1263 debug_assert_eq!(out.len(), values.len());
1264
1265 for (out_value, value) in out.iter_mut().zip(values) {
1266 *out_value += value;
1267 }
1268}
1269
1270fn observation_weight_sum<Obs>(obs: &Obs) -> f64
1271where
1272 for<'row> Obs: ObservationView<'row>,
1273{
1274 (0..obs.len()).map(|row| obs.weight_at(row)).sum()
1275}
1276
1277fn validate_len(name: &'static str, actual: usize, expected: usize) -> Result<(), ModelError> {
1279 if actual == expected {
1280 Ok(())
1281 } else if name == "gradient" {
1282 Err(ModelError::GradientLength { expected, actual })
1283 } else {
1284 Err(ModelError::BetaLength { expected, actual })
1285 }
1286}
1287
1288fn validate_beta_and_gradient_len(
1289 expected: usize,
1290 beta: &[f64],
1291 grad: &[f64],
1292) -> Result<(), ModelError> {
1293 validate_len("theta", beta.len(), expected)?;
1294 validate_len("gradient", grad.len(), expected)
1295}
1296
1297fn validate_row(row: usize, nrows: usize) -> Result<(), ModelError> {
1298 if row < nrows {
1299 Ok(())
1300 } else {
1301 Err(ModelError::RowOutOfBounds { row, nrows })
1302 }
1303}
1304
1305fn validate_prediction_blocks<F, Blocks, PBlocks>(
1306 expected_blocks: &Blocks,
1307 blocks: &PBlocks,
1308) -> Result<(), ModelError>
1309where
1310 F: Family,
1311 Blocks: GamlssBlocks<F>,
1312 PBlocks: GamlssBlocks<F>,
1313{
1314 blocks.validate(blocks.nrows())?;
1315 if expected_blocks.has_same_parameter_layout(blocks) {
1316 Ok(())
1317 } else {
1318 Err(ModelError::PredictionLayoutMismatch {
1319 expected: expected_blocks.parameter_layout(),
1320 got: blocks.parameter_layout(),
1321 })
1322 }
1323}
1324
1325#[cfg(test)]
1326mod tests {
1327 use approx::assert_relative_eq;
1328
1329 use crate::{
1330 DenseDesign, Family, Gamlss, GlobalPenalty, Identity, LinearPredictorBlock, ModelError, Mu,
1331 NoPenalty, Nu, Objective, ObjectiveScale, ObservationView, ParameterBlock, ParameterBlocks,
1332 ParameterLayout, ParameterName, ParameterSlice, ParameterizedFamily, PredictorBlock,
1333 RidgePenalty, Sigma, SumBlock, Tau,
1334 };
1335
1336 #[derive(Debug, Clone, Copy)]
1337 struct FixedSigmaNormal;
1338
1339 impl Family for FixedSigmaNormal {
1340 type Eta = f64;
1341 type Theta = f64;
1342 type NllGradientEta = f64;
1343 type Observation<'obs> = f64;
1344
1345 fn theta(&self, eta: Self::Eta) -> Self::Theta {
1346 eta
1347 }
1348
1349 fn nll(&self, y: f64, theta: Self::Theta) -> f64 {
1350 let residual = y - theta;
1351 0.5 * residual * residual
1352 }
1353
1354 fn nll_and_gradient_eta(&self, y: f64, eta: Self::Eta) -> (f64, Self::NllGradientEta) {
1355 (self.nll(y, self.theta(eta)), eta - y)
1356 }
1357 }
1358
1359 impl ParameterizedFamily<1> for FixedSigmaNormal {
1360 type Params = (Mu,);
1361 type Links = (Identity,);
1362 }
1363
1364 #[derive(Debug, Clone, Copy)]
1365 struct TwoParameterMock;
1366
1367 impl Family for TwoParameterMock {
1368 type Eta = (f64, f64);
1369 type Theta = (f64, f64);
1370 type NllGradientEta = (f64, f64);
1371 type Observation<'obs> = f64;
1372
1373 fn theta(&self, eta: Self::Eta) -> Self::Theta {
1374 eta
1375 }
1376
1377 fn nll(&self, y: f64, theta: Self::Theta) -> f64 {
1378 let first = theta.0 - y;
1379 let second = theta.1 - 1.0;
1380 0.5 * (first * first + second * second)
1381 }
1382
1383 fn nll_and_gradient_eta(&self, y: f64, eta: Self::Eta) -> (f64, Self::NllGradientEta) {
1384 let gradient = (eta.0 - y, eta.1 - 1.0);
1385 (self.nll(y, eta), gradient)
1386 }
1387 }
1388
1389 impl ParameterizedFamily<2> for TwoParameterMock {
1390 type Params = (Mu, Sigma);
1391 type Links = (Identity, Identity);
1392 }
1393
1394 #[derive(Debug, Clone, Copy, PartialEq)]
1395 struct ShiftedObservations<'a> {
1396 y: &'a [f64],
1397 shift: f64,
1398 weight: f64,
1399 }
1400
1401 impl<'row> ObservationView<'row> for ShiftedObservations<'_> {
1402 type Observation = f64;
1403
1404 fn len(&self) -> usize {
1405 self.y.len()
1406 }
1407
1408 fn observation_at(&'row self, row: usize) -> Self::Observation {
1409 self.y[row] + self.shift
1410 }
1411
1412 fn weight_at(&self, _row: usize) -> f64 {
1413 self.weight
1414 }
1415 }
1416
1417 #[test]
1418 fn custom_one_parameter_family_uses_generic_family_contract() {
1419 let y = vec![1.0, 2.0];
1420 let x = DenseDesign::intercept(y.len());
1421 let mu = ParameterBlock::<Mu, Identity, _, _>::linear(x, NoPenalty, 0);
1422 let mut model = Gamlss::try_new(FixedSigmaNormal, (mu,), &y).unwrap();
1423 let beta = vec![1.5];
1424 let mut grad = vec![0.0];
1425
1426 assert_relative_eq!(model.value(&beta).unwrap(), 0.25);
1427
1428 model.gradient(&beta, &mut grad).unwrap();
1429
1430 assert_relative_eq!(grad[0], 0.0);
1431 }
1432
1433 #[test]
1434 fn model_accepts_user_defined_observation_view() {
1435 let y = vec![1.0, 2.0];
1436 let obs = ShiftedObservations {
1437 y: &y,
1438 shift: 1.0,
1439 weight: 0.5,
1440 };
1441 let x = DenseDesign::intercept(obs.len());
1442 let mu = ParameterBlock::<Mu, Identity, _, _>::linear(x, NoPenalty, 0);
1443 let mut model = Gamlss::try_new_with_observations(FixedSigmaNormal, (mu,), obs).unwrap();
1444 let beta = vec![2.5];
1445 let mut grad = vec![0.0];
1446
1447 assert_relative_eq!(model.value(&beta).unwrap(), 0.125);
1448
1449 model.gradient(&beta, &mut grad).unwrap();
1450
1451 assert_relative_eq!(grad[0], 0.0);
1452 }
1453
1454 #[test]
1455 fn custom_observation_view_rejects_invalid_weight() {
1456 let y = vec![1.0, 2.0];
1457 let obs = ShiftedObservations {
1458 y: &y,
1459 shift: 0.0,
1460 weight: f64::NAN,
1461 };
1462 let x = DenseDesign::intercept(obs.len());
1463 let mu = ParameterBlock::<Mu, Identity, _, _>::linear(x, NoPenalty, 0);
1464
1465 assert_eq!(
1466 Gamlss::try_new_with_observations(FixedSigmaNormal, (mu,), obs).unwrap_err(),
1467 ModelError::InvalidWeight { index: 0 }
1468 );
1469 }
1470
1471 #[test]
1472 fn model_borrows_response_without_copying() {
1473 let y = vec![1.0, 2.0];
1474 let x = DenseDesign::intercept(y.len());
1475 let mu = ParameterBlock::<Mu, Identity, _, _>::linear(x, NoPenalty, 0);
1476 let model = Gamlss::try_new(FixedSigmaNormal, (mu,), &y).unwrap();
1477
1478 assert_eq!(model.obs.as_ptr(), y.as_ptr());
1479 assert_eq!(model.obs, y.as_slice());
1480 assert_eq!(model.obs.weight_at(0), 1.0);
1481 }
1482
1483 #[test]
1484 fn unweighted_model_matches_unit_weights() {
1485 let y = vec![1.0, 2.0];
1486 let unit_weights = vec![1.0, 1.0];
1487 let x = DenseDesign::intercept(y.len());
1488 let mu = ParameterBlock::<Mu, Identity, _, _>::linear(x.clone(), NoPenalty, 0);
1489 let weighted_mu = ParameterBlock::<Mu, Identity, _, _>::linear(x, NoPenalty, 0);
1490 let model = Gamlss::try_new(FixedSigmaNormal, (mu,), &y).unwrap();
1491 let weighted =
1492 Gamlss::try_new_weighted(FixedSigmaNormal, (weighted_mu,), &y, &unit_weights).unwrap();
1493 let beta = vec![1.5];
1494 let mut grad = vec![0.0];
1495 let mut weighted_grad = vec![0.0];
1496
1497 assert_relative_eq!(
1498 model.try_value(&beta).unwrap(),
1499 weighted.try_value(&beta).unwrap()
1500 );
1501
1502 model.try_gradient_into(&beta, &mut grad).unwrap();
1503 weighted
1504 .try_gradient_into(&beta, &mut weighted_grad)
1505 .unwrap();
1506
1507 assert_relative_eq!(grad[0], weighted_grad[0]);
1508 }
1509
1510 #[test]
1511 fn zero_weight_excludes_observation_from_value_and_gradient() {
1512 let y = vec![1.0, 10.0];
1513 let weights = vec![1.0, 0.0];
1514 let x = DenseDesign::intercept(y.len());
1515 let mu = ParameterBlock::<Mu, Identity, _, _>::linear(x, NoPenalty, 0);
1516 let model = Gamlss::try_new_weighted(FixedSigmaNormal, (mu,), &y, &weights).unwrap();
1517 let beta = vec![1.0];
1518 let mut grad = vec![f64::NAN];
1519
1520 assert_relative_eq!(model.try_value(&beta).unwrap(), 0.0);
1521
1522 model.try_gradient_into(&beta, &mut grad).unwrap();
1523
1524 assert_relative_eq!(grad[0], 0.0);
1525 }
1526
1527 #[test]
1528 fn zero_weight_excludes_invalid_observation_from_value_and_gradient() {
1529 let y = vec![1.0, f64::NAN];
1530 let weights = vec![1.0, 0.0];
1531 let x = DenseDesign::intercept(y.len());
1532 let mu = ParameterBlock::<Mu, Identity, _, _>::linear(x, NoPenalty, 0);
1533 let model = Gamlss::try_new_weighted(FixedSigmaNormal, (mu,), &y, &weights).unwrap();
1534 let beta = vec![1.0];
1535 let mut grad = vec![f64::NAN];
1536
1537 assert_relative_eq!(model.try_value(&beta).unwrap(), 0.0);
1538
1539 model.try_gradient_into(&beta, &mut grad).unwrap();
1540
1541 assert_relative_eq!(grad[0], 0.0);
1542 }
1543
1544 #[test]
1545 fn weighted_model_rejects_invalid_weights() {
1546 let y = vec![1.0, 2.0];
1547 let short_weights = vec![1.0];
1548 let infinite_weights = vec![1.0, f64::INFINITY];
1549 let negative_weights = vec![1.0, -0.1];
1550 let mu =
1551 ParameterBlock::<Mu, Identity, _, _>::linear(DenseDesign::intercept(2), NoPenalty, 0);
1552
1553 assert_eq!(
1554 Gamlss::try_new_weighted(FixedSigmaNormal, (mu.clone(),), &y, &short_weights,)
1555 .unwrap_err(),
1556 ModelError::WeightLength {
1557 expected: 2,
1558 actual: 1,
1559 }
1560 );
1561 assert_eq!(
1562 Gamlss::try_new_weighted(FixedSigmaNormal, (mu.clone(),), &y, &infinite_weights,)
1563 .unwrap_err(),
1564 ModelError::InvalidWeight { index: 1 }
1565 );
1566 assert_eq!(
1567 Gamlss::try_new_weighted(FixedSigmaNormal, (mu,), &y, &negative_weights).unwrap_err(),
1568 ModelError::InvalidWeight { index: 1 }
1569 );
1570 }
1571
1572 #[test]
1573 fn prediction_api_returns_eta_and_theta_for_one_parameter_model() {
1574 let y = vec![1.0, 2.0];
1575 let x = DenseDesign::from_rows(&[[1.0, 0.0], [1.0, 2.0]]);
1576 let mu = ParameterBlock::<Mu, Identity, _, _>::linear(x, NoPenalty, 0);
1577 let model = Gamlss::try_new(FixedSigmaNormal, (mu,), &y).unwrap();
1578 let beta = vec![0.5, 0.25];
1579
1580 assert_relative_eq!(model.predict_eta_row(&beta, 1).unwrap(), 1.0);
1581 assert_relative_eq!(model.predict_theta_row(&beta, 1).unwrap(), 1.0);
1582 assert_eq!(model.predict_eta(&beta).unwrap(), vec![0.5, 1.0]);
1583 assert_eq!(model.predict_theta(&beta).unwrap(), vec![0.5, 1.0]);
1584 }
1585
1586 #[test]
1587 fn prediction_api_rejects_invalid_theta_length_and_row() {
1588 let y = vec![1.0];
1589 let x = DenseDesign::intercept(y.len());
1590 let mu = ParameterBlock::<Mu, Identity, _, _>::linear(x, NoPenalty, 0);
1591 let model = Gamlss::try_new(FixedSigmaNormal, (mu,), &y).unwrap();
1592
1593 assert_eq!(
1594 model.predict_eta_row(&[], 0).unwrap_err(),
1595 ModelError::BetaLength {
1596 expected: 1,
1597 actual: 0,
1598 }
1599 );
1600 assert_eq!(
1601 model.predict_eta_row(&[0.0], 1).unwrap_err(),
1602 ModelError::RowOutOfBounds { row: 1, nrows: 1 }
1603 );
1604 }
1605
1606 #[test]
1607 fn prediction_api_uses_compatible_blocks_for_new_rows() {
1608 let y = vec![1.0, 2.0];
1609 let train_x = DenseDesign::from_rows(&[[1.0, 0.0], [1.0, 1.0]]);
1610 let mu = ParameterBlock::<Mu, Identity, _, _>::linear(train_x, NoPenalty, 0);
1611 let model = Gamlss::try_new(FixedSigmaNormal, (mu,), &y).unwrap();
1612 let prediction_x = DenseDesign::from_rows(&[[1.0, 2.0], [1.0, 3.0], [1.0, 4.0]]);
1613 let prediction_mu =
1614 ParameterBlock::<Mu, Identity, _, _>::linear(prediction_x, NoPenalty, 0);
1615 let prediction_blocks = (prediction_mu,);
1616 let beta = vec![0.5, 0.25];
1617
1618 assert_relative_eq!(
1619 model
1620 .predict_eta_row_with_blocks(&beta, &prediction_blocks, 1)
1621 .unwrap(),
1622 1.25
1623 );
1624 assert_eq!(
1625 model
1626 .predict_eta_with_blocks(&beta, &prediction_blocks)
1627 .unwrap(),
1628 vec![1.0, 1.25, 1.5]
1629 );
1630 assert_eq!(
1631 model
1632 .predict_theta_with_blocks(&beta, &prediction_blocks)
1633 .unwrap(),
1634 vec![1.0, 1.25, 1.5]
1635 );
1636 }
1637
1638 #[test]
1639 fn prediction_api_rejects_incompatible_prediction_blocks() {
1640 let y = vec![1.0, 2.0];
1641 let train_x = DenseDesign::from_rows(&[[1.0, 0.0], [1.0, 1.0]]);
1642 let mu = ParameterBlock::<Mu, Identity, _, _>::linear(train_x, NoPenalty, 0);
1643 let model = Gamlss::try_new(FixedSigmaNormal, (mu,), &y).unwrap();
1644 let prediction_x = DenseDesign::from_rows(&[[1.0, 2.0, 3.0]]);
1645 let prediction_mu =
1646 ParameterBlock::<Mu, Identity, _, _>::linear(prediction_x, NoPenalty, 0);
1647 let prediction_blocks = (prediction_mu,);
1648
1649 assert_eq!(
1650 model
1651 .predict_eta_with_blocks(&[0.5, 0.25], &prediction_blocks)
1652 .unwrap_err(),
1653 ModelError::PredictionLayoutMismatch {
1654 expected: ParameterLayout::new(vec![ParameterSlice {
1655 name: "mu",
1656 range: 0..2,
1657 }]),
1658 got: ParameterLayout::new(vec![ParameterSlice {
1659 name: "mu",
1660 range: 0..3,
1661 }]),
1662 }
1663 );
1664 }
1665
1666 #[test]
1667 fn prediction_api_rejects_same_length_different_parameter_layout() {
1668 let y = vec![1.0, 2.0];
1669 let train_mu_x = DenseDesign::from_rows(&[[1.0, 0.0], [1.0, 1.0]]);
1670 let train_sigma_x = DenseDesign::intercept(y.len());
1671 let train_mu = ParameterBlock::<Mu, Identity, _, _>::linear(train_mu_x, NoPenalty, 0);
1672 let train_sigma =
1673 ParameterBlock::<Sigma, Identity, _, _>::linear(train_sigma_x, NoPenalty, 2);
1674 let model = Gamlss::try_new(TwoParameterMock, (train_mu, train_sigma), &y).unwrap();
1675
1676 let prediction_mu_x = DenseDesign::intercept(1);
1677 let prediction_sigma_x = DenseDesign::from_rows(&[[1.0, 2.0]]);
1678 let prediction_mu =
1679 ParameterBlock::<Mu, Identity, _, _>::linear(prediction_mu_x, NoPenalty, 0);
1680 let prediction_sigma =
1681 ParameterBlock::<Sigma, Identity, _, _>::linear(prediction_sigma_x, NoPenalty, 1);
1682 let prediction_blocks = (prediction_mu, prediction_sigma);
1683
1684 assert_eq!(
1685 model
1686 .predict_eta_with_blocks(&[0.5, 0.25, 0.75], &prediction_blocks)
1687 .unwrap_err(),
1688 ModelError::PredictionLayoutMismatch {
1689 expected: ParameterLayout::new(vec![
1690 ParameterSlice {
1691 name: "mu",
1692 range: 0..2,
1693 },
1694 ParameterSlice {
1695 name: "sigma",
1696 range: 2..3,
1697 },
1698 ]),
1699 got: ParameterLayout::new(vec![
1700 ParameterSlice {
1701 name: "mu",
1702 range: 0..1,
1703 },
1704 ParameterSlice {
1705 name: "sigma",
1706 range: 1..3,
1707 },
1708 ]),
1709 }
1710 );
1711 }
1712
1713 #[test]
1714 fn rejects_overflowing_parameter_block_ranges() {
1715 let y = vec![1.0];
1716 let x = DenseDesign::intercept(y.len());
1717 let mu = ParameterBlock::<Mu, Identity, _, _>::linear(x, NoPenalty, usize::MAX);
1718
1719 assert_eq!(
1720 Gamlss::try_new(FixedSigmaNormal, (mu,), &y).unwrap_err(),
1721 ModelError::BlockRangeOverflow {
1722 parameter: "mu",
1723 offset: usize::MAX,
1724 len: 1,
1725 }
1726 );
1727 }
1728
1729 #[test]
1730 fn workspace_objective_matches_model_gradient_on_repeated_calls() {
1731 let y = vec![1.0, 2.0];
1732 let x = DenseDesign::from_rows(&[[1.0, 0.0], [1.0, 1.0]]);
1733 let mu = ParameterBlock::<Mu, Identity, _, _>::linear(x, NoPenalty, 0);
1734 let model = Gamlss::try_new(FixedSigmaNormal, (mu,), &y).unwrap();
1735 let mut workspace_objective = model.clone().into_workspace_objective();
1736
1737 for beta in [vec![1.0, 0.25], vec![1.5, -0.1]] {
1738 let mut expected_grad = vec![0.0; beta.len()];
1739 let mut workspace_grad = vec![0.0; beta.len()];
1740
1741 model.try_gradient_into(&beta, &mut expected_grad).unwrap();
1742 let workspace_value = workspace_objective
1743 .value_gradient(&beta, &mut workspace_grad)
1744 .unwrap();
1745
1746 assert_relative_eq!(workspace_value, model.try_value(&beta).unwrap());
1747 for (actual, expected) in workspace_grad.iter().zip(&expected_grad) {
1748 assert_relative_eq!(actual, expected);
1749 }
1750 }
1751 }
1752
1753 #[test]
1754 fn value_gradient_matches_separate_value_and_gradient() {
1755 let y = vec![1.0, 2.0];
1756 let weights = vec![0.5, 2.0];
1757 let x = DenseDesign::from_rows(&[[1.0, 0.0], [1.0, 1.0]]);
1758 let mu = ParameterBlock::<Mu, Identity, _, _>::linear(x, RidgePenalty::new(0.25), 0);
1759 let model = Gamlss::try_new_weighted(FixedSigmaNormal, (mu,), &y, &weights).unwrap();
1760 let beta = vec![0.75, 0.5];
1761 let mut separate_grad = vec![0.0; beta.len()];
1762 let mut fused_grad = vec![f64::NAN; beta.len()];
1763
1764 let separate_value = model.try_value(&beta).unwrap();
1765 model.try_gradient_into(&beta, &mut separate_grad).unwrap();
1766 let fused_value = model
1767 .try_value_gradient_into(&beta, &mut fused_grad)
1768 .unwrap();
1769
1770 assert_relative_eq!(fused_value, separate_value);
1771 for (actual, expected) in fused_grad.iter().zip(&separate_grad) {
1772 assert_relative_eq!(actual, expected);
1773 }
1774 }
1775
1776 #[test]
1777 fn mean_objective_scales_likelihood_but_not_penalties() {
1778 let y = vec![0.0, 3.0];
1779 let x = DenseDesign::intercept(y.len());
1780 let mu = ParameterBlock::<Mu, Identity, _, _>::linear(x, RidgePenalty::new(0.5), 0);
1781 let model = Gamlss::try_new(FixedSigmaNormal, (mu,), &y)
1782 .unwrap()
1783 .with_objective_scale(ObjectiveScale::Mean)
1784 .with_global_penalties(GlobalSquarePenalty { lambda: 2.0 });
1785 let beta = vec![1.0];
1786 let mut grad = vec![0.0];
1787
1788 let value = model.clone().value(&beta).unwrap();
1789 let fused_value = model.clone().value_gradient(&beta, &mut grad).unwrap();
1790
1791 assert_relative_eq!(value, 3.75);
1792 assert_relative_eq!(fused_value, value);
1793 assert_relative_eq!(grad[0], 4.5);
1794 }
1795
1796 #[test]
1797 fn value_gradient_rejects_invalid_lengths() {
1798 let y = vec![1.0];
1799 let x = DenseDesign::intercept(y.len());
1800 let mu = ParameterBlock::<Mu, Identity, _, _>::linear(x, NoPenalty, 0);
1801 let model = Gamlss::try_new(FixedSigmaNormal, (mu,), &y).unwrap();
1802 let mut grad = vec![0.0];
1803
1804 assert_eq!(
1805 model.try_value_gradient_into(&[], &mut grad).unwrap_err(),
1806 ModelError::BetaLength {
1807 expected: 1,
1808 actual: 0,
1809 }
1810 );
1811
1812 assert_eq!(
1813 model.try_value_gradient_into(&[0.0], &mut []).unwrap_err(),
1814 ModelError::GradientLength {
1815 expected: 1,
1816 actual: 0,
1817 }
1818 );
1819 }
1820
1821 #[derive(Debug, Clone, Copy)]
1822 struct SoftplusIntercept {
1823 nrows: usize,
1824 }
1825
1826 impl PredictorBlock for SoftplusIntercept {
1827 fn nrows(&self) -> usize {
1828 self.nrows
1829 }
1830
1831 fn nparams(&self) -> usize {
1832 1
1833 }
1834
1835 fn eta_row(&self, _: usize, beta: &[f64]) -> f64 {
1836 softplus(beta[0])
1837 }
1838
1839 fn add_gradient(&self, scores: &[f64], beta: &[f64], grad: &mut [f64]) {
1840 debug_assert_eq!(grad.len(), 1);
1841 grad[0] += scores.iter().sum::<f64>() * sigmoid(beta[0]);
1842 }
1843 }
1844
1845 #[test]
1846 fn sum_block_supports_user_defined_nonlinear_predictors() {
1847 let y = vec![1.0, 2.0];
1848 let linear = crate::LinearPredictorBlock::new(DenseDesign::intercept(y.len()));
1849 let nonlinear = SoftplusIntercept { nrows: y.len() };
1850 let predictor = SumBlock::new((linear, nonlinear));
1851 let mu = ParameterBlock::<Mu, Identity, _, _>::new(predictor, NoPenalty, 0);
1852 let mut model = Gamlss::try_new(FixedSigmaNormal, (mu,), &y).unwrap();
1853 let beta = vec![0.4, -0.2];
1854 let eps = 1.0e-6;
1855 let mut grad = vec![0.0; beta.len()];
1856
1857 model.gradient(&beta, &mut grad).unwrap();
1858
1859 for index in 0..beta.len() {
1860 let mut plus = beta.clone();
1861 plus[index] += eps;
1862 let mut minus = beta.clone();
1863 minus[index] -= eps;
1864 let finite_difference =
1865 (model.value(&plus).unwrap() - model.value(&minus).unwrap()) / (2.0 * eps);
1866
1867 assert_relative_eq!(grad[index], finite_difference, epsilon = 1.0e-6);
1868 }
1869 }
1870
1871 #[derive(Debug, Clone, Copy)]
1872 struct StatefulLocation {
1873 target_shift: f64,
1874 }
1875
1876 impl Family for StatefulLocation {
1877 type Eta = f64;
1878 type Theta = f64;
1879 type NllGradientEta = f64;
1880 type Observation<'obs> = f64;
1881
1882 fn theta(&self, eta: Self::Eta) -> Self::Theta {
1883 eta + self.target_shift
1884 }
1885
1886 fn nll(&self, y: f64, theta: Self::Theta) -> f64 {
1887 let residual = y - theta;
1888 0.5 * residual * residual
1889 }
1890
1891 fn nll_and_gradient_eta(&self, y: f64, eta: Self::Eta) -> (f64, Self::NllGradientEta) {
1892 let theta = self.theta(eta);
1893 (self.nll(y, theta), theta - y)
1894 }
1895 }
1896
1897 impl ParameterizedFamily<1> for StatefulLocation {
1898 type Params = (Mu,);
1899 type Links = (Identity,);
1900 }
1901
1902 #[test]
1903 fn family_instance_state_participates_in_objective() {
1904 let y = vec![2.0];
1905 let x = DenseDesign::intercept(y.len());
1906 let mu = ParameterBlock::<Mu, Identity, _, _>::linear(x, NoPenalty, 0);
1907 let mut model = Gamlss::try_new(StatefulLocation { target_shift: 1.0 }, (mu,), &y).unwrap();
1908 let beta = vec![0.5];
1909 let mut grad = vec![0.0];
1910
1911 assert_relative_eq!(model.value(&beta).unwrap(), 0.125);
1912
1913 model.gradient(&beta, &mut grad).unwrap();
1914
1915 assert_relative_eq!(grad[0], -0.5);
1916 }
1917
1918 #[derive(Debug, Clone, Copy)]
1919 struct BivariateLocation;
1920
1921 impl Family for BivariateLocation {
1922 type Eta = f64;
1923 type Theta = f64;
1924 type NllGradientEta = f64;
1925 type Observation<'obs> = [f64; 2];
1926
1927 fn theta(&self, eta: Self::Eta) -> Self::Theta {
1928 eta
1929 }
1930
1931 fn nll(&self, observation: Self::Observation<'_>, theta: Self::Theta) -> f64 {
1932 let first = theta - observation[0];
1933 let second = theta - observation[1];
1934 0.5 * (first * first + second * second)
1935 }
1936
1937 fn nll_and_gradient_eta(
1938 &self,
1939 observation: Self::Observation<'_>,
1940 eta: Self::Eta,
1941 ) -> (f64, Self::NllGradientEta) {
1942 let gradient = (eta - observation[0]) + (eta - observation[1]);
1943 (self.nll(observation, eta), gradient)
1944 }
1945 }
1946
1947 impl ParameterizedFamily<1> for BivariateLocation {
1948 type Params = (Mu,);
1949 type Links = (Identity,);
1950 }
1951
1952 #[test]
1953 fn model_accepts_multivariate_observation_rows() {
1954 let y = vec![[1.0, 3.0], [2.0, 4.0]];
1955 let x = DenseDesign::intercept(y.len());
1956 let mu = ParameterBlock::<Mu, Identity, _, _>::linear(x, NoPenalty, 0);
1957 let mut model =
1958 Gamlss::try_new_with_observations(BivariateLocation, (mu,), y.as_slice()).unwrap();
1959 let beta = vec![2.0];
1960 let mut grad = vec![0.0];
1961
1962 assert_relative_eq!(model.value(&beta).unwrap(), 3.0);
1963
1964 model.gradient(&beta, &mut grad).unwrap();
1965
1966 assert_relative_eq!(grad[0], -2.0);
1967 }
1968
1969 #[derive(Debug, Clone, PartialEq)]
1970 struct BorrowedRows {
1971 rows: Vec<Vec<f64>>,
1972 }
1973
1974 impl<'row> ObservationView<'row> for BorrowedRows {
1975 type Observation = &'row [f64];
1976
1977 fn len(&self) -> usize {
1978 self.rows.len()
1979 }
1980
1981 fn observation_at(&'row self, row: usize) -> Self::Observation {
1982 self.rows[row].as_slice()
1983 }
1984
1985 fn weight_at(&self, _row: usize) -> f64 {
1986 1.0
1987 }
1988 }
1989
1990 #[derive(Debug, Clone, Copy)]
1991 struct BorrowedRowMean;
1992
1993 impl Family for BorrowedRowMean {
1994 type Eta = f64;
1995 type Theta = f64;
1996 type NllGradientEta = f64;
1997 type Observation<'obs> = &'obs [f64];
1998
1999 fn theta(&self, eta: Self::Eta) -> Self::Theta {
2000 eta
2001 }
2002
2003 fn nll(&self, observation: Self::Observation<'_>, theta: Self::Theta) -> f64 {
2004 let mean = observation.iter().sum::<f64>() / observation.len() as f64;
2005 let residual = theta - mean;
2006 0.5 * residual * residual
2007 }
2008
2009 fn nll_and_gradient_eta(
2010 &self,
2011 observation: Self::Observation<'_>,
2012 eta: Self::Eta,
2013 ) -> (f64, Self::NllGradientEta) {
2014 let mean = observation.iter().sum::<f64>() / observation.len() as f64;
2015 (self.nll(observation, eta), eta - mean)
2016 }
2017 }
2018
2019 impl ParameterizedFamily<1> for BorrowedRowMean {
2020 type Params = (Mu,);
2021 type Links = (Identity,);
2022 }
2023
2024 #[test]
2025 fn model_accepts_borrowed_dynamic_observation_rows() {
2026 let obs = BorrowedRows {
2027 rows: vec![vec![1.0, 3.0], vec![2.0, 4.0]],
2028 };
2029 let x = DenseDesign::intercept(obs.len());
2030 let mu = ParameterBlock::<Mu, Identity, _, _>::linear(x, NoPenalty, 0);
2031 let mut model = Gamlss::try_new_with_observations(BorrowedRowMean, (mu,), obs).unwrap();
2032 let beta = vec![2.0];
2033 let mut grad = vec![0.0];
2034
2035 assert_relative_eq!(model.value(&beta).unwrap(), 0.5);
2036
2037 model.gradient(&beta, &mut grad).unwrap();
2038
2039 assert_relative_eq!(grad[0], -1.0);
2040 }
2041
2042 #[derive(Debug, Clone, Copy)]
2043 struct ThreeParameterMock;
2044
2045 impl Family for ThreeParameterMock {
2046 type Eta = (f64, f64, f64);
2047 type Theta = (f64, f64, f64);
2048 type NllGradientEta = (f64, f64, f64);
2049 type Observation<'obs> = f64;
2050
2051 fn theta(&self, eta: Self::Eta) -> Self::Theta {
2052 eta
2053 }
2054
2055 fn nll(&self, y: f64, theta: Self::Theta) -> f64 {
2056 let first = theta.0 - y;
2057 let second = theta.1 - 1.0;
2058 let third = theta.2 + 1.0;
2059 0.5 * (first * first + second * second + third * third)
2060 }
2061
2062 fn nll_and_gradient_eta(&self, y: f64, eta: Self::Eta) -> (f64, Self::NllGradientEta) {
2063 let gradient = (eta.0 - y, eta.1 - 1.0, eta.2 + 1.0);
2064 (self.nll(y, eta), gradient)
2065 }
2066 }
2067
2068 impl ParameterizedFamily<3> for ThreeParameterMock {
2069 type Params = (Mu, Sigma, Nu);
2070 type Links = (Identity, Identity, Identity);
2071 }
2072
2073 #[test]
2074 fn custom_three_parameter_family_uses_generic_blocks() {
2075 let y = vec![2.0];
2076 let first = ParameterBlock::<Mu, Identity, _, _>::linear(
2077 DenseDesign::intercept(y.len()),
2078 NoPenalty,
2079 0,
2080 );
2081 let second = ParameterBlock::<Sigma, Identity, _, _>::linear(
2082 DenseDesign::intercept(y.len()),
2083 NoPenalty,
2084 1,
2085 );
2086 let third = ParameterBlock::<Nu, Identity, _, _>::linear(
2087 DenseDesign::intercept(y.len()),
2088 NoPenalty,
2089 2,
2090 );
2091 let mut model = Gamlss::try_new(ThreeParameterMock, (first, second, third), &y).unwrap();
2092 let beta = vec![1.5, 0.5, -0.5];
2093 let mut grad = vec![0.0; 3];
2094
2095 assert_relative_eq!(model.value(&beta).unwrap(), 0.375);
2096
2097 model.gradient(&beta, &mut grad).unwrap();
2098
2099 assert_relative_eq!(grad[0], -0.5);
2100 assert_relative_eq!(grad[1], -0.5);
2101 assert_relative_eq!(grad[2], 0.5);
2102 }
2103
2104 #[derive(Debug, Clone, Copy)]
2105 struct FourParameterMock;
2106
2107 impl Family for FourParameterMock {
2108 type Eta = (f64, f64, f64, f64);
2109 type Theta = (f64, f64, f64, f64);
2110 type NllGradientEta = (f64, f64, f64, f64);
2111 type Observation<'obs> = f64;
2112
2113 fn theta(&self, eta: Self::Eta) -> Self::Theta {
2114 (eta.0 + 1.0, eta.1 + 2.0, eta.2 + 3.0, eta.3 + 4.0)
2115 }
2116
2117 fn nll(&self, y: f64, theta: Self::Theta) -> f64 {
2118 theta.0 + theta.1 + theta.2 + theta.3 + y
2119 }
2120
2121 fn nll_and_gradient_eta(&self, y: f64, eta: Self::Eta) -> (f64, Self::NllGradientEta) {
2122 (self.nll(y, self.theta(eta)), (1.0, 1.0, 1.0, 1.0))
2123 }
2124 }
2125
2126 impl ParameterizedFamily<4> for FourParameterMock {
2127 type Params = (Mu, Sigma, Nu, Tau);
2128 type Links = (Identity, Identity, Identity, Identity);
2129 }
2130
2131 #[derive(Debug, Clone, Copy)]
2132 struct Fifth;
2133
2134 impl ParameterName for Fifth {
2135 const NAME: &'static str = "fifth";
2136 }
2137
2138 #[derive(Debug, Clone, Copy)]
2139 struct FiveParameterMock;
2140
2141 impl Family for FiveParameterMock {
2142 type Eta = (f64, f64, f64, f64, f64);
2143 type Theta = (f64, f64, f64, f64, f64);
2144 type NllGradientEta = (f64, f64, f64, f64, f64);
2145 type Observation<'obs> = f64;
2146
2147 fn theta(&self, eta: Self::Eta) -> Self::Theta {
2148 eta
2149 }
2150
2151 fn nll(&self, y: f64, theta: Self::Theta) -> f64 {
2152 let targets = [y, 1.0, 2.0, 3.0, 4.0];
2153 let values = [theta.0, theta.1, theta.2, theta.3, theta.4];
2154 0.5 * values
2155 .iter()
2156 .zip(targets)
2157 .map(|(value, target)| {
2158 let residual = value - target;
2159 residual * residual
2160 })
2161 .sum::<f64>()
2162 }
2163
2164 fn nll_and_gradient_eta(&self, y: f64, eta: Self::Eta) -> (f64, Self::NllGradientEta) {
2165 (
2166 self.nll(y, eta),
2167 (
2168 eta.0 - y,
2169 eta.1 - 1.0,
2170 eta.2 - 2.0,
2171 eta.3 - 3.0,
2172 eta.4 - 4.0,
2173 ),
2174 )
2175 }
2176 }
2177
2178 impl ParameterizedFamily<5> for FiveParameterMock {
2179 type Params = (Mu, Sigma, Nu, Tau, Fifth);
2180 type Links = (Identity, Identity, Identity, Identity, Identity);
2181 }
2182
2183 #[test]
2184 fn prediction_api_returns_eta_and_theta_for_four_parameter_model() {
2185 let y = vec![2.0];
2186 let first = ParameterBlock::<Mu, Identity, _, _>::linear(
2187 DenseDesign::intercept(y.len()),
2188 NoPenalty,
2189 0,
2190 );
2191 let second = ParameterBlock::<Sigma, Identity, _, _>::linear(
2192 DenseDesign::intercept(y.len()),
2193 NoPenalty,
2194 1,
2195 );
2196 let third = ParameterBlock::<Nu, Identity, _, _>::linear(
2197 DenseDesign::intercept(y.len()),
2198 NoPenalty,
2199 2,
2200 );
2201 let fourth = ParameterBlock::<Tau, Identity, _, _>::linear(
2202 DenseDesign::intercept(y.len()),
2203 NoPenalty,
2204 3,
2205 );
2206 let model = Gamlss::try_new(FourParameterMock, (first, second, third, fourth), &y).unwrap();
2207 let beta = vec![0.5, 1.5, 2.5, 3.5];
2208
2209 assert_eq!(
2210 model.predict_eta_row(&beta, 0).unwrap(),
2211 (0.5, 1.5, 2.5, 3.5)
2212 );
2213 assert_eq!(
2214 model.predict_theta_row(&beta, 0).unwrap(),
2215 (1.5, 3.5, 5.5, 7.5)
2216 );
2217 }
2218
2219 #[test]
2220 fn custom_five_parameter_family_uses_generic_blocks() {
2221 let y = vec![2.0];
2222 let blocks = ParameterBlocks::new((
2223 intercept_block::<Mu>(y.len()),
2224 intercept_block::<Sigma>(y.len()),
2225 intercept_block::<Nu>(y.len()),
2226 intercept_block::<Tau>(y.len()),
2227 intercept_block::<Fifth>(y.len()),
2228 ));
2229 let mut model = Gamlss::try_new(FiveParameterMock, blocks, &y).unwrap();
2230 let beta = vec![1.5, 0.5, 1.5, 2.5, 3.5];
2231 let mut grad = vec![0.0; 5];
2232
2233 assert_relative_eq!(model.value(&beta).unwrap(), 0.625);
2234
2235 model.gradient(&beta, &mut grad).unwrap();
2236
2237 assert_eq!(model.parameter_layout().slice("fifth").unwrap(), 4..5);
2238 assert_relative_eq!(grad[0], -0.5);
2239 assert_relative_eq!(grad[1], -0.5);
2240 assert_relative_eq!(grad[2], -0.5);
2241 assert_relative_eq!(grad[3], -0.5);
2242 assert_relative_eq!(grad[4], -0.5);
2243 }
2244
2245 fn intercept_block<P>(
2246 nrows: usize,
2247 ) -> ParameterBlock<P, Identity, LinearPredictorBlock<DenseDesign>, NoPenalty> {
2248 ParameterBlock::linear(DenseDesign::intercept(nrows), NoPenalty, 99)
2249 }
2250
2251 #[test]
2252 fn parameter_layout_and_unpack_use_distribution_parameter_names() {
2253 let y = vec![2.0];
2254 let first = ParameterBlock::<Mu, Identity, _, _>::linear(
2255 DenseDesign::intercept(y.len()),
2256 NoPenalty,
2257 0,
2258 );
2259 let second = ParameterBlock::<Sigma, Identity, _, _>::linear(
2260 DenseDesign::intercept(y.len()),
2261 NoPenalty,
2262 1,
2263 );
2264 let third = ParameterBlock::<Nu, Identity, _, _>::linear(
2265 DenseDesign::intercept(y.len()),
2266 NoPenalty,
2267 2,
2268 );
2269 let model = Gamlss::try_new(ThreeParameterMock, (first, second, third), &y).unwrap();
2270 let theta = vec![1.5, 0.5, -0.5];
2271 let layout = model.parameter_layout();
2272 let unpacked = model.unpack_theta(&theta).unwrap();
2273
2274 assert_eq!(layout.len(), 3);
2275 assert!(!layout.is_empty());
2276 assert_eq!(layout.ncoefficients(), theta.len());
2277 assert_eq!(layout.slice("mu").unwrap(), 0..1);
2278 assert_eq!(layout.slice_of::<Mu>().unwrap(), 0..1);
2279 assert_eq!(layout.slice("sigma").unwrap(), 1..2);
2280 assert_eq!(layout.slice_of::<Sigma>().unwrap(), 1..2);
2281 assert_eq!(layout.slice("nu").unwrap(), 2..3);
2282 assert_eq!(layout.slice_of::<Nu>().unwrap(), 2..3);
2283 assert_eq!(unpacked.coefficients("mu").unwrap(), &[1.5]);
2284 assert_eq!(unpacked.coefficients_of::<Mu>().unwrap(), &[1.5]);
2285 assert_eq!(unpacked.block_of::<Mu>().unwrap().name, "mu");
2286 assert_eq!(unpacked.coefficients("sigma").unwrap(), &[0.5]);
2287 assert_eq!(unpacked.coefficients("nu").unwrap(), &[-0.5]);
2288 }
2289
2290 #[test]
2291 fn visitor_apis_match_allocating_layout_helpers() {
2292 let y = vec![2.0];
2293 let first = ParameterBlock::<Mu, Identity, _, _>::linear(
2294 DenseDesign::intercept(y.len()),
2295 NoPenalty,
2296 0,
2297 );
2298 let second = ParameterBlock::<Sigma, Identity, _, _>::linear(
2299 DenseDesign::intercept(y.len()),
2300 NoPenalty,
2301 1,
2302 );
2303 let third = ParameterBlock::<Nu, Identity, _, _>::linear(
2304 DenseDesign::intercept(y.len()),
2305 NoPenalty,
2306 2,
2307 );
2308 let model = Gamlss::try_new(ThreeParameterMock, (first, second, third), &y).unwrap();
2309
2310 let mut visited_ranges = Vec::new();
2311 model.visit_block_ranges(|index, range| visited_ranges.push((index, range)));
2312 assert_eq!(
2313 visited_ranges,
2314 model
2315 .block_ranges()
2316 .into_iter()
2317 .enumerate()
2318 .collect::<Vec<_>>()
2319 );
2320
2321 let mut visited_slices = Vec::new();
2322 model.visit_parameter_slices(|index, name, range| {
2323 visited_slices.push((index, name, range));
2324 });
2325 let layout = model.parameter_layout();
2326 assert_eq!(
2327 visited_slices,
2328 layout
2329 .slices()
2330 .iter()
2331 .enumerate()
2332 .map(|(index, slice)| (index, slice.name, slice.range.clone()))
2333 .collect::<Vec<_>>()
2334 );
2335
2336 let mut layout_visited = Vec::new();
2337 layout.visit_slices(|index, name, range| layout_visited.push((index, name, range)));
2338 assert_eq!(layout_visited, visited_slices);
2339 }
2340
2341 #[test]
2342 fn training_diagnostics_report_train_nll_penalty_and_gradient_norm() {
2343 let y = vec![1.0, 2.0];
2344 let x = DenseDesign::intercept(y.len());
2345 let mu = ParameterBlock::<Mu, Identity, _, _>::linear(x, RidgePenalty::new(0.5), 0);
2346 let model = Gamlss::try_new(FixedSigmaNormal, (mu,), &y).unwrap();
2347 let theta = vec![1.5];
2348 let diagnostics = model.training_diagnostics(&theta).unwrap();
2349
2350 assert_relative_eq!(diagnostics.train_nll, 0.25);
2351 assert_relative_eq!(diagnostics.penalty, 1.125);
2352 assert_relative_eq!(diagnostics.objective, 1.375);
2353 assert_relative_eq!(diagnostics.gradient_norm, 1.5);
2354 assert_eq!(diagnostics.nonfinite_gradient_count, 0);
2355 }
2356
2357 #[derive(Debug, Clone, Copy)]
2358 struct DifferenceGlobalPenalty {
2359 lambda: f64,
2360 }
2361
2362 impl GlobalPenalty for DifferenceGlobalPenalty {
2363 fn value(&self, beta: &[f64]) -> f64 {
2364 let diff = beta[0] - beta[1];
2365 self.lambda * diff * diff
2366 }
2367
2368 fn add_gradient(&self, beta: &[f64], grad: &mut [f64]) {
2369 let diff = beta[0] - beta[1];
2370 let slope = 2.0 * self.lambda * diff;
2371 grad[0] += slope;
2372 grad[1] -= slope;
2373 }
2374 }
2375
2376 #[derive(Debug, Clone, Copy)]
2377 struct GlobalSquarePenalty {
2378 lambda: f64,
2379 }
2380
2381 impl GlobalPenalty for GlobalSquarePenalty {
2382 fn value(&self, beta: &[f64]) -> f64 {
2383 self.lambda * beta[0] * beta[0]
2384 }
2385
2386 fn add_gradient(&self, beta: &[f64], grad: &mut [f64]) {
2387 grad[0] += 2.0 * self.lambda * beta[0];
2388 }
2389 }
2390
2391 #[test]
2392 fn global_penalty_adds_value_and_gradient_to_full_objective() {
2393 let y = vec![0.0, 0.0];
2394 let x = DenseDesign::from_rows(&[[1.0, 0.0], [0.0, 1.0]]);
2395 let mu = ParameterBlock::<Mu, Identity, _, _>::linear(x, NoPenalty, 0);
2396 let mut model = Gamlss::try_new(FixedSigmaNormal, (mu,), &y)
2397 .unwrap()
2398 .with_global_penalties(DifferenceGlobalPenalty { lambda: 1.0 });
2399 let beta = vec![1.0, -1.0];
2400 let mut grad = vec![0.0; beta.len()];
2401
2402 assert_relative_eq!(model.value(&beta).unwrap(), 5.0);
2403
2404 assert_relative_eq!(model.value_gradient(&beta, &mut grad).unwrap(), 5.0);
2405
2406 assert_relative_eq!(grad[0], 5.0);
2407 assert_relative_eq!(grad[1], -5.0);
2408 }
2409
2410 #[test]
2411 fn block_objective_for_projects_mu_coefficients() {
2412 #[derive(Debug, Clone, Copy)]
2414 struct TwoParamMock;
2415
2416 impl Family for TwoParamMock {
2417 type Eta = (f64, f64);
2418 type Theta = (f64, f64);
2419 type NllGradientEta = (f64, f64);
2420 type Observation<'obs> = f64;
2421
2422 fn theta(&self, eta: Self::Eta) -> Self::Theta {
2423 eta
2424 }
2425
2426 fn nll(&self, y: f64, theta: Self::Theta) -> f64 {
2427 let first = theta.0 - y;
2428 let second = theta.1 - 1.0;
2429 0.5 * (first * first + second * second)
2430 }
2431
2432 fn nll_and_gradient_eta(&self, y: f64, eta: Self::Eta) -> (f64, Self::NllGradientEta) {
2433 let gradient = (eta.0 - y, eta.1 - 1.0);
2434 (self.nll(y, eta), gradient)
2435 }
2436 }
2437
2438 impl ParameterizedFamily<2> for TwoParamMock {
2439 type Params = (Mu, Sigma);
2440 type Links = (Identity, Identity);
2441 }
2442
2443 let y = vec![1.0, 2.0, 3.0];
2444 let x_mu = DenseDesign::from_rows(&[[1.0, 0.5], [1.0, 1.5], [1.0, 2.5]]);
2445 let x_sigma = DenseDesign::intercept(y.len());
2446 let mu = ParameterBlock::<Mu, Identity, _, _>::linear(x_mu, NoPenalty, 0);
2447 let sigma = ParameterBlock::<Sigma, Identity, _, _>::linear(x_sigma, NoPenalty, 0);
2448 let (mu, sigma) = ParameterBlocks::new((mu, sigma));
2449 let mut model = Gamlss::try_new(TwoParamMock, (mu, sigma), &y).unwrap();
2450
2451 let beta = vec![0.5, 0.2, 0.3];
2452 let nparams = model.nparams();
2453
2454 let (mu_dim, mu_value, mu_grad) = {
2456 let mut mu_block = model.block_objective_for::<Mu>(beta.clone()).unwrap();
2457 let dim = mu_block.dim();
2458 let mut block_grad = vec![0.0; dim];
2459 let value = mu_block
2460 .value_gradient(&beta[..2], &mut block_grad)
2461 .unwrap();
2462 (dim, value, block_grad)
2463 };
2464
2465 assert_eq!(mu_dim, 2);
2466 assert_relative_eq!(mu_value, model.value(&beta).unwrap());
2467
2468 let mut full_grad = vec![0.0; nparams];
2469 model.gradient(&beta, &mut full_grad).unwrap();
2470
2471 assert_relative_eq!(mu_grad[0], full_grad[0]);
2472 assert_relative_eq!(mu_grad[1], full_grad[1]);
2473 }
2474
2475 #[test]
2476 fn block_objective_for_rejects_wrong_full_beta_length() {
2477 let y = vec![1.0, 2.0];
2478 let mu =
2479 ParameterBlock::<Mu, Identity, _, _>::linear(DenseDesign::intercept(2), NoPenalty, 0);
2480 let mut model = Gamlss::try_new(FixedSigmaNormal, (mu,), &y).unwrap();
2481
2482 assert_eq!(
2483 model.block_objective_for::<Mu>(Vec::new()).unwrap_err(),
2484 ModelError::BetaLength {
2485 expected: 1,
2486 actual: 0,
2487 }
2488 );
2489 }
2490
2491 #[test]
2492 fn workspace_block_objective_for_rejects_wrong_full_beta_length() {
2493 let y = vec![1.0, 2.0];
2494 let mu =
2495 ParameterBlock::<Mu, Identity, _, _>::linear(DenseDesign::intercept(2), NoPenalty, 0);
2496 let model = Gamlss::try_new(FixedSigmaNormal, (mu,), &y).unwrap();
2497 let mut objective = model.into_workspace_objective();
2498
2499 assert_eq!(
2500 objective.block_objective_for::<Mu>(Vec::new()).unwrap_err(),
2501 ModelError::BetaLength {
2502 expected: 1,
2503 actual: 0,
2504 }
2505 );
2506 }
2507
2508 fn softplus(value: f64) -> f64 {
2509 if value > 30.0 {
2510 value
2511 } else if value < -30.0 {
2512 value.exp()
2513 } else {
2514 value.exp().ln_1p()
2515 }
2516 }
2517
2518 fn sigmoid(value: f64) -> f64 {
2519 if value >= 0.0 {
2520 1.0 / (1.0 + (-value).exp())
2521 } else {
2522 let exp_value = value.exp();
2523 exp_value / (1.0 + exp_value)
2524 }
2525 }
2526}