1use crate::dist::{Distribution, DistributionMethods, RegressionDistn};
2use crate::scores::{CRPScore, LogScore, Scorable};
3use ndarray::{array, Array1, Array2, Array3};
4use rand::prelude::*;
5use statrs::distribution::{Continuous, ContinuousCDF, Normal as NormalDist};
6
7#[derive(Debug, Clone)]
12pub struct HalfNormal {
13 pub scale: Array1<f64>,
15 _params: Array2<f64>,
17}
18
19impl Distribution for HalfNormal {
20 fn from_params(params: &Array2<f64>) -> Self {
21 let scale = params.column(0).mapv(f64::exp);
22 HalfNormal {
23 scale,
24 _params: params.clone(),
25 }
26 }
27
28 fn fit(y: &Array1<f64>) -> Array1<f64> {
29 let n = y.len();
32 if n == 0 {
33 return array![0.0];
34 }
35
36 let sum_sq: f64 = y.iter().map(|&x| x * x).sum();
37 let scale = (sum_sq / n as f64).sqrt().max(1e-6);
38
39 array![scale.ln()]
40 }
41
42 fn n_params(&self) -> usize {
43 1
44 }
45
46 fn predict(&self) -> Array1<f64> {
47 let sqrt_2_over_pi = (2.0 / std::f64::consts::PI).sqrt();
49 &self.scale * sqrt_2_over_pi
50 }
51
52 fn params(&self) -> &Array2<f64> {
53 &self._params
54 }
55}
56
57impl RegressionDistn for HalfNormal {}
58
59impl DistributionMethods for HalfNormal {
60 fn mean(&self) -> Array1<f64> {
61 let sqrt_2_over_pi = (2.0 / std::f64::consts::PI).sqrt();
63 &self.scale * sqrt_2_over_pi
64 }
65
66 fn variance(&self) -> Array1<f64> {
67 let one_minus_2_over_pi = 1.0 - 2.0 / std::f64::consts::PI;
69 &self.scale * &self.scale * one_minus_2_over_pi
70 }
71
72 fn std(&self) -> Array1<f64> {
73 self.variance().mapv(f64::sqrt)
74 }
75
76 fn pdf(&self, y: &Array1<f64>) -> Array1<f64> {
77 let sqrt_2_over_pi = (2.0 / std::f64::consts::PI).sqrt();
79 let mut result = Array1::zeros(y.len());
80
81 for i in 0..y.len() {
82 if y[i] >= 0.0 {
83 let scale_sq = self.scale[i] * self.scale[i];
84 result[i] =
85 sqrt_2_over_pi / self.scale[i] * (-y[i] * y[i] / (2.0 * scale_sq)).exp();
86 }
87 }
88 result
89 }
90
91 fn logpdf(&self, y: &Array1<f64>) -> Array1<f64> {
92 let log_sqrt_2_over_pi = (2.0 / std::f64::consts::PI).sqrt().ln();
94 let mut result = Array1::zeros(y.len());
95
96 for i in 0..y.len() {
97 if y[i] >= 0.0 {
98 let scale_sq = self.scale[i] * self.scale[i];
99 result[i] =
100 log_sqrt_2_over_pi - self.scale[i].ln() - (y[i] * y[i]) / (2.0 * scale_sq);
101 } else {
102 result[i] = f64::NEG_INFINITY;
103 }
104 }
105 result
106 }
107
108 fn cdf(&self, y: &Array1<f64>) -> Array1<f64> {
109 let std_normal = NormalDist::new(0.0, 1.0).unwrap();
112 let mut result = Array1::zeros(y.len());
113
114 for i in 0..y.len() {
115 if y[i] >= 0.0 {
116 result[i] = 2.0 * std_normal.cdf(y[i] / self.scale[i]) - 1.0;
117 }
118 }
119 result
120 }
121
122 fn ppf(&self, q: &Array1<f64>) -> Array1<f64> {
123 let std_normal = NormalDist::new(0.0, 1.0).unwrap();
125 let mut result = Array1::zeros(q.len());
126
127 for i in 0..q.len() {
128 let q_clamped = q[i].clamp(0.0, 1.0 - 1e-15);
129 result[i] = self.scale[i] * std_normal.inverse_cdf((1.0 + q_clamped) / 2.0);
130 }
131 result
132 }
133
134 fn sample(&self, n_samples: usize) -> Array2<f64> {
135 let n_obs = self.scale.len();
136 let mut samples = Array2::zeros((n_samples, n_obs));
137 let mut rng = rand::rng();
138
139 for i in 0..n_obs {
140 let std_normal = NormalDist::new(0.0, 1.0).unwrap();
141 for s in 0..n_samples {
142 let u: f64 = rng.random();
144 let z = std_normal.inverse_cdf(u);
145 samples[[s, i]] = self.scale[i] * z.abs();
146 }
147 }
148 samples
149 }
150
151 fn median(&self) -> Array1<f64> {
152 let std_normal = NormalDist::new(0.0, 1.0).unwrap();
154 let median_factor = std_normal.inverse_cdf(0.75);
155 &self.scale * median_factor
156 }
157
158 fn mode(&self) -> Array1<f64> {
159 Array1::zeros(self.scale.len())
161 }
162}
163
164impl Scorable<LogScore> for HalfNormal {
165 fn score(&self, y: &Array1<f64>) -> Array1<f64> {
166 let log_sqrt_2_over_pi = (2.0 / std::f64::consts::PI).sqrt().ln();
170 let mut scores = Array1::zeros(y.len());
171
172 for i in 0..y.len() {
173 let scale_sq = self.scale[i] * self.scale[i];
174 scores[i] = -log_sqrt_2_over_pi + self.scale[i].ln() + (y[i] * y[i]) / (2.0 * scale_sq);
175 }
176 scores
177 }
178
179 fn d_score(&self, y: &Array1<f64>) -> Array2<f64> {
180 let n_obs = y.len();
181 let mut d_params = Array2::zeros((n_obs, 1));
182
183 for i in 0..n_obs {
184 let scale_sq = self.scale[i] * self.scale[i];
185 d_params[[i, 0]] = 1.0 - (y[i] * y[i]) / scale_sq;
189 }
190
191 d_params
192 }
193
194 fn metric(&self) -> Array3<f64> {
195 let n_obs = self.scale.len();
197 let mut fi = Array3::zeros((n_obs, 1, 1));
198
199 for i in 0..n_obs {
200 fi[[i, 0, 0]] = 2.0;
201 }
202
203 fi
204 }
205}
206
207impl Scorable<CRPScore> for HalfNormal {
208 fn score(&self, y: &Array1<f64>) -> Array1<f64> {
209 let std_normal = NormalDist::new(0.0, 1.0).unwrap();
222 let sqrt_pi = std::f64::consts::PI.sqrt();
223 let sqrt_2 = std::f64::consts::SQRT_2;
224
225 let mut scores = Array1::zeros(y.len());
226 for i in 0..y.len() {
227 let y_i = y[i].max(0.0); let sigma = self.scale[i];
229 let z = y_i / sigma;
230
231 let phi_z = std_normal.pdf(z);
236 let big_phi_z = std_normal.cdf(z);
237 let big_phi_z_sqrt2 = std_normal.cdf(z * sqrt_2);
238
239 scores[i] = sigma
241 * (z * (2.0 * big_phi_z - 1.0) + 2.0 * phi_z
242 - (2.0 * big_phi_z_sqrt2 - 1.0) / sqrt_pi
243 - 1.0 / sqrt_pi);
244 }
245 scores
246 }
247
248 fn d_score(&self, y: &Array1<f64>) -> Array2<f64> {
249 let std_normal = NormalDist::new(0.0, 1.0).unwrap();
251 let sqrt_pi = std::f64::consts::PI.sqrt();
252 let sqrt_2 = std::f64::consts::SQRT_2;
253
254 let n_obs = y.len();
255 let mut d_params = Array2::zeros((n_obs, 1));
256
257 for i in 0..n_obs {
258 let y_i = y[i].max(0.0);
259 let sigma = self.scale[i];
260 let z = y_i / sigma;
261
262 let phi_z = std_normal.pdf(z);
263 let big_phi_z = std_normal.cdf(z);
264 let phi_z_sqrt2 = std_normal.pdf(z * sqrt_2);
265 let big_phi_z_sqrt2 = std_normal.cdf(z * sqrt_2);
266
267 let ds_dz = 2.0 * big_phi_z - 1.0 - 2.0 * sqrt_2 * phi_z_sqrt2 / sqrt_pi;
276
277 let s = z * (2.0 * big_phi_z - 1.0) + 2.0 * phi_z
278 - (2.0 * big_phi_z_sqrt2 - 1.0) / sqrt_pi
279 - 1.0 / sqrt_pi;
280
281 d_params[[i, 0]] = sigma * (s - z * ds_dz);
283 }
284
285 d_params
286 }
287
288 fn metric(&self) -> Array3<f64> {
289 let n_obs = self.scale.len();
292 let mut fi = Array3::zeros((n_obs, 1, 1));
293
294 let metric_val = 2.0 / std::f64::consts::PI;
297
298 for i in 0..n_obs {
299 fi[[i, 0, 0]] = metric_val;
300 }
301
302 fi
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use approx::assert_relative_eq;
310
311 #[test]
312 fn test_halfnormal_distribution_methods() {
313 let params = Array2::from_shape_vec((2, 1), vec![0.0, 2.0_f64.ln()]).unwrap();
316 let dist = HalfNormal::from_params(¶ms);
317
318 let mean = dist.mean();
320 let sqrt_2_over_pi = (2.0 / std::f64::consts::PI).sqrt();
321 assert_relative_eq!(mean[0], sqrt_2_over_pi, epsilon = 1e-10);
322 assert_relative_eq!(mean[1], 2.0 * sqrt_2_over_pi, epsilon = 1e-10);
323
324 let var = dist.variance();
326 let one_minus_2_over_pi = 1.0 - 2.0 / std::f64::consts::PI;
327 assert_relative_eq!(var[0], one_minus_2_over_pi, epsilon = 1e-10);
328 assert_relative_eq!(var[1], 4.0 * one_minus_2_over_pi, epsilon = 1e-10);
329
330 let mode = dist.mode();
332 assert_relative_eq!(mode[0], 0.0, epsilon = 1e-10);
333 assert_relative_eq!(mode[1], 0.0, epsilon = 1e-10);
334 }
335
336 #[test]
337 fn test_halfnormal_cdf_ppf() {
338 let params = Array2::from_shape_vec((3, 1), vec![0.0, 0.0, 0.0]).unwrap();
340 let dist = HalfNormal::from_params(¶ms);
341
342 let y = Array1::from_vec(vec![0.0, 0.0, 0.0]);
344 let cdf = dist.cdf(&y);
345 assert_relative_eq!(cdf[0], 0.0, epsilon = 1e-10);
346
347 let q = Array1::from_vec(vec![0.0, 0.0, 0.0]);
349 let ppf = dist.ppf(&q);
350 assert_relative_eq!(ppf[0], 0.0, epsilon = 1e-10);
351
352 let q = Array1::from_vec(vec![0.25, 0.5, 0.75]);
354 let ppf = dist.ppf(&q);
355 let cdf_of_ppf = dist.cdf(&ppf);
356 assert_relative_eq!(cdf_of_ppf[0], 0.25, epsilon = 1e-6);
357 assert_relative_eq!(cdf_of_ppf[1], 0.5, epsilon = 1e-6);
358 assert_relative_eq!(cdf_of_ppf[2], 0.75, epsilon = 1e-6);
359 }
360
361 #[test]
362 fn test_halfnormal_pdf() {
363 let params = Array2::from_shape_vec((1, 1), vec![0.0]).unwrap();
364 let dist = HalfNormal::from_params(¶ms);
365
366 let y = Array1::from_vec(vec![0.0]);
368 let pdf = dist.pdf(&y);
369 let sqrt_2_over_pi = (2.0 / std::f64::consts::PI).sqrt();
370 assert_relative_eq!(pdf[0], sqrt_2_over_pi, epsilon = 1e-10);
371
372 let y_neg = Array1::from_vec(vec![-1.0]);
374 let pdf_neg = dist.pdf(&y_neg);
375 assert_relative_eq!(pdf_neg[0], 0.0, epsilon = 1e-10);
376 }
377
378 #[test]
379 fn test_halfnormal_sample() {
380 let params = Array2::from_shape_vec((1, 1), vec![2.0_f64.ln()]).unwrap();
382 let dist = HalfNormal::from_params(¶ms);
383
384 let samples = dist.sample(1000);
385 assert_eq!(samples.shape(), &[1000, 1]);
386
387 assert!(samples.iter().all(|&x| x >= 0.0));
389
390 let sample_mean: f64 = samples.column(0).iter().sum::<f64>() / samples.nrows() as f64;
392 let expected_mean = 2.0 * (2.0 / std::f64::consts::PI).sqrt();
393 assert!((sample_mean - expected_mean).abs() / expected_mean < 0.15);
394 }
395
396 #[test]
397 fn test_halfnormal_fit() {
398 let y = Array1::from_vec(vec![0.5, 1.0, 1.5, 2.0, 2.5]);
399 let params = HalfNormal::fit(&y);
400 assert_eq!(params.len(), 1);
401 }
403
404 #[test]
405 fn test_halfnormal_logscore() {
406 let params = Array2::from_shape_vec((1, 1), vec![0.0]).unwrap();
407 let dist = HalfNormal::from_params(¶ms);
408
409 let y = Array1::from_vec(vec![1.0]);
410 let score = Scorable::<LogScore>::score(&dist, &y);
411
412 assert!(score[0].is_finite());
414 assert!(score[0] > 0.0);
415 }
416
417 #[test]
418 fn test_halfnormal_d_score() {
419 let params = Array2::from_shape_vec((1, 1), vec![0.0]).unwrap();
420 let dist = HalfNormal::from_params(¶ms);
421
422 let y = Array1::from_vec(vec![1.0]);
423 let d_score = Scorable::<LogScore>::d_score(&dist, &y);
424
425 assert!(d_score[[0, 0]].is_finite());
427 }
428
429 #[test]
430 fn test_halfnormal_median() {
431 let params = Array2::from_shape_vec((1, 1), vec![0.0]).unwrap();
432 let dist = HalfNormal::from_params(¶ms);
433
434 let median = dist.median();
436 assert_relative_eq!(median[0], 0.6744897501960817, epsilon = 1e-6);
437 }
438
439 #[test]
440 fn test_halfnormal_survival_function() {
441 let params = Array2::from_shape_vec((1, 1), vec![0.0]).unwrap();
442 let dist = HalfNormal::from_params(¶ms);
443
444 let y = Array1::from_vec(vec![0.0]);
446 let sf = dist.sf(&y);
447 assert_relative_eq!(sf[0], 1.0, epsilon = 1e-10);
448
449 let y = Array1::from_vec(vec![1.0]);
451 let sf = dist.sf(&y);
452 let cdf = dist.cdf(&y);
453 assert_relative_eq!(sf[0] + cdf[0], 1.0, epsilon = 1e-10);
454 }
455
456 #[test]
457 fn test_halfnormal_crpscore() {
458 let params = Array2::from_shape_vec((1, 1), vec![0.0]).unwrap(); let dist = HalfNormal::from_params(¶ms);
460
461 let y = Array1::from_vec(vec![1.0]);
462 let score = Scorable::<CRPScore>::score(&dist, &y);
463
464 assert!(score[0].is_finite());
466 assert!(score[0] >= 0.0);
467
468 let y_zero = Array1::from_vec(vec![0.0]);
470 let score_zero = Scorable::<CRPScore>::score(&dist, &y_zero);
471 assert!(score_zero[0].is_finite());
472 assert!(score_zero[0] >= 0.0);
473 }
474
475 #[test]
476 fn test_halfnormal_crpscore_d_score() {
477 let params = Array2::from_shape_vec((1, 1), vec![0.0]).unwrap();
478 let dist = HalfNormal::from_params(¶ms);
479
480 let y = Array1::from_vec(vec![1.0]);
481 let d_score = Scorable::<CRPScore>::d_score(&dist, &y);
482
483 assert!(d_score[[0, 0]].is_finite());
485 }
486
487 #[test]
488 fn test_halfnormal_crpscore_metric() {
489 let params = Array2::from_shape_vec((1, 1), vec![0.0]).unwrap();
490 let dist = HalfNormal::from_params(¶ms);
491
492 let metric = Scorable::<CRPScore>::metric(&dist);
493
494 assert!(metric[[0, 0, 0]] > 0.0);
496 }
497
498 #[test]
499 fn test_halfnormal_crpscore_scaling() {
500 let params1 = Array2::from_shape_vec((1, 1), vec![0.0]).unwrap(); let params2 = Array2::from_shape_vec((1, 1), vec![2.0_f64.ln()]).unwrap(); let dist1 = HalfNormal::from_params(¶ms1);
504 let dist2 = HalfNormal::from_params(¶ms2);
505
506 let y = Array1::from_vec(vec![0.0]);
508 let score1 = Scorable::<CRPScore>::score(&dist1, &y);
509 let score2 = Scorable::<CRPScore>::score(&dist2, &y);
510
511 assert_relative_eq!(score2[0] / score1[0], 2.0, epsilon = 1e-10);
512 }
513}