2@file tracksimulation.py
5@date 2025-06-28T15:45:26+09:00
6@brief simple tracking simulrator with difution parameter of drift electron
8import matplotlib.pyplot
as plt
9import matplotlib.cm
as cm
11from matplotlib.path
import Path
12import matplotlib.colors
as mcolors
19 @brief simple track simulation class
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: [])
38 self.mapping = {
'x': 0,
'y': 1,
'z': 2}
39 self.beaminfo =
"136Xe@100[MeV/u] in D2gas@40[kPa]"
46 self.uniform_width = 0
47 self.mc_track_pram = []
49 def set_beaminfo(self, beaminfo):
51 @brief set beam information
54 self.beaminfo = beaminfo
56 def set_dedx(self, dedx):
58 @brief set stoping power
63 def set_gain(self, gain):
70 def set_padarray(self, data):
72 @brief set pad array object
77 def genarate_track(self, start_point, e_angle, a_angle, z_range, points):
79 @brief generate tracking parameter
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
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)])
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])
98 self.track_point = line_points
101 self, plane='xz',track_flag='track', map_values=[],
106 @brief generate tracking parameter
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
116 index1 = self.padsinfo.mapping.get(plane[0], 0)
117 index2 = self.padsinfo.mapping.get(plane[1], 0)
119 fig=plt.figure(figsize=(6, 5))
120 ax = fig.add_subplot(111)
121 fig.patch.set_alpha(0.)
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)
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)
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')
145 plt.xlabel(str(plane[0])+
" position [mm]")
146 plt.ylabel(str(plane[1])+
" position [mm]")
149 if png_save_path
is not None:
150 plt.savefig(png_save_path)
152 def value_to_color(self, value):
154 @brief plot utilities
155 @detail calculate color from counts
159 @return mcolors.to_hex(rgba_color)
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)
168 def get_value_null_distribution(self, value):
170 @brief tracking utilities
178 def get_value_gaus_distribution(self, value, sigma):
180 @brief genarate random value with gaussian function
182 @param value refference value
183 @param sigma standard deviation
185 @return np.random.normal(value, sigma)
187 return np.random.normal(value, sigma)
189 def get_value_uniform_distribution(self, value, width):
191 @brief genarate random value with uniform function
193 @param value refference value
194 @param width width of uniform function
196 @return np.random.uniform(value, sigma)
198 return np.random.uniform(value-width,value+width)
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' ):
202 @brief genarate random value with uniform function
203 @detail execute monte carlo simulation. after executing, added track parameter to `self.mc_track_pram`
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')
212 self.mc_track_pram = []
214 xyzea_ditribution_list = xyzea_ditribution.split(
',')
215 xyzea_parameter_list = [float(x)
for x
in xyzea_parameter.split(
',')]
218 for j
in range(nmax):
219 track_parameters_new = []
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]) )
229 self.mc_track_pram.append(track_parameters_new)
231 def genarate_ionized_electrons(self, start_point, e_angle, a_angle, z_range, points=100):
233 @brief genarate electrons and ions
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
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)])
247 t_max = z_range / vector[2]
248 L = np.linalg.norm( t_max * vector )
250 num_of_electron = int(self.dedx*10e6 * L / self.W)
251 self.track_electron_points_factor = int( num_of_electron / points )
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])
256 self.track_electron_points = electron_points
258 def calclate_difused_point(self, plane='xz', gain=1, sigma=1):
260 @brief genarate electrons and ions
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
268 index1 = self.mapping.get(plane[0], 0)
269 index2 = self.mapping.get(plane[1], 0)
273 for i
in range(len(self.track_electron_points)):
275 center = [self.track_electron_points[i][index1], self.track_electron_points[i][index2]]
276 cov_matrix = [[sigma, 0], [0, sigma]]
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])
287 self.track_difused_points = np.array(random_points)
289 def calclate_pad_electrons(self):
291 @brief Determine whether the generated electron reaches the each pad.
296 for i
in range(len(self.padsinfo.charges)):
297 self.padsinfo.charges[i]=0
299 for i
in range(len(self.padsinfo.pads)):
301 for vertex
in self.padsinfo.pads[i]:
302 projected_polygon.append([vertex[0], vertex[2]])
303 xzpads.append(projected_polygon)
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
313 def calclate_pad_charge(self, difusion_gain=1):
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
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
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):
328 @brief initialize track simulator
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
343 @return simulator object (TrackSimulator)
346 simulator.set_padarray(pad)
348 x_values = [point[0]
for point
in simulator.padsinfo.centers]
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]
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]]
358 simulator.set_gain(gain)
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)
366 simulator.show_track(
'xz',
'difused', simulator.padsinfo.charges, png_save_path, flag)
370def chk_mc_prm(simulator, flag=True, png_save_path=None):
372 @brief check generated parameter
374 @param png_save_path output file path
375 @param flag save flag
377 @return simulator object (TrackSimulator)
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 ]
386 fig=plt.figure(figsize=(10, 10))
387 fig.patch.set_alpha(0.)
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]")
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]")
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]")
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]")
411 if png_save_path
is not None:
412 plt.savefig(png_save_path)
414def simulate_pad_charge(simulator, gain=60, difusion_gain=20, difusion=0.5):
416 @brief simulate position and charge
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)
423 @return x positions, charges ([],[])
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]
433 simulator.set_gain(gain)
435 for i
in range(len(simulator.mc_track_pram)):
438 prm = simulator.mc_track_pram[i]
439 pos = [prm[0], prm[1], prm[2]]
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)
446 for j
in range(len(simulator.padsinfo.charges)):
447 icharge.append(simulator.padsinfo.charges[j])
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)))
452 charge.append(icharge)
456def calculate_pad_charge_threshold(centers, charge, threshold=0.08):
458 @brief remove data using threshold
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
464 @return x positions, charges ([],[])
469 for i
in range(len(charge)):
472 for j
in range(len(charge[i])):
473 if charge[i][j] > threshold:
474 icharge_thre.append(charge[i][j])
476 icharge_thre.append(0)
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)
482 return xpos_thre, charge_thre
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
491 @brief calculate x position using removed charge list
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
505 xpos_thre, charge_thre = calculate_pad_charge_threshold(centers, charge, threshold)
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}")
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]")
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]")
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]")
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]")
531 ax = fig.add_subplot(325)
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]")
538 ax = fig.add_subplot(326)
540 for i
in range(len(charge)):
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")
551 if png_save_path
is not None:
552 plt.savefig(png_save_path)
554def execute_simulataion():
556 @brief method for script to execute mc simulation
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
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()
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
596 if pad_type ==
"beamtpc":
597 padobj = btpad.get_trail_beamtpc_array(pad_version)
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
605 pad_array = simulator.padsinfo.centers
608 chk_mc_prm(simulator, flag=
False)
611 xpos, charge = simulate_pad_charge(simulator, gain=gem_gain, difusion_gain=difusion_gain, difusion=difusion_value)
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
620 print(
'trial pad type dose not exist')
simple track simulation class