def __init__(self): kd = keydir(os.environ["OPTICKS_KEY"]) ri = np.load(os.path.join(kd, "GScintillatorLib/LS_ori/RINDEX.npy")) ri[:,0] *= 1e6 ri_ = lambda e:np.interp(e, ri[:,0], ri[:,1] ) self.ri = ri self.ri_ = ri_ self.s2x = np.repeat( np.nan, len(self.ri) )
def __init__(self): kd = keydir(os.environ["OPTICKS_KEY"]) ri = np.load(os.path.join(kd, "GScintillatorLib/LS_ori/RINDEX.npy")) ri[:, 0] *= 1e6 ri_ = lambda e: np.interp(e, ri[:, 0], ri[:, 1]) nMax = ri[:, 1].max() nMin = ri[:, 1].min() self.ri = ri self.ri_ = ri_ self.nMax = nMax self.nMin = nMin
def scint_wavelength(self): """ See:: ana/wavelength.py ana/wavelength_cfplot.py """ w0 = np.load(os.path.join(self.FOLD, "wavelength_scint_hd20.npy")) path1 = "/tmp/G4OpticksAnaMgr/wavelength.npy" w1 = np.load(path1) if os.path.exists(path1) else None kd = keydir(os.environ["OPTICKS_KEY"]) aa = np.load(os.path.join(kd, "GScintillatorLib/GScintillatorLib.npy")) a = aa[0, :, 0] b = np.linspace(0, 1, len(a)) u = np.random.rand(1000000) w2 = np.interp(u, b, a) #bins = np.arange(80, 800, 4) bins = np.arange(300, 600, 4) h0 = np.histogram(w0, bins) h1 = np.histogram(w1, bins) h2 = np.histogram(w2, bins) fig, ax = plt.subplots() ax.plot(bins[:-1], h0[0], drawstyle="steps-post", label="OK.QCtxTest") ax.plot(bins[:-1], h1[0], drawstyle="steps-post", label="G4") ax.plot(bins[:-1], h2[0], drawstyle="steps-post", label="OK.GScint.interp") ylim = ax.get_ylim() for w in [320, 340, 360, 380, 400, 420, 440, 460, 480, 500, 520, 540]: ax.plot([w, w], ylim) pass ax.legend() plt.show() self.w0 = w0 self.w1 = w1 self.w2 = w2
# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # import sys, os, numpy as np, logging, argparse log = logging.getLogger(__name__) tx_load = lambda _: list(map(str.strip, open(_).readlines()) ) # py3 needs the list, otherwise stays as map from opticks.ana.key import keydir KEYDIR = keydir() class BLib(object): @classmethod def parse_args(cls, doc, **kwa): np.set_printoptions(suppress=True, precision=3) parser = argparse.ArgumentParser(doc) #parser.add_argument( "path", nargs="?", help="Geocache directory", default=kwa.get("path",None) ) parser.add_argument("--level", default="info", help="logging level") parser.add_argument("-b", "--brief", action="store_true", default=False) parser.add_argument("-n", "--names",
:: ipython -i tests/reemissionTest.py """ import os, sys, logging, numpy as np import matplotlib.pyplot as plt from opticks.ana.nload import np_load from opticks.ana.key import keydir log = logging.getLogger(__name__) if __name__ == '__main__': ok = os.environ["OPTICKS_KEY"] kd = keydir(ok) aa = np_load(os.path.join(kd, "GScintillatorLib/GScintillatorLib.npy")) fc = np_load(os.path.join(kd, "GScintillatorLib/LS/FASTCOMPONENT.npy")) sc = np_load(os.path.join(kd, "GScintillatorLib/LS/SLOWCOMPONENT.npy")) print("aa:%s" % str(aa.shape)) print("fc:%s" % str(fc.shape)) print("sc:%s" % str(sc.shape)) assert aa.shape == (1, 4096, 1) assert np.all(fc == sc) path = os.path.expandvars("$TMP/optixrap/reemissionTest/out.npy") w = np.load(path) plt.ion()
print(str(p)) continue #print(sh) for pa in sh.patches(): ax.add_patch(pa) if not art3d is None: art3d.pathpatch_2d_to_3d(pa, z=0, zdir="y") pass pass pass if __name__ == '__main__': logging.basicConfig(level=logging.INFO) kd = keydir() log.info(kd) assert os.path.exists(kd), kd os.environ[ "IDPATH"] = kd ## TODO: avoid having to do this, due to prim internals mm0 = Geom2d(kd, ridx=0) import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D import mpl_toolkits.mplot3d.art3d as art3d plt.ion() fig = plt.figure(figsize=(6, 5.5)) ax = fig.add_subplot(111, projection='3d')
self.prim.shape), repr(self.part.shape), repr(self.tran.shape)) ]) if __name__ == '__main__': logging.basicConfig(level=logging.INFO) ddir = "/usr/local/opticks/geocache/DayaBay_VGDX_20140414-1300/g4_00.dae/96ff965744a2f6b78c24e33c80d3a4cd/103/GPartsAnalytic/5" dir_ = sys.argv[1] if len(sys.argv) > 1 else ddir sli_ = sys.argv[2] if len(sys.argv) > 2 else "0:10" sli = slice(*map(int, sli_.split(":"))) if dir_ == ddir: log.warning("using hardcoded dir") pass from opticks.ana.key import keydir kd = keydir(os.environ["OPTICKS_KEY"]) d = Dir(dir_, kd) print("Dir(dir_)", d) pp = d.prims print("dump sliced prims from the dir slice %s " % repr(sli)) for p in pp[sli]: print(p) pass #print(d.tran)
class CK(object): FIGPATH = "/tmp/ck/ck_rejection_sampling.png" PATH = "/tmp/ck/ck_%d.npy" kd = keydir() rindex_path = os.path.join(kd, "GScintillatorLib/LS_ori/RINDEX.npy") #random_path = os.path.expandvars("/tmp/$USER/opticks/TRngBufTest_0.npy") random_path = "/tmp/QCtxTest/rng_sequence_f_ni1000000_nj16_nk16_tranche100000" def init_random(self, num): rnd, paths = np_load(self.random_path) if len(paths) == 0: log.fatal( "failed to find any precooked randoms, create them with : TEST=F QSimTest" ) assert 0 pass if num is None: num = len(rnd) else: enough = num <= len(rnd) if not enough: log.fatal("not enough precooked randoms len(rnd) %d num %d " % (len(rnd), num)) pass assert enough pass cursors = np.zeros(num, dtype=np.int32) self.cursors = cursors self.rnd_paths = paths self.rnd = rnd self.num = num def init_rindex(self, BetaInverse): rindex = np.load(self.rindex_path) rindex[:, 0] *= 1e6 # into eV rindex_ = lambda ev: np.interp(ev, rindex[:, 0], rindex[:, 1]) Pmin = rindex[0, 0] Pmax = rindex[-1, 0] nMax = rindex[:, 1].max() maxCos = BetaInverse / nMax maxSin2 = (1.0 - maxCos) * (1.0 + maxCos) smry = "nMax %6.4f BetaInverse %6.4f maxCos %6.4f maxSin2 %6.4f" % ( nMax, BetaInverse, maxCos, maxSin2) print(smry) self.BetaInverse = BetaInverse self.maxCos = maxCos self.maxCosi = 1. - maxCos self.maxSin2 = maxSin2 self.rindex = rindex self.Pmin = Pmin self.Pmax = Pmax self.nMax = nMax self.rindex_ = rindex_ self.p = np.zeros((num, 4, 4), dtype=np.float64) def __init__(self, num=None, BetaInverse=1.5, random=True): self.init_rindex(BetaInverse) if random: self.init_random(num) pass def energy_sample_all(self, method="mxs2"): for idx in range(self.num): self.energy_sample(idx, method=method) if idx % 1000 == 0: print(" idx %d num %d " % (idx, self.num)) pass pass def stepfraction_sample_all(self): for idx in range(self.num): self.stepfraction_sample(idx) if idx % 1000 == 0: print(" idx %d num %d " % (idx, self.num)) pass pass def stepfraction_sample(self, idx): """ What is the expectation for the stepfraction pdf ? A linear scaling proportionate to the numbers of photons at each end. G4double NumberOfPhotons, N; do { rand = G4UniformRand(); NumberOfPhotons = MeanNumberOfPhotons1 - rand * (MeanNumberOfPhotons1-MeanNumberOfPhotons2); N = G4UniformRand() * std::max(MeanNumberOfPhotons1,MeanNumberOfPhotons2); // Loop checking, 07-Aug-2015, Vladimir Ivanchenko } while (N > NumberOfPhotons); N = M1 - u (M1 - M2) = M1 + u (M2 - M1) """ MeanNumberOfPhotons = np.array([1000., 1.]) self.MeanNumberOfPhotons = MeanNumberOfPhotons rnd = self.rnd cursors = self.cursors uu = rnd[idx].ravel() loop = 0 NumberOfPhotons = 0. N = 0. stepfraction = 0. while True: loop += 1 u0 = uu[cursors[idx]] cursors[idx] += 1 stepfraction = u0 NumberOfPhotons = MeanNumberOfPhotons[0] - stepfraction * ( MeanNumberOfPhotons[0] - MeanNumberOfPhotons[1]) # stepfraction=0 -> MeanNumberOfPhotons[0] # stepfraction=1 -> MeanNumberOfPhotons[1] u1 = uu[cursors[idx]] cursors[idx] += 1 N = u1 * MeanNumberOfPhotons.max() # why is this range not from .min to .max ? # because the sampled number can and will be less that the mean # in some places reject = N > NumberOfPhotons if not reject: break pass pass p = self.p[idx] i = self.p[idx].view(np.uint64) p[1, 0] = stepfraction p[1, 1] = NumberOfPhotons p[1, 2] = N p[2, 0] = u0 p[2, 1] = u1 i[3, 1] = loop def stepfraction_sample_globals(self): self.globals("p", self.p, "stepfraction", self.p[:, 1, 0], "NumberOfPhotons", self.p[:, 1, 1], "N", self.p[:, 1, 2], "u0", self.p[:, 2, 0], "u1", self.p[:, 2, 1], "loop", self.p.view(np.uint64)[:, 3, 1]) def stepfraction_plot(self): self.stepfraction_sample_globals() fdom = np.linspace(0, 1, 100) frg = np.array([0, 1]) nrg = self.MeanNumberOfPhotons xf_ = lambda f: np.interp(f, frg, nrg) self.xf_ = xf_ h_stepfraction = np.histogram(stepfraction) h_loop = np.histogram(loop, np.arange(loop.max() + 1)) title = "ana/ck.py:stepfraction_plot : sampling stepfraction between extremes MeanNumberOfPhotons : %s " % repr( self.MeanNumberOfPhotons) fig, axs = plt.subplots(2, 3, figsize=ok.figsize) plt.suptitle(title) ax = axs[0, 0] ax.scatter(u0, u1, label="(u0,u1)", s=0.1) ax.legend() ax = axs[0, 1] ax.scatter(NumberOfPhotons, N, label="(NumberOfPhotons,N)", s=0.1) ax.legend() ax = axs[0, 2] h = h_stepfraction ax.plot(h[1][:-1], h[0], label="h_stepfraction", drawstyle="steps-post") scale = h[0][0] / xf_(0) ax.plot(fdom, scale * xf_(fdom), label="xf_ scaled to hist") ax.legend() ax = axs[1, 0] h = h_loop ax.plot(h[1][:-1], h[0], label="h_loop", drawstyle="steps-post") ax.legend() ax = axs[1, 1] ax.plot(fdom, xf_(fdom), label="xf_") ax.legend() fig.show() def energy_sample(self, idx, method="mxs2"): """ Why the small difference between s2 when sampling and "expectation-interpolating" in energy regions far from achoring points ? The difference is also visible in ct but less clearly. Comparising directly the sampled rs and rindex its difficult to see any difference. When sampling the energy is a random value taked from a flat energy distribution and interpolated individually to give the refractive index. When "expectation-interpolating" the energy domain is an abstract analytic ideal sort of like a "sample" taken from an infinity of possible values. """ rnd = self.rnd rindex = self.rindex rindex_ = self.rindex_ cursors = self.cursors num = self.num Pmin = self.Pmin Pmax = self.Pmax nMax = self.nMax BetaInverse = self.BetaInverse maxSin2 = self.maxSin2 maxCosi = self.maxCosi uu = rnd[idx].ravel() dump = idx < 10 or idx > num - 10 loop = 0 while True: u0 = uu[cursors[idx]] cursors[idx] += 1 u1 = uu[cursors[idx]] cursors[idx] += 1 sampledEnergy = Pmin + u0 * (Pmax - Pmin) sampledRI = rindex_(sampledEnergy) cosTheta = BetaInverse / sampledRI sin2Theta = (1. - cosTheta) * (1. + cosTheta) if method == "mxs2": u1_maxSin2 = u1 * maxSin2 keep_sampling = u1_maxSin2 > sin2Theta elif method == "mxct": ## CANNOT DO THIS : MUST USE THE "CONTROLLING" S2 PDF u1_maxCosi = u1 * maxCosi keep_sampling = u1_maxCosi > 1. - cosTheta else: assert 0 pass loop += 1 if dump: fmt = "method %s idx %5d u0 %10.5f sampledEnergy %10.5f sampledRI %10.5f cosTheta %10.5f sin2Theta %10.5f u1 %10.5f" vals = (method, idx, u0, sampledEnergy, sampledRI, cosTheta, sin2Theta, u1) print(fmt % vals) pass if not keep_sampling: break pass pass hc_eVnm = 1239.8418754200 # G4: h_Planck*c_light/(eV*nm) sampledWavelength = hc_eVnm / sampledEnergy p = self.p[idx] i = self.p[idx].view(np.uint64) p[0, 0] = sampledEnergy p[0, 1] = sampledWavelength p[0, 2] = sampledRI p[0, 3] = cosTheta p[1, 0] = sin2Theta p[2, 0] = u0 p[2, 1] = u1 i[3, 1] = loop def globals(self, *args): assert len(args) % 2 == 0 for i in range(len(args) // 2): k = args[2 * i + 0] v = args[2 * i + 1] print("%10s : %s " % (k, str(v.shape))) globals()[k] = v pass def energy_sample_globals(self): p = self.p u0 = p[:, 2, 0] u1 = p[:, 2, 1] en = p[:, 0, 0] wl = p[:, 0, 1] rs = p[:, 0, 2] ct = p[:, 0, 3] s2 = p[:, 1, 0] self.globals("p", p, "u0", u0, "u1", u1, "en", en, "ct", ct, "s2", s2, "rs", rs) def save(self): path = self.PATH % self.num fold = os.path.dirname(path) if not os.path.exists(fold): os.makedirs(fold) pass log.info("save to %s " % path) np.save(path, self.p) @classmethod def Load(cls, num): path = cls.PATH % num return np.load(path)
class CKN(object): """ Reproduces the G4Cerenkov Frank-Tamm integration to give average number of photons for a BetaInverse and RINDEX profile. """ FOLD = os.path.expandvars("/tmp/$USER/opticks/ana/ckn") kd = keydir() rindex_path = os.path.join(kd, "GScintillatorLib/LS_ori/RINDEX.npy") def __init__(self): ri = np.load(self.rindex_path) ri[:, 0] *= 1e6 # into eV ri_ = lambda ev: np.interp(ev, ri[:, 0], ri[:, 1]) self.ri = ri self.ri_ = ri_ self.BuildThePhysicsTable() self.BuildThePhysicsTable_2() assert np.allclose(self.cai, self.cai2) self.paths = [] def BuildThePhysicsTable(self, dump=False): """ See G4Cerenkov_modified::BuildThePhysicsTable This is applying the composite trapezoidal rule to do a numerical energy integral of n^(-2) = 1./(ri[:,1]*ri[:,1]) """ ri = self.ri en = ri[:, 0] ir2 = 1. / (ri[:, 1] * ri[:, 1]) mir2 = 0.5 * (ir2[1:] + ir2[:-1]) de = en[1:] - en[:-1] assert len(mir2) == len(ri) - 1 # averaging points looses one value mir2_de = mir2 * de cai = np.zeros(len(ri)) # leading zero regains one value np.cumsum(mir2_de, out=cai[1:]) if dump: print("cai", cai) pass self.cai = cai self.ir2 = ir2 self.mir2 = mir2 self.de = de self.mir2_de = mir2_de def BuildThePhysicsTable_2(self, dump=False): """ np.trapz does the same thing as above : applying composite trapezoidal integration https://numpy.org/doc/stable/reference/generated/numpy.trapz.html """ ri = self.ri en = ri[:, 0] ir2 = 1. / (ri[:, 1] * ri[:, 1]) cai2 = np.zeros(len(ri)) for i in range(len(ri)): cai2[i] = np.trapz(ir2[:i + 1], en[:i + 1]) pass self.cai2 = cai2 if dump: print("cai2", cai2) pass @classmethod def BuildThePhysicsTable_s2i(cls, ri, BetaInverse, dump=False): """ No need for a physics table when do integral directly on s2 """ en = ri[:, 0] s2i = np.zeros(len(ri)) for i in range(len(ri)): s2i[i] = np.trapz(s2[:i + 1], en[:i + 1]) pass return s2i def GetAverageNumberOfPhotons_s2(self, BetaInverse, charge=1, dump=False, s2cross=False): """ Simplfied Alternative to _s2messy following C++ implementation. Allowed regions are identified by s2 being positive avoiding the need for separately getting crossings. Instead get the crossings and do the trapezoidal numerical integration in one pass, improving simplicity and accuracy. See opticks/examples/Geant4/CerenkovStandalone/G4Cerenkov_modified.cc """ s2integral = 0. for i in range(len(self.ri) - 1): en = np.array([self.ri[i, 0], self.ri[i + 1, 0]]) ri = np.array([self.ri[i, 1], self.ri[i + 1, 1]]) ct = BetaInverse / ri s2 = (1. - ct) * (1. + ct) en_0, en_1 = en ri_0, ri_1 = ri s2_0, s2_1 = s2 if s2_0 * s2_1 < 0.: en_cross_A = (s2_1 * en_0 - s2_0 * en_1) / (s2_1 - s2_0) en_cross_B = en_0 + (BetaInverse - ri_0) * (en_1 - en_0) / (ri_1 - ri_0) en_cross = en_cross_A if s2cross else en_cross_B else: en_cross = np.nan pass if s2_0 <= 0. and s2_1 <= 0.: pass elif s2_0 < 0. and s2_1 > 0.: s2integral += (en_1 - en_cross) * s2_1 * 0.5 elif s2_0 >= 0. and s2_1 >= 0.: s2integral += (en_1 - en_0) * (s2_0 + s2_1) * 0.5 elif s2_0 > 0. and s2_1 < 0.: s2integral += (en_cross - en_0) * s2_0 * 0.5 else: 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)) assert 0 pass pass Rfact = 369.81 / 10. # Geant4 mm=1 cm=10 NumPhotons = Rfact * charge * charge * s2integral return NumPhotons def GetAverageNumberOfPhotons_s2messy(self, BetaInverse, charge=1, dump=False): """ NB see GetAverageNumberOfPhotons_s2 it gives exactly the same results as this and is simpler Alternate approach doing the numerical integration directly of s2 rather than doing it on n^-2 and combining it later with the integral of the 1 and the BetaInverse*BetaInverse Doing the integral of s2 avoids inconsistencies in the numerical approximations which prevents the average number of photons going negative in the region when the BetaInverse "sea level" rises to almost engulf the last rindex peak.:: BetaInverse*BetaInverse Integral ( s2 ) = Integral( 1 - --------------------------- ) = Integral ( 1 - c2 ) ri*ri """ self.BetaInverse = BetaInverse ckn = self ri = ckn.ri en = ri[:, 0] s2 = np.zeros((len(ri), 2), dtype=np.float64) ct = BetaInverse / ri[:, 1] s2[:, 0] = ri[:, 0] s2[:, 1] = (1. - ct) * (1. + ct) cross = ckn.FindCrossings(s2, 0.) s2integral = 0. for i in range(len(cross) // 2): en0 = cross[2 * i + 0] en1 = cross[2 * i + 1] # select bins within the range s2_sel = s2[np.logical_and(s2[:, 0] >= en0, s2[:, 0] <= en1)] # fabricate partial bins before and after the full ones # that correspond to s2 zeros fs2 = np.zeros((2 + len(s2_sel), 2), dtype=np.float64) fs2[0] = [en0, 0.] fs2[1:-1] = s2_sel fs2[-1] = [en1, 0.] s2integral += np.trapz(fs2[:, 1], fs2[:, 0]) # trapezoidal integration pass Rfact = 369.81 # (eV * cm)^-1 Rfact *= 0.1 # cm to mm ? Geant4: mm = 1. cm = 10. NumPhotons = Rfact * charge * charge * s2integral self.NumPhotons = NumPhotons if dump: print(" s2integral %10.4f " % (s2integral)) pass return NumPhotons def GetAverageNumberOfPhotons_asis(self, BetaInverse, charge=1, dump=False): """ This duplicates the results from G4Cerenkov_modified::GetAverageNumberOfPhotons including negative numbers of photons for BetaInverse close to the rindex peak. Frank-Tamm formula gives number of Cerenkov photons per mm as an energy:: BetaInverse^2 N_photon = 370. Integral ( 1 - ----------------- ) dE ri(E)^2 Where the integration is over regions where : ri(E) > BetaInverse which corresponds to a real cone angle and the above bracket being positive:: BetaInverse cos th = -------------- < 1 ri(E) The bracket above is in fact : 1 - cos^2 th = sin^2 th which must be +ve so getting -ve numbers of photons is clearly a bug from the numerical approximations being made. Presumably the problem is due to the splitting of the integral into CerenkovAngleIntegral "cai" which is the cumulative integral of 1./ri(E)^2 followed by linear interpolation of this in order to get the integral between crossings. G4Cerenkov:: Rfact = 369.81/(eV * cm); https://www.nevis.columbia.edu/~haas/frank_epe_course/cherenkov.ps has 370(eV.cm)^-1 ~/opticks_refs/nevis_cherenkov.ps hc = 1240 eV nm = 1240 eV cm * 1e-7 ( nm:1e-9 cm 1e-2) In [8]: 2*np.pi*1e7/(137*1240) # fine-structure-constant 1/137 and hc = 1240 eV nm Out[8]: 369.860213514221 alpha/hc = 370 (eV.cm)^-1 See ~/opticks/examples/UseGeant4/UseGeant4.cc UseGeant4::physical_constants:: UseGeant4::physical_constants eV 1e-06 cm 10 fine_structure_const 0.00729735 one_over_fine_structure_const 137.036 fine_structure_const_over_hbarc*(eV*cm) 369.81021 fine_structure_const_over_hbarc 36981020.84589 Rfact = 369.81/(eV * cm) 36981000.00000[as used by G4Cerenkov::GetAverageNumberOfPhotons] 2*pi*1e7/(1240*137) 369.86021 eplus 1.00000 electron_mass_c2 0.51099891 proton_mass_c2 938.27201300 neutron_mass_c2 939.56536000 Crossing points from similar triangles:: x - prevPM currentPM - prevPM ------------------------------ = ------------------------ BetaInverse - prevRI currentRI - prevRI x - prevPM = (BetaInverse-prevRI)/(currentRI-prevRI)*(currentPM-prevPM) (currentPM, currentRI) + / / / / / / * / (x,BetaInverse) / / / / + (prevPM, prevRI) """ self.BetaInverse = BetaInverse ri = self.ri cai = self.cai en = ri[:, 0] cross = self.FindCrossings(ri, BetaInverse) if dump: print(" cross %s " % repr(cross)) pass self.cross = cross dp1 = 0. ge1 = 0. for i in range(len(cross) // 2): en0 = cross[2 * i + 0] en1 = cross[2 * i + 1] dp1 += en1 - en0 # interpolating the cai is an approximation that is the probable cause of NumPhotons # going negative for BetaInverse close to the "peak" of rindex cai0 = np.interp(en0, en, cai) cai1 = np.interp(en1, en, cai) ge1 += cai1 - cai0 pass Rfact = 369.81 # (eV * cm)^-1 Rfact *= 0.1 # cm to mm ? NumPhotons = Rfact * charge * charge * ( dp1 - ge1 * BetaInverse * BetaInverse) self.dp1 = dp1 self.ge1 = ge1 self.NumPhotons = NumPhotons if dump: print(" dp1 %10.4f ge1 %10.4f " % (dp1, ge1)) pass return NumPhotons @classmethod def FindCrossings(cls, pq, pv): """ :param pq: property array of shape (n,2) :param pv: scalar value :return cross: array of values where pv crosses the linear interpolated pq """ assert len(pq.shape) == 2 and pq.shape[1] == 2 mx = pq[:, 1].max() mi = pq[:, 1].min() cross = [] if pv <= mi: cross.append(pq[0, 0]) cross.append(pq[-1, 0]) elif pv >= mx: pass else: if pq[0, 1] >= pv: cross.append(pq[0, 0]) pass assert len(pq) > 2 for ii in range(1, len(pq) - 1): prevPM, prevRI = pq[ii - 1] currPM, currRI = pq[ii] down = prevRI >= pv and currRI < pv up = prevRI < pv and currRI >= pv if down or up: cross.append((pv - prevRI) / (currRI - prevRI) * (currPM - prevPM) + prevPM) pass pass if pq[-1, 1] >= pv: cross.append(pq[-1, 1]) pass pass assert len(cross) % 2 == 0, cross return cross def test_GetAverageNumberOfPhotons(self, BetaInverse, dump=False): NumPhotons_asis = self.GetAverageNumberOfPhotons_asis(BetaInverse) NumPhotons_s2 = self.GetAverageNumberOfPhotons_s2(BetaInverse) NumPhotons_s2messy = self.GetAverageNumberOfPhotons_s2messy( BetaInverse) res = np.array( [BetaInverse, NumPhotons_asis, NumPhotons_s2, NumPhotons_s2messy]) if dump: fmt = "BetaInverse %6.4f _asis %6.4f _s2 %6.4f _s2messy %6.4f " print(fmt % tuple(res)) pass return res def scan_GetAverageNumberOfPhotons(self, x0=1., x1=2., nx=1001): """ Creates ckn.scan comparing GetAverageNumberOfPhotons from three algorithms across the domain of BetaInverse. * GetAverageNumberOfPhotons_asis * GetAverageNumberOfPhotons_s2 * GetAverageNumberOfPhotons_s2messy :: In [12]: ckn.scan Out[12]: array([[ 1. , 293.2454, 293.2454, 293.2454], [ 1.001 , 292.7999, 292.7999, 292.7999], [ 1.002 , 292.354 , 292.354 , 292.354 ], ..., [ 1.998 , 0. , 0. , 0. ], [ 1.999 , 0. , 0. , 0. ], [ 2. , 0. , 0. , 0. ]]) In [13]: ckn.scan.shape Out[13]: (1001, 4) """ scan = np.zeros((nx, 4), dtype=np.float64) for i, BetaInverse in enumerate(np.linspace(x0, x1, nx)): NumPhotons_asis = self.GetAverageNumberOfPhotons_asis(BetaInverse) NumPhotons_s2 = self.GetAverageNumberOfPhotons_s2(BetaInverse, s2cross=False) NumPhotons_s2cross = self.GetAverageNumberOfPhotons_s2( BetaInverse, s2cross=True) #NumPhotons_s2messy = self.GetAverageNumberOfPhotons_s2messy(BetaInverse) scan[i] = [ BetaInverse, NumPhotons_asis, NumPhotons_s2, NumPhotons_s2cross ] fmt = " bi %7.3f _asis %7.3f _s2 %7.3f _s2cross %7.3f " if i % 100 == 0: print("%5d : %s " % (i, fmt % tuple(scan[i]))) pass self.scan = scan self.numPhotonASIS_ = lambda bi: np.interp(bi, self.scan[:, 0], self. scan[:, 1]) self.numPhotonS2_ = lambda bi: np.interp(bi, self.scan[:, 0], self. scan[:, 2]) self.nMin = self.ri[:, 1].min() self.nMax = self.ri[:, 1].max() d23 = scan[:, 2] - scan[:, 3] log.info(" d23.max %10.4f d23.min %10.4f " % (d23.max(), d23.min())) def scan_GetAverageNumberOfPhotons_plot(self, bir=None): ckn = self en = ckn.scan[:, 0] bi = [self.nMin, self.nMax] if bir is None else bir numPhotonMax = self.numPhotonS2_(np.linspace( bi[0], bi[1], 101)).max() # max in the BetaInverse range extra = "%6.4f_%6.4f" % (bi[0], bi[1]) titls = [ "ana/ckn.py : scan_GetAverageNumberOfPhotons_plot %s " % extra, "_asis goes slightly negative near rindex peak due to linear interpolation approx on top of trapezoidal integration", "_s2 avoids that by doing the integration in one pass directly on s2 (sin^2 theta) and using s2 zero crossings to improve accuracy" ] title = "\n".join(titls) fig, ax = plt.subplots(figsize=ok.figsize) fig.suptitle(title) ax.set_xlim(*bi) ax.set_ylim(-1., numPhotonMax) ax.scatter(en, ckn.scan[:, 1], label="GetAverageNumberOfPhotons_asis", s=3) ax.plot(en, ckn.scan[:, 1], label="GetAverageNumberOfPhotons_asis") ax.plot(en, ckn.scan[:, 2], label="GetAverageNumberOfPhotons_s2") ax.scatter(en, ckn.scan[:, 2], label="GetAverageNumberOfPhotons_s2", s=3) xlim = ax.get_xlim() ylim = ax.get_ylim() ax.plot(xlim, [0, 0], linestyle="dotted", label="zero") for n in ckn.ri[:, 1]: ax.plot([n, n], ylim) #ax.plot( [n, n], ylim, label="%s" % n ) pass ax.legend() fig.show() self.save_fig(fig, "scan_GetAverageNumberOfPhotons_plot_%s.png" % extra) def save_fig(self, fig, name): path = os.path.join(self.FOLD, name) if not os.path.exists(os.path.dirname(path)): os.makedirs(os.path.dirname(path)) pass log.info("save to %s " % path) fig.savefig(path) assert os.path.exists(path) self.paths.append(path) def scan_GetAverageNumberOfPhotons_difference_plot(self): ckn = self bi = ckn.scan[:, 0] titls = [ "ana/ckn.py : scan_GetAverageNumberOfPhotons_difference_plot : GetAverageNumberOfPhotons_s2 - GetAverageNumberOfPhotons_asis vs BetaInverse ", "refractive index values show by vertical lines", "parabolic nature of the difference because _asis as using a linear approximation for a parabolic integral " ] title = "\n".join(titls) bi_range = [self.nMin, self.nMax] fig, ax = plt.subplots(figsize=ok.figsize) fig.suptitle(title) ax.set_xlim(*bi_range) #ax.set_ylim( -1., numPhotonMax ) ax.scatter(bi, ckn.scan[:, 2] - ckn.scan[:, 1], s=3) ax.plot(bi, ckn.scan[:, 2] - ckn.scan[:, 1]) ylim = ax.get_ylim() for n in ckn.ri[:, 1]: #ax.plot( [n, n], ylim, label="%s" % n ) ax.plot([n, n], ylim) pass ax.legend() fig.show() self.save_fig(fig, "scan_GetAverageNumberOfPhotons_difference_plot.png") def compareOtherScans(self): """ This compares the python and C++ implementations of the same double precision algorithms, agreement should be (and is) very good eg 1e-13 level """ self.compare_with_QCerenkovTest() self.compare_with_G4Cerenkov_modified() def compare_with_QCerenkovTest(self): """ Create/update QCerenkovTest scan with:: qu cd tests ./QCerenkovTest.sh """ qck_path = os.path.expandvars( "/tmp/$USER/opticks/QCerenkovTest/test_GetAverageNumberOfPhotons_s2.npy" ) if os.path.exists(qck_path): self.qck_scan = np.load(qck_path) else: log.error("missing %s " % qck_path) pass ckn = self cf_qck_dom = np.abs(ckn.scan[:, 0] - ckn.qck_scan[:, 0]) cf_qck_num = np.abs(ckn.scan[:, 2] - ckn.qck_scan[:, 1]) log.info("cf_qck_dom.min*1e6 %10.4f cf_qck_dom.max*1e6 %10.4f " % (cf_qck_dom.min() * 1e6, cf_qck_dom.max() * 1e6)) log.info("cf_qck_num.min*1e6 %10.4f cf_qck_num.max*1e6 %10.4f " % (cf_qck_num.min() * 1e6, cf_qck_num.max() * 1e6)) assert cf_qck_dom.max() < 1e-10 assert cf_qck_num.max() < 1e-10 def compare_with_G4Cerenkov_modified(self): """ Create/update scan arrays with:: cks ./G4Cerenkov_modifiedTest.sh """ cks_path = "/tmp/G4Cerenkov_modifiedTest/scan_GetAverageNumberOfPhotons.npy" if os.path.exists(cks_path): self.cks_scan = np.load(cks_path) else: log.error("missing %s " % cks_path) pass ckn = self cf_cks_dom = np.abs(ckn.scan[:, 0] - ckn.cks_scan[:, 0]) cf_cks_num_asis = np.abs(ckn.scan[:, 1] - ckn.cks_scan[:, 1]) cf_cks_num_s2 = np.abs(ckn.scan[:, 2] - ckn.cks_scan[:, 2]) log.info("cf_cks_dom.min*1e6 %10.4f cf_cks_dom.max*1e6 %10.4f " % (cf_cks_dom.min() * 1e6, cf_cks_dom.max() * 1e6)) log.info( "cf_cks_num_asis.min*1e6 %10.4f cf_cks_num_asis.max*1e6 %10.4f " % (cf_cks_num_asis.min() * 1e6, cf_cks_num_asis.max() * 1e6)) log.info("cf_cks_num_s2.min*1e6 %10.4f cf_cks_num_s2.max*1e6 %10.4f " % (cf_cks_num_s2.min() * 1e6, cf_cks_num_s2.max() * 1e6)) assert cf_cks_dom.max() < 1e-10 assert cf_cks_num_asis.max() < 1e-10 assert cf_cks_num_s2.max() < 1e-10 def test_GetAverageNumberOfPhotons_plot(self, BetaInverse=1.7): """ runs test_GetAverageNumberOfPhotons for a particular BetaInverse in order to get the internals : cross, cai """ ckn = self res = ckn.test_GetAverageNumberOfPhotons(BetaInverse) titls = [ "ana/ckn.py : test_GetAverageNumberOfPhotons_plot : %s " % str(res), "attempt to understand how _asis manages to go negative " ] title = "\n".join(titls) cross = ckn.cross ri = ckn.ri cai = ckn.cai fig, axs = plt.subplots(1, 2, figsize=ok.figsize) fig.suptitle(title) ax = axs[0] ax.plot(ri[:, 0], ri[:, 1], label="linear interpolation") ax.scatter(ri[:, 0], ri[:, 1], label="ri") xlim = ax.get_xlim() ylim = ax.get_ylim() ax.plot(xlim, [BetaInverse, BetaInverse], label="BetaInverse:%6.4f" % BetaInverse) for e in cross: ax.plot([e, e], ylim, label="cross") pass ax.legend() ax = axs[1] ax.plot(ri[:, 0], cai, label="cai") ylim = ax.get_ylim() for e in cross: ax.plot([e, e], ylim, label="cross") pass ax.legend() fig.show() self.save_fig(fig, "test_GetAverageNumberOfPhotons_plot.png") def load_QCerenkov_s2slv(self): """ s2slv: sliver integrals across domains of BetaInverse and energy In [4]: ckn.s2slv.shape Out[4]: (1001, 280) cs2slv: is cumulative sum of the above sliver integrals with the same shape In [5]: ckn.cs2slv.shape Out[5]: (1001, 280) s2slv_sum: sum over energy domain of s2slv In [8]: ckn.s2slv_sum.shape Out[8]: (1001,) cs2slv_last: last of the (energy axis) cumulative sum values : this very closely matches s2slv_sum In [9]: ckn.cs2slv_last.shape Out[9]: (1001,) c2slv_norm: energy axis cumsum divided by the last : making this the CDF In [10]: ckn.cs2slv_norm.shape Out[10]: (1001, 280) Notice lots of nan, for CK disallowed BetaInverse """ s2slv_path = "/tmp/blyth/opticks/QCerenkovTest/test_getS2SliverIntegrals_many.npy" log.info("load %s " % s2slv_path) s2slv = np.load(s2slv_path) if os.path.exists(s2slv_path) else None globals()["ss"] = s2slv cs2slv_path = "/tmp/blyth/opticks/QCerenkovTest/test_getS2SliverIntegrals_many_cs2slv.npy" log.info("load %s " % cs2slv_path) cs2slv = np.load(cs2slv_path) if os.path.exists(cs2slv_path) else None cs2slv_last = cs2slv[:, -1] cs2slv_norm_path = "/tmp/blyth/opticks/QCerenkovTest/test_getS2SliverIntegrals_many_cs2slv_norm.npy" log.info("load %s " % cs2slv_norm_path) cs2slv_norm = np.load(cs2slv_norm_path) if os.path.exists( cs2slv_norm_path) else None globals()["cs"] = cs2slv globals()["csl"] = cs2slv_last globals()["csn"] = cs2slv_norm self.s2slv = s2slv self.cs2slv = cs2slv self.cs2slv_last = cs2slv_last self.cs2slv_norm = cs2slv_norm s2slv_sum = s2slv.sum(axis=1) self.s2slv_sum = s2slv_sum sum_vs_last = np.abs(s2slv_sum - cs2slv_last) sum_vs_last_mx = sum_vs_last.max() log.info("sum_vs_last_mx %s " % sum_vs_last_mx) assert sum_vs_last_mx < 1e-10 def load_QCerenkov_s2slv_plot(self): """ """ titls = [ "ana/ckn.py : load_QCerenkov_s2slv_plot : compare sum of s2slv from many sliver bins to the rindex big bin result", "deviation of up to 0.4 of a photon between the sum of sliver integrals and the big bin integrals is as yet unexplained" ] title = "\n".join(titls) fig, ax = plt.subplots(figsize=ok.figsize) fig.suptitle(title) ax.plot(self.qck_scan[:, 0], self.qck_scan[:, 1], label="qck_scan") ax.plot(self.qck_scan[:, 0], self.s2slv_sum, label="s2slv_sum") ax.plot(self.qck_scan[:, 0], self.cs2slv_last, label="cs2slv_last") ax.legend() axr = ax.twinx() axr.plot(self.qck_scan[:, 0], self.s2slv_sum - self.qck_scan[:, 1], label="diff", linestyle="dotted") axr.legend(loc='lower right') fig.show() self.ax = ax
class CKS(object): PATH = "/tmp/cks/cks.npy" kd = keydir() rindex_path = os.path.join(kd, "GScintillatorLib/LS_ori/RINDEX.npy") random_path = os.path.expandvars("/tmp/$USER/opticks/TRngBufTest_0.npy") def __init__(self): rnd = np.load(self.random_path) num = len(rnd) cursors = np.zeros(num, dtype=np.int32) self.rnd = rnd self.num = num rindex = np.load(self.rindex_path) rindex[:, 0] *= 1e6 # into eV rindex_ = lambda ev: np.interp(ev, rindex[:, 0], rindex[:, 1]) Pmin = rindex[0, 0] Pmax = rindex[-1, 0] nMax = rindex[:, 1].max() self.rindex = rindex self.Pmin = Pmin self.Pmax = Pmax self.nMax = nMax self.rindex_ = rindex_ self.cursors = cursors self.p = np.zeros((num, 4, 4), dtype=np.float64) def energy_sample_all(self, BetaInverse=1.5): for idx in range(self.num): self.energy_sample(idx, BetaInverse=BetaInverse) pass def energy_sample(self, idx, BetaInverse=1.5): rnd = self.rnd rindex = self.rindex rindex_ = self.rindex_ cursors = self.cursors num = self.num Pmin = self.Pmin Pmax = self.Pmax nMax = self.nMax uu = rnd[idx].ravel() maxCos = BetaInverse / nMax maxSin2 = (1.0 - maxCos) * (1.0 + maxCos) dump = idx < 10 or idx > num - 10 loop = 0 while True: u0 = uu[cursors[idx]] cursors[idx] += 1 u1 = uu[cursors[idx]] cursors[idx] += 1 sampledEnergy = Pmin + u0 * (Pmax - Pmin) sampledRI = rindex_(sampledEnergy) cosTheta = BetaInverse / sampledRI sin2Theta = (1. - cosTheta) * (1. + cosTheta) u1_maxSin2 = u1 * maxSin2 keep_sampling = u1_maxSin2 > sin2Theta loop += 1 if dump: fmt = "idx %5d u0 %10.5f sampledEnergy %10.5f sampledRI %10.5f cosTheta %10.5f sin2Theta %10.5f u1 %10.5f" vals = (idx, u0, sampledEnergy, sampledRI, cosTheta, sin2Theta, u1) print(fmt % vals) pass if not keep_sampling: break pass pass hc_eVnm = 1239.8418754200 # G4: h_Planck*c_light/(eV*nm) sampledWavelength = hc_eVnm / sampledEnergy p = self.p[idx] i = self.p[idx].view(np.uint64) p[0, 0] = sampledEnergy p[0, 1] = sampledWavelength p[0, 2] = sampledRI p[0, 3] = cosTheta p[1, 0] = sin2Theta i[3, 1] = loop def save(self): fold = os.path.dirname(self.PATH) if not os.path.exists(fold): os.makedirs(fold) pass log.info("save to %s " % self.PATH) np.save(self.PATH, self.p) @classmethod def Load(cls): return np.load(cls.PATH)
In [44]: nrpo[nrpo[:,1] == 5] Out[44]: array([[ 3199, 5, 0, 0], [ 3200, 5, 0, 1], [ 3201, 5, 0, 2], ..., [11410, 5, 671, 2], [11411, 5, 671, 3], [11412, 5, 671, 4]], dtype=uint32) """ nidx = np.arange(len(tid), dtype=np.uint32) ridx,pidx,oidx = cls.Decode(tid) nrpo = np.zeros( (len(tid),4), dtype=np.uint32 ) nrpo[:,0] = nidx nrpo[:,1] = ridx nrpo[:,2] = pidx nrpo[:,3] = oidx return nrpo if __name__ == '__main__': import os, numpy as np from opticks.ana.key import keydir avi = np.load(os.path.join(keydir(),"GNodeLib/all_volume_identity.npy")) tid = avi[:,1] nrpo = OpticksIdentity.NRPO(tid)