Skip to main content

gamlss_core/
model.rs

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