Warning, /epic/bin/g4MaterialScan_to_csv is written in an unsupported language. File is not indexed.
0001 #!/usr/bin/env python3
0002
0003 # SPDX-License-Identifier: LGPL-3.0-or-later
0004 # Copyright (C) 2023 Chao Peng
0005 '''
0006 A script to run a 2D (eta, phi) scan using Geant4 kernel.
0007 Scan results are collected in a CSV format file.
0008 '''
0009
0010 import os
0011 import sys
0012 import errno
0013 import argparse
0014 import pandas as pd
0015 import numpy as np
0016 from io import StringIO
0017 from collections import OrderedDict as odict
0018 from wurlitzer import pipes
0019
0020 import DDG4
0021 import g4units
0022
0023 # pd.set_option('display.max_rows', 1000)
0024 PROGRESS_STEP = 1
0025
0026
0027 # a class to simplify the material scan, avoid the use of multipe instances of it
0028 class g4MaterialScanner:
0029 def __init__(self, compact, phy_list='QGSP_BERT'):
0030 kernel = DDG4.Kernel()
0031 kernel.loadGeometry(compact)
0032 DDG4.Core.setPrintFormat(str("%-32s %6s %s"))
0033 self.kernel = kernel
0034
0035 # use DDG4 Geant4 wrapper to simplify the setup
0036 geant4 = DDG4.Geant4(kernel)
0037 geant4.setupCshUI(ui=None)
0038 for i in geant4.description.detectors():
0039 o = DDG4.DetElement(i.second.ptr())
0040 sd = geant4.description.sensitiveDetector(o.name())
0041 if sd.isValid():
0042 typ = sd.type()
0043 if typ in geant4.sensitive_types:
0044 geant4.setupDetector(o.name(), geant4.sensitive_types[typ])
0045 else:
0046 logger.error('+++ %-32s type:%-12s --> Unknown Sensitive type: %s', o.name(), typ, typ)
0047 sys.exit(errno.EINVAL)
0048 geant4.setupPhysics(phy_list)
0049
0050 # save the gun for micromanagement later
0051 self.gun = geant4.setupGun('Scanner',
0052 Standalone=True,
0053 particle='geantino',
0054 energy=20*g4units.GeV,
0055 position='(0, 0, 0)',
0056 direction='(0, 0, 1)',
0057 multiplicity=1,
0058 isotrop=False)
0059
0060 scan = DDG4.SteppingAction(self.kernel, 'Geant4MaterialScanner/MaterialScan')
0061 self.kernel.steppingAction().adopt(scan)
0062 self.kernel.configure()
0063 self.kernel.initialize()
0064 self.kernel.NumEvents = 1
0065
0066 def __del__(self):
0067 self.kernel.terminate()
0068
0069 def scan(self, position, direction, phy_list='QGSP_BERT'):
0070 self.gun.position = '({},{},{})'.format(*position)
0071 self.gun.direction = '({},{},{})'.format(*direction)
0072 with pipes() as (out, err):
0073 self.kernel.run()
0074 return self.parse_scan_output(out.getvalue())
0075
0076 # A parser function to convert the output from Gean4MaterialScanner to a pandas dataframe
0077 @staticmethod
0078 def parse_scan_output(output):
0079 # find material scan lines
0080 lines = []
0081 add_line = False
0082
0083 for l in output.split('\n'):
0084 if add_line:
0085 lines.append(l.strip())
0086 if l.strip().startswith('MaterialScan'):
0087 add_line = True
0088
0089
0090 # NOTE: the code below depends on the output from DDG4::Geant4MaterialScanner
0091 # it is valid on 09/15/2023
0092 scans = []
0093 first_digit = False
0094 for i, l in enumerate(lines):
0095 line = l.strip('| ')
0096 if not first_digit and not line[:1].isdigit():
0097 continue
0098 first_digit = True
0099 if not line[:1].isdigit():
0100 break
0101 # break coordinates for endpoint, which has a format of (x, y, z)
0102 scans.append(line.strip('| ').translate({ord(i): None for i in '()'}).replace(',', ' '))
0103
0104 cols = [
0105 'material', 'Z', 'A', 'density',
0106 'rad_length', 'int_length', 'thickness', 'path_length',
0107 'int_X0', 'int_lambda', 'end_x', 'end_y', 'end_z'
0108 ]
0109
0110 dft = pd.read_csv(StringIO('\n'.join(scans)), sep='\s+', header=None, index_col=0, names=cols)
0111 print(dft)
0112 return dft.astype({key: np.float64 for key in cols[1:]})
0113
0114
0115 '''
0116 A helper function to convert a string (<min>[:<max>[:<step>]]) to an array
0117 '''
0118 def args_array(arg, step=1, include_end=True):
0119 vals = [float(x.strip()) for x in arg.split(':')]
0120 # empty or only one value
0121 if len(vals) < 2:
0122 return np.array(vals)
0123 # has step input
0124 if len(vals) > 2:
0125 step = vals[2]
0126 # inclusion of the endpoint (max)
0127 if include_end:
0128 vals[1] += step
0129 return np.arange(vals[0], vals[1], step)
0130
0131
0132 if __name__ == '__main__':
0133 parser = argparse.ArgumentParser(
0134 prog='g4MaterialScan_to_csv',
0135 description = 'A python script to scan materials of a DD4Hep geometry in geant4 kernel.'
0136 + '\n ' # 7 spaces for "usage: "
0137 + 'The scanned results are saved in a CSV file.'
0138 )
0139 parser.add_argument(
0140 '-c', '--compact', dest='compact', required=True,
0141 help='Top-level xml file of the detector description.'
0142 )
0143 parser.add_argument(
0144 '-o', '--output', default='g4mat_scan.csv',
0145 help='Path of the output csv file. Support python formatting of args, e.g., g4mat_scan_{eta}_{phi}.csv.'
0146 )
0147 parser.add_argument(
0148 '--start-point', default='0,0,0',
0149 help='Start point of the scan, use the format \"x,y,z\", unit is cm.'
0150 )
0151 parser.add_argument(
0152 '--eta', default='-4.0:4.0:0.1',
0153 help='Eta range, in the format of \"<min>[:<max>[:<step>]]\".'
0154 )
0155 parser.add_argument(
0156 '--eta-values', default=None,
0157 help='a list of eta values, separated by \",\", this option overwrites --eta.'
0158 )
0159 parser.add_argument(
0160 '--phi', default='0:30:1',
0161 help='Phi angle range, in the format of \"<min>[:<max>[:<step>]]\" (degree).'
0162 )
0163 parser.add_argument(
0164 '--phi-values', default=None,
0165 help='a list of phi values, separated by \",\", this option overwrites --phi.'
0166 )
0167 parser.add_argument(
0168 '--mat-buffer-size', type=int, default=50,
0169 help='Maximum number of materials included in the aggregated output.'
0170 )
0171 parser.add_argument(
0172 '--raw-output', action='store_true',
0173 help='Turn on to save the raw outputs from scan.'
0174 )
0175 parser.add_argument(
0176 '--sep', default='\t',
0177 help='Seperator for the CSV file.'
0178 )
0179 args = parser.parse_args()
0180
0181 if not os.path.exists(args.compact):
0182 print('Cannot find compact file {}'.format(args.compact))
0183 exit(-1)
0184
0185 start_point = np.array([float(v.strip()) for v in args.start_point.split(',')])
0186 if args.eta_values is not None:
0187 etas = np.array([float(xval.strip()) for xval in args.eta_values.split(',')])
0188 else:
0189 etas = args_array(args.eta)
0190
0191 if args.phi_values is not None:
0192 phis = np.array([float(xval.strip()) for xval in args.phi_values.split(',')])
0193 else:
0194 phis = args_array(args.phi)
0195 # sanity check
0196 if not len(phis):
0197 print('No phi values from the input {}, aborted!'.format(args.phi))
0198 exit(-1)
0199
0200 eta_phi = []
0201 mats_indices = odict()
0202 # should exist in the scan output as int_{value_type}
0203 value_types = ['X0', 'lambda']
0204 # a data buffer for the X0 values of ((eta, phi), materials, (X0, Lambda))
0205 data = np.zeros(shape=(len(etas)*len(phis), args.mat_buffer_size, len(value_types)))
0206
0207 # scan over eta and phi
0208 scanner = g4MaterialScanner(args.compact)
0209 print('Scanning {:d} eta values in [{:.2f}, {:.2f}] and {:d} phi values in [{:.2f}, {:.2f}] (degree).'\
0210 .format(len(etas), etas[0], etas[-1], len(phis), phis[0], phis[-1]))
0211 for i, eta in enumerate(etas):
0212 for j, phi in enumerate(phis):
0213
0214 nlines = i*len(phis) + j
0215 if nlines % PROGRESS_STEP == 0:
0216 print('Scanned {:d}/{:d} lines.'.format(nlines, len(etas)*len(phis)), end='\r', flush=True)
0217
0218 # scan
0219 direction = (np.cos(phi/180.*np.pi), np.sin(phi/180.*np.pi), np.sinh(eta))
0220 dfa = scanner.scan(start_point, direction)
0221 for vt in value_types:
0222 dfa.loc[:, vt] = dfa['int_{}'.format(vt)].diff(1).fillna(dfa['int_{}'.format(vt)])
0223
0224 if args.raw_output:
0225 dfa.to_csv('scan_raw_eta={:.3f}_phi={:.3f}.csv'.format(eta, phi), sep=args.sep, float_format='%g')
0226 # group by materials
0227 single_scan = dfa.groupby('material')[value_types].sum()
0228 # print(single_scan)
0229 for mat, xvals in single_scan.iterrows():
0230 if mat not in mats_indices:
0231 if len(mats_indices) >= args.mat_buffer_size:
0232 print('Number of materials exceeds MAT_BUFFER_SIZE ({:d}), dropped material {}.'.format(args.mat_buffer_size, mat))
0233 print('Hint: increase the buffer size with --mat-buffer-size.')
0234 continue
0235 mats_indices[mat] = len(mats_indices)
0236 k = mats_indices.get(mat)
0237 data[len(eta_phi), k] = xvals.values
0238 eta_phi.append((eta, phi))
0239 print('Scanned {:d}/{:d} lines.'.format(len(etas)*len(phis), len(etas)*len(phis)))
0240
0241 # save results
0242 result = dict()
0243 for i, vt in enumerate(value_types):
0244 indices = pd.MultiIndex.from_tuples(eta_phi, names=['eta', 'phi'])
0245 result[vt] = pd.DataFrame(columns=mats_indices.keys(), index=indices, data=data[:, :len(mats_indices), i])
0246 out_path = args.output.format(**vars(args))
0247 pd.concat(result, names=['value_type']).to_csv(out_path, sep=args.sep, float_format='%g')
0248 print('Scanned results saved to \"{}\"'.format(out_path))