junoPMTOpticalModel CPU tests reveal issues, all from FastSim use:
Replacing FastSim use with a CustomG4OpBoundaryProcess:
CustomG4OpBoundaryProcess ?
BUT : need to change very nasty Geant4 impl:
inline sboundary::sboundary():
_E2_t(make_float2( 2.f*n1c1/(n1c1+n2c2) ,
                   2.f*n1c1/(n2c1+n1c2) )),// (ts,tp)
_E2_r(make_float2( _E2_t.x - 1.f       ,
                   n2*_E2_t.y/n1 - 1.f  )),// (rs,rp)
A_transverse(cross(p.mom, orient*normal)),
E1_perp(dot(p.pol, A_transverse)),
E1(make_float2(E1_perp,sqrtf(1.f-E1_perp*E1_perp)))),
E2_t(_E2_t*E1),RR(normalize(E2_r)),
E2_r(_E2_r*E1),TT(normalize(E2_t)),
{
p.mom = reflect
      ?
         p.mom + 2.0f*c1*orient*normal
      :
         eta*(p.mom) + (eta*c1 - c2)*orient*normal
      ;
A_parallel = normalize(cross(p.mom, A_transverse));
p.pol =
        ( reflect ?
             ( tir ?
                   -p.pol + 2.f*EdotN*orient*normal
             :
                   RR.x*A_transverse + RR.y*A_parallel
             )
          :
             TT.x*A_transverse + TT.y*A_parallel
        )
      ;
}
Reflect/Refract Polarization
 void junoPMTOpticalModel::Reflect()
 {
     dir -= 2.*(dir*norm)*norm;
     pol -= 2.*(pol*norm)*norm; // similar to G4 TIR pol, NOT Fresnel 
 }
 void junoPMTOpticalModel::Refract()
 {
     dir = (real(_cos_theta4) -
            _cos_theta1*_n1/_n4)*norm + (_n1/_n4)*dir;
     pol = (pol-(pol*dir)*dir).unit();
 }
G4OpBoundaryProcess polarization from:
Opticks : sysrap/sboundary.h qudarap/qsim.h follows G4, ===>
Derive Fresnel from Maxwell Boundary Conditions
| G4OpBoundaryProcess/qsim.h/sboundary.h : Only S-polarized survives | junoPMTOpticalModel::Reflect : very different | 
Brewster (or polarizing) incident angle th1 : tan(th1) = n2/n1 ; th1 + th2 = pi/2
| G4OpBoundaryProcess/qsim.h/sboundary.h : partial pol | junoPMTOpticalModel::Refract : not partially pol | 
Minor changes for standalone use #ifdef PMTFASTSIM_STANDALONE
Optical only Geant4 simulation, with full step point recording
N=0/1 ./U4SimulateTest.sh
Geant4 Simtrace intersection : for 2D geometry plotting
N=0/1 ./U4SimtraceTest.sh
CustomART integrated with InstrumentedG4OpBoundaryProcess
hama_UsePMTOpticalModel=0 REVERSE=1 NOGS=1 ~/opticks/extg4/xxv.sh
body hidden under pmt : WHY?
Old Surface POM
+---------------pmt-Pyrex----------------+ | +-------------body-Pyrex-------------+ | | | | | | | | | | | +------------------------+ | | | | | | | | | | | | | | | | | inner1-Vacuum | |-| | | | | |1e-3 | | | | | | | | +~~coincident~face~~~~~~~+ | | | | | | | | | | | | | | | | | inner2-Vacuum | | | | | | | | | | | | | | | | | +------------------------+ | | | | | | | | | | | +------------------------------------+ | +----------------------------------------+
photons DO NOT enter PMT vacuum
| m_Photocathode_opsurf | inner1<->body | pv<->pv | 
| m_mirror_opsurf | inner2<->body | pv<->pv | 
Why pmt+body Pyrex (1e-3 mm separation) ? GUESS:
Why split vacuum inner1/inner2 ?
Alternatively : with CustomG4OpBoundaryProcess
body hidden under inner1 inner2
body is FastSim envelope volume
Current MultiLayer POM
+---------------pmt-Pyrex----------------+ | | | | | +----body-Pyrex-(FSim-env)---+ | | | +------------------------+ | | | | | | | | | | | | | | | | | inner1-Vacuum |-| | | | | |1e-3 | | | | | | | | | +~~coincident~face~~~~~~~+ | | | | | | | | | | | | | | | | | inner2-Vacuum | | | | | | | | | | | | | | | | | +------------------------+ | | | +----------------------------+ | | | | | +----------------------------------------+
   80 G4bool junoPMTOpticalModel::ModelTrigger(const G4FastTrack &fastTrack)
   81 {   //  Contorted as "pre-trigger" on where next 
   83     if(fastTrack.GetPrimaryTrack()->GetVolume() == _inner2_phys){
   84         return false;
   85     }
   ...
   89     pos     = fastTrack.GetPrimaryTrackLocalPosition();
   90     dir     = fastTrack.GetPrimaryTrackLocalDirection();
   94
   95     if(fastTrack.GetPrimaryTrack()->GetVolume() == _inner1_phys){
   96         whereAmI = kInVacuum;
   97     }else{
   98         whereAmI = kInGlass;
   99     }
  100
  101     if(whereAmI == kInGlass){
  102         dist1 = _inner1_solid->DistanceToIn(pos, dir);
  103         dist2 = _inner2_solid->DistanceToIn(pos, dir);
  104
  105         if(dist1 == kInfinity){
  106             return false;
  107         }else if(dist1>dist2){
  108             return false;
  109         }else{
  110             return true;
  111         }
  112     }else{
  113         dist1 = _inner1_solid->DistanceToOut(pos, dir);
  114         dist2 = _inner2_solid->DistanceToIn(pos, dir);
  115
  116         if(dist2 == kInfinity){
  117             return true;
  118         }
  119     }
  120     return false;
  121 }
SFastSim_Debug.hh
 struct SYSRAP_API SFastSim_Debug
 {
     static std::vector<SFastSim_Debug> record ;
     static constexpr const char* NAME = "SFastSim_Debug.npy" ;
     static constexpr int LIMIT = 100000 ;
     static void Save(const char* dir);
     void add();
     double posx ;  // fs[:,0,:3]
     double posy ;
     double posz ;
     double time ;  // fs[:,0,3]
     double dirx ;  // fs[:,1,:3]
     double diry ;
     double dirz ;
     double dist1 ; // fs[:,1,3]
     double polx ;  // fs[:,2,:3]
     double poly ;
     double polz ;
     double dist2 ; // fs[:,2,3]
     double ModelTrigger; // fs[:,3,0].astype(np.int64)
     double whereAmI ;    // fs[:,3,1].astype(np.int64)
     double c ;           // fs[:,3,2]
     double PhotonId ;    // fs[:,3,3].astype(np.int64)
};
epsilon:tests blyth$ PID=726 ./U4SimulateTest.sh nana
 In [1]: extra.shape
 Out[1]: (14, 4, 4)
 In [2]: extra
 Out[2]:
 array([[[-112.83 ,    0.   ,  164.918,    0.164],
         [   0.032,    0.   ,   -0.999,    0.001],    // dist1:1e-3 mm
         [   0.   ,   -1.   ,    0.   ,  165.005],
         [   1.   ,    1.   ,    0.   ,  726.   ]], ## ModelTrigger:1 
        [[-112.83 ,    0.   ,  164.917,    0.164],
         [  -0.138,    0.   ,   -0.99 ,  166.512],    // dist1 and dist2 equal
         [   0.   ,   -1.   ,    0.   ,  166.512],
         [   0.   ,    2.   ,    0.   ,  726.   ]], ## ModelTrigger:0 
        [[-156.577,    0.   , -148.846,    1.778],
         [   0.81 ,    0.   ,    0.587,  253.614],
         [   0.   ,    1.   ,    0.   ,    0.   ],
         [   0.   ,    1.   ,    0.   ,  726.   ]],
        [[ -95.   ,    0.   , -104.211,    2.166],
         [  -0.81 ,    0.   ,    0.587,  177.561],
         [   0.   ,   -1.   ,    0.   ,  169.042],
         [   0.   ,    1.   ,    0.   ,  726.   ]],
        [[-238.764,    0.   ,   -0.   ,    3.071],
         [  -0.81 ,    0.   ,    0.587,   12.404],
         [   0.   ,   -1.   ,    0.   ,   -1.   ],
         [   1.   ,    2.   ,    0.   ,  726.   ]],
         ...
ModelTrigger "pre-triggering" dist1 ahead as DoIt will shift by dist1
Starts at upper left, here -->
Simplified MultiLayer POM
+---------------pmt-Pyrex----------------+ | | | | | | | +~inner~Vacuum~(FSim~env)+ | | ! ! | | ! ! | | ! ! | | ! ! | | ! ! | | + + | | | | | | | | | | | | | | | | | | | | | | +------------------------+ | | | | | | | +----------------------------------------+
 G4bool junoPMTOpticalModelSimple::ModelTrigger(
         const G4FastTrack &fastTrack)
 {
      //  Simple + Cheap due to natural geometry 
     return fastTrack.GetPrimaryTrackLocalPosition().z() > 0. ;
 }
https://github.com/simoncblyth/j/blob/main/PMTFastSim/junoPMTOpticalModelSimple.cc
Simplified MultiLayer POM
+---------------pmt-Pyrex----------------+ | | | | | | | +~inner~Vacuum~(FSim~env)+ | | ! ! | | ! ! | | ! ! | | ! ! | | ! ! | | + + | | | | | | | | | | | | | | | | | | | | | | +------------------------+ | | | | | | | +----------------------------------------+
 void junoPMTOpticalModelSimple::DoIt(const G4FastTrack& fastTrack,
       G4FastStep &fastStep)
 {
     G4double energy = fastTrack.GetPrimaryTrack()->GetKineticEnergy();
     wavelength_nm = twopi*hbarc/energy/nm ;
     position = fastTrack.GetPrimaryTrackLocalPosition();
     direction = fastTrack.GetPrimaryTrackLocalDirection();
     polarization = fastTrack.GetPrimaryTrackLocalPolarization();
     G4VSolid* envelope_solid = fastTrack.GetEnvelopeSolid();
     surface_normal = envelope_solid->SurfaceNormal(position);
     minus_cos_theta = direction*surface_normal ;
     whereAmI = minus_cos_theta < 0. ? kInGlass : kInVacuum ;
     StackSpec spec ;
     // ... skip : collect refractive indices, thickness into spec ...
     Stack stack(      wavelength_nm, minus_cos_theta, spec );
     Stack stackNormal(wavelength_nm, -1.            , spec );
     // ...
}
   Custom Boundary POM
+---------------pmt-Pyrex----------------+ | | | | | | | +~inner~Vacuum~~~~~~~~~~~+ | | ! ! | | ! ! | | ! ! | | ! ! | | ! ! | | + + | | | | | | | | | | | | | | | | | | | | | | +------------------------+ | | | | | | | +----------------------------------------+
OpSurfaceName[0] == '@'
| Current FastSim POM | 4 Solid, 4 LV, 4 PV | 
| Custom Boundary POM | 2 Solid, 2 LV, 2 PV | 
CustomART<J>::maybe_doIt
template<typename J>
inline char CustomART<J>::maybe_doIt(
     const char* OpticalSurfaceName,
     const G4Track& aTrack, const G4Step& aStep )
{
    if( OpticalSurfaceName == nullptr ||
        OpticalSurfaceName[0] != '@') return 'N' ;
    const G4AffineTransform& transform =
      aTrack.GetTouchable()
      ->GetHistory()->GetTopTransform();
    G4ThreeVector localPoint =
      transform.TransformPoint(theGlobalPoint);
    if(localPoint.z() <= 0) return 'Z' ;
    return doIt(aTrack, aStep) ;
}
doIt TMM calc, runs only for:
#include "JPMT.h"
#include "Layr.h"
#include "U4Touchable.h"
template<typename J> struct CustomART
{
    J* parameter_accessor ;
    G4double& theTransmittance ;
    G4double& theReflectivity ;
    G4double& theEfficiency ;
    const G4ThreeVector& theGlobalPoint ;
    const G4ThreeVector& OldMomentum ;
    const G4ThreeVector& OldPolarization ;
    const G4ThreeVector& theRecoveredNormal ;
    const G4double& thePhotonMomentum ;
    CustomART(
        G4double& theTransmittance,
        G4double& theReflectivity,
        G4double& theEfficiency,
        const G4ThreeVector& theGlobalPoint,
        const G4ThreeVector& OldMomentum,
        const G4ThreeVector& OldPolarization,
        const G4ThreeVector& theRecoveredNormal,
        const G4double& thePhotonMomentum
    );
    char maybe_doIt(const char* OpticalSurfaceName,
              const G4Track& aTrack, const G4Step& aStep );
    char doIt(const G4Track& aTrack, const G4Step& aStep );
};
TO BT BT BT BT SR SR BT BR BR BT SR SR SR BT BR BT SR BT SA 00 01 [02] 03 [04] 05 06 [07] 08 09 [10] 11 12 13 [14] 15 [16] 17 [18] 19 (7/20 Fake)
TO BT BT SR SR BR BR SR SR SR BR SR BR SR SA 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 00 01 03 05 06 08 09 11 12 13 15 17 19
u4t ; ./U4SimulateTest.sh cf
In [12]: np.c_[ar[:,0],np.arange(len(ar)),br[:,0]]
Out[12]:
array([[-113.   ,    0.   ,  200.   ,    0.   ,    0.   , -113.   ,    0.   ,  200.   ,    0.   ],
       [-113.   ,    0.   ,  170.163,    0.137,    1.   , -113.   ,    0.   ,  170.163,    0.137],
       [-112.83 ,    0.   ,  164.918,    0.164,    2.   , -112.83 ,    0.   ,  164.917,    0.164],
       [-112.83 ,    0.   ,  164.917,    0.164,    3.   , -156.577,    0.   , -148.846,    1.22 ],
       [-135.824,    0.   ,    0.   ,    1.012,    4.   ,  -95.   ,    0.   , -104.211,    1.474],
       [-156.577,    0.   , -148.846,    1.778,    5.   , -248.807,    0.   ,    7.28 ,    2.108],
       [ -95.   ,    0.   , -104.211,    2.166,    6.   ,   53.206,    0.   ,  180.727,    3.269],
       [-238.764,    0.   ,   -0.   ,    3.071,    7.   ,  245.605,    0.   ,  -35.443,    4.235],
       [-248.807,    0.   ,    7.28 ,    3.112,    8.   ,   95.   ,    0.   ,  -99.428,    4.781],
       [  53.205,    0.   ,  180.727,    4.274,    9.   ,  177.724,    0.   , -134.574,    5.08 ],
       [ 214.06 ,    0.   ,    0.   ,    5.507,   10.   ,  141.059,    0.   ,  152.451,    6.046],
       [ 245.605,    0.   ,  -35.443,    5.749,   11.   , -239.66 ,    0.   ,  -55.195,    7.492],
       [  95.   ,    0.   ,  -99.428,    6.583,   12.   ,  237.91 ,    0.   ,   54.597,    9.127],
       [ 177.724,    0.   , -134.574,    7.041,   13.   ,   50.   ,    0.   ,  -63.74 ,    9.867],
       [ 160.533,    0.   ,    0.   ,    7.732,   14.   ,   58.352,    0.   ,  -69.   ,    9.9  ],
       [ 141.059,    0.   ,  152.451,    8.245,   15.   ,    0.   ,    0.   ,    0.   ,    0.   ],
       [-138.46 ,    0.   ,    0.   ,    9.867,   16.   ,    0.   ,    0.   ,    0.   ,    0.   ],
       [-239.66 ,    0.   ,  -55.195,   10.455,   17.   ,    0.   ,    0.   ,    0.   ,    0.   ],
       [   0.427,    0.   ,    0.   ,   11.71 ,   18.   ,    0.   ,    0.   ,    0.   ,    0.   ],
       [ 237.91 ,    0.   ,   54.596,   12.523,   19.   ,    0.   ,    0.   ,    0.   ,    0.   ],
       [   0.   ,    0.   ,    0.   ,    0.   ,   20.   ,    0.   ,    0.   ,    0.   ,    0.   ],
       [   0.   ,    0.   ,    0.   ,    0.   ,   21.   ,    0.   ,    0.   ,    0.   ,    0.   ],
       [   0.   ,    0.   ,    0.   ,    0.   ,   22.   ,    0.   ,    0.   ,    0.   ,    0.   ],
       [   0.   ,    0.   ,    0.   ,    0.   ,   23.   ,    0.   ,    0.   ,    0.   ,    0.   ],
| ar | a.record[PID] | N=0 current geom | degenerate and fake intersect points | 
| bb | b.record[PID] | N=1 natural geom | less points, simpler history | 
Need point-to-point mapping to compare
 u4t ; ./U4SimulateTest.sh cf
 In [2]: b2a  ## point-to-point mapping to skip a fakes
 Out[2]: array([ 0,  1,  3,  5,  6,  8,  9, 11, 12, 13, 15, 17, 19])
 In [4]: abr = np.c_[ar[b2a,0],br[:len(b2a),0],ar[b2a,0]-br[:len(b2a),0]] ; abr
 Out[4]:
 array([[-113.   ,    0.   ,  200.   ,    0.   , -113.   ,    0.   ,  200.   ,    0.   ,    0.   ,    0.   ,    0.   ,    0.   ],
        [-113.   ,    0.   ,  170.163,    0.137, -113.   ,    0.   ,  170.163,    0.137,    0.   ,    0.   ,    0.   ,    0.   ],
        [-112.83 ,    0.   ,  164.917,    0.164, -112.83 ,    0.   ,  164.917,    0.164,    0.   ,    0.   ,    0.   ,   -0.   ],
        [-156.577,    0.   , -148.846,    1.778, -156.577,    0.   , -148.846,    1.22 ,   -0.   ,    0.   ,    0.   ,    0.558],
        [ -95.   ,    0.   , -104.211,    2.166,  -95.   ,    0.   , -104.211,    1.474,    0.   ,    0.   ,    0.   ,    0.692],
        [-248.807,    0.   ,    7.28 ,    3.112, -248.807,    0.   ,    7.28 ,    2.108,    0.   ,    0.   ,   -0.   ,    1.004],
        [  53.205,    0.   ,  180.727,    4.274,   53.206,    0.   ,  180.727,    3.269,   -0.   ,    0.   ,    0.   ,    1.004],
        [ 245.605,    0.   ,  -35.443,    5.749,  245.605,    0.   ,  -35.443,    4.235,    0.   ,    0.   ,    0.   ,    1.514],
        [  95.   ,    0.   ,  -99.428,    6.583,   95.   ,    0.   ,  -99.428,    4.781,    0.   ,    0.   ,    0.   ,    1.802],
        [ 177.724,    0.   , -134.574,    7.041,  177.724,    0.   , -134.574,    5.08 ,    0.   ,    0.   ,    0.   ,    1.96 ],
        [ 141.059,    0.   ,  152.451,    8.245,  141.059,    0.   ,  152.451,    6.046,   -0.   ,    0.   ,    0.   ,    2.199],
        [-239.66 ,    0.   ,  -55.195,   10.455, -239.66 ,    0.   ,  -55.195,    7.492,    0.   ,    0.   ,    0.   ,    2.963],
        [ 237.91 ,    0.   ,   54.596,   12.523,  237.91 ,    0.   ,   54.597,    9.127,    0.   ,    0.   ,   -0.   ,    3.397]], dtype=float32)
Positions match closely, some times are way off
rvtime_ = lambda r:np.diff(r[:,0,3])
rvstep_ = lambda r:np.diff(r[:,0,:3],axis=0 )
rvdist_ = lambda r:np.sqrt(np.sum(rvstep_(r)*rvstep_(r),axis=1))
rvspeed_ = lambda r:rvdist_(r)/rvtime_(r)
In [14]: np.c_[rvtime_(ar[b2a]), rvtime_(br[:len(b2a)]),
               rvdist_(ar[b2a]), rvdist_(br[:len(b2a)]),
              rvspeed_(ar[b2a]),rvspeed_(br[:len(b2a)])]
Out[14]:
array([[  0.137,   0.137,  29.837,  29.837, 218.038, 218.038],  ## Water
       [  0.027,   0.027,   5.249,   5.249, 196.216, 196.215],  ## Pyrex
       [  1.615,   1.057, 316.798, 316.798, 196.215, 299.792],  ## Vacuum
       [  0.388,   0.254,  76.053,  76.053, 196.215, 299.792],
       [  0.946,   0.634, 189.965, 189.965, 200.744, 299.792],  ## comb. of Vacuum and Pyrex speeds, split at Fake 
       [  1.162,   1.162, 348.275, 348.275, 299.792, 299.792],
       [  1.475,   0.965, 289.392, 289.392, 196.215, 299.792],
       [  0.834,   0.546, 163.634, 163.634, 196.215, 299.792],
       [  0.458,   0.3  ,  89.881,  89.881, 196.215, 299.792],
       [  1.204,   0.965, 289.357, 289.357, 240.315, 299.792],  ## comb. of Vacuum and Pyrex speeds, split at Fake
       [  2.21 ,   1.447, 433.663, 433.663, 196.215, 299.792],
       [  2.068,   1.635, 490.027, 490.027, 236.919, 299.792]], dtype=float32)
| N=0 | Pyrex speed within PMT Vacuum | FastSim->SlowSim transitions miss speed setup ? | 
| N=1 | Always Vacuum speed in Vacuum | All standard Geant4 with customized G4OpBoundaryProcess | 
Save/Load g4state into NP array
Efficiently save/restore thousands of states
MixMaxRng (G4 1042 random engine)
State of engine : 38*uint32
U4Engine::SaveState( NP* states, int idx ); U4Engine::RestoreState( NP* states, int idx );
Rerun "big bouncer" photon
vi U4SimulateTest.sh # SRM_G4STATE_SAVE N=0 ./U4SimulateTest.sh # save g4state into ALL0 vi U4SimulateTest.sh # SRM_G4STATE_RERUN N=1 ./U4SimulateTest.sh # load ALL0, save SEL1
CustomG4OpBoundaryProcess ?
BUT : need to change very nasty Geant4 impl:
CPU
GPU
More standard approach and simpler geometry makes it easier