Skip to main content

gamlss_core/
model.rs

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
18/// Tuple-контракт для набора parameter blocks, совместимого с family `F`.
19///
20/// Implementations are generated for typed tuples of [`ParameterBlock`]. The
21/// model validates observation count, predictor row counts and coefficient
22/// ranges before hot-path evaluation; generated methods may then assume
23/// compatible row counts, finite non-negative weights and non-overlapping block
24/// ranges.
25pub trait GamlssBlocks<F>
26where
27    F: Family,
28{
29    /// Число наблюдений в blocks.
30    fn nrows(&self) -> usize;
31    /// Длина общего beta-вектора, покрывающего все blocks.
32    fn len(&self) -> usize;
33
34    /// `true`, если blocks не требуют коэффициентов.
35    fn is_empty(&self) -> bool {
36        self.len() == 0
37    }
38
39    /// Проверяет, что blocks совместимы с observation count `nobs`.
40    fn validate(&self, nobs: usize) -> Result<(), ModelError>;
41    /// Weighted negative log-likelihood without penalties.
42    ///
43    /// `obs` has already been validated by the model constructor. Each scalar
44    /// likelihood contribution is multiplied by the corresponding observation
45    /// weight.
46    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    /// Аддитивные предикторы на link-шкале для одной строки.
50    fn eta_row(&self, beta: &[f64], row: usize) -> F::Eta
51    where
52        F: Family;
53    /// Penalty value depending on coefficient blocks.
54    fn penalty_value(&self, beta: &[f64]) -> f64;
55    /// Adds the local penalty gradient into an existing full gradient vector.
56    ///
57    /// Implementations with local penalties should override this method. It is
58    /// used by objective scaling to rescale likelihood gradients without
59    /// changing the meaning of penalty weights. The default implementation is
60    /// correct only for block collections whose [`penalty_value`](Self::penalty_value)
61    /// has zero gradient.
62    fn add_penalty_gradient(&self, _beta: &[f64], _grad: &mut [f64]) {}
63    /// Значение weighted negative log-likelihood плюс penalties.
64    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    /// Creates reusable buffers for repeated gradient evaluations.
71    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    /// Добавляет weighted gradient, переиспользуя временные буферы из `workspace`.
82    ///
83    /// The default implementation uses the fused value-gradient path and
84    /// discards the value.
85    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    /// Computes weighted objective value and gradient in one observation pass.
99    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    /// Диапазоны коэффициентов каждого block в общем beta-векторе.
110    fn block_ranges(&self) -> Vec<Range<usize>>;
111    /// Возвращает размещение coefficient blocks внутри плоского beta-вектора.
112    fn parameter_layout(&self) -> ParameterLayout;
113
114    /// Visits coefficient ranges for each parameter block in model order without allocating.
115    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    /// Visits named parameter slices in model order without allocating.
142    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/// Scaling convention for the likelihood part of a compiled objective.
183///
184/// Local and global penalties are not scaled. This keeps penalty weights on a
185/// stable scale when switching between summed and mean likelihood objectives.
186#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
187pub enum ObjectiveScale {
188    /// Use the weighted likelihood sum.
189    #[default]
190    Sum,
191    /// Use the weighted likelihood mean.
192    ///
193    /// For weighted observations the denominator is the sum of observation
194    /// weights. If all weights are zero, the likelihood contribution is left
195    /// unscaled at zero.
196    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/// Скомпилированная типизированная GAMLSS-модель.
211///
212/// `F` задаёт распределение response, а `Blocks` задаёт по одному predictor
213/// block для каждого параметра family.
214///
215/// The model owns the family and parameter blocks, and stores an observation
216/// view supplied by the caller. This keeps `gamlss-core` independent of the
217/// caller's storage backend while static dispatch preserves zero-cost hot-path
218/// evaluation.
219#[derive(Debug, Clone, PartialEq)]
220pub struct Gamlss<F, Blocks, Obs> {
221    /// Family распределения response.
222    pub family: F,
223    /// Типизированные parameter blocks.
224    pub blocks: Blocks,
225    /// Observation view used for training objective evaluation.
226    pub obs: Obs,
227    /// Scaling applied to the likelihood part of the objective.
228    pub objective_scale: ObjectiveScale,
229}
230
231/// GAMLSS objective with reusable gradient buffers.
232///
233/// This wrapper is intended for optimizers that call `gradient` repeatedly.
234/// It owns the compiled model and keeps a [`GradientWorkspace`] between calls,
235/// avoiding per-call allocation of row-gradient and local-gradient vectors.
236#[derive(Debug, Clone, PartialEq)]
237pub struct WorkspaceGamlss<F, Blocks, Obs> {
238    /// Wrapped compiled model.
239    pub model: Gamlss<F, Blocks, Obs>,
240    /// Reusable gradient workspace.
241    pub workspace: GradientWorkspace,
242}
243
244/// Обёртка objective, добавляющая штрафы, зависящие от полного beta-вектора.
245///
246/// В отличие от [`Penalty`], который действует локально на один блок,
247/// [`GlobalPenalty`] позволяет coupling нескольких блоков (например,
248/// центрирующие или LASSO-подобные штрафы).
249#[derive(Debug, Clone, PartialEq)]
250pub struct WithGlobalPenalties<O, GP> {
251    /// Wrapped objective.
252    pub objective: O,
253    /// Global penalties evaluated on the full parameter vector.
254    pub penalties: GP,
255}
256
257impl<F, Blocks, Obs> Gamlss<F, Blocks, Obs> {
258    /// Wraps the model with penalties evaluated on the full beta vector.
259    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    /// Создаёт модель после проверки observation view и blocks.
274    ///
275    /// This is the extension point for custom storage backends. The observation
276    /// view is stored by value, so callers can pass lightweight borrowed views,
277    /// owned adapters, or newtypes around external dataframe/columnar storage.
278    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    /// Число наблюдений.
298    pub fn nobs(&self) -> usize {
299        self.obs.len()
300    }
301
302    /// Число коэффициентов в общем beta-векторе.
303    pub fn nparams(&self) -> usize {
304        self.blocks.len()
305    }
306
307    /// Returns the likelihood scaling convention used by this objective.
308    pub fn objective_scale(&self) -> ObjectiveScale {
309        self.objective_scale
310    }
311
312    /// Returns `self` with a different likelihood scaling convention.
313    #[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    /// Updates the likelihood scaling convention in place.
320    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    /// Нулевой initial beta-вектор нужной длины.
330    pub fn initial_zeros(&self) -> Vec<f64> {
331        vec![0.0; self.nparams()]
332    }
333
334    /// Initial theta vector for external optimizers.
335    pub fn initial_theta(&self) -> Result<Vec<f64>, ModelError> {
336        Ok(self.initial_zeros())
337    }
338
339    /// Creates reusable gradient buffers sized for this model.
340    pub fn gradient_workspace(&self) -> GradientWorkspace {
341        self.blocks.gradient_workspace(self.obs.len())
342    }
343
344    /// Wraps the model as an objective with reusable gradient buffers.
345    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    /// Диапазоны coefficient blocks внутри beta.
354    pub fn block_ranges(&self) -> Vec<Range<usize>> {
355        self.blocks.block_ranges()
356    }
357
358    /// Visits coefficient ranges for each parameter block in model order without allocating.
359    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    /// Layout of named parameter blocks inside theta.
367    pub fn parameter_layout(&self) -> ParameterLayout {
368        self.blocks.parameter_layout()
369    }
370
371    /// Visits named parameter slices in model order without allocating.
372    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    /// Creates a [`BlockObjective`] projected to the coefficients of parameter `P`.
380    ///
381    /// This is the zero-cost building block for staged/block-wise fitting:
382    /// optimise one distribution parameter (e.g. `Mu`) while keeping the
383    /// remaining coefficients fixed at the values in `full_beta`.
384    ///
385    /// # Errors
386    ///
387    /// Returns [`ModelError::UnknownParameter`] if the model blocks do not
388    /// contain a parameter named `P::NAME`. When blocks are constructed through
389    /// the typed [`ParameterBlock`] API this cannot happen in practice — the
390    /// compiler guarantees that `ParameterBlock<Mu, …>` registers itself as
391    /// `"mu"`.
392    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    /// Unpacks a flat theta vector into named coefficient blocks.
408    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    /// Computes training diagnostics for a candidate theta vector.
425    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    /// Predicts link-scale distribution predictors for one training row.
454    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    /// Predicts natural-scale distribution parameters for one training row.
464    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    /// Predicts link-scale distribution predictors for all training rows.
472    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    /// Predicts natural-scale distribution parameters for all training rows.
483    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    /// Predicts link-scale distribution predictors for one row from compatible prediction blocks.
494    ///
495    /// # Errors
496    ///
497    /// Returns an error if `theta` has the wrong length, if `blocks` do not
498    /// match this model's parameter layout, or if `row` is out of bounds for
499    /// the supplied prediction blocks.
500    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    /// Predicts natural-scale distribution parameters for one row from compatible prediction blocks.
517    ///
518    /// # Errors
519    ///
520    /// Returns an error if `theta` has the wrong length, if `blocks` do not
521    /// match this model's parameter layout, or if `row` is out of bounds for
522    /// the supplied prediction blocks.
523    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    /// Predicts link-scale distribution predictors for all rows in compatible prediction blocks.
539    ///
540    /// # Errors
541    ///
542    /// Returns an error if `theta` has the wrong length or if `blocks` do not
543    /// match this model's parameter layout.
544    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    /// Predicts natural-scale distribution parameters for all rows in compatible prediction blocks.
561    ///
562    /// # Errors
563    ///
564    /// Returns an error if `theta` has the wrong length or if `blocks` do not
565    /// match this model's parameter layout.
566    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    /// Проверяет длину beta и вычисляет objective.
583    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    /// Проверяет размеры beta/grad и записывает gradient.
592    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    /// Проверяет размеры beta/grad и записывает gradient, переиспользуя workspace.
598    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    /// Проверяет размеры beta/grad и вычисляет value + gradient за один проход.
619    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    /// Проверяет размеры beta/grad и вычисляет fused value + gradient с workspace.
629    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    /// Создаёт unweighted модель после проверки response и blocks.
679    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    /// Создаёт модель с observation weights после проверки response, weights и blocks.
690    ///
691    /// Weights must have the same length as `y`; each weight must be finite and
692    /// non-negative. Zero weights are accepted and exclude the corresponding
693    /// observation from likelihood and gradient contributions.
694    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    /// Creates a workspace-backed objective from a compiled model.
711    pub fn new(model: Gamlss<F, Blocks, Obs>) -> Self {
712        model.into_workspace_objective()
713    }
714
715    /// Returns the wrapped model.
716    pub fn model(&self) -> &Gamlss<F, Blocks, Obs> {
717        &self.model
718    }
719
720    /// Returns the wrapped model mutably.
721    pub fn model_mut(&mut self) -> &mut Gamlss<F, Blocks, Obs> {
722        &mut self.model
723    }
724
725    /// Consumes the workspace-backed objective and returns the wrapped model.
726    pub fn into_model(self) -> Gamlss<F, Blocks, Obs> {
727        self.model
728    }
729
730    /// Returns the likelihood scaling convention used by this objective.
731    pub fn objective_scale(&self) -> ObjectiveScale {
732        self.model.objective_scale()
733    }
734
735    /// Returns `self` with a different likelihood scaling convention.
736    #[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    /// Updates the likelihood scaling convention in place.
743    pub fn set_objective_scale(&mut self, objective_scale: ObjectiveScale) {
744        self.model.set_objective_scale(objective_scale);
745    }
746
747    /// Creates a [`BlockObjective`] projected to the coefficients of parameter `P`.
748    ///
749    /// Delegates to the inner model's [`Gamlss::block_objective_for`] through
750    /// [`model_mut`](Self::model_mut), so the returned objective borrows the
751    /// workspace-backed model and reuses its gradient buffers.
752    ///
753    /// # Errors
754    ///
755    /// Returns [`ModelError::UnknownParameter`] if the model blocks do not
756    /// contain a parameter named `P::NAME`.
757    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    /// Wraps the workspace-backed objective with penalties evaluated on the full beta vector.
774    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
862/// Макрос, генерирующий реализацию [`GamlssBlocks`] для tuple parameter blocks.
863///
864/// Принимает арность `K`, списки parameter-типов, link-типов, design-типов и
865/// penalty-типов, а также имена внутренних переменных. На выходе даёт
866/// zero-cost реализацию `train_nll`, `value_gradient_into_workspace`, `penalty_value` и
867/// вспомогательных методов без dynamic dispatch.
868macro_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
1237/// Проверяет, что число строк predictor-а совпадает с длиной response.
1238fn 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
1254/// Проверяет пересечение двух диапазонов (непустое пересечение).
1255fn ranges_overlap(first: Range<usize>, second: Range<usize>) -> bool {
1256    first.start < second.end && second.start < first.end
1257}
1258
1259/// Поэлементно добавляет `values` к `out`.
1260///
1261/// Вызывающий код должен гарантировать `out.len() == values.len()`.
1262fn 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
1277/// Проверяет длину вектора (beta или gradient) и возвращает typed error.
1278fn 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        // Simple 2-param mock: identity link for both, NLL = 0.5 * sum of squares.
2413        #[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        // Scope the block objective to release the mutable borrow on model.
2455        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}