File indexing completed on 2026-04-09 07:48:49
0001
0002 """
0003 piecewise.py
0004 ==============
0005
0006 ::
0007
0008 ipython -i piecewise.py
0009
0010
0011 This succeeds to create a piecewise defined rindex using sympy
0012 and symbolically obtains Cerenkov s2 from that,
0013 using fixed BetaInverse = 1
0014
0015 However, attempts to integrate that fail.
0016
0017 Hmm seems that sympy doesnt like a mix of symbolic and floating point,
0018 see cumtrapz.py for attempt to use scipy.integrate.cumtrapz to
0019 check my s2 integrals.
0020
0021
0022 https://stackoverflow.com/questions/43852159/wrong-result-when-integrating-piecewise-function-in-sympy
0023
0024
0025 Programming for Computations, Hans Petter Langtangen
0026 ------------------------------------------------------
0027
0028 http://hplgit.github.io
0029
0030 http://hplgit.github.io/prog4comp/doc/pub/p4c-sphinx-Python/._pylight004.html
0031
0032 http://hplgit.github.io/prog4comp/doc/pub/p4c-sphinx-Python/index.html
0033
0034
0035 Solvers seem not to work with sympy, so role simple bisection solver ?
0036 -------------------------------------------------------------------------
0037
0038 https://personal.math.ubc.ca/~pwalls/math-python/roots-optimization/bisection/
0039
0040
0041
0042 QCerenkov : Whacky Parabolic Ideas
0043 -------------------------------------
0044
0045 * https://en.wikipedia.org/wiki/Simpson%27s_rule
0046 * https://en.wikipedia.org/wiki/Adaptive_Simpson%27s_method
0047
0048 Looks like storing the mid-bin value of the cumulative integral
0049 would be sufficient to give you the parabola. Allowing in principal
0050 to recover the equation of the parabola from 3 points and giving
0051 energy lookup.
0052
0053 * y = a x^2 + b x + c
0054
0055 * 3 points -> 3 equations in 3 unknowns (a,b,c) : matrix inversion gives you (a,b,c)
0056
0057
0058 2
0059 .
0060 .
0061 1
0062 .
0063 0
0064
0065
0066 Obtaining piecewise parabolic cumulative integral by storing
0067 (a,b,c) parameters of the parabola ?
0068 Which would avoid the need for loadsa bins.
0069
0070 * TODO: explore this by ana/piecewise.py sympy expts handling
0071 each bin separately to avoid symbolic integration troubles
0072 and construct the symbolic cumulative integral
0073
0074 * hmm: better to solve once and store (a,b,c) for each bin
0075 * then can do energy lookup by solving quadratic, it will be monotonic
0076 so no problem of picking the parabolic piece applicable and then
0077 picking the root ?
0078
0079
0080
0081
0082
0083
0084
0085
0086
0087 """
0088 import logging, os
0089 log = logging.getLogger(__name__)
0090 import numpy as np
0091 import matplotlib.pyplot as plt
0092
0093 from opticks.ana.edges import divide_bins
0094
0095 from sympy.plotting import plot
0096 from sympy import Piecewise, piecewise_fold, Symbol, Interval, integrate, Max, Min
0097 from sympy import lambdify, ccode
0098 from sympy.utilities.codegen import codegen
0099
0100 from sympy.abc import x, y
0101 from sympy.solvers import solve
0102
0103 e = Symbol('e', positive=True)
0104
0105
0106
0107 from opticks.ana.bisect import bisect
0108
0109
0110 def ri_load():
0111 ri_approx = np.array([
0112 [ 1.55 , 1.478],
0113 [ 1.795, 1.48 ],
0114 [ 2.105, 1.484],
0115 [ 2.271, 1.486],
0116 [ 2.551, 1.492],
0117 [ 2.845, 1.496],
0118 [ 3.064, 1.499],
0119 [ 4.133, 1.526],
0120 [ 6.2 , 1.619],
0121 [ 6.526, 1.618],
0122 [ 6.889, 1.527],
0123 [ 7.294, 1.554],
0124 [ 7.75 , 1.793],
0125 [ 8.267, 1.783],
0126 [ 8.857, 1.664],
0127 [ 9.538, 1.554],
0128 [10.33 , 1.454],
0129 [15.5 , 1.454]
0130 ])
0131
0132 ri_path = os.path.expandvars("$OPTICKS_KEYDIR/GScintillatorLib/LS_ori/RINDEX.npy")
0133 if os.path.exists(ri_path):
0134 log.info("load from %s " % ri_path)
0135 ri = np.load(ri_path)
0136 ri[:,0] *= 1e6
0137 else:
0138 log.fatal("default to approximate ri")
0139 ri = ri_approx
0140 pass
0141 return ri
0142 pass
0143
0144 ri = ri_load()
0145
0146
0147 def make_piece(i, b=1.5):
0148 """
0149 Max(,0.) complicates the integrals obtained,
0150 but is a great simplification in that do not need to manually control
0151 the range to skip regions where s2 dips negative
0152
0153 Note that with b=1.5 the the last piece from make_piece(16)
0154 reduces to zero because v0 = v1 which are both -ve
0155 and sympy succeeds to simplify to zero.
0156
0157 Curiously make_piece(0) does not similarly reduce to zero
0158 although it could do.
0159 """
0160 assert i >= 0
0161 assert i <= len(ri)-1
0162
0163
0164 e0, r0 = ri[i]
0165 e1, r1 = ri[i+1]
0166
0167 v0 = ( 1 - b/r0 ) * ( 1 + b/r0 )
0168 v1 = ( 1 - b/r1 ) * ( 1 + b/r1 )
0169
0170 fr = (e-e0)/(e1-e0)
0171 pt = ( Max(v0*(1-fr) + v1*fr,0), (e > e0) & (e < e1) )
0172 ot = (0, True )
0173
0174 pw = Piecewise( pt, ot )
0175 return pw
0176
0177
0178
0179 def get_check(BetaInverse=1.55):
0180 ck = np.zeros_like(ri)
0181 ck[:,0] = ri[:,0]
0182 ck[:,1] = (1. - BetaInverse/ri[:,1])*(1.+BetaInverse/ri[:,1])
0183 return ck
0184
0185
0186 class S2Integral(object):
0187 """
0188 Attemps to treat BetaInverse symbolically rather than as a constant cause integration to fail
0189 or just not happen ... kinda makes sense the BetaInverse has a drastic effect on the s2
0190 so seems that must subs it before integrating. Or for maximum simplicity just handle as float constant.
0191
0192 BUT: the integrals are just parabolas and the BetaInverse just changes the coefficients
0193 of the piecewise linerar s2 so with patience could write down the piecewise integral function
0194 with the BetaInverse still symbolic. The problem is the Max(_,0.)
0195
0196 * https://math.stackexchange.com/questions/3714720/if-f-and-g-are-both-continuous-then-the-functions-max-f-g-and-min
0197
0198 ::
0199 a + b + | a - b | a + |a|
0200 max( a, b ) = -------------------- max( a, 0 ) = ----------
0201 2 2
0202
0203 a + b - | a - b | a - |a|
0204 min( a, b ) = -------------------- min( a, 0 ) = ------------
0205 2 2
0206
0207
0208 max( a, b ) + min( a, b) = a + b max(a, 0) + min(a, 0) = a
0209
0210
0211
0212 """
0213 FINE_STRUCTURE_OVER_HBARC_EVMM = 36.981
0214
0215 def __init__(self, BetaInverse=1.55, symbolic_add=False):
0216 self.b = BetaInverse
0217
0218 s2 = {}
0219 is2 = {}
0220
0221 emn = ri[0,0]
0222 emx = ri[-1,0]
0223 emd = (emn+emx)/2.
0224
0225 log.info("s2, is2...")
0226 for i in range( len(ri)-1 ):
0227 s2[i] = make_piece( i, b=BetaInverse )
0228 is2[i] = integrate( s2[i], e )
0229 print("s2[%d]" % i, s2[i])
0230 print("is2[%d]" % i, is2[i])
0231 pass
0232 pass
0233
0234 if symbolic_add:
0235 log.info("[ is2a...")
0236 is2a = sum(is2.values())
0237 log.info("] is2a...")
0238
0239 norm = is2a.subs(e, emx)
0240 is2n = is2a/norm
0241 pass
0242 log.info("lambdify")
0243 g = lambdify(e, is2n, "numpy")
0244 vg = np.vectorize( g )
0245
0246 else:
0247 is2a = None
0248 is2n = None
0249 norm = None
0250 g = None
0251 vg = None
0252 pass
0253
0254 qwns = "s2 is2 is2a norm is2n emn emx emd g vg".split()
0255 for qwn in qwns:
0256 setattr(self, qwn, locals()[qwn])
0257 pass
0258
0259 def is2c(self, ee):
0260 return self.vg(ee)
0261
0262 def cliff_plot(self):
0263 is2 = self.is2
0264 emn = self.emn
0265 emx = self.emx
0266
0267 plot( *is2.values(), (e, emn, emx), show=True, adaptive=False, nb_of_points=500)
0268
0269 def s2_plot(self):
0270 is2n = self.is2n
0271 emn = self.emn
0272 emx = self.emx
0273
0274 plot(is2n[BetaInverse], (e, emn, emx), show=True )
0275
0276 def energy_lookup(self, u=0.5):
0277 is2n = self.is2n
0278 emn = self.emn
0279 emx = self.emx
0280
0281 fn = lambda x:is2n.subs(e, x) - u
0282 en = bisect( fn, emn, emx, 20 )
0283 return en
0284
0285 def is2a_(self, en):
0286 """
0287 multiplying by a float constant slows sympy down so do scaling here
0288 """
0289 is2a = self.is2a
0290 Rfact = self.FINE_STRUCTURE_OVER_HBARC_EVMM
0291 return Rfact*is2a.subs(e, en)
0292
0293 def is2sum_(self, en):
0294 """
0295 try numerical adding instead of symbolic adding
0296 """
0297 is2 = self.is2
0298 Rfact = self.FINE_STRUCTURE_OVER_HBARC_EVMM
0299 tot = 0.
0300 for i in range( len(ri)-1 ):
0301 tot += Rfact*is2[i].subs(e, en)
0302 pass
0303 return tot
0304
0305
0306 def export_as_c_code(self, name="s2integral", base="/tmp"):
0307 """
0308 # see ~/np/tests/s2integralTest.cc for testing the generated code
0309 """
0310 is2n = self.is2n
0311 expr = is2n
0312 cg = codegen( (name, expr), "C99", name, header=True )
0313 open(os.path.join(base, cg[0][0]), "w").write(cg[0][1])
0314 open(os.path.join(base, cg[1][0]), "w").write(cg[1][1])
0315
0316
0317 def plot_is2c(self):
0318 emn = self.emn
0319 emx = self.emx
0320 ee = np.linspace( emn, emx, 200 )
0321 ie = self.is2c(ee)
0322 BetaInverse = self.b
0323
0324 title = "ana/piecewise.py : plot_is2c : BetaInverse %7.4f " % BetaInverse
0325
0326 fig, ax = plt.subplots(figsize=[12.8, 7.2])
0327 fig.suptitle(title)
0328 ax.plot( ee, ie, label="plot_is2c" )
0329 ax.legend()
0330 fig.show()
0331
0332 @classmethod
0333 def Scan(cls, tt, mul):
0334 """
0335 see QUDARap/tests/QCerenkovTest.cc .py for comparison with numerical integral
0336 """
0337 ee = divide_bins(ri[:,0], mul)
0338 pass
0339 sc = np.zeros( (len(tt), len(ee), 2) )
0340 bis = np.zeros( len(tt) )
0341
0342 for i, k in enumerate(sorted(tt)):
0343 t = tt[k]
0344 BetaInverse = t.b
0345 bis[i] = BetaInverse
0346 for j,en in enumerate(ee):
0347 is2sum_val = t.is2sum_(en)
0348 sc[i, j] = en, is2sum_val
0349 print("BetaInverse %10.5f en %10.5f is2sum_val : %10.5f " % (BetaInverse, en, is2sum_val) )
0350 pass
0351 pass
0352 fold = "/tmp/ana/piecewise"
0353 if not os.path.exists(fold):
0354 os.makedirs(fold)
0355 pass
0356 log.info("save scan to %s " % fold )
0357 np.save(os.path.join(fold, "scan.npy"), sc)
0358 np.save(os.path.join(fold, "bis.npy"), bis)
0359 return sc
0360
0361
0362 if __name__ == '__main__':
0363 logging.basicConfig(level=logging.INFO)
0364 plt.ion()
0365
0366 bis_ = "1 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.792"
0367
0368 mul = 10
0369
0370 tt = {}
0371 bis = list(map(float, bis_.split()))
0372 for BetaInverse in bis:
0373 tt[BetaInverse] = S2Integral(BetaInverse=BetaInverse)
0374 pass
0375
0376
0377
0378
0379
0380
0381
0382 S2Integral.Scan(tt, mul=mul)
0383
0384