Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-04-09 07:48:50

0001 #!/usr/bin/env python
0002 """
0003 rindex.py : creating a 2D Cerenkov ICDF lookup texture
0004 ========================================================
0005 
0006 This demonstrates a possible approach for constructing a 2d Cerenkov ICDF texture::
0007 
0008     ( BetaInverse[nMin:nMax], u[0:1] )  
0009 
0010 The result is being able to sample Cerenkov energies for photons radiated by a particle 
0011 traversing the media with BetaInverse (between nMin and nMax of the material) 
0012 with a single random throw followed by a single 2d texture lookup.
0013 That compares with the usual rejection sampling approach which requires a variable number of throws 
0014 (sometimes > 100) with double precision math needed to reproduce Geant4 results.
0015 
0016 For BetaInverse 
0017 
0018 
0019 """
0020 import os, numpy as np, logging
0021 log = logging.getLogger(__name__)
0022 
0023 import matplotlib.pyplot as plt 
0024 from opticks.ana.main import opticks_main 
0025 from opticks.ana.key import keydir
0026 
0027 
0028 def findbin(ee, e):
0029     """
0030     :param ee: array of ascending values 
0031     :param e: value to check 
0032     :return ie: index of first bin that contains e or -1 if not found
0033 
0034     A value equal to lower bin edge is regarded to be contained in the bin. 
0035 
0036     Hmm currently a value equal to the upper edge of the last bin 
0037     is not regarded as being in the bin::
0038 
0039         In [22]: findbin(ri[:,0], 15.5)
0040         Out[22]: -1
0041 
0042         In [23]: findbin(ri[:,0], 15.5-1e-6)
0043         Out[23]: 16
0044 
0045 
0046     Hmm could use binary search to do this quicker.
0047     """
0048     assert np.all(np.diff(ee) > 0)
0049     ie = -1
0050     for i in range(len(ee)-1):
0051         if ee[i] <= e and e < ee[i+1]: 
0052             ie = i
0053             break 
0054         pass 
0055     pass
0056     return ie    
0057 
0058 
0059 
0060 def find_cross(ri, BetaInverse):
0061     """
0062     The interpolated rindex is piecewise linear 
0063     so can find the roots (where rindex crosses BetaInverse)
0064     without using optimization : just observing sign changes
0065     to find crossing bins and then some linear calc::
0066                  
0067                   (x1,v1)
0068                    *
0069                   / 
0070                  /
0071                 /
0072         -------?(x,v)----    v = 0    when values are "ri_ - BetaInverse"
0073               /
0074              /
0075             /
0076            /
0077           * 
0078         (x0,v0)      
0079 
0080 
0081          Only x is unknown 
0082 
0083 
0084               v1 - v        v - v0
0085              ----------  =  ----------
0086               x1 - x        x - x0  
0087 
0088 
0089            v1 (x - x0 ) =  -v0  (x1 - x )
0090 
0091            v1.x - v1.x0 = - v0.x1 + v0.x  
0092 
0093            ( v1 - v0 ) x = v1*x0 - v0*x1
0094 
0095 
0096                          v1*x0 - v0*x1
0097                x    =   -----------------
0098                           ( v1 - v0 ) 
0099 
0100 
0101     Hmm with "v0*v1 < 0" this is failing to find a crossing at a bin edge::
0102 
0103         find_cross(ckr.ri, 1.4536)  = []
0104 
0105     ::
0106 
0107                                                      +----
0108                                                     / 
0109            ----+                                   /
0110                 \                                 /
0111                  \                               /
0112          . . . . .+----------------+------------+ . . . .
0113                   ^                ^
0114               initially          causes nan 
0115               not found          without v0 != v1 protection
0116 
0117     With "v0*v1 <= 0 and v0 != v1" it manages to find the crossing.
0118     Need protection against equal v0 and v1 to avoid nan from 
0119     mid or endpoint of "tangent" plateaus::
0120 
0121         find_cross(ckr.ri, 1.4536)  = array([10.33])
0122 
0123     """
0124     cross = []
0125     for i in range(len(ri)-1):
0126         x0 = ri[i,0]
0127         x1 = ri[i+1,0]
0128         v0 = BetaInverse - ri[i,1]
0129         v1 = BetaInverse - ri[i+1,1]
0130 
0131         if v0*v1 <= 0 and v0 != v1:
0132             x = (v1*x0 - v0*x1)/(v1-v0)     
0133             #print("find_cross i %d x0 %6.4f x1 %6.4f v0 %6.4f v1 %6.4f x %6.4f " % (i, x0,x1,v0,v1,x))
0134             cross.append(x)
0135             pass
0136         pass   
0137     pass
0138     return np.array(cross) 
0139 
0140 
0141 class CKRindex(object):
0142     """
0143     Cerenkov Sampling cases:
0144 
0145     * BetaInverse between 1 and nMin there are no crossings and Cerenkov is permitted across the full domain
0146     * BetaInverse  > nMax there are no crossins and Cerenkov is not-permitted across the full domain
0147     * BetaInverse between nMin and nMax there will be crossings and permitted/non-permitted regions depending on rindex 
0148 
0149     """
0150     def __init__(self):
0151         kd = keydir(os.environ["OPTICKS_KEY"])
0152         ri = np.load(os.path.join(kd, "GScintillatorLib/LS_ori/RINDEX.npy"))
0153         ri[:,0] *= 1e6  
0154         ri_ = lambda e:np.interp(e, ri[:,0], ri[:,1] )
0155         nMax = ri[:,1].max() 
0156         nMin = ri[:,1].min() 
0157 
0158         self.ri = ri
0159         self.ri_ = ri_
0160         self.nMax = nMax
0161         self.nMin = nMin
0162 
0163 
0164     def find_energy_range(self, bis):
0165         """
0166         :param bis: array of BetaInverse 
0167 
0168         For each BetaInverse bi sets:
0169  
0170         self.xri[bi]
0171              array of domain energy values at which BetaInverse equals the RINDEX
0172         self.xrg[bi]
0173              array of two values energy domain values (min, max) were Cerenkov is allowed OR None 
0174 
0175         """
0176         log.info("find_energy_range")
0177         ri = self.ri 
0178         xri = {} 
0179         xrg = {}  # dict of range arrays with min and max permissable energies
0180 
0181         for bi in bis:
0182             xri[bi] = find_cross(ri, BetaInverse=bi)   # list of crossings 
0183 
0184             lhs = ri[0,1] - bi > 0   # ck allowed at left edge of domain ?
0185             rhs = ri[-1,1] - bi > 0  # ck allowed at right edge of domain ?  
0186 
0187             if len(xri[bi]) > 1:
0188                 xrg[bi] = np.array( [xri[bi].min(), xri[bi].max()])
0189             elif len(xri[bi]) == 1:
0190                 # one crossing needs special handling as need to define an energy range with one side or the other
0191                 if lhs and not rhs:
0192                     xrg[bi] = np.array([ ri[0,0], xri[bi][0] ])
0193                 elif not lhs and rhs:   
0194                     xrg[bi] = np.array([ xri[bi][0], ri[-1,0] ])
0195                 else:
0196                     log.fatal("unexpected 1-crossing")
0197                     assert 0 
0198                 pass
0199                 #log.info("bi %s one crossing : lhs %s rhs %s xrg[bi] %s  " % (bi, lhs, rhs, str(xrg[bi]) ))
0200             elif len(xri[bi]) == 0:
0201                 if lhs and rhs:
0202                     xrg[bi] = np.array( [ ri[0,0], ri[-1,0] ])
0203                 else:
0204                     xrg[bi] = None
0205                 pass
0206                 #log.info("bi %s zero crossing : lhs %s rhs %s xrg[bi] %s  " % (bi, lhs, rhs, str(xrg[bi]) ))
0207             else:
0208                 xrg[bi] = None
0209             pass
0210         pass
0211         #log.info("xri\n%s", xri)
0212         #log.info("xrg\n%s", xrg)
0213         self.xri = xri
0214         self.xrg = xrg
0215     pass
0216 
0217     def find_energy_range_plot(self, bis):
0218         """
0219         """
0220         ri = self.ri 
0221         xri = self.xri
0222         xrg = self.xrg
0223 
0224         title = "rindex.py : find_energy_range_plot"
0225 
0226         fig, ax = plt.subplots(figsize=ok.figsize); 
0227         fig.suptitle(title)
0228 
0229         # steps make no sense for rindex, as it is inherently interpolated between measured points
0230         ax.plot( ri[:,0], ri[:,1] )  
0231 
0232         xlim = ax.get_xlim()
0233         ylim = ax.get_ylim()
0234 
0235         ax.plot( xlim, [self.nMax,self.nMax], label="nMax", linestyle="dotted", color="r" )
0236         ax.plot( xlim, [self.nMin,self.nMin], label="nMin", linestyle="dotted", color="r" )
0237 
0238         # horizontal bi lines
0239         #for bi in bis:
0240         #    ax.plot( xlim, [bi,bi], linestyle="dotted", color="r" )
0241         #pass  
0242         ax.plot( [ri[0,0], ri[0,0]], ylim, linestyle="dotted", color="r" )
0243         ax.plot( [ri[-1,0], ri[-1,0]], ylim, linestyle="dotted", color="r" )
0244 
0245         for bi in bis:
0246             print(" bi %7.4f xrg[bi] %s " % (bi, xrg[bi]) )
0247             ax.plot( xrg[bi], [bi, bi], label="xrg %s " % xrg[bi] )
0248             xx = xri[bi]
0249             ax.scatter( xx, np.repeat(bi, len(xx)) )
0250         pass
0251 
0252         #for bi in bis:
0253         #    for x in xri[bi]:
0254         #        ax.plot( [x,x], ylim, linestyle="dotted", color="r" )
0255         #    pass
0256         pass
0257         ax.scatter( ri[:,0], ri[:,1] )
0258 
0259 
0260 
0261         ax.legend()
0262         fig.show()
0263 
0264 
0265     def s2_cumsum(self, bis):
0266         """
0267         cheap and cheerful s2 CDF
0268 
0269         This uses a lot of energy bins across the allowed Cerenkov range 
0270         so probably no need for careful bin crossing the allowed edges ?
0271         """
0272         log.info("s2_cumsum")
0273         xrg = self.xrg
0274         ed = {}     # dict of energy domain arrays
0275         s2e = {}    # dict of s2 arrays across the energy domain
0276         cs2e = {}   # dict of cumumlative sums across the energy domain
0277 
0278         ri = self.ri  
0279         ## np.minimum prevents the cos(th) from exceeding 1 in disallowed regions
0280         ct_ = lambda bi,e:np.minimum(1.,bi/np.interp(e, ri[:,0], ri[:,1] ))
0281         s2_ = lambda bi,e:(1-ct_(bi,e))*(1+ct_(bi,e))
0282 
0283         for bi in bis:
0284             if xrg[bi] is None: continue
0285             ed[bi] = np.linspace(xrg[bi][0],xrg[bi][1],4096)    # energy range from min to max allowable
0286             s2e[bi] = s2_(bi,ed[bi])  
0287             cs2e[bi] = np.cumsum(s2e[bi])  
0288 
0289             if cs2e[bi][-1] > 0.:
0290                 cs2e[bi] /= cs2e[bi][-1]      # last bin will inevitably be maximum one as cumulative   
0291             else:
0292                 log.fatal("bi %7.4f zero cannot normalize " % bi )
0293             pass
0294         pass 
0295         self.ed = ed  
0296         self.s2e = s2e  
0297         self.cs2e = cs2e  
0298 
0299     def s2_cumsum_plot(self, bis):
0300         xrg = self.xrg
0301         ed = self.ed 
0302         cs2e = self.cs2e
0303         title = "rindex.py : quick and dirty s2cdf : s2_cumsum_plot"
0304 
0305         fig, axs = plt.subplots(figsize=ok.figsize) 
0306         fig.suptitle(title)
0307         for i, bi in enumerate(bis):
0308             if xrg[bi] is None: continue
0309             ax = axs if axs.__class__.__name__ == 'AxesSubplot' else axs[i] 
0310             ax.plot( ed[bi], cs2e[bi], label="cs2e : integrated s2 vs e bi:%6.4f  " % bi )
0311             #ax.set_xlim( xrg[1.5][0], xrg[1.5][1] )
0312             ax.set_ylim( -0.1, 1.1 ) 
0313             ax.legend()
0314  
0315         pass
0316         fig.show()
0317 
0318     def s2_integrate__(self, BetaInverse, en_0, en_1, ri_0, ri_1 ):
0319         """
0320         :param BetaInverse: 
0321         :param en_0: 
0322         :param en_1: 
0323         :param ri_0: 
0324         :param ri_1: 
0325         :return ret: scalar integrated value for the bin
0326 
0327         When s2 is positive across the bin this returns the trapezoidal area.
0328 
0329         When there is an s2 zero crossing a linear calculation is used to find 
0330         the crossing point and the triangle area is returned that excludes negative contributions. 
0331 
0332         See s2.py : better to find en_cross from ri-BetaInverse crossings as that is linear, unlike s2.
0333         """
0334         ct_0 = BetaInverse/ri_0
0335         ct_1 = BetaInverse/ri_1
0336 
0337         s2_0 = (1.-ct_0)*(1.+ct_0) 
0338         s2_1 = (1.-ct_1)*(1.+ct_1) 
0339 
0340         ret = 0.
0341         if s2_0 <= 0. and s2_1 <= 0.:
0342             ret = 0.
0343         elif s2_0 < 0. and s2_1 > 0.:    # s2 goes +ve 
0344             en_cross = (s2_1*en_0 - s2_0*en_1)/(s2_1 - s2_0)
0345             ret = (en_1 - en_cross)*s2_1*0.5
0346         elif s2_0 >= 0. and s2_1 >= 0.:   # s2 +ve or zero across full range 
0347             ret = (en_1 - en_0)*(s2_0 + s2_1)*0.5
0348         elif s2_0 > 0. and s2_1 <= 0.:     # s2 goes -ve or to zero 
0349             en_cross = (s2_1*en_0 - s2_0*en_1)/(s2_1 - s2_0) 
0350             ret = (en_cross - en_0)*s2_0*0.5
0351         else:
0352             print( "s2_integrate__  en_0 %10.5f ri_0 %10.5f s2_0 %10.5f  en_1 %10.5f ri_1 %10.5f s2_1 %10.5f " % (en_0, ri_0, s2_0, en_1, ri_1, s2_1 )) 
0353             assert 0 
0354         pass
0355         assert ret >= 0. 
0356         return ret
0357 
0358 
0359     def s2_integrate_(self, BetaInverse, edom):
0360         """
0361         :param BetaInverse: scalar
0362         :param edom: array of energy cuts
0363         :return s2ij: array of the same length as edom containing the integral values up to the edom energy cuts 
0364 
0365         Note that s2in should be strictly monotonic, never decreasing, from bin to bin
0366         Bin before last somehow manages to break that for BetaInverse 1.4536::
0367 
0368              np.c_[ckr.edom[1.4536], ckr.s2ij[1.4536]][2000:2100] 
0369              np.c_[ckr.edom[1.4536], ckr.s2ij[1.4536]][4000:]    
0370 
0371         Possible dispositions of the ecut with respect to the bin: 
0372                                         
0373                     en0_b    en1_b
0374  
0375                       |        |     [1]      en0_b < ecut and en1_b < ecut 
0376                       |        |    
0377                       |  [2]   |              en0_b < ecut and ecut < en1_b     
0378                       |        |    
0379                  [3]  |        |              bin does not contibute    
0380                       |        |    
0381 
0382         """
0383         ri_ = self.ri_
0384         ni = len(edom)
0385         nj = len(self.ri)-1
0386         s2ij = np.zeros( (ni,nj), dtype=edom.dtype)
0387 
0388         # for each energy cut, find contributions from each rindex bin
0389         # hmm this seems crazily repetitive, 
0390         # also are just going to sum over the rindex bins in the end
0391 
0392         for i in range(ni):        
0393             en_cut = edom[i]  
0394             ri_cut = ri_(en_cut)
0395 
0396             for j in range(nj):
0397                 en0_b = self.ri[j,0]
0398                 en1_b = self.ri[j+1,0]
0399 
0400                 ri0_b = self.ri[j,1]
0401                 ri1_b = self.ri[j+1,1]
0402 
0403                 allowed = True
0404                 if en0_b < en_cut and en1_b <= en_cut:      # full bin included in cumulative range     
0405                     en_0, en_1 = en0_b, en1_b  
0406                     ri_0, ri_1 = ri0_b, ri1_b 
0407                 elif en0_b <= en_cut and en_cut <= en1_b:   #  en0_b < ecut < en1_b :  ecut divides the bin 
0408                     en_0, en_1 = en0_b, en_cut 
0409                     ri_0, ri_1 = ri0_b, ri_cut
0410                 else:
0411                     allowed = False
0412                 pass
0413                 if allowed:
0414                     s2ij[i,j] = self.s2_integrate__( BetaInverse, en_0, en_1, ri_0, ri_1 ) 
0415                 pass
0416             pass    
0417         pass
0418         return s2ij
0419 
0420 
0421     def s2_integrate_again_(self, BetaInverse, edom ):
0422         """
0423         :param BetaInverser: scalar
0424         :param edom: array of shape (n,)
0425         :return s2ji: array of shape (n,)
0426         """
0427         pass
0428         ni = len(edom)
0429         nj = len(self.ri)-1
0430 
0431         ## s2 integrals in each rindex bin 
0432         s2j = np.zeros( nj, dtype=edom.dtype )
0433 
0434         ## cumulative s2 integrals across a fine energy domain 
0435         s2i = np.zeros( ni, dtype=edom.dtype)
0436 
0437         for j in range(nj):
0438             en_0 = self.ri[j,0]
0439             en_1 = self.ri[j+1,0]
0440             ri_0 = self.ri[j,1]
0441             ri_1 = self.ri[j+1,1]
0442             s2j[j] = self.s2_integrate__( BetaInverse, en_0, en_1, ri_0, ri_1 ) 
0443         pass
0444 
0445 
0446         #for i in range(ni):        
0447         #    s2i[i] = 
0448         #pass
0449          
0450 
0451 
0452 
0453 
0454 
0455     def s2_integrate(self, bis, nx=4096):
0456         """
0457         * CONCLUSION : USE S2SLIVER NOT THIS 
0458 
0459         This approach is slow and has a problem of 
0460         giving slightly non-monotonic CDF for the pathalogical BetaInverse=nMin. 
0461         As a result of this implemented s2sliver_integrate which is much 
0462         faster and avoids the problem
0463 
0464         This was aiming to be more accurate that the original quick 
0465         and dirty s2_cumsum but in retrospect the s2sliver_integrate 
0466         approach is much better combining better accuracy, speed
0467         and simplicity.
0468 
0469         Follows approach of ckn.py:GetAverageNumberOfPhotons_s2  
0470         although it turns out to not be simpler that np.cumsum approach.
0471 
0472         Need cumulative s2 integral.
0473         For each BetaInverse need to compute the s2 integral over
0474         an increasing energy range that will progressively 
0475         cover more and more rindex bins until eventually 
0476         covering them all.
0477 
0478         Remember that there can be holes in permissability.
0479 
0480         Notice no need to compute permissable ranges as the 
0481         sign of s2 shows that with no further effort.
0482 
0483         HMM the "cdf" is going above 1 for the top bin for BetaInverse below nMin 
0484         where CK can happen across entire range. 
0485 
0486         * That was a "<" which on changing to "<=" avoided the problem of not properly 
0487           accounting for the last bin.
0488 
0489         Also when BetaInverse = 1.4536 = ckr.ri[:,1].min() that 
0490         exactly matches the rindex of the last two energy points::
0491 
0492                ...
0493                [ 9.538 ,  1.5545],
0494                [10.33  ,  1.4536],
0495                [15.5   ,  1.4536]])
0496 
0497         so the ct = 1 and s2 = 0 across the entire top bin. 
0498         That somehow causes mis-normalization for full-range CK ?
0499 
0500         So there is a discrepancy in handling of the top bin between the np.cumsum 
0501         and the trapezoidal integral approaches.
0502 
0503         Possibly an off-by-one with the handling of the edom/ecut ?
0504 
0505         Sort of yes, actually most of the issue seems due to using "< ecut" and not "<= ecut" 
0506         which resulted in s2in not including the top bin causing it to be non-monotonic and 
0507         resultued in the normalization being off.
0508 
0509         Hmm that is an advantage with using cumsum rather than lots of integrals
0510         with increasing range.
0511 
0512         Getting a small decrement with increasing ecut in range 10.30-10.33 for the probematic 1.4536::
0513 
0514             In [6]: np.c_[np.diff(ckr.s2in[1.4536])[-20:]*1e6,ckr.s2in[1.4536][-20:],ckr.edom[1.4536][-20:]]                                                                                                
0515             Out[6]: 
0516             array([[  3.0319,   1.2293,  10.2893],
0517                    [  2.1562,   1.2293,  10.2914],
0518                    [  1.2797,   1.2293,  10.2936],
0519                    [  0.4026,   1.2293,  10.2957],
0520                    [ -0.4753,   1.2293,  10.2978],
0521                    [ -1.3539,   1.2293,  10.3   ],
0522                    [ -2.2333,   1.2293,  10.3021],
0523                    [ -3.1134,   1.2293,  10.3043],
0524                    [ -3.9942,   1.2293,  10.3064],
0525                    [ -4.8758,   1.2293,  10.3086],
0526                    [ -5.7581,   1.2293,  10.3107],
0527                    [ -6.6411,   1.2293,  10.3128],
0528                    [ -7.5249,   1.2293,  10.315 ],
0529                    [ -8.4094,   1.2293,  10.3171],
0530                    [ -9.2947,   1.2293,  10.3193],
0531                    [-10.1807,   1.2293,  10.3214],
0532                    [-11.0674,   1.2293,  10.3236],
0533                    [-11.9549,   1.2293,  10.3257],
0534                    [-12.8431,   1.2292,  10.3279],
0535                    [-13.7321,   1.2292,  10.33  ]])
0536 
0537             In [7]:                                                             
0538 
0539         """
0540         log.info("s2_integrate")
0541         ri = self.ri
0542 
0543         s2ij = {}
0544         s2in = {}
0545         cs2in = {}
0546         edom = {}
0547         yrg = {}
0548 
0549         for BetaInverse in bis:
0550             numPhotons_s2, emin, emax = self.GetAverageNumberOfPhotons_s2(BetaInverse)  # get energy range for the BetaInverse
0551             yrg[BetaInverse] = [numPhotons_s2, emin, emax]
0552             if numPhotons_s2 <= 0.: continue
0553             edom[BetaInverse] = np.linspace(emin, emax, nx) 
0554             s2ij[BetaInverse] = self.s2_integrate_(BetaInverse, edom[BetaInverse])
0555             s2in[BetaInverse] = s2ij[BetaInverse].sum(axis=1)
0556             cs2in[BetaInverse] = s2in[BetaInverse]/s2in[BetaInverse][-1]   # normalization to last bin
0557             d = np.diff(s2in[BetaInverse])
0558             if len(d[d<0]) > 0:
0559                 log.fatal(" monotonic diff check fails for BetaInverse %s  d[d<0] %s " % (BetaInverse, str(d[d<0])))   
0560                 print(d)
0561             pass
0562         pass
0563 
0564         self.s2ij = s2ij
0565         self.s2in = s2in
0566         self.cs2in = cs2in
0567         self.edom = edom
0568         self.yrg = yrg
0569 
0570     def s2_integrate_plot(self, bis):
0571         cs2in = self.cs2in
0572         edom = self.edom
0573         title = "rindex.py : s2_integrate_plot"
0574 
0575         fig, axs = plt.subplots(figsize=ok.figsize) 
0576         fig.suptitle(title)
0577         for i, bi in enumerate(bis):
0578             if not bi in cs2in: continue
0579             ax = axs if axs.__class__.__name__ == 'AxesSubplot' else axs[i] 
0580             ax.plot( edom[bi], cs2in[bi], label="cs2in : integrated s2 vs e bi:%6.4f  " % bi )
0581             ax.set_ylim( -0.1, 1.1 )  
0582             ax.legend()
0583         pass
0584         fig.show()
0585 
0586     def s2sliver_check(self, edom):
0587         """
0588         seems not needed
0589         """
0590         ri = self.ri
0591         eb = ri[:,0]
0592         contain = 0 
0593         straddle = 0 
0594 
0595         for i in range(len(edom)-1):
0596 
0597             en_0 = edom[i]
0598             en_1 = edom[i+1]
0599 
0600             ## find bin indices corresponding to left and right sliver edges
0601             b0 = findbin(eb, en_0)  
0602             b1 = findbin(eb, en_1)
0603 
0604             found =  b0 > -1 and b1 > -1
0605             if not found:
0606                 log.fatal(" bin not found %s %s %d %d " % (en_0,en_1,b0,b1 )) 
0607             pass
0608             assert(found)
0609 
0610             if b0 == b1:
0611                 # sliver is contained in single rindex bin
0612                 pass
0613                 contain += 1 
0614             elif b1 == b0 + 1:
0615                 # sliver straddles bin edge 
0616                 pass
0617                 straddle += 1 
0618             else:
0619                 log.fatal("unexpected bin disposition en_0 %s en_1 %s b0 %d b1 %d " % (en_0,en_1,b0,b1)) 
0620                 assert 0
0621             pass
0622         pass
0623         log.info("contain %d straddle %d sum %d " % (contain, straddle, contain+straddle))
0624 
0625 
0626     def s2sliver_integrate_(self, BetaInverse, edom):
0627         """
0628         :param BetaInverse: scalar
0629         :param edom: array of energy values in eV 
0630         :return s2slv: array with same shape as edom containing s2 definite integral values within energy slivers 
0631 
0632 
0633         Possible dispositions of the energy sliver with respect to the bin: 
0634                                         
0635         1. sliver fully inside a bin:: 
0636 
0637  
0638                       |  .   .    |                       |
0639                       |  .   .    |                       | 
0640                       |  .   .    |                       |  
0641                       |  .   .    |                       | 
0642 
0643  
0644         2. sliver straddles edge of bin::
0645  
0646                       |          . | .                    |   
0647                       |          . | .                    |
0648                       |          . | .                    |
0649                       |          . | .                    |
0650 
0651 
0652         Within the sliver there is the possibility of s2 crossings
0653         that will require constriction of the sliver, this is handled 
0654         by s2_integrate__
0655         """
0656         ri_ = self.ri_
0657         s2slv = np.zeros( (len(edom)), dtype=edom.dtype )
0658         for i in range(len(edom)-1):
0659             en_0 = edom[i]
0660             en_1 = edom[i+1]
0661             ri_0 = ri_(en_0) 
0662             ri_1 = ri_(en_1) 
0663             s2slv[i+1] = self.s2_integrate__( BetaInverse, en_0, en_1, ri_0, ri_1 )  
0664             # s2_integrate__ accounts for s2 crossings : "chopping bins" and giving triangle areas 
0665         pass
0666         return s2slv 
0667 
0668 
0669     def s2sliver_integrate(self, bis, nx=4096):
0670         """
0671         Issues with small breakage of monotonic, makes me think its better to structure the calculation 
0672         to make monotonic inevitable, by storing "sliver" integrals and np.cumsum adding them up
0673         to give the cumulative CDF.
0674 
0675        Do this by slicing the energy range for each BetaInverse into small "slivers" 
0676         Can assume that the sliver size is smaller than smallest rindex energy bin size, 
0677         so the sliver can either be fully inside the bin or straddling the bin edge.
0678         Then get the total integral by np.cumsum adding the pieces.
0679         This will be much more efficient too as just does 
0680         the integral over each energy sliver once. 
0681 
0682         But suspect that having too many slivers will be an accuracy problem.
0683         As the underlying RINDEX is linearly interpolated between measured
0684         points it is more accurate to have fewer slivers in the all but the final bin ? 
0685 
0686         See s2.py that fixes the non-monotonic issue by ensuring en_cross only 
0687         obtained once for a bin and uses ri-BetaInverse rather than s2 zero 
0688         crossings as linear nature will make it more precise. 
0689         """
0690         log.info("s2sliver_integrate")
0691 
0692         vdom = {}
0693         vrg = {}
0694         s2slv = {}
0695         cs2slv = {}
0696 
0697         for BetaInverse in bis:
0698             numPhotons_s2, emin, emax = self.GetAverageNumberOfPhotons_s2(BetaInverse)
0699             vrg[BetaInverse] = [numPhotons_s2, emin, emax]
0700             if numPhotons_s2 <= 0.: continue
0701             vdom[BetaInverse] = np.linspace(emin, emax, nx) 
0702             s2slv[BetaInverse] = self.s2sliver_integrate_(BetaInverse, vdom[BetaInverse])
0703             cs2slv[BetaInverse] = np.cumsum(s2slv[BetaInverse])
0704             cs2slv[BetaInverse] /= cs2slv[BetaInverse][-1]      # CDF normalize
0705         pass
0706         self.vrg = vrg
0707         self.vdom = vdom
0708         self.s2slv = s2slv
0709         self.cs2slv = cs2slv
0710 
0711     def s2sliver_integrate_plot(self, bis):
0712         cs2slv = self.cs2slv
0713         vdom = self.vdom
0714         title = "rindex.py : s2sliver_integrate_plot"
0715 
0716         fig, axs = plt.subplots(figsize=ok.figsize) 
0717         fig.suptitle(title)
0718         for i, bi in enumerate(bis):
0719             if not bi in cs2slv: continue
0720             ax = axs if axs.__class__.__name__ == 'AxesSubplot' else axs[i] 
0721             ax.plot( vdom[bi], cs2slv[bi], label="cs2slv : cumsum of s2 sliver integrals,  bi:%6.4f  " % bi )
0722             #ax.set_ylim( -0.1, 1.1 )  
0723             ax.legend()
0724         pass
0725         fig.show()
0726 
0727 
0728     def comparison_plot(self, bis):
0729 
0730         cs2in = self.cs2in if hasattr(self, 'cs2in') else None
0731         edom = self.edom if hasattr(self, 'edom') else None
0732         yrg = self.yrg if hasattr(self, 'yrg') else None
0733 
0734         cs2slv = self.cs2slv
0735         vdom = self.vdom
0736 
0737         ri = self.ri  
0738         xrg = self.xrg
0739         ed = self.ed 
0740         cs2e = self.cs2e
0741 
0742         titls = ["rindex.py : comparison_plot %s " % str(bis), ]
0743 
0744         bi = bis[0] if len(bis) == 1 else None
0745         if len(bis) == 1:
0746             if not xrg is None and bi in xrg: 
0747                 titls.append(" xrg[bi] %s " % str(xrg[bi]))
0748             pass
0749             if not yrg is None and bi in yrg: 
0750                 titls.append(" yrg[bi] %s " % str(yrg[bi]))
0751             pass
0752             if not edom is None and bi in edom:
0753                 titls.append(" edom[bi] %s " % str(edom[bi]))
0754             pass
0755         pass
0756         title = "\n".join(titls) 
0757 
0758         fig, axs = plt.subplots(figsize=ok.figsize) 
0759         fig.suptitle(title)
0760 
0761         for i, bi in enumerate(bis):
0762             if xrg[bi] is None: continue
0763 
0764             if not cs2in is None: 
0765                 if not bi in cs2in: continue
0766             pass
0767             if not bi in vdom: continue
0768 
0769 
0770             ax = axs if axs.__class__.__name__ == 'AxesSubplot' else axs[i] 
0771             ax.plot( ed[bi], cs2e[bi], label="cs2e : integrated s2 vs e bi:%6.4f  " % bi )
0772 
0773             if not edom is None and not cs2in is None:
0774                 ax.plot( edom[bi], cs2in[bi], label="cs2in : integrated s2 vs e bi:%6.4f  " % bi )
0775             pass
0776 
0777             ax.plot( vdom[bi], cs2slv[bi], label="cs2slv : cumsum of s2 sliver integrals vs e bi:%6.4f  " % bi )
0778 
0779             ylim = ax.get_ylim()
0780             xlim = ax.get_xlim()
0781             ax.plot( xlim, [1., 1.], label="one", linestyle="dotted", color="r" )
0782 
0783             for e in ri[:,0]:
0784                 ax.plot( [e, e], ylim, linestyle="dotted", color="r" )
0785             pass
0786 
0787             ax.legend()
0788             #ax.set_ylim( 0, 1 )   # huh getting > 1 on rhs ?
0789         pass
0790         fig.show()
0791 
0792 
0793     def GetAverageNumberOfPhotons_s2(self, BetaInverse, charge=1, dump=False ):
0794         """
0795         see ana/ckn.py for development of this
0796 
0797         Simplfied Alternative to _s2messy following C++ implementation. 
0798         Allowed regions are identified by s2 being positive avoiding the need for 
0799         separately getting crossings. Instead get the crossings and do the trapezoidal 
0800         numerical integration in one pass, improving simplicity and accuracy.  
0801     
0802         See opticks/examples/Geant4/CerenkovStandalone/G4Cerenkov_modified.cc
0803         """
0804         s2integral = 0.
0805         emin = self.ri[-1, 0]  # start at maximum
0806         emax = self.ri[0, 0]   # start at minimum 
0807 
0808         for j in range(len(self.ri)-1):
0809 
0810             en0_b = self.ri[j,0]
0811             en1_b = self.ri[j+1,0]
0812 
0813             ri0_b = self.ri[j,1]
0814             ri1_b = self.ri[j+1,1]
0815 
0816             en = np.array([en0_b, en1_b ]) 
0817             ri = np.array([ri0_b, ri1_b ]) 
0818 
0819             ct = BetaInverse/ri
0820             s2 = (1.-ct)*(1.+ct) 
0821 
0822             ## The en and s2 start off corresponding to full bin.
0823             ## When s2 crossings happen within the bin the en and s2 are adjusted 
0824             ## for the crossing point so can add edge triangles to body trapezoids.
0825 
0826             if s2[0] <= 0. and s2[1] <= 0.:     # Cerenkov not permissable in this bin 
0827                 en = None                        
0828             elif s2[0] < 0. and s2[1] > 0.:     # Cerenkov can happen at high energy side of bin
0829                 en[0] = (s2[1]*en[0] - s2[0]*en[1])/(s2[1] - s2[0])
0830                 s2[0] = 0.
0831             elif s2[0] >= 0. and s2[1] >= 0.:   # Cerenkov can happen across entire bin 
0832                 pass
0833             elif s2[0] > 0. and s2[1] < 0.:     # Cerenkov can happen at low energy side of bin 
0834                 en[1] = (s2[1]*en[0] - s2[0]*en[1])/(s2[1] - s2[0]) 
0835                 s2[1] = 0. 
0836             else:                               # unhandled situation  
0837                 en = None
0838                 print( " en_0 %10.5f ri_0 %10.5f s2_0 %10.5f  en_1 %10.5f ri_1 %10.5f s2_1 %10.5f " % (en[0], ri[0], s2[0], en[1], ri[1], s2[1] )) 
0839                 assert 0 
0840             pass
0841 
0842             if dump:
0843                 print( " j %2d en_0 %10.5f ri_0 %10.5f s2_0 %10.5f  en_1 %10.5f ri_1 %10.5f s2_1 %10.5f " % (j, en[0], ri[0], s2[0], en[1], ri[1], s2[1] )) 
0844             pass
0845 
0846             if not en is None:
0847                 emin = min(en[0], emin)
0848                 emax = max(en[1], emax)
0849                 s2integral +=  (en[1] - en[0])*(s2[0] + s2[1])*0.5    # tapezoidal integration 
0850             pass
0851         pass
0852         Rfact = 369.81 / 10. #        Geant4 mm=1 cm=10    
0853         NumPhotons = Rfact * charge * charge * s2integral
0854         return NumPhotons, emin, emax 
0855 
0856 
0857     def make_lookup_samples(self, bis):
0858         """
0859         After viewing some rejection sampling youtube vids realise 
0860         that must use s2 (sin^2(th) in order to correspond to the pdf that the 
0861         rejection sampling is using. Doing that is giving a good match (chi2/ndf 1.1-1.2) 
0862         using numerical integration and inversion with 4096 bins.
0863 
0864         So this becomes a real cheap (and constant) way to sample from the Cerenkov energy distrib.  
0865         But the problem is the BetaInverse input.
0866 
0867         Presumably would need to use 2d texture lookup to bake lots of different ICDF 
0868         for different BetaInverse values.
0869 
0870         Although the "cutting" effect of BetaInverse on the CDF is tantalising, 
0871         it seems difficult to use.
0872         """
0873         log.info("make_lookup_samples")
0874         xrg = self.xrg
0875         cs2e = self.cs2e   # dict of cumulative sums across energy domain keyed by BetaInverse
0876         ed = self.ed
0877 
0878 
0879         ## THIS IS THE CRUCIAL INVERSION OF CDF : 
0880         ## FORMING A FUNCTION THAT TAKES RANDOM u(normalized CDF value)  AND BetaInverse 
0881         ## AS INPUT AND RETURNS AN ENERGY SAMPLE
0882         ## IMPLEMENTED VIA LINEAR INTERPOLATION OF THE RELEVANT CDF FOR THE BetaInverse
0883         ## 
0884         look_ = lambda bi,u:np.interp(u, cs2e[bi], ed[bi] )
0885 
0886         l = {}
0887         for bi in bis:
0888             if xrg[bi] is None: continue
0889             u = np.random.rand(1000000)   
0890             l[bi] = look_(bi,u) 
0891         pass
0892         self.l = l 
0893 
0894     def save_lookup_samples(self, bis):
0895         log.info("save_lookup_samples")
0896         xrg = self.xrg
0897         l = self.l
0898         fold = "/tmp/rindex" 
0899         for i,bi in enumerate(bis):
0900             if xrg[bi] is None: continue
0901             path = os.path.join(fold,"en_integrated_lookup_1M_%d.npy" % i ) 
0902             if not os.path.exists(os.path.dirname(path)):
0903                 os.makedirs(os.path.dirname(path))
0904             pass
0905             print("save to %s " % path)
0906             np.save(path, l[bi] ) 
0907         pass
0908 
0909     def make_lookup_samples_plot(self, bis):
0910         xrg = self.xrg
0911         l = self.l
0912         title = "rindex.py : make_lookup_samples_plot"
0913 
0914         fig, axs = plt.subplots(figsize=ok.figsize)
0915         fig.suptitle(title)
0916         for i, bi in enumerate(bis):
0917             if xrg[bi] is None: continue
0918 
0919             ax = axs if axs.__class__.__name__ == 'AxesSubplot' else axs[i] 
0920             hd = np.arange(xrg[bi][0],xrg[bi][1],0.1)
0921             log.info("make_lookup_samples_plot i %3d bi %7.4f  xrg[bi] %s  len(hd) %d " % (i, bi, str(xrg[bi]), len(hd) )) 
0922   
0923             if len(hd) == 0: continue
0924  
0925             h = np.histogram(l[bi], hd )
0926             ax.plot( h[1][:-1], h[0][0:], drawstyle="steps-post", label="bi %6.4f " % bi)  
0927             ax.legend()
0928         pass
0929         fig.show()
0930 
0931 
0932     def load_QCerenkov_s2slv(self):
0933         """
0934         see also ckn.py for comparison with the numPhotons 
0935         """
0936         path = "/tmp/blyth/opticks/QCerenkovTest/test_getS2SliverIntegrals_many.npy"
0937         log.info("load %s " % path )
0938         s2slv = np.load(path) if os.path.exists(path) else None
0939         self.s2slv = s2slv 
0940         globals()["ss"] = s2slv
0941 
0942 if __name__ == '__main__':
0943 
0944     plt.ion()
0945     ok = opticks_main()
0946 
0947     ckr = CKRindex()
0948     #bis = np.array(  [1.5,1.6,1.7] )
0949     #bis = np.array(  [1.6] )
0950     #bis = np.array(  [1.457] )   ## with one crossing, need to form a range with one side or the other depending on rindex at edges
0951 
0952 
0953     #bis = np.linspace(1.,ckr.nMin,10)
0954     bis = np.linspace(ckr.nMin,ckr.nMax,10)
0955     sbis = bis[6:7]
0956     #sbis = bis[-1:]
0957 
0958 
0959 
0960    
0961 
0962 
0963 
0964 if 0:
0965     ckr.find_energy_range(bis)
0966     #ckr.find_energy_range_plot(bis)
0967 
0968     ckr.s2_cumsum(bis)
0969     ckr.s2_cumsum_plot(bis)
0970 
0971 if 0:
0972     ckr.make_lookup_samples(bis)
0973     #ckr.save_lookup_samples(bis)        # allows chi2 comparison using ana/wavelength_cfplot.py 
0974     ckr.make_lookup_samples_plot(bis)
0975 
0976     #ckr.s2_integrate(bis)
0977     #ckr.s2_integrate_plot(bis)
0978 
0979 
0980     ckr.s2sliver_integrate(bis)
0981     ckr.s2sliver_integrate_plot(bis)
0982 
0983     ckr.comparison_plot(sbis)
0984 
0985 
0986     ckr.load_QCerenkov_s2slv()
0987 
0988