1use crate::array::Array;
7use crate::error::Result;
8use crate::random::state::RandomState;
9use num_traits::{Float, NumCast};
10use scirs2_core::random::prelude::Rng;
11use std::fmt::{Debug, Display};
12
13fn get_global_random_state() -> Result<std::sync::MutexGuard<'static, RandomState>> {
15 crate::random::distributions::get_global_random_state()
16}
17
18pub fn truncated_normal<T>(mean: T, std: T, low: T, high: T, shape: &[usize]) -> Result<Array<T>>
32where
33 T: Float
34 + NumCast
35 + Clone
36 + Debug
37 + Display
38 + scirs2_core::ndarray::distributions::uniform::SampleUniform,
39{
40 let rng = get_global_random_state()?;
41 rng.truncated_normal(mean, std, low, high, shape)
42}
43
44pub fn vonmises<T>(mu: T, kappa: T, shape: &[usize]) -> Result<Array<T>>
56where
57 T: Float + NumCast + Clone + Debug + Display,
58{
59 let rng = get_global_random_state()?;
60 rng.vonmises(mu, kappa, shape)
61}
62
63pub fn noncentral_chisquare<T>(df: T, nonc: T, shape: &[usize]) -> Result<Array<T>>
75where
76 T: Float + NumCast + Clone + Debug + Display,
77{
78 let rng = get_global_random_state()?;
79 rng.noncentral_chisquare(df, nonc, shape)
80}
81
82pub fn noncentral_f<T>(dfnum: T, dfden: T, nonc: T, shape: &[usize]) -> Result<Array<T>>
95where
96 T: Float + NumCast + Clone + Debug + Display,
97{
98 let rng = get_global_random_state()?;
99 rng.noncentral_f(dfnum, dfden, nonc, shape)
100}
101
102pub fn maxwell<T>(scale: T, shape: &[usize]) -> Result<Array<T>>
113where
114 T: Float + NumCast + Clone + Debug + Display,
115{
116 let rng = get_global_random_state()?;
117 rng.maxwell(scale, shape)
118}
119
120pub fn power<T>(a: T, shape: &[usize]) -> Result<Array<T>>
131where
132 T: Float + NumCast + Clone + Debug + Display,
133{
134 let rng = get_global_random_state()?;
135 rng.power(a, shape)
136}
137
138pub fn multivariate_normal_cholesky<T>(means: &[T], cov: &Array<T>, size: usize) -> Result<Array<T>>
150where
151 T: Float + NumCast + Clone + Debug + Display,
152{
153 let rng = get_global_random_state()?;
154 rng.multivariate_normal_cholesky(means, cov, size)
155}
156
157pub fn random_correlation_matrix<T>(n: usize) -> Result<Array<T>>
167where
168 T: Float
169 + NumCast
170 + Clone
171 + Debug
172 + Display
173 + scirs2_core::ndarray::distributions::uniform::SampleUniform,
174{
175 let rng = get_global_random_state()?;
176 rng.random_correlation_matrix(n)
177}
178
179pub fn mixture_of_normals<T>(
192 weights: &[T],
193 means: &[T],
194 stds: &[T],
195 shape: &[usize],
196) -> Result<Array<T>>
197where
198 T: Float + NumCast + Clone + Debug + Display,
199{
200 let rng = get_global_random_state()?;
201 rng.mixture_of_normals(weights, means, stds, shape)
202}
203
204pub fn sobol_sequence<T>(dim: usize, n: usize) -> Result<Array<T>>
215where
216 T: Float + NumCast + Clone + Debug + Display,
217{
218 let rng = get_global_random_state()?;
219 rng.sobol_sequence(dim, n)
220}
221
222pub fn latin_hypercube<T>(dim: usize, n: usize) -> Result<Array<T>>
233where
234 T: Float + NumCast + Clone + Debug + Display,
235{
236 let rng = get_global_random_state()?;
237 rng.latin_hypercube(dim, n)
238}
239
240pub fn copula<T>(corr: &Array<T>, n: usize, copula_type: &str) -> Result<Array<T>>
252where
253 T: Float + NumCast + Clone + Debug + Display,
254{
255 let rng = get_global_random_state()?;
256 rng.copula(corr, n, copula_type)
257}
258
259impl RandomState {
261 pub fn truncated_normal<T>(
263 &self,
264 mean: T,
265 std: T,
266 low: T,
267 high: T,
268 shape: &[usize],
269 ) -> Result<Array<T>>
270 where
271 T: Float + NumCast + Clone + Debug + Display,
272 {
273 if low >= high {
274 return Err(crate::error::NumRs2Error::InvalidOperation(format!(
275 "Lower bound must be less than upper bound, got low={}, high={}",
276 low, high
277 )));
278 }
279
280 if std <= T::zero() {
281 return Err(crate::error::NumRs2Error::InvalidOperation(format!(
282 "Standard deviation must be positive, got {}",
283 std
284 )));
285 }
286
287 let size: usize = shape.iter().product();
288 let mut vec = Vec::with_capacity(size);
289 let mut rng = self.get_rng()?;
290
291 let mean_f64 = mean.to_f64().unwrap_or(0.0);
293 let std_f64 = std.to_f64().unwrap_or(1.0);
294 let low_f64 = low.to_f64().unwrap_or(f64::NEG_INFINITY);
295 let high_f64 = high.to_f64().unwrap_or(f64::INFINITY);
296
297 for _ in 0..size {
299 let mut sample;
300 let mut attempts = 0;
301 loop {
302 let u1 = rng.random::<f64>();
304 let u2 = rng.random::<f64>();
305 let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
306
307 sample = mean_f64 + std_f64 * z;
309
310 if sample >= low_f64 && sample <= high_f64 {
312 break;
313 }
314
315 attempts += 1;
316 if attempts > 1000 {
318 sample = (low_f64 + high_f64) / 2.0;
319 break;
320 }
321 }
322
323 let val = <T as NumCast>::from(sample).unwrap_or_else(|| {
324 if sample <= low.to_f64().unwrap_or(f64::NEG_INFINITY) {
325 low
326 } else {
327 high
328 }
329 });
330
331 vec.push(val);
332 }
333
334 Ok(Array::from_vec(vec).reshape(shape))
335 }
336
337 pub fn vonmises<T>(&self, mu: T, kappa: T, shape: &[usize]) -> Result<Array<T>>
339 where
340 T: Float + NumCast + Clone + Debug + Display,
341 {
342 if kappa < T::zero() {
343 return Err(crate::error::NumRs2Error::InvalidOperation(format!(
344 "Concentration parameter must be non-negative, got {}",
345 kappa
346 )));
347 }
348
349 let size: usize = shape.iter().product();
350 let mut vec = Vec::with_capacity(size);
351 let mut rng = self.get_rng()?;
352
353 let kappa_f64 = kappa.to_f64().unwrap_or(0.0);
354 let mu_f64 = mu.to_f64().unwrap_or(0.0);
355
356 for _ in 0..size {
358 let sample = if kappa_f64 < 1e-6 {
359 rng.random::<f64>() * 2.0 * std::f64::consts::PI - std::f64::consts::PI
361 } else {
362 let a = 1.0 + (1.0 + 4.0 * kappa_f64 * kappa_f64).sqrt();
364 let b = (a - (2.0 * a).sqrt()) / (2.0 * kappa_f64);
365 let r = (1.0 + b * b) / (2.0 * b);
366
367 let mut attempts = 0;
368 loop {
369 attempts += 1;
370 if attempts > 1000 {
371 break rng.random::<f64>() * 2.0 * std::f64::consts::PI
373 - std::f64::consts::PI;
374 }
375
376 let u1 = rng.random::<f64>();
377 let z = (1.0 - u1).cos();
378 let f = (1.0 + r * z) / (r + z);
379 let c = kappa_f64 * (r - f);
380
381 let u2 = rng.random::<f64>();
382
383 if c * (2.0 - c) - u2 > 0.0 {
384 let u3 = rng.random::<f64>();
385 let theta = if u3 - 0.5 > 0.0 { f.acos() } else { -f.acos() };
386 break theta;
387 }
388
389 if (c / u2.max(1e-10)).ln() + 1.0 - c >= 0.0 {
390 let u3 = rng.random::<f64>();
391 let theta = if u3 - 0.5 > 0.0 { f.acos() } else { -f.acos() };
392 break theta;
393 }
394 }
395 };
396
397 let angle = mu_f64 + sample;
398 let normalized = ((angle + std::f64::consts::PI) % (2.0 * std::f64::consts::PI))
399 - std::f64::consts::PI;
400
401 vec.push(<T as NumCast>::from(normalized).unwrap_or(T::zero()));
402 }
403
404 Ok(Array::from_vec(vec).reshape(shape))
405 }
406
407 pub fn noncentral_chisquare<T>(&self, df: T, nonc: T, shape: &[usize]) -> Result<Array<T>>
409 where
410 T: Float + NumCast + Clone + Debug + Display,
411 {
412 if df <= T::zero() {
413 return Err(crate::error::NumRs2Error::InvalidOperation(format!(
414 "Degrees of freedom must be positive, got {}",
415 df
416 )));
417 }
418
419 if nonc < T::zero() {
420 return Err(crate::error::NumRs2Error::InvalidOperation(format!(
421 "Non-centrality parameter must be non-negative, got {}",
422 nonc
423 )));
424 }
425
426 let size: usize = shape.iter().product();
427 let mut vec = Vec::with_capacity(size);
428
429 let df_f64 = df.to_f64().unwrap_or(0.0);
430 let nonc_f64 = nonc.to_f64().unwrap_or(0.0);
431
432 for _ in 0..size {
434 let pois: Array<u64> = self.poisson(nonc_f64 / 2.0, &[1])?;
436 let n = <usize as NumCast>::from(pois.to_vec()[0]).unwrap_or(0);
437
438 let chi2 = self.chisquare(
440 <T as NumCast>::from(df_f64 + 2.0 * n as f64).unwrap_or(T::zero()),
441 &[1],
442 )?;
443
444 vec.push(chi2.to_vec()[0]);
445 }
446
447 Ok(Array::from_vec(vec).reshape(shape))
448 }
449
450 pub fn noncentral_f<T>(&self, dfnum: T, dfden: T, nonc: T, shape: &[usize]) -> Result<Array<T>>
452 where
453 T: Float + NumCast + Clone + Debug + Display,
454 {
455 if dfnum <= T::zero() || dfden <= T::zero() {
456 return Err(crate::error::NumRs2Error::InvalidOperation(format!(
457 "Degrees of freedom must be positive, got dfnum={}, dfden={}",
458 dfnum, dfden
459 )));
460 }
461
462 if nonc < T::zero() {
463 return Err(crate::error::NumRs2Error::InvalidOperation(format!(
464 "Non-centrality parameter must be non-negative, got {}",
465 nonc
466 )));
467 }
468
469 let size: usize = shape.iter().product();
470 let mut vec = Vec::with_capacity(size);
471
472 let dfnum_f64 = dfnum.to_f64().unwrap_or(0.0);
473 let dfden_f64 = dfden.to_f64().unwrap_or(0.0);
474 let nonc_f64 = nonc.to_f64().unwrap_or(0.0);
475
476 for _ in 0..size {
479 let nc_chi2 = self.noncentral_chisquare(
481 <T as NumCast>::from(dfnum_f64).unwrap_or(T::zero()),
482 <T as NumCast>::from(nonc_f64).unwrap_or(T::zero()),
483 &[1],
484 )?;
485
486 let chi2 =
488 self.chisquare(<T as NumCast>::from(dfden_f64).unwrap_or(T::zero()), &[1])?;
489
490 let f_val = (nc_chi2.to_vec()[0] / dfnum) / (chi2.to_vec()[0] / dfden);
492 vec.push(f_val);
493 }
494
495 Ok(Array::from_vec(vec).reshape(shape))
496 }
497
498 pub fn maxwell<T>(&self, scale: T, shape: &[usize]) -> Result<Array<T>>
500 where
501 T: Float + NumCast + Clone + Debug + Display,
502 {
503 if scale <= T::zero() {
504 return Err(crate::error::NumRs2Error::InvalidOperation(format!(
505 "Scale parameter must be positive, got {}",
506 scale
507 )));
508 }
509
510 let size: usize = shape.iter().product();
511 let mut vec = Vec::with_capacity(size);
512
513 for _ in 0..size {
516 let x = self.normal(T::zero(), scale, &[3])?;
518
519 let magnitude =
521 (x.to_vec()[0].powi(2) + x.to_vec()[1].powi(2) + x.to_vec()[2].powi(2)).sqrt();
522
523 vec.push(magnitude);
524 }
525
526 Ok(Array::from_vec(vec).reshape(shape))
527 }
528
529 pub fn power<T>(&self, a: T, shape: &[usize]) -> Result<Array<T>>
531 where
532 T: Float + NumCast + Clone + Debug + Display,
533 {
534 if a <= T::zero() {
535 return Err(crate::error::NumRs2Error::InvalidOperation(format!(
536 "Power parameter must be positive, got {}",
537 a
538 )));
539 }
540
541 let size: usize = shape.iter().product();
542 let mut vec = Vec::with_capacity(size);
543 let mut rng = self.get_rng()?;
544
545 for _ in 0..size {
548 let u = rng.random::<f64>();
549 let val = u.powf(1.0 / a.to_f64().unwrap_or(1.0));
550 vec.push(<T as NumCast>::from(val).unwrap_or(T::zero()));
551 }
552
553 Ok(Array::from_vec(vec).reshape(shape))
554 }
555
556 pub fn multivariate_normal_cholesky<T>(
558 &self,
559 means: &[T],
560 cov: &Array<T>,
561 size: usize,
562 ) -> Result<Array<T>>
563 where
564 T: Float + NumCast + Clone + Debug + Display,
565 {
566 let n = means.len();
567 let cov_shape = cov.shape();
568
569 if cov_shape.len() != 2 || cov_shape[0] != n || cov_shape[1] != n {
571 return Err(crate::error::NumRs2Error::InvalidOperation(
572 format!("Covariance matrix must be square with dimensions matching mean vector length ({}), got shape {:?}", n, cov_shape)
573 ));
574 }
575
576 let cov_data = cov.to_vec();
578 let mut chol = vec![T::zero(); n * n];
579
580 for i in 0..n {
581 for j in 0..=i {
582 let mut s = T::zero();
583 for k in 0..j {
584 s = s + chol[i * n + k] * chol[j * n + k];
585 }
586
587 if i == j {
588 let val = cov_data[i * n + i] - s;
589 if val <= T::zero() {
590 return Err(crate::error::NumRs2Error::InvalidOperation(
591 "Covariance matrix is not positive definite".to_string(),
592 ));
593 }
594 chol[i * n + j] = val.sqrt();
595 } else {
596 chol[i * n + j] = (cov_data[i * n + j] - s) / chol[j * n + j];
597 }
598 }
599 }
600
601 let std_normal = self.standard_normal::<T>(&[size, n])?;
603 let std_normal_data = std_normal.to_vec();
604
605 let mut result = vec![T::zero(); size * n];
607
608 for i in 0..size {
609 for j in 0..n {
610 let mut sum = T::zero();
611 for k in 0..n {
612 if j >= k {
613 sum = sum + chol[j * n + k] * std_normal_data[i * n + k];
615 }
616 }
617 result[i * n + j] = means[j] + sum;
618 }
619 }
620
621 Ok(Array::from_vec(result).reshape(&[size, n]))
622 }
623
624 pub fn random_correlation_matrix<T>(&self, n: usize) -> Result<Array<T>>
626 where
627 T: Float
628 + NumCast
629 + Clone
630 + Debug
631 + Display
632 + scirs2_core::ndarray::distributions::uniform::SampleUniform,
633 {
634 if n < 2 {
635 return Err(crate::error::NumRs2Error::InvalidOperation(
636 "Correlation matrix dimension must be at least 2".to_string(),
637 ));
638 }
639
640 let uniform = self.uniform::<T>(
642 <T as NumCast>::from(-1.0).unwrap_or(T::zero()),
643 <T as NumCast>::from(1.0).unwrap_or(T::zero()),
644 &[n, n],
645 )?;
646
647 let uniform_data = uniform.to_vec();
648
649 let mut sym_matrix = vec![T::zero(); n * n];
651 for i in 0..n {
652 for j in 0..n {
653 match i.cmp(&j) {
654 std::cmp::Ordering::Equal => {
655 sym_matrix[i * n + j] = <T as NumCast>::from(1.0).unwrap_or(T::zero());
656 }
657 std::cmp::Ordering::Less => {
658 sym_matrix[i * n + j] = uniform_data[i * n + j];
659 sym_matrix[j * n + i] = uniform_data[i * n + j];
660 }
661 std::cmp::Ordering::Greater => {}
662 }
663 }
664 }
665
666 for i in 0..n {
672 sym_matrix[i * n + i] = <T as NumCast>::from(1.0).unwrap_or(T::zero());
673 }
674
675 let mut corr_matrix = sym_matrix.clone();
677 let max_iter = 10;
678
679 for _ in 0..max_iter {
680 let mut is_pd = true;
682 let mut chol = vec![T::zero(); n * n];
683
684 'decomp: for i in 0..n {
685 for j in 0..=i {
686 let mut s = T::zero();
687 for k in 0..j {
688 s = s + chol[i * n + k] * chol[j * n + k];
689 }
690
691 if i == j {
692 let val = corr_matrix[i * n + i] - s;
693 if val <= T::zero() {
694 is_pd = false;
695 break 'decomp;
696 }
697 chol[i * n + j] = val.sqrt();
698 } else {
699 chol[i * n + j] = (corr_matrix[i * n + j] - s) / chol[j * n + j];
700 }
701 }
702 }
703
704 if is_pd {
705 break;
706 }
707
708 let factor = <T as NumCast>::from(0.9).unwrap_or(T::zero());
710 for i in 0..n {
711 for j in 0..n {
712 if i != j {
713 corr_matrix[i * n + j] = corr_matrix[i * n + j] * factor;
714 }
715 }
716 }
717 }
718
719 for i in 0..n {
721 let diag_val = corr_matrix[i * n + i].sqrt();
722 for j in 0..n {
723 corr_matrix[i * n + j] = corr_matrix[i * n + j] / diag_val;
724 corr_matrix[j * n + i] = corr_matrix[j * n + i] / diag_val;
725 }
726 }
727
728 for i in 0..n {
730 for j in 0..n {
731 corr_matrix[i * n + j] = corr_matrix[i * n + j]
732 / (corr_matrix[i * n + i].sqrt() * corr_matrix[j * n + j].sqrt());
733 }
734 }
735
736 for i in 0..n {
738 corr_matrix[i * n + i] = <T as NumCast>::from(1.0).unwrap_or(T::zero());
739 }
740
741 Ok(Array::from_vec(corr_matrix).reshape(&[n, n]))
742 }
743
744 pub fn mixture_of_normals<T>(
746 &self,
747 weights: &[T],
748 means: &[T],
749 stds: &[T],
750 shape: &[usize],
751 ) -> Result<Array<T>>
752 where
753 T: Float + NumCast + Clone + Debug + Display,
754 {
755 let n_components = weights.len();
756
757 if n_components < 1 {
759 return Err(crate::error::NumRs2Error::InvalidOperation(
760 "Mixture must have at least one component".to_string(),
761 ));
762 }
763
764 if means.len() != n_components || stds.len() != n_components {
765 return Err(crate::error::NumRs2Error::InvalidOperation(
766 "Weights, means, and stds must have the same length".to_string(),
767 ));
768 }
769
770 let sum_weights: T = weights.iter().fold(T::zero(), |acc, w| acc + *w);
772 if (sum_weights - <T as NumCast>::from(1.0).unwrap_or(T::zero())).abs()
773 > <T as NumCast>::from(1e-6).unwrap_or(T::zero())
774 {
775 return Err(crate::error::NumRs2Error::InvalidOperation(format!(
776 "Weights must sum to 1, got sum={}",
777 sum_weights
778 )));
779 }
780
781 for &std in stds {
783 if std <= T::zero() {
784 return Err(crate::error::NumRs2Error::InvalidOperation(
785 "Standard deviations must be positive".to_string(),
786 ));
787 }
788 }
789
790 let size: usize = shape.iter().product();
791 let mut vec = Vec::with_capacity(size);
792 let mut rng = self.get_rng()?;
793
794 let mut norm_weights = Vec::with_capacity(n_components);
796 for &w in weights {
797 norm_weights.push(w / sum_weights);
798 }
799
800 let mut cumulative_weights = Vec::with_capacity(n_components);
802 let mut sum = T::zero();
803 for &w in &norm_weights {
804 sum = sum + w;
805 cumulative_weights.push(sum);
806 }
807
808 let mut component_selections = Vec::with_capacity(size);
810
811 for _ in 0..size {
813 let u = <T as NumCast>::from(rng.random::<f64>()).unwrap_or(T::zero());
814 let mut selected_component = 0;
815
816 for (i, &cw) in cumulative_weights.iter().enumerate() {
817 if u <= cw {
818 selected_component = i;
819 break;
820 }
821 }
822 component_selections.push(selected_component);
823 }
824
825 let mut component_counts = vec![0usize; n_components];
827 for &comp in &component_selections {
828 component_counts[comp] += 1;
829 }
830
831 let mut component_samples = Vec::with_capacity(n_components);
833 for i in 0..n_components {
834 if component_counts[i] > 0 {
835 let samples = self.normal(means[i], stds[i], &[component_counts[i]])?;
836 component_samples.push(samples.to_vec());
837 } else {
838 component_samples.push(Vec::new());
839 }
840 }
841
842 let mut component_indices = vec![0usize; n_components];
844 for &selected_component in &component_selections {
845 vec.push(component_samples[selected_component][component_indices[selected_component]]);
846 component_indices[selected_component] += 1;
847 }
848
849 Ok(Array::from_vec(vec).reshape(shape))
850 }
851
852 pub fn sobol_sequence<T>(&self, dim: usize, n: usize) -> Result<Array<T>>
854 where
855 T: Float + NumCast + Clone + Debug + Display,
856 {
857 if !(1..=40).contains(&dim) {
858 return Err(crate::error::NumRs2Error::InvalidOperation(format!(
859 "Dimension must be between 1 and 40, got {}",
860 dim
861 )));
862 }
863
864 if n < 1 {
865 return Err(crate::error::NumRs2Error::InvalidOperation(
866 "Number of points must be at least 1".to_string(),
867 ));
868 }
869
870 let direction_numbers: Vec<Vec<u32>> = vec![
873 vec![1], vec![1, 3], vec![1, 3, 5], vec![1, 3, 5, 15], vec![1, 3, 5, 15, 17], vec![1, 3, 5, 15, 17, 51], vec![1, 3, 5, 15, 17, 51, 85], vec![1, 3, 5, 15, 17, 51, 85, 255], ];
883
884 let mut result = vec![T::zero(); n * dim];
885
886 for i in 0..n {
888 let g = i ^ (i >> 1);
890
891 for d in 0..std::cmp::min(dim, direction_numbers.len()) {
892 let mut x = 0u32;
893 let mut mask = 1u32;
894
895 for j in 0..32 {
896 if j < direction_numbers[d].len() && (g & (mask as usize)) != 0 {
897 x ^= direction_numbers[d][j];
898 }
899 mask <<= 1;
900 }
901
902 let val = <T as NumCast>::from(x as f64 / 2f64.powi(32)).unwrap_or(T::zero());
904 result[i * dim + d] = val;
905 }
906
907 let mut rng = self.get_rng()?;
910 for d in direction_numbers.len()..dim {
911 result[i * dim + d] =
912 <T as NumCast>::from(rng.random::<f64>()).unwrap_or(T::zero());
913 }
914 }
915
916 Ok(Array::from_vec(result).reshape(&[n, dim]))
917 }
918
919 pub fn latin_hypercube<T>(&self, dim: usize, n: usize) -> Result<Array<T>>
921 where
922 T: Float + NumCast + Clone + Debug + Display,
923 {
924 if dim < 1 {
925 return Err(crate::error::NumRs2Error::InvalidOperation(
926 "Dimension must be at least 1".to_string(),
927 ));
928 }
929
930 if n < 2 {
931 return Err(crate::error::NumRs2Error::InvalidOperation(
932 "Number of samples must be at least 2".to_string(),
933 ));
934 }
935
936 let mut result = vec![T::zero(); n * dim];
937
938 for d in 0..dim {
940 let mut perm: Vec<usize> = (0..n).collect();
942
943 let mut rng = self.get_rng()?;
945 for i in (1..n).rev() {
946 let j = (rng.random::<f64>() * (i + 1) as f64) as usize;
947 perm.swap(i, j);
948 }
949
950 for i in 0..n {
952 let u = rng.random::<f64>();
953 let val =
954 <T as NumCast>::from((perm[i] as f64 + u) / n as f64).unwrap_or(T::zero());
955 result[i * dim + d] = val;
956 }
957 }
958
959 Ok(Array::from_vec(result).reshape(&[n, dim]))
960 }
961
962 pub fn copula<T>(&self, corr: &Array<T>, n: usize, copula_type: &str) -> Result<Array<T>>
964 where
965 T: Float + NumCast + Clone + Debug + Display,
966 {
967 let corr_shape = corr.shape();
968
969 if corr_shape.len() != 2 || corr_shape[0] != corr_shape[1] {
971 return Err(crate::error::NumRs2Error::InvalidOperation(
972 "Correlation matrix must be square".to_string(),
973 ));
974 }
975
976 let dim = corr_shape[0];
977
978 let valid_types = ["gaussian", "t"];
980 if !valid_types.contains(&copula_type) {
981 return Err(crate::error::NumRs2Error::InvalidOperation(format!(
982 "Unsupported copula type: {}. Supported types: {:?}",
983 copula_type, valid_types
984 )));
985 }
986
987 let means = vec![T::zero(); dim];
989 let mvn_samples = self.multivariate_normal_cholesky(&means, corr, n)?;
990 let mvn_data = mvn_samples.to_vec();
991
992 let mut result = vec![T::zero(); n * dim];
994
995 for i in 0..n {
996 for d in 0..dim {
997 let z = mvn_data[i * dim + d].to_f64().unwrap_or(0.0);
998
999 let u = match copula_type {
1001 "gaussian" => normal_cdf(z),
1002 "t" => {
1003 student_t_cdf(z, 4)
1005 }
1006 _ => normal_cdf(z), };
1008
1009 result[i * dim + d] = <T as NumCast>::from(u).unwrap_or(T::zero());
1010 }
1011 }
1012
1013 Ok(Array::from_vec(result).reshape(&[n, dim]))
1014 }
1015}
1016
1017fn normal_cdf(x: f64) -> f64 {
1021 0.5 * (1.0 + erf(x / std::f64::consts::SQRT_2))
1022}
1023
1024#[allow(dead_code)]
1026fn normal_inv_cdf(p: f64) -> f64 {
1027 if p <= 0.0 {
1028 return f64::NEG_INFINITY;
1029 }
1030 if p >= 1.0 {
1031 return f64::INFINITY;
1032 }
1033
1034 let q = p - 0.5;
1036
1037 if q.abs() <= 0.425 {
1038 let r = 0.180625 - q * q;
1040 return q
1041 * (((((((2.509_080_928_730_122_6 * r + 3.343_057_558_358_813e1) * r
1042 + 6.726_577_092_700_87e1)
1043 * r
1044 + 4.592_195_393_154_987e1)
1045 * r
1046 + 1.373_169_376_550_946_2e1)
1047 * r
1048 + 1.421_413_764_013_155_7)
1049 * r
1050 + 2.298_979_990_914_786_5e-1)
1051 / (((((((4.374_317_029_667_823e-2 * r + 3.739_716_869_366_193_3) * r
1052 + 4.692_163_145_304_143_5e1)
1053 * r
1054 + 2.266_863_181_546_454_5e2)
1055 * r
1056 + 5.396_173_702_892_064e2)
1057 * r
1058 + 6.573_191_171_972_302e2)
1059 * r
1060 + 3.734_237_715_407_137e2)
1061 * r
1062 + 1.0));
1063 }
1064
1065 let r = if q > 0.0 { 1.0 - p } else { p };
1067
1068 if r <= 0.0 {
1069 return if q > 0.0 {
1070 f64::INFINITY
1071 } else {
1072 f64::NEG_INFINITY
1073 };
1074 }
1075
1076 let r = (-r.ln()).sqrt();
1077
1078 let mut ret = ((((((1.811_625_079_763_736_7e1 * r + 7.928_172_819_374_223e1) * r
1079 + 1.373_169_376_550_946_2e2)
1080 * r
1081 + 1.193_147_912_264_617e2)
1082 * r
1083 + 4.926_845_824_098_105e1)
1084 * r
1085 + 8.400_547_514_910_246)
1086 * r
1087 + 3.050_789_888_729_818e-1)
1088 / ((((((3.020_637_025_121_939_4e-1 * r + 5.595_303_579_120_197) * r
1089 + 3.042_975_173_014_595e1)
1090 * r
1091 + 6.493_763_419_991_991e1)
1092 * r
1093 + 5.786_293_056_261_984e1)
1094 * r
1095 + 2.121_379_430_158_66e1)
1096 * r
1097 + 2.659_135_201_941_675)
1098 * r
1099 + 1.0;
1100
1101 ret = if q < 0.0 { -ret } else { ret };
1102 ret
1103}
1104
1105fn erf(x: f64) -> f64 {
1107 let a1 = 0.254829592;
1109 let a2 = -0.284496736;
1110 let a3 = 1.421413741;
1111 let a4 = -1.453152027;
1112 let a5 = 1.061405429;
1113 let p = 0.3275911;
1114
1115 let sign = if x < 0.0 { -1.0 } else { 1.0 };
1116 let x = x.abs();
1117
1118 let t = 1.0 / (1.0 + p * x);
1119 let y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * (-x * x).exp();
1120
1121 sign * y
1122}
1123
1124fn student_t_cdf(x: f64, df: usize) -> f64 {
1126 if x == 0.0 {
1128 return 0.5;
1129 }
1130
1131 if df > 100 {
1133 return normal_cdf(x);
1134 }
1135
1136 let df_f64 = df as f64;
1138 let t = x / (df_f64 + x * x).sqrt();
1139 let u = 0.5 + 0.5 * t * (1.0 - t * t / (df_f64 + 2.0)).sqrt();
1140
1141 u.clamp(0.0, 1.0)
1143}
1144
1145#[cfg(test)]
1149mod tests {
1150 use super::*;
1151
1152 #[test]
1153 fn test_truncated_normal() {
1154 let mean = 0.0;
1155 let std = 1.0;
1156 let low = -2.0;
1157 let high = 2.0;
1158
1159 let samples = truncated_normal(mean, std, low, high, &[100])
1161 .expect("test: truncated_normal should succeed");
1162 let data = samples.to_vec();
1163
1164 for &val in &data {
1166 assert!(val >= low && val <= high, "Value outside bounds: {}", val);
1167 }
1168
1169 let mean_val: f64 = data.iter().sum::<f64>() / data.len() as f64;
1171 assert!(
1172 (mean_val - mean).abs() < 0.5,
1173 "Mean too far from expected: {}",
1174 mean_val
1175 );
1176 }
1177
1178 #[test]
1179 fn test_vonmises() {
1180 let mu = 0.0;
1181 let kappa = 2.0;
1182
1183 let samples = vonmises(mu, kappa, &[1000]).expect("test: vonmises should succeed");
1185 let data = samples.to_vec();
1186
1187 for &val in &data {
1189 assert!(
1190 (-std::f64::consts::PI..std::f64::consts::PI).contains(&val),
1191 "Value outside bounds: {}",
1192 val
1193 );
1194 }
1195
1196 let sum_sin: f64 = data.iter().map(|&x| x.sin()).sum();
1198 let sum_cos: f64 = data.iter().map(|&x| x.cos()).sum();
1199 let mean_angle = sum_sin.atan2(sum_cos);
1200
1201 let angle_diff = (mean_angle - mu + std::f64::consts::PI) % (2.0 * std::f64::consts::PI)
1203 - std::f64::consts::PI;
1204 assert!(
1205 angle_diff.abs() < 0.5,
1206 "Mean direction too far from expected: {}",
1207 mean_angle
1208 );
1209 }
1210
1211 #[test]
1212 fn test_latin_hypercube() {
1213 let dim = 2;
1214 let n = 10;
1215
1216 let samples = latin_hypercube::<f64>(dim, n).expect("test: latin_hypercube should succeed");
1218 let data = samples.to_vec();
1219
1220 for d in 0..dim {
1222 let mut counts = vec![0; n];
1223
1224 for i in 0..n {
1225 let val = data[i * dim + d];
1226 let stratum = (val * n as f64).floor() as usize;
1227 let stratum = std::cmp::min(stratum, n - 1); counts[stratum] += 1;
1229 }
1230
1231 for (s, &count) in counts.iter().enumerate() {
1233 assert_eq!(count, 1, "Stratum {} has {} samples, expected 1", s, count);
1234 }
1235 }
1236 }
1237
1238 #[test]
1239 #[ignore = "Performance optimization needed - function works but is too slow for CI"]
1240 fn test_mixture_of_normals() {
1241 let weights = vec![0.3, 0.7];
1242 let means = vec![-3.0, 3.0];
1243 let stds = vec![1.0, 1.0];
1244
1245 let samples = mixture_of_normals(&weights, &means, &stds, &[100])
1247 .expect("test: mixture_of_normals should succeed");
1248
1249 let data = samples.to_vec();
1250
1251 let mut sorted_data = data.clone();
1254 sorted_data.sort_by(|a, b| {
1255 a.partial_cmp(b)
1256 .expect("test: f64 comparison should succeed for non-NaN values")
1257 });
1258
1259 let p25 = sorted_data[(0.25 * sorted_data.len() as f64) as usize];
1261 let p75 = sorted_data[(0.75 * sorted_data.len() as f64) as usize];
1262
1263 assert!(p25 < 0.0, "25th percentile should be negative, got {}", p25);
1267 assert!(p75 > 0.0, "75th percentile should be positive, got {}", p75);
1268 }
1269}