File indexing completed on 2026-09-13 08:23:55
0001 """Helper object for particle gun properties"""
0002
0003 from DDSim.Helper.ConfigHelper import ConfigHelper
0004 from g4units import GeV
0005 from math import atan, exp
0006 import logging
0007 import textwrap
0008
0009 logger = logging.getLogger(__name__)
0010
0011
0012 class Gun(ConfigHelper):
0013 """Configuration for the DDG4 ParticleGun"""
0014
0015 def __init__(self):
0016 super(Gun, self).__init__()
0017 self.particle = "mu-"
0018 self.multiplicity = 1
0019 self._position = (0.0, 0.0, 0.0)
0020 self._isotrop = False
0021 self._direction = (0, 0, 1)
0022
0023 self._phiMin_EXTRA = {'help': "Minimal azimuthal angle for random distribution"}
0024 self.phiMin = None
0025 self._phiMax_EXTRA = {'help': "Maximal azimuthal angle for random distribution"}
0026 self.phiMax = None
0027 self._thetaMin_EXTRA = {'help': "Minimal polar angle for random distribution"}
0028 self.thetaMin = None
0029 self._thetaMax_EXTRA = {'help': "Maximal polar angle for random distribution"}
0030 self.thetaMax = None
0031 self._etaMin_EXTRA = {'help': "Minimal pseudorapidity for random distibution (overrides thetaMax)"}
0032 self.etaMin = None
0033 self._etaMax_EXTRA = {'help': "Maximal pseudorapidity for random distibution (overrides thetaMin)"}
0034 self.etaMax = None
0035 self._momentumMin_EXTRA = {'help': "Minimal momentum when using distribution (default = 0.0)"}
0036 self.momentumMin = 0 * GeV
0037 self._momentumMax_EXTRA = {'help': "Maximal momentum when using distribution (default = 0.0)"}
0038 self.momentumMax = 10 * GeV
0039 self._energy_EXTRA = {'help': "Total energy (including mass) for the particle gun.\n\n"
0040 "If not None, it will overwrite the setting of momentumMin and momentumMax"}
0041 self.energy = None
0042
0043 self._halton_EXTRA = {'help': textwrap.dedent("""\
0044 Use scrambled Halton sequence (RQMC) for particle gun sampling.
0045
0046 Replaces the standard PRNG with a low-discrepancy sequence that gives
0047 superior phase-space coverage. The scrambling shifts are seeded from
0048 the simulation's Geant4Random engine (controlled by --random.seed).
0049
0050 Note: the standard 1/sqrt(N) error estimate assumes independent and
0051 identically distributed (i.i.d.) samples and does NOT apply here.
0052 To estimate statistical errors, run M independent replications with
0053 different random seeds and use the spread across runs.
0054
0055 Incompatible with distribution='ffbar': acceptance-rejection sampling
0056 cannot be driven by a fixed per-particle Halton point.
0057 """)}
0058 self.halton = False
0059 self._haltonOffset_EXTRA = {'help': textwrap.dedent("""\
0060 Starting index in the Halton sequence.
0061
0062 Set to k*N*m for parallel jobs (job k, N events, multiplicity m).
0063 """)}
0064 self.haltonOffset = 0
0065
0066 self._distribution_EXTRA = {'choices': ['uniform', 'cos(theta)',
0067 'eta', 'pseudorapidity',
0068 'ffbar']}
0069 self._distribution = None
0070 self._closeProperties()
0071
0072 @property
0073 def distribution(self):
0074 """choose the distribution of the random direction for theta
0075
0076 Options for random distributions:
0077
0078 'uniform' is the default distribution, flat in theta
0079 'cos(theta)' is flat in cos(theta)
0080 'eta', or 'pseudorapidity' is flat in pseudorapity
0081 'ffbar' is distributed according to 1+cos^2(theta)
0082
0083 Setting a distribution will set isotrop = True
0084 """
0085 return self._distribution
0086
0087 @distribution.setter
0088 def distribution(self, val):
0089 if val is None:
0090 return
0091 possibleDistributions = self._distribution_EXTRA['choices']
0092 if not isinstance(val, str):
0093 raise RuntimeError("malformed input '%s' for gun.distribution. Need a string : %s " %
0094 (val, ",".join(possibleDistributions)))
0095 if val not in possibleDistributions:
0096
0097 stringified = ["'%s'" % _ for _ in possibleDistributions]
0098 raise RuntimeError("Unknown distribution '%s', Use one of: %s " % (val,
0099 ", ".join(stringified)))
0100 self._distribution = val
0101 self._isotrop = True
0102
0103 @property
0104 def isotrop(self):
0105 """ isotropic distribution for the particle gun
0106
0107 use the options phiMin, phiMax, thetaMin, and thetaMax to limit the range of randomly distributed directions
0108 if one of these options is not None the random distribution will be set to True and cannot be turned off!
0109 """
0110 return self._isotrop or bool(self._distribution)
0111
0112 @isotrop.setter
0113 def isotrop(self, val):
0114 """check that value is equivalent to bool"""
0115 try:
0116 self._isotrop = ConfigHelper.makeBool(val)
0117 except RuntimeError:
0118 raise RuntimeError("malformed input '%s' for gun.isotrop " % val)
0119 if val and self.distribution is None:
0120 self.distribution = 'uniform'
0121
0122 @property
0123 def direction(self):
0124 """ direction of the particle gun, 3 vector """
0125 return self._direction
0126
0127 @direction.setter
0128 def direction(self, val):
0129 """ make sure the direction is parseable by boost, i.e. (1.0, 1.0, 1.0) """
0130 self._direction = ConfigHelper.makeTuple(val)
0131 if len(self._direction) != 3:
0132 raise RuntimeError(
0133 " gun.direction: malformed input '%s', needs to be a string representing a three vector " % (val,))
0134
0135 @property
0136 def position(self):
0137 """ position of the particle gun, 3 vector """
0138 return self._position
0139
0140 @position.setter
0141 def position(self, val):
0142 """check that the position is a three vector and can be parsed by ddg4"""
0143 self._position = ConfigHelper.makeTuple(val)
0144 if len(self._position) != 3:
0145 raise RuntimeError(
0146 " gun.position: malformed input '%s', needs to be a string representing a three vector " % (val,))
0147
0148 def setOptions(self, ddg4Gun):
0149 """set the starting properties of the DDG4 particle gun"""
0150 try:
0151 if self.energy:
0152 ddg4Gun.Energy = self.energy
0153 ddg4Gun.particle = self.particle
0154 ddg4Gun.multiplicity = self.multiplicity
0155 ddg4Gun.position = self.position
0156 ddg4Gun.isotrop = self.isotrop
0157 ddg4Gun.direction = self.direction
0158 ddg4Gun.Distribution = self.distribution
0159 if self.thetaMin is not None:
0160 ddg4Gun.ThetaMin = self.thetaMin
0161 ddg4Gun.isotrop = True
0162 if self.thetaMax is not None:
0163 ddg4Gun.ThetaMax = self.thetaMax
0164 ddg4Gun.isotrop = True
0165 if self.phiMin is not None:
0166 ddg4Gun.PhiMin = self.phiMin
0167 ddg4Gun.isotrop = True
0168 if self.phiMax is not None:
0169 ddg4Gun.PhiMax = self.phiMax
0170 ddg4Gun.isotrop = True
0171 if self.etaMin is not None:
0172 ddg4Gun.ThetaMax = 2. * atan(exp(-float(self.etaMin)))
0173 ddg4Gun.isotrop = True
0174 if self.etaMax is not None:
0175 ddg4Gun.ThetaMin = 2. * atan(exp(-float(self.etaMax)))
0176 ddg4Gun.isotrop = True
0177
0178 ddg4Gun.MomentumMin = self.momentumMin if self.momentumMin else 0.0
0179 ddg4Gun.MomentumMax = self.momentumMax
0180 if self.halton:
0181 ddg4Gun.Halton = True
0182 ddg4Gun.HaltonOffset = int(self.haltonOffset)
0183 except Exception as e:
0184 logger.error("parsing gun options:\n%s\nException: %s " % (self, e))
0185 exit(1)