catm-python-lib
Loading...
Searching...
No Matches
tracksimulation.py
Go to the documentation of this file.
1"""!
2@file tracksimulation.py
3@version 1
4@author Fumitaka ENDO
5@date 2025-06-28T15:45:26+09:00
6@brief simple tracking simulrator with difution parameter of drift electron
7"""
8import matplotlib.pyplot as plt
9import matplotlib.cm as cm
10import numpy as np
11from matplotlib.path import Path
12import matplotlib.colors as mcolors
13import catmlib.simulator.trialpad as btpad
14import argparse
15
16class TrackSimulator():
17 """!
18 @class TrackSimulator
19 @brief simple track simulation class
20 """
21
22 def __init__(self):
23 """!
24 @brief initialze object
25 @param mapping label of axsis (0:x, 1:y, 2:z)
26 @param beaminfo text for debug
27 @param dedx stoppoing power [MeV/mm] (default: 0.0708)
28 @param W electron-ion generation energy [eV] (default: 37 eV [deuteron])
29 @param gain gem gain (default: 100)
30 @param pC charge converstion parameter (default: 1.0e12 [C to pC])
31 @param Qe elementary charge [C] (default: 1.602e-19 [C])
32 @param gaus_sigma difusion parameter (default: 0)
33 @param uniform_width unused paramater (default: 0)
34 @param mc_track_pram list for storing tracking parameters (default: [])
35
36 @return None
37 """
38 self.mapping = {'x': 0, 'y': 1, 'z': 2}
39 self.beaminfo = "136Xe@100[MeV/u] in D2gas@40[kPa]"
40 self.dedx = 0.0708 #MeV/mm
41 self.W = 37 #eV
42 self.gain = 100
43 self.pC = 1.0e12 # C to pC
44 self.Qe = 1.602e-19 #C
45 self.gaus_sigma = 0
46 self.uniform_width = 0
47 self.mc_track_pram = []
48
49 def set_beaminfo(self, beaminfo):
50 """!
51 @brief set beam information
52 @return None
53 """
54 self.beaminfo = beaminfo
55
56 def set_dedx(self, dedx):
57 """!
58 @brief set stoping power
59 @return None
60 """
61 self.dedx = dedx
62
63 def set_gain(self, gain):
64 """!
65 @brief set gem gain
66 @return None
67 """
68 self.gain = gain
69
70 def set_padarray(self, data):
71 """!
72 @brief set pad array object
73 @return None
74 """
75 self.padsinfo = data
76
77 def genarate_track(self, start_point, e_angle, a_angle, z_range, points):
78 """!
79 @brief generate tracking parameter
80
81 @param start_point starting point [x, y, z]
82 @param e_angle elevation angle in degree
83 @param a_angle azimuth angle in degree
84 @param z_range track distance
85 @param points number of dividing point
86
87 @return None
88 """
89
90 theta = 0.0174533 * e_angle
91 phi = 0.0174533 * a_angle
92 vector = np.array([np.sin(theta)*np.cos(phi), np.sin(theta) * np.sin(phi), np.cos(theta)])
93
94 t_max = z_range / vector[2]
95 t_values = np.linspace(0, t_max, points)
96 line_points = np.array([np.array(start_point) + t * vector for t in t_values])
97
98 self.track_point = line_points
99
100 def show_track(
101 self, plane='xz',track_flag='track', map_values=[],
102 png_save_path=None,
103 fig_flag=False
104 ):
105 """!
106 @brief generate tracking parameter
107
108 @param plane starting point [x, y, z]
109 @param track_flag elevation angle in degree
110 @param map_values list of count of reached electorons in each pad
111 @param png_save_path save path
112 @param fig_flag save flag
113
114 @return None
115 """
116 index1 = self.padsinfo.mapping.get(plane[0], 0)
117 index2 = self.padsinfo.mapping.get(plane[1], 0)
118
119 fig=plt.figure(figsize=(6, 5))
120 ax = fig.add_subplot(111)
121 fig.patch.set_alpha(0.)
122
123 if len(map_values) == len(self.padsinfo.pads):
124 for i in range(len(self.padsinfo.pads)):
125 xs = [vertex[index1] for vertex in self.padsinfo.pads[i] ]
126 ys = [vertex[index2] for vertex in self.padsinfo.pads[i] ]
127 color = self.padsinfo.charges[i]/max(self.padsinfo.charges)
128 ax.fill(xs, ys, edgecolor='black', facecolor=str(self.value_to_color(color)), lw=0.5, zorder=1)
129 else:
130 for i in range(len(self.padsinfo.pads)):
131 xs = [vertex[index1] for vertex in self.padsinfo.pads[i] ]
132 ys = [vertex[index2] for vertex in self.padsinfo.pads[i] ]
133 ax.fill(xs, ys, edgecolor='black', facecolor='#d3d3d3', lw=0.5, zorder=1)
134
135
136 if track_flag == 'track':
137 ax.plot(self.track_point[:, index1], self.track_point[:, index2], zorder=2)
138 elif track_flag == 'ionized':
139 ax.plot(self.track_electron_points[:, index1], self.track_electron_points[:, index2],lw=1, zorder=2)
140 elif track_flag == 'difused':
141 ax.plot(self.track_electron_points[:, index1], self.track_electron_points[:, index2], c='#483d8b',lw=1, zorder=4)
142 ax.scatter(self.track_difused_points[:, index1], self.track_difused_points[:, index2],s=2,alpha=0.25, c='#ee82ee', zorder=3)
143 ax.set_aspect('equal')
144
145 plt.xlabel(str(plane[0])+" position [mm]")
146 plt.ylabel(str(plane[1])+" position [mm]")
147 plt.show()
148 if fig_flag:
149 if png_save_path is not None:
150 plt.savefig(png_save_path)
151
152 def value_to_color(self, value):
153 """!
154 @brief plot utilities
155 @detail calculate color from counts
156
157 @param value counts
158
159 @return mcolors.to_hex(rgba_color)
160
161 """
162 if not 0 <= value <= 1:
163 raise ValueError("Value must be between 0 and 1")
164 rgba_color = cm.terrain_r(value)
165 hex_color = mcolors.to_hex(rgba_color)
166 return hex_color
167
168 def get_value_null_distribution(self, value):
169 """!
170 @brief tracking utilities
171
172 @param value
173
174 @return value
175 """
176 return value
177
178 def get_value_gaus_distribution(self, value, sigma):
179 """!
180 @brief genarate random value with gaussian function
181
182 @param value refference value
183 @param sigma standard deviation
184
185 @return np.random.normal(value, sigma)
186 """
187 return np.random.normal(value, sigma)
188
189 def get_value_uniform_distribution(self, value, width):
190 """!
191 @brief genarate random value with uniform function
192
193 @param value refference value
194 @param width width of uniform function
195
196 @return np.random.uniform(value, sigma)
197 """
198 return np.random.uniform(value-width,value+width)
199
200 def monte_carlo_track(self, nmax=10, track_parameters=[25/2,0,-2,0,0], xyzea_ditribution='null,null,null,null,null',xyzea_parameter='0,0,0,0,0' ):
201 """!
202 @brief genarate random value with uniform function
203 @detail execute monte carlo simulation. after executing, added track parameter to `self.mc_track_pram`
204
205 @param nmax number of loop
206 @param track_parameters list of trackin parameters
207 @param xyzea_ditribution list of distributtion type along each axis (default : 'null,null,null,null,null')
208 @param xyzea_parameter list of parameters for each function (default : '0,0,0,0,0')
209
210 @return None
211 """
212 self.mc_track_pram = []
213
214 xyzea_ditribution_list = xyzea_ditribution.split(',')
215 xyzea_parameter_list = [float(x) for x in xyzea_parameter.split(',')]
216
217
218 for j in range(nmax):
219 track_parameters_new = []
220
221 for i in range(len(track_parameters)):
222 if xyzea_ditribution_list[i] == 'null':
223 track_parameters_new.append( self.get_value_null_distribution(track_parameters[i]) )
224 elif xyzea_ditribution_list[i] == 'gaus':
225 track_parameters_new.append( self.get_value_gaus_distribution(track_parameters[i],xyzea_parameter_list[i]) )
226 elif xyzea_ditribution_list[i] == 'uniform':
227 track_parameters_new.append( self.get_value_uniform_distribution(track_parameters[i], xyzea_parameter_list[i]) )
228
229 self.mc_track_pram.append(track_parameters_new)
230
231 def genarate_ionized_electrons(self, start_point, e_angle, a_angle, z_range, points=100):
232 """!
233 @brief genarate electrons and ions
234
235 @param start_point starting point [x, y, z]
236 @param e_angle elevation angle in degree
237 @param a_angle azimuth angle in degree
238 @param z_range track distance
239 @param points number of electron/ion to be generated
240
241 @return None
242 """
243 theta = 0.0174533 * e_angle
244 phi = 0.0174533 * a_angle
245 vector = np.array([np.sin(theta)*np.cos(phi), np.sin(theta) * np.sin(phi), np.cos(theta)])
246
247 t_max = z_range / vector[2]
248 L = np.linalg.norm( t_max * vector )
249
250 num_of_electron = int(self.dedx*10e6 * L / self.W)
251 self.track_electron_points_factor = int( num_of_electron / points )
252
253 e_values = np.linspace(0, t_max, points)
254 electron_points = np.array([np.array(start_point) + t * vector for t in e_values])
255
256 self.track_electron_points = electron_points
257
258 def calclate_difused_point(self, plane='xz', gain=1, sigma=1):
259 """!
260 @brief genarate electrons and ions
261
262 @plane select plane to be smeared
263 @gain difusion gain [note] this parameter is not realistic value based on physical process.
264 @sigma paramter for smearing
265
266 @return None
267 """
268 index1 = self.mapping.get(plane[0], 0)
269 index2 = self.mapping.get(plane[1], 0)
270
271 random_points = []
272
273 for i in range(len(self.track_electron_points)):
274
275 center = [self.track_electron_points[i][index1], self.track_electron_points[i][index2]]
276 cov_matrix = [[sigma, 0], [0, sigma]]
277
278 num_points = gain
279
280 for j in range(num_points):
281 difused_points = np.random.multivariate_normal(center, cov_matrix)
282 xpos = difused_points[0]
283 ypos = self.track_electron_points[i][1]
284 zpos = difused_points[1]
285 random_points.append([xpos, ypos, zpos])
286
287 self.track_difused_points = np.array(random_points)
288
289 def calclate_pad_electrons(self):
290 """!
291 @brief Determine whether the generated electron reaches the each pad.
292 @return None
293 """
294 xzpads=[]
295
296 for i in range(len(self.padsinfo.charges)):
297 self.padsinfo.charges[i]=0
298
299 for i in range(len(self.padsinfo.pads)):
300 projected_polygon=[]
301 for vertex in self.padsinfo.pads[i]:
302 projected_polygon.append([vertex[0], vertex[2]])
303 xzpads.append(projected_polygon)
304
305 for i in range(len(self.track_difused_points)):
306 pointxz = (np.array([self.track_difused_points[i][0], self.track_difused_points[i][2]]))
307 for j in range(len(xzpads)):
308 polygon_path = Path(xzpads[j])
309 is_inside = polygon_path.contains_point(pointxz)
310 if is_inside == True:
311 self.padsinfo.charges[j] += 1
312
313 def calclate_pad_charge(self, difusion_gain=1):
314 """!
315 @brief calculate the total chrage in each pad
316 @param difusion_gain modify the difusion gain, which is used in `calclate_difused_point` method
317 @return None
318 """
319 for i in range(len(self.padsinfo.charges)):
320 self.padsinfo.charges[i] = self.padsinfo.charges[i] *self.track_electron_points_factor * self.gain * self.Qe * self.pC /difusion_gain
321
322def init_track_simulator(
323 pad, num=1, start_y=19.8, deg_theta=0, deg_phi=0,
324 mc_option='gaus,gaus,null,gaus,gaus' ,mc_prm='5,5,5,10,10',
325 gain=60, difusion_gain=20, difusion=0.5,
326 png_save_path=None ,flag=True):
327 """!
328 @brief initialize track simulator
329
330 @param pad pad object to be used
331 @param num maximum number of loop (default = 1)
332 @param start_y (default = 19.8)
333 @param deg_theta (default = 0)
334 @param deg_phi (default = 0)
335 @param mc_option distribution along each axis for smearing electron (default = 'gaus,gaus,null,gaus,gaus')
336 @param mc_prm list of paramters for distributions (default = '5,5,5,10,10')
337 @param gain gem gain (default = 60)
338 @param difusion_gain difusion gain (default = 20)
339 @param difusion difufsion parameter (default = 0.5)
340 @param png_save_path output file path
341 @param flag save flag
342
343 @return simulator object (TrackSimulator)
344 """
345 simulator = TrackSimulator()
346 simulator.set_padarray(pad)
347
348 x_values = [point[0] for point in simulator.padsinfo.centers]
349 z_values = []
350 for i in range(len( simulator.padsinfo.pads)):
351 z_values.append([point[2] for point in simulator.padsinfo.pads[i]])
352 z_values = [item for sublist in z_values for item in sublist]
353
354 simulator.monte_carlo_track(num, [np.mean(x_values), start_y, min(z_values)-1, deg_theta, deg_phi], mc_option ,mc_prm)
355 prm = simulator.mc_track_pram[0]
356 pos = [prm[0],prm[1],prm[2]]
357
358 simulator.set_gain(gain)
359
360 simulator.genarate_ionized_electrons(pos,prm[3],prm[4], max(z_values)-min(z_values)+2 )
361 simulator.calclate_difused_point('xz', difusion_gain, difusion)
362 simulator.calclate_pad_electrons()
363 simulator.calclate_pad_charge(difusion_gain)
364
365 if flag:
366 simulator.show_track('xz', 'difused', simulator.padsinfo.charges, png_save_path, flag)
367
368 return simulator
369
370def chk_mc_prm(simulator, flag=True, png_save_path=None):
371 """!
372 @brief check generated parameter
373
374 @param png_save_path output file path
375 @param flag save flag
376
377 @return simulator object (TrackSimulator)
378 """
379 data = simulator.mc_track_pram
380 x = [prm[0] for prm in data ]
381 y = [prm[1] for prm in data ]
382 z = [prm[2] for prm in data ]
383 e_angle = [prm[3] for prm in data ]
384 a_angle = [prm[4] for prm in data ]
385
386 fig=plt.figure(figsize=(10, 10))
387 fig.patch.set_alpha(0.)
388
389 ax = fig.add_subplot(221)
390 ax.hist2d(x, y, bins=30, cmap='Blues')
391 ax.set_xlabel("x position [mm]")
392 ax.set_ylabel("y position [mm]")
393
394 ax = fig.add_subplot(222)
395 ax.hist2d(y, z, bins=30, cmap='Blues')
396 ax.set_xlabel("y position [mm]")
397 ax.set_ylabel("z position [mm]")
398
399 ax = fig.add_subplot(223)
400 ax.hist2d(z, x, bins=30, cmap='Blues')
401 ax.set_xlabel("z position [mm]")
402 ax.set_ylabel("x position [mm]")
403
404 ax = fig.add_subplot(224)
405 ax.hist2d(e_angle, a_angle, bins=30, cmap='Blues')
406 ax.set_xlabel("theta [deg]")
407 ax.set_ylabel("phi [deg]")
408 plt.show()
409
410 if flag:
411 if png_save_path is not None:
412 plt.savefig(png_save_path)
413
414def simulate_pad_charge(simulator, gain=60, difusion_gain=20, difusion=0.5):
415 """!
416 @brief simulate position and charge
417
418 @param simulator simulator object
419 @param gain gem gain (default = 60)
420 @param difusion_gain difusion gain (default = 20)
421 @param difusion difufsion parameter (default = 0.5)
422
423 @return x positions, charges ([],[])
424 """
425 xpos = []
426 charge = []
427
428 z_values = []
429 for i in range(len( simulator.padsinfo.pads)):
430 z_values.append([point[2] for point in simulator.padsinfo.pads[i]])
431 z_values = [item for sublist in z_values for item in sublist]
432
433 simulator.set_gain(gain)
434
435 for i in range(len(simulator.mc_track_pram)):
436 icharge = []
437
438 prm = simulator.mc_track_pram[i]
439 pos = [prm[0], prm[1], prm[2]]
440
441 simulator.genarate_ionized_electrons(pos, prm[3], prm[4], max(z_values)-min(z_values)+2 )
442 simulator.calclate_difused_point('xz', difusion_gain, difusion)
443 simulator.calclate_pad_electrons()
444 simulator.calclate_pad_charge(difusion_gain)
445
446 for j in range(len(simulator.padsinfo.charges)):
447 icharge.append(simulator.padsinfo.charges[j])
448
449 x_values = [point[0] for point in simulator.padsinfo.centers]
450 xpos.append(sum(np.array(x_values)*np.array(icharge))/sum(np.array(icharge)))
451
452 charge.append(icharge)
453
454 return xpos, charge
455
456def calculate_pad_charge_threshold(centers, charge, threshold=0.08):
457 """!
458 @brief remove data using threshold
459
460 @param centers list of center position for each pad
461 @param charge list of charge for each pad
462 @param threshold threshold value to removing the data
463
464 @return x positions, charges ([],[])
465 """
466 xpos_thre = []
467 charge_thre = []
468
469 for i in range(len(charge)):
470 icharge_thre = []
471
472 for j in range(len(charge[i])):
473 if charge[i][j] > threshold:
474 icharge_thre.append(charge[i][j])
475 else:
476 icharge_thre.append(0)
477
478 x_values = [point[0] for point in centers]
479 xpos_thre.append(sum(np.array(x_values)*np.array(icharge_thre))/sum(np.array(icharge_thre)))
480 charge_thre.append(icharge_thre)
481
482 return xpos_thre, charge_thre
483
484def calculate_xposition_from_charge(
485 centers, xpos, charge, gain=60,
486 difusion_gain=20, difusion=0.5, threshold=0.2,
487 global_threshold_value = 0.2,
488 png_save_path=None, flag=True
489 ):
490 """!
491 @brief calculate x position using removed charge list
492
493 @param centers list of center position for each pad
494 @param charge list of charge for each pad
495 @param xpos list of position
496 @param threshold threshold value to removing the data
497 @param gain gem gain (default = 60)
498 @param difusion_gain difusion gain (default = 20)
499 @param difusion difufsion parameter (default = 0.5)
500 @param global_threshold_value global threshold value to removing the data
501 @param png_save_path output file path
502 @param flag save flag
503 """
504
505 xpos_thre, charge_thre = calculate_pad_charge_threshold(centers, charge, threshold)
506
507 fig=plt.figure(figsize=(16, 12))
508 fig.patch.set_alpha(0.)
509 fig.suptitle(f"Simulation Result @ gain:{gain}, difusion_gain{difusion_gain}:, difusion:{difusion}, threshold:{threshold}")
510
511 ax = fig.add_subplot(321)
512 flattened_charge = [item for sublist in charge for item in sublist]
513 flattened_charge = [item for item in flattened_charge if item != 0]
514 ax.hist(flattened_charge, bins=100, alpha=0.5)
515 ax.set_xlabel("original charge in pad [pC]")
516
517 ax = fig.add_subplot(322)
518 flattened_charge = [item for sublist in charge_thre for item in sublist]
519 flattened_charge = [item for item in flattened_charge if item != 0]
520 ax.hist(flattened_charge, bins=100, alpha=0.5)
521 ax.set_xlabel("cut charge in pad [pC]")
522
523 ax = fig.add_subplot(323)
524 ax.hist(xpos, bins=400, log=True, alpha=0.5)
525 ax.set_xlabel("original x position [mm]")
526
527 ax = fig.add_subplot(324)
528 ax.hist(xpos_thre, bins=400, log=True, alpha=0.5)
529 ax.set_xlabel("simulated x position [mm]")
530
531 ax = fig.add_subplot(325)
532 resolution = []
533 for i in range(len(xpos_thre)):
534 resolution.append( xpos[i] - xpos_thre[i] )
535 ax.hist(resolution, bins=100, alpha=0.5)
536 ax.set_xlabel("position difference [mm]")
537
538 ax = fig.add_subplot(326)
539 multiplicuities=[]
540 for i in range(len(charge)):
541 multiplicuity=0
542 for j in range(len(charge[i])):
543 if charge[i][j] > global_threshold_value:
544 multiplicuity = multiplicuity + 1
545 multiplicuities.append(multiplicuity)
546 ax.hist(multiplicuities, bins=15, range=(-0.5, 14.5), alpha=0.5)
547 ax.set_xlabel("measured pad multiplicity")
548
549 plt.show()
550 if flag:
551 if png_save_path is not None:
552 plt.savefig(png_save_path)
553
554def execute_simulataion():
555 """!
556 @brief method for script to execute mc simulation
557
558 CLI argument:
559 @arg pad-type select trial pad type
560 @arg pad-version select trail pad version
561 @arg nmax select trial pad type
562 @arg gem-gain select trail pad version
563 @arg difusion-gain select trail pad version
564 @arg difusion-value select trail pad version
565 @arg global-threshold-value select trail pad version
566 @arg flag-track-example plot track example
567 @arg flag-mc-parameter plot generated track parameter
568 @arg flag-position-charge plot postion and charge
569 """
570 parser = argparse.ArgumentParser()
571 parser.add_argument("-t","--pad-type", help="select trial pad type", type=str, default="beamtpc")
572 parser.add_argument("-tv","--pad-version", help="select trail pad version", type=int, default=0)
573 parser.add_argument("-nm","--nmax", help="select trial pad type", type=int, default=1)
574 parser.add_argument("-gg","--gem-gain", help="select trail pad version", type=int, default=120)
575 parser.add_argument("-dg","--difusion-gain", help="select trail pad version", type=int, default=20)
576 parser.add_argument("-dv","--difusion-value", help="select trail pad version", type=int, default=0.5)
577 parser.add_argument("-gtv","--global-threshold-value", help="select trail pad version", type=int, default=0.2)
578 parser.add_argument("-fte","--flag-track-example", help="plot track example", action="store_true")
579 parser.add_argument("-fmc","--flag-mc-parameter", help="plot generated track parameter", action="store_true")
580 parser.add_argument("-fpc","--flag-position-charge", help="plot postion and charge", action="store_true")
581 args = parser.parse_args()
582
583 pad_type: str = args.pad_type
584 pad_version: int = args.pad_version
585 n_max: int = args.nmax
586 gem_gain: float = args.gem_gain
587 difusion_gain: float = args.difusion_gain
588 difusion_value: float = args.difusion_value
589 global_threshold_value: float = args.global_threshold_value
590 chk_flag_example: bool = args.flag_track_example
591 chk_flag_mc_prm: bool = args.flag_mc_parameter
592 chk_flag_charge: bool = args.flag_position_charge
593
594 padobj = None
595
596 if pad_type == "beamtpc":
597 padobj = btpad.get_trail_beamtpc_array(pad_version)
598
599 if padobj is not None:
600 simulator = init_track_simulator(
601 padobj, n_max, gain=gem_gain, difusion_gain=difusion_gain,
602 difusion=difusion_value, png_save_path=None, flag=chk_flag_example
603 )
604
605 pad_array = simulator.padsinfo.centers
606
607 if chk_flag_mc_prm:
608 chk_mc_prm(simulator, flag=False)
609
610 if chk_flag_charge:
611 xpos, charge = simulate_pad_charge(simulator, gain=gem_gain, difusion_gain=difusion_gain, difusion=difusion_value)
612
613 calculate_xposition_from_charge(
614 pad_array, xpos, charge,
615 gem_gain, difusion_gain, difusion_value,
616 global_threshold_value, png_save_path=None, flag=False
617 )
618
619 else:
620 print('trial pad type dose not exist')
621
simple track simulation class