55import openmc
66import openmc .stats
77from scipy .integrate import trapezoid
8-
8+ from scipy import stats as sps
99
1010def assert_sample_mean (samples , expected_mean ):
1111 # Calculate sample standard deviation
@@ -506,3 +506,123 @@ def test_combine_distributions():
506506 # uncertainty of the expected value
507507 samples = combined .sample (10_000 )
508508 assert_sample_mean (samples , 0.25 )
509+
510+ @pytest .mark .parametrize ("x, skew_true, kurt_true" , [
511+ ([- 1 , 0 , 1 ], 0.0 , 1.5 ),
512+ ([- 1 , 1 ], 0.0 , 1.0 ),
513+ ([0 , 0 , 1 , 1 ], 0.0 , 1.0 ),
514+ ([0 , 1 , 2 , 3 ], 0.0 , 41 / 25 ),
515+ ])
516+
517+ def test_b1_b2_analytical (x , skew_true , kurt_true ):
518+ x = np .asarray (x , dtype = float )
519+ n = x .size
520+ mean = x .mean ()
521+ s2 = (x ** 2 ).sum ()
522+ s3 = (x ** 3 ).sum ()
523+ s4 = (x ** 4 ).sum ()
524+
525+ sqrt_b1 , b2 = openmc .tally_stats ._calc_b1_b2 (n , np .array ([mean ]),
526+ np .array ([s2 ]), np .array ([s3 ]), np .array ([s4 ]))
527+ skew = sqrt_b1 .item ()
528+ kurt = b2 .item ()
529+
530+ assert np .isclose (skew , skew_true , rtol = 0 , atol = 1e-15 )
531+ assert np .isclose (kurt , kurt_true , rtol = 0 , atol = 1e-14 )
532+
533+ @pytest .mark .parametrize ("draw, skew_true, kurt_true" , [
534+ (lambda rng , n : rng .normal (0 , 1 , n ), 0.0 , 3.0 ),
535+ (lambda rng , n : rng .random (n ), 0.0 , 1.8 ),
536+ (lambda rng , n : rng .exponential (1.0 , n ), 2.0 , 9.0 ),
537+ (lambda rng , n : (rng .random (n ) < 0.3 ).astype (float ),
538+ (1 - 2 * 0.3 )/ np .sqrt (0.3 * 0.7 ),
539+ (1 - 3 * 0.3 + 3 * 0.3 ** 2 )/ (0.3 * 0.7 )),
540+ ])
541+
542+ def test_b1_b2_scipy (draw , skew_true , kurt_true ):
543+ rng = np .random .default_rng (12345 )
544+ N = 200_000
545+ x = draw (rng , N )
546+
547+ n = x .size
548+ mean = x .mean ()
549+ s2 = (x ** 2 ).sum ()
550+ s3 = (x ** 3 ).sum ()
551+ s4 = (x ** 4 ).sum ()
552+
553+ sqrt_b1 , b2 = openmc .tally_stats ._calc_b1_b2 (n , np .array ([mean ]),
554+ np .array ([s2 ]), np .array ([s3 ]), np .array ([s4 ]))
555+ skew_mine = sqrt_b1 .item ()
556+ kurt_mine = b2 .item ()
557+
558+ # Scipy values
559+ skew_sp = sps .skew (x , bias = True )
560+ kurt_sp = sps .kurtosis (x , fisher = False , bias = True )
561+
562+ # Compare to SciPy
563+ assert np .isclose (skew_mine , skew_sp , rtol = 0 , atol = 5e-3 )
564+ assert np .isclose (kurt_mine , kurt_sp , rtol = 0 , atol = 5e-3 )
565+
566+ tol_skew = 0.02 if abs (skew_true ) < 0.5 else 0.05
567+ tol_kurt = 0.03 if kurt_true < 4 else 0.1
568+
569+ assert abs (skew_mine - skew_true ) < tol_skew
570+ assert abs (kurt_mine - kurt_true ) < tol_kurt
571+
572+ def ztests_scipy_comparison ():
573+ rng = np .random .default_rng (987 )
574+ x_norm = rng .normal (size = 50_000 )
575+ x_exp = rng .exponential (size = 50_000 )
576+
577+ # Normal dataset
578+ n0 = x_norm .size
579+ mean0 = x_norm .mean ()
580+ s20 = (x_norm ** 2 ).sum ()
581+ s30 = (x_norm ** 3 ).sum ()
582+ s40 = (x_norm ** 4 ).sum ()
583+ Zb1_0 , p_skew_0 , _ = openmc .tally_stats .skewness_test (n0 , mean0 , s20 , s30 , s40 )
584+ Zb2_0 , p_kurt_0 , _ = openmc .tally_stats .kurtosis_test (n0 , mean0 , s20 , s30 , s40 )
585+ K2_0 , p_omni_0 = openmc .tally_stats .k2_test (Zb1_0 , Zb2_0 )
586+
587+ # SciPy Z and p values
588+ z_skew_0 , p_skew_sp0 = sps .skewtest (x_norm )
589+ z_kurt_0 , p_kurt_sp0 = sps .kurtosistest (x_norm )
590+ k2_sp0 , p_omni_sp0 = sps .normaltest (x_norm )
591+
592+ assert np .isclose (Zb1_0 , z_skew_0 , atol = 0.15 )
593+ assert np .isclose (Zb2_0 , z_kurt_0 , atol = 0.15 )
594+ assert np .isclose (K2_0 , k2_sp0 , atol = 0.3 )
595+ assert np .isclose (p_skew_0 , p_skew_sp0 , atol = 5e-3 )
596+ assert np .isclose (p_kurt_0 , p_kurt_sp0 , atol = 5e-3 )
597+ assert np .isclose (p_omni_0 , p_omni_sp0 , atol = 5e-3 )
598+
599+ # Exponential dataset
600+ n1 = x_exp .size
601+ mean1 = x_exp .mean ()
602+ s21 = (x_exp ** 2 ).sum ()
603+ s31 = (x_exp ** 3 ).sum ()
604+ s41 = (x_exp ** 4 ).sum ()
605+ Zb1_1 , p_skew_1 , _ = openmc .tally_stats .skewness_test (n1 , mean1 , s21 , s31 , s41 )
606+ Zb2_1 , p_kurt_1 , _ = openmc .tally_stats .kurtosis_test (n1 , mean1 , s21 , s31 , s41 )
607+ K2_1 , p_omni_1 = openmc .tally_stats .k2_test (Zb1_1 , Zb2_1 )
608+
609+ # SciPy Z and p values
610+ z_skew_1 , p_skew_sp1 = sps .skewtest (x_exp )
611+ z_kurt_1 , p_kurt_sp1 = sps .kurtosistest (x_exp )
612+ k2_sp1 , p_omni_sp1 = sps .normaltest (x_exp )
613+
614+ # Both pipelines should reject (p ≪ 1e-6)
615+ assert p_skew_1 < 1e-6 and p_skew_sp1 < 1e-6
616+ assert p_kurt_1 < 1e-6 and p_kurt_sp1 < 1e-6
617+ assert p_omni_1 < 1e-6 and p_omni_sp1 < 1e-6
618+
619+ # Right-skewed: large positive Zb1
620+ assert Zb1_1 > 30
621+ assert z_skew_1 > 30
622+ # Heavy-tailed: large positive Zb2
623+ assert Zb2_1 > 30
624+ assert z_kurt_1 > 30
625+ # Combined omnibus rejects
626+ assert K2_1 > 2000
627+ assert k2_sp1 > 2000
628+
0 commit comments