File indexing completed on 2026-04-09 07:48:50
0001
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
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 = {}
0180
0181 for bi in bis:
0182 xri[bi] = find_cross(ri, BetaInverse=bi)
0183
0184 lhs = ri[0,1] - bi > 0
0185 rhs = ri[-1,1] - bi > 0
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
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
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
0207 else:
0208 xrg[bi] = None
0209 pass
0210 pass
0211
0212
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
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
0239
0240
0241
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
0253
0254
0255
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 = {}
0275 s2e = {}
0276 cs2e = {}
0277
0278 ri = self.ri
0279
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)
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]
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
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.:
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.:
0347 ret = (en_1 - en_0)*(s2_0 + s2_1)*0.5
0348 elif s2_0 > 0. and s2_1 <= 0.:
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
0389
0390
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:
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:
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
0432 s2j = np.zeros( nj, dtype=edom.dtype )
0433
0434
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
0447
0448
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)
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]
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
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
0612 pass
0613 contain += 1
0614 elif b1 == b0 + 1:
0615
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
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]
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
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
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]
0806 emax = self.ri[0, 0]
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
0823
0824
0825
0826 if s2[0] <= 0. and s2[1] <= 0.:
0827 en = None
0828 elif s2[0] < 0. and s2[1] > 0.:
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.:
0832 pass
0833 elif s2[0] > 0. and s2[1] < 0.:
0834 en[1] = (s2[1]*en[0] - s2[0]*en[1])/(s2[1] - s2[0])
0835 s2[1] = 0.
0836 else:
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
0850 pass
0851 pass
0852 Rfact = 369.81 / 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
0876 ed = self.ed
0877
0878
0879
0880
0881
0882
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
0949
0950
0951
0952
0953
0954 bis = np.linspace(ckr.nMin,ckr.nMax,10)
0955 sbis = bis[6:7]
0956
0957
0958
0959
0960
0961
0962
0963
0964 if 0:
0965 ckr.find_energy_range(bis)
0966
0967
0968 ckr.s2_cumsum(bis)
0969 ckr.s2_cumsum_plot(bis)
0970
0971 if 0:
0972 ckr.make_lookup_samples(bis)
0973
0974 ckr.make_lookup_samples_plot(bis)
0975
0976
0977
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