Opticks replaces Geant4 optical photon simulation with an equivalent implementation that benefits from state-of-the-art GPU ray tracing from NVIDIA OptiX. All optically relevant aspects of Geant4 context must be translated+copied to GPU:
- geometry : solids, structure, material+surface properties
- generation : Cerenkov + Scintillation (using Gensteps from Geant4)
- propagation : Rayleigh scattering, absorption, reemission, boundary
Achieving+maintaining equivalence is time consuming, BUT:
- transformative performance benefits >1000x Geant4
Integrate NVIDIA OptiX with Geant4
Other Geometry/Translation Issues
Fastener base_steel : multiple-Rmin Polycone
Hamamatsu neck : cylinder minus torus
PMT mask : CSG coincidence issue
XJanchorConstruction : spurious G4 intersects
SJReceiverConstruction
Fastener : interfering subtraction-of-subtraction issue
Cutdown PMT breaks Opticks translation
solidXJfixture : ~10/64 overlaps with fasteners
solidXJfixture : extreme coincidence, many spurious intersects
GPU Cerenkov float/double investigations
New transform enabled TORCH genstep (num_gs,6,4,4)
opticks/sysrap/SEvent::MakeCenterExtentGensteps
Transform local frame photon source positions (eg XZ plane, XYZ grid) into global frame using transforms of any piece of geometry.
671 template <typename T> inline QSIM_METHOD void qsim<T>::generate_photon_torch(quad4& p, curandStateXORWOW& rng, const quad6& gs)
672 {
673 p.q0.f = gs.q1.f ; // start with local frame position, eg (0,0,0)
674
675 float u = curand_uniform(&rng);
676 float sinPhi, cosPhi;
677 sincosf(2.f*M_PIf*u,&sinPhi,&cosPhi);
678
679 // local frame XZ plane directions
680 p.q1.f.x = cosPhi ; p.q1.f.y = 0.f ; p.q1.f.z = sinPhi ; p.q1.f.w = 0.f ;
681
682 qat4 qt(gs) ; // copy 4x4 transform from last 4 quads of genstep
683 qt.right_multiply_inplace( p.q0.f, 1.f ); // position transform local->global
684 qt.right_multiply_inplace( p.q1.f, 0.f ); // direction
685 }
mask tail "bowl" cutting across PMT bulb ?
2D slice render composed of actual intersects
actual intersects => ideal for debugging
Contrast with indirect Geant4 OpenGL Viz:
coincident face CSG subtraction issue
Fasteners : performance+validity
2D render reveals cause : spurious intersects
two horizontal green lines just below (0,0) are spurious
- => wrong Water/Acrylic properties used
- Opticks "SI BT BT BT BT AB" excess
- CAUSE : poor G4 geometry modelling
- => interfering subtraction of subtraction
Geometry Modelling Experience
Geant4: Each volume is created by describing its shape and its physical characteristics, and then placing it inside a containing volume.
CSG Subtracting Daughter Cavities
If gap between acrylic cone and steel rods is not important
Model with hierarchy of 2 volumes:
This is the approach taken with option:
--additionacrylic-simplify-csg
If gap between acrylic cone and steel rods is important
Model with hierarchy of 3 volumes:
Executive Summary
Multiple non-interfering CSG subtractions work fine
CSG Sub. "pipe" cylinder => spurious inner intersects
Problem Arises from inherent CSG fragility regarding coincident faces
Possibility : Volume-centric CSG Implementation more like Geant4 ?
Opticks CSG is surface-centric
G4double X4Intersect::Distance(const G4VSolid* solid, const G4ThreeVector& pos, const G4ThreeVector& dir) { EInside in = solid->Inside(pos) ; G4double t = kInfinity ; switch( in ) { case kInside: t = solid->DistanceToOut( pos, dir ) ; break ; case kSurface: t = solid->DistanceToOut( pos, dir ) ; break ; case kOutside: t = solid->DistanceToIn( pos, dir ) ; break ; } return t ; }
G4VPhysicalVolume has no convenient "Distance" methods ... so scan solids individually and present together after applying structure transforms.
Usage of xxv.sh script which runs executable and ipython:
cd ~/opticks/extg4 ./xxv.sh GEOM=body_phys ./xxv.sh
ZSolid::Draw [-1] nameprefix _body_solid_ NODE:19 PRIM:10 UNDEFINED:19 EXCLUDE:0 INCLUDE:0 MIXED:0 Order:IN Int U 17 Uni Pol U U 15 18 Uni Pol U U 13 16 Uni Pol U U 11 14 Uni Ell U U 9 12 Uni Pol U U 5 10 Uni Sub U U 3 7 Uni Ell Pol Tor U U U U 1 4 6 8 Ell Pol U U 0 2 0.0 -2.5 -5.0 -179.2 -210.0 -242.5 -275.0 -385.0 -420.0 3.4 zdelta 190.0 0.0 -5.0 -148.4 -130.0 -210.0 -275.0 -350.0 -420.0 190.0 az1 0.0 -5.0 -195.0 -210.0 -290.0 -275.0 -365.0 -420.0 -450.0 -183.2 az0 I 1_2 II 1_3 III 1_4 IV_tub IV IV_tor 1_5 V 1_6 VI 1_8 VIII 1_9 IX cut intubs
Bad Practice : Using G4IntersectionSolid to apply Z-cut to PMT
485 // Reduce the size when real surface is enabled. 487 if (m_useRealSurface ) { ... 546 const double body_height = m_pmt_h; 547 const double body_half_height = body_height / 2; 548 const G4ThreeVector cut_body_displacement(0., 0., m_z_equator-pmt_half_height); 549 G4VSolid* cut_body_solid = new G4Tubs( GetName() + "_body_solid_intubs", 550 0., 551 helper_sep_tube_r+1E-9*mm, 552 body_half_height, 553 0., 360.*degree); 554 body_solid = new G4IntersectionSolid( GetName() + "_body_solid_cut", 555 body_solid, 556 cut_body_solid, 557 NULL, 558 cut_body_displacement);
SOLUTION ACTUALLY CUT THE CSG TREE using https://github.com/simoncblyth/j/blob/main/PMTSim/ZSolid.hh
G4VSolid* ZSolid::ApplyZCutTree( const G4VSolid* original, double zcut)
G4VSolid* ZSolid::ApplyZCutTree(const G4VSolid* orig, double zcut)
ZSolid::SetRight
G4BooleanSolid* src = dynamic_cast<G4BooleanSolid*>(node) ; G4String name = src->GetName() ; G4VSolid* left = src->GetConstituentSolid(0) ; G4SolidStore::GetInstance()->DeRegister(src); src1 = new (src) G4UnionSolid(name, left, new_right) assert( src1 == src );
Similar trick used to cut G4Polycone ZSolid::ApplyZCut_G4Polycone
https://github.com/simoncblyth/j/blob/main/PMTSim/ZSolid.hh
Applying Z-cut to G4VSolid CSG Tree
Pruning and Reconnection
ZSolid::apply_cut before prune [ 0] nameprefix maker_body_solid_zcut-183.2246_ NODE:15 PRIM:8 UNDEFINED:0 EXCLUDE:4 INCLUDE:7 MIXED:4 Order:IN Uni I : include IE 13 E : exclude Uni Pol IE: mixed include/exclude IE E 11 14 X : "crux" node Uni Pol S : survivor node IE E 9 12 Uni Ell IE E 7 10 X Uni Pol I E 5 8 S Uni Pol I I 3 6 Uni Ell I I 1 4 Ell Pol I I 0 2 0.0 -2.5 -5.0 -179.2 -242.5 -275.0 -385.0 -420.0 zdelta 190.0 0.0 -5.0 -162.0 -210.0 -275.0 -350.0 -420.0 az1 0.0 -5.0 -183.2 -183.2 -275.0 -365.0 -420.0 -450.0 az0 I 1_2 II 1_3 III 1_4 IV 1_5 V 1_6 VI 1_8 VIII 1_9 IX
CSG constituents are classified INCLUDE/EXCLUDE/MIXED against the Z-cut
Tree Stats
CSG node tree | Stage | ||
---|---|---|---|
node | leaf | height | |
15 | 8 | 7 | Original Solid |
17 | 9 | 8 | After "cutting" with G4IntersectionSolid |
7 | 4 | 3 | After actual tree cutting ZSolid::ApplyZCutTree |
tree::apply_cut after prune and re-classify [ 3] nameprefix maker_body_solid_zcut-183.2246_ NODE:7 PRIM:4 UNDEFINED:0 EXCLUDE:0 INCLUDE:7 MIXED:0 Order:IN Uni I 5 S Uni Pol I I 3 6 Uni Ell I I 1 4 Ell Pol I I 0 2 0.0 -2.5 -5.0 -179.2 zdelta 190.0 0.0 -5.0 -162.0 az1 0.0 -5.0 -183.2 -183.2 az0 I 1_2 II 1_3 III 1_4 IV
Getting JUNO to work on GPU, improves it on CPU too
JUNO Opticks OptiX 7 Ray-trace
"CSGFoundry" CPU/GPU Geometry
Fast Render => Fast Simulation
-e t0, : NOT 0 : 3084:sWorld : exclude global remainder volumes
Same viewpoint, vary GPU geometry
Recent Geometry Fixes
Substantial speedup after fixing geometry issues
ALL PMTs : 0.0097 -> 0.0061 (x1.6 faster)
ALL : 0.6240 -> 0.0054 (x155 faster)
idx | -e | time(s) | relative | enabled geometry description 3dbec4dc |
---|---|---|---|---|
0 | 5, | 0.0004 | 0.0643 | ONLY: 1:sStrutBallhead |
1 | 9, | 0.0004 | 0.0658 | ONLY: 130:sPanel |
2 | 7, | 0.0005 | 0.0782 | ONLY: 1:base_steel |
3 | 8, | 0.0006 | 0.0966 | ONLY: 1:uni_acrylic1 |
4 | 6, | 0.0006 | 0.1009 | ONLY: 1:uni1 |
5 | 1, | 0.0009 | 0.1476 | ONLY: 5:PMT_3inch_pmt_solid FAST cf 20in |
6 | 4, | 0.0015 | 0.2386 | ONLY: 4:mask_PMT_20inch_vetosMask |
7 | 3, | 0.0033 | 0.5373 | ONLY: 5:HamamatsuR12860sMask SLOW cf 3in |
8 | 0, | 0.0040 | 0.6556 | ONLY: 3084:sWorld |
9 | 2, | 0.0040 | 0.6627 | ONLY: 5:NNVTMCPPMTsMask SLOW cf 3in |
10 | t4, | 0.0050 | 0.8307 | EXCL: 4:mask_PMT_20inch_vetosMask |
11 | t2, | 0.0051 | 0.8391 | EXCL: 5:NNVTMCPPMTsMask |
12 | t3, | 0.0052 | 0.8514 | EXCL: 5:HamamatsuR12860sMask |
13 | t6, | 0.0053 | 0.8799 | EXCL: 1:uni1 |
14 | t7, | 0.0054 | 0.8809 | EXCL: 1:base_steel |
15 | t0 | 0.0054 | 0.8843 | ALL |
16 | t5, | 0.0054 | 0.8843 | EXCL: 1:sStrutBallhead |
17 | t9, | 0.0054 | 0.8855 | EXCL: 130:sPanel |
18 | t1, | 0.0054 | 0.8860 | EXCL: 5:PMT_3inch_pmt_solid |
19 | t8, | 0.0055 | 0.9013 | EXCL: 1:uni_acrylic1 |
20 | t0, | 0.0059 | 0.9753 | EXCL: 3084:sWorld |
21 | 1,2,3,4 | 0.0061 | 1.0000 | ONLY PMT |
22 | t8,0 | 0.0062 | 1.0217 | EXCL: 1:uni_acrylic1 3084:sWorld |
Perhaps solid angle correction would even out render speeds
epsilon:ana blyth$ tds3gun.sh 1 ## seqhis: 64bit uint : 16x4bit step flags for each photon ab.ahis . all_seqhis_ana cfo:sum 1:g4live:tds3gun -1:g4live:tds3gun . TOTALS: 11300 11300 94.69 94.69/62 = 1.53 pvalue:P[C2>]:1.000 1-pvalue:P[C2<]:0.000 n iseq a b a-b (a-b)^2/(a+b) a/b b/a [ns] label 0000 7cccccc2 1832 1795 37 0.38 1.021 +- 0.024 0.980 +- 0.023 [8 ] SI BT BT BT BT BT BT SD 0001 42 1727 1683 44 0.57 1.026 +- 0.025 0.975 +- 0.024 [2 ] SI AB 0002 7cccccc62 766 713 53 1.90 1.074 +- 0.039 0.931 +- 0.035 [9 ] SI SC BT BT BT BT BT BT SD 0003 7cccccc52 522 518 4 0.02 1.008 +- 0.044 0.992 +- 0.044 [9 ] SI RE BT BT BT BT BT BT SD 0004 452 515 505 10 0.10 1.020 +- 0.045 0.981 +- 0.044 [3 ] SI RE AB 0005 462 379 377 2 0.01 1.005 +- 0.052 0.995 +- 0.051 [3 ] SI SC AB 0006 41 256 262 -6 0.07 0.977 +- 0.061 1.023 +- 0.063 [2 ] CK AB 0007 7cccccc662 246 249 -3 0.02 0.988 +- 0.063 1.012 +- 0.064 [10] SI SC SC BT BT BT BT BT BT SD 0008 7cccccc652 212 230 -18 0.73 0.922 +- 0.063 1.085 +- 0.072 [10] SI RE SC BT BT BT BT BT BT SD 0009 7cccccc552 167 168 -1 0.00 0.994 +- 0.077 1.006 +- 0.078 [10] SI RE RE BT BT BT BT BT BT SD 0010 4552 173 162 11 0.36 1.068 +- 0.081 0.936 +- 0.074 [4 ] SI RE RE AB 0011 8cc2 118 173 -55 10.40 0.682 +- 0.063 1.466 +- 0.111 [4 ] SI BT BT SA 0012 4652 119 120 -1 0.00 0.992 +- 0.091 1.008 +- 0.092 [4 ] SI RE SC AB 0013 cccccc6662 124 104 20 1.75 1.192 +- 0.107 0.839 +- 0.082 [10] SI SC SC SC BT BT BT BT BT BT 0014 cccccc6652 111 114 -3 0.04 0.974 +- 0.092 1.027 +- 0.096 [10] SI RE SC SC BT BT BT BT BT BT 0015 4662 100 110 -10 0.48 0.909 +- 0.091 1.100 +- 0.105 [4 ] SI SC SC AB . TOTALS: 11300 11300 94.69 94.69/62 = 1.53 pvalue:P[C2>]:1.000 1-pvalue:P[C2<]:0.000
prior check excluded sticks geometry due to Fastener, giving chi2/ndf ~ 1 : (now with full geometry : ~1.5 )
Largest discrepancy is an Opticks lack of "SI BT BT SA" compared to Geant4
Selecting the largest chi2 contribs:
In [4]: np.set_printoptions(linewidth=100) In [5]: ab.his.ss[ab.his.c2 > 2] Out[5]: array(['0011 8cc2 118 173 -55 10.40 SI BT BT SA', '0018 7ccccccc2 70 100 -30 5.29 SI BT BT BT BT BT BT BT SD', '0019 cccccccc62 77 59 18 2.38 SI SC BT BT BT BT BT BT BT BT', '0031 ccccc66652 35 54 -19 4.06 SI RE SC SC SC BT BT BT BT BT', '0042 4ccc2 54 8 46 34.13 SI BT BT BT AB', '0045 ccccc65552 21 36 -15 3.95 SI RE RE RE SC BT BT BT BT BT', '0059 cccbcccc52 24 13 11 3.27 SI RE BT BT BT BT BR BT BT BT'], dtype='|S98') In [6]:
"Counterpart" Opticks excess is "SI BT BT BT AB" which happens very little for Geant4.
Fixing that one pair of discrepancies would get chi2/ndf below 1.
Looks like only one issue big enough to chase at this sample size.
In [8]: a.sel = "SI BT BT BT AB" ## select Opticks photons with this history In [9]: a.bn.shape Out[9]: (54, 1, 4) In [13]: a.bn.view(np.int8).shape Out[13]: (54, 1, 16) In [14]: a.bn.view(np.int8) ## step-by-step boundary indices within selection, suspicious -24, 24 pairs Out[14]: A([[[ 18, -17, 17, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], [[ 18, -17, -24, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], [[ 18, -17, -24, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], [[ 18, -17, -24, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], ... In [26]: a.blib.bname(17-1) Out[26]: 'Water///Acrylic' In [27]: a.blib.bname(24-1) Out[27]: 'Water///PE_PA' epsilon:issues blyth$ jgr PE_PA ... ./Simulation/DetSimV2/CentralDetector/include/XJfixtureConstruction.hh: G4Material* PE_PA ; ./Simulation/DetSimV2/CentralDetector/src/XJfixtureConstruction.cc: PE_PA = G4Material::GetMaterial("PE_PA"); ./Simulation/DetSimV2/CentralDetector/src/XJfixtureConstruction.cc: PE_PA,
Delving in shows the excess Opticks photons absorbed by "PE_PA" and with suspicions sign flip last boundaries.
So many spurious intersects : Easier to start over with fewer constituent solids
Unexpected spurious vertical at 35 In Y box is at 50 +- 15 (only appears with 4 box, not 2) 35 50 65 : : : : altar frame fixture frame : -------------+ +---+---+-+-----+ - - - - - - - - - - 18.5+13 = 31.5 6.5 | | | : | 13/2=6.5 + + + : + : : - - - - - - - - - 18.5+6.5 = 25 0.0 | | | : | : +------------+----------------+-----25-----+---20--+-+-----+ - - - - - - - 8.5+10 = 18.5 -6.5 | | + up2 + + - - - - - - - - - - 8.5+5 = 13.5 -11.5 | | +---------+^^^^^^^^^^^^^^^^^^^+^^^^^^^^^^^^^^^^^^+---------+ - - - - - - - - - - 8.5 -16.5 | | | 17/2=8.5 + up1 + - - - - - - - - - - 0.0 -25.0 | | | | +-------------------+-------40---------+ - - - - - - - - - - - - - -8.5 -33.5 | | : | Z 0 25 35 45 | | : | | | : | +-- Y | : outer tubs / | : X | spurious vertical from box edge (why? it is within the tubs ring) inner tubs
Due to construction technique and Geant4 limitations : simple anti-coincidence tricks cannot be used
RTP Derivatives Basis
Radial (red) Theta (green) Phi (blue)
orient views and rays relative to any geometry
cd ~/opticks/CSGOptiX for n in $(seq 0 63) ; do EYE=2,-1,-1 LOOK=0,0,0 UP=1,0,0 MOI=solidXJfixture:$n:-3 ./cxr_view.sh done
MOI identifier picks volume using solid names and repeat indices | |
---|---|
Instanced Geometry | Global Geometry |
NNVT:0:1000 | solidXJfixture:10 (default cartesian XYZ) |
Hama:0:2000 | solidXJfixture:10:-3 (RTP tangential frame) |
Components of MOI Mname:mOrdinal:InstanceIndex:
LSExpDetectorConstruction::setupCD_Sticks Implemented poorly
12 large copy/pasted blocks of code with small changes in each
Clarity requires higher level code (simply use static method to handle the repetitive parts)
member | class | pos_file | radius | #pos |
---|---|---|---|---|
m_strut_name | StrutAcrylicConstruction | m_strut_pos_file | strut_r | 370 |
m_strut2_name | StrutBar2AcrylicConstruction | m_strut2_pos_file | strut2_r | 220 |
m_strutballhead_name | StrutBallheadAcrylicConstruction | m_fastener_pos_file | strutballhead_r | 590 |
m_fastener_name | FastenerAcrylicConstruction | m_fastener_pos_file | fastener_r | |
m_upper_name | UpperAcrylicConstruction | m_fastener_pos_file | upper_r | |
m_addition_name | AdditionAcrylicConstruction | m_fastener_pos_file | addition_r | |
m_xjanchor_name | XJanchorConstruction | m_xjanchor_pos_file | xjanchor_r | 56 |
m_xjfixture_name | XJfixtureConstruction | m_xjanchor_pos_file | xjfixture_r | |
m_sjclsanchor_name | SJCLSanchorConstruction | m_sjclsanchor_pos_file | sjclsanchor_r | 2 |
m_sjfixture_name | SJFixtureConstruction | m_sjfixture_pos_file | sjfixture_r | 36 |
m_sjreceiver_name | SJReceiverConstruction | m_sjreceiver_pos_file | sjreceiver_r | 8 |
m_sjreceiver_fastener_name | XJfixtureConstruction | m_sjreceiver_pos_file | sjreceiver_fastener_r |
Geant4 docs : component solids must not be disjoint |
cd ~/opticks/extg4 ; GEOM=SJReceiverConstruction_XZ ./xxs.sh
class | solid | lv | mat | G4VSolid constituents |
---|---|---|---|---|
StrutAcrylicConstruction | sStrut | lSteel | StrutSteel | Tubs |
StrutBar2AcrylicConstruction | sStrut | lSteel2 | StrutSteel | Tubs |
StrutBallheadAcrylicConstruction | sStrutBallhead | lSteel | Steel | Orb |
FastenerAcrylicConstruction | uni1 | lFasteners | Steel | Tubs, Union |
UpperAcrylicConstruction (1) | base_steel | lUpper | Steel | Polycone |
AdditionAcrylicConstruction (2) | uni_acrylic3/1 | lAddition | Acrylic | Polycone, Sphere, Sub. |
XJanchorConstruction (3) | solidXJanchor | lXJanchor | Acrylic | Tubs, Cons, Sphere, Sub., Union |
XJfixtureConstruction (4)(5) | solidXJfixture | lXJfixture | PE_PA | Tubs, Box, Union |
SJCLSanchorConstruction | solidSJCLSanchor | lSJCLSanchor | Acrylic | Box, Cons, Sphere, Sub., Union |
SJFixtureConstruction | solidSJFixture | lSJFixture | Acrylic | Box, Cons, Sphere, Sub., Union |
SJReceiverConstruction (6) | solidSJReceiver | lSJReceiver | Acrylic | Tubs, Cons, Box, Sphere, Sub., Union |
JUNO Simulation -> Opticks -> GPU | |
Benefits: |
|
Status: |
|
Next Steps
Many Thanks to : Yuxiang Hu, Wang Zike
https://bitbucket.org/simoncblyth/opticks | code repository |
https://simoncblyth.bitbucket.io | presentations and videos |
BetaInverse^2 N_photon/369.81 = Integral ( 1 - ----------------- ) dE where BetaInverse < ri[E] ri(E)^2 = Integral [ 1 ] dE - BetaInverse^2 * Integral[ 1./(ri[E]*ri[E]) ] dE
G4Cerenkov::BuildThePhysicsTable -> CerenkovAngleIntegrals (misnomer)
Problems with G4Cerenkov::GetAverageNumberOfPhotons integration
636 G4double CAImax = CerenkovAngleIntegrals->GetMaxValue(); 637 638 G4double dp, ge; 642 if (nMax < BetaInverse) // ... no photons 649 else if (nMin > BetaInverse) { 650 dp = Pmax - Pmin; 651 ge = CAImax; 660 } else { 661 Pmin = Rindex->GetEnergy(BetaInverse); 662 dp = Pmax - Pmin; 667 G4double CAImin = CerenkovAngleIntegrals->GetValue(Pmin, isOutRange); 668 ge = CAImax - CAImin; 674 } 677 G4double NumPhotons = Rfact * charge/eplus * charge/eplus * (dp - ge * BetaInverse*BetaInverse);
Trapezoidal s2 Integration
s2(E) : from RINDEX(E) values and BetaInverse
BetaInverse*BetaInverse Integral [ 1. - ----------------------- ] (for BetaInverse < RINDEX) RINDEX * RINDEX Integral [ 1. - cos^2 theta ] Integral [ sin^2 theta ] Integral [ s2 ] ( s2 > 0 )
Do not split the integral, do "s2" integral in one pass. Advantages:
Cerenkov Photons Energy Distrib
New approach : Cerenkov ICDF sampling ?
Sampling : generate (x0,x1,x2,... ) that follow distribution f(x)
Rejection Sampling (used by current G4Cerenkov)
Inverse Transform (ICDF) Sampling
Cerenkov radiation is electromagnetic equivalent of sonic boom in air or bow waves in water
Created with:
cd ~/opticks/qudarap QCerenkovIntegralTest # create ICDF lookups for many BetaInverse QCKTest # load ICDF, create lookup+rejection samples ipython -i tests/QCKTest.py # chi2 comparisons on energy distribs
Plots show:
Note: ICDF becomes flat in disallowed energy regions