Back to home page

EIC code displayed by LXR

 
 

    


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']}  # (1+cos^2 theta)
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       # surround options by quots to be explicit
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       # this avoids issues if momentumMin is None because of previous default
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:  # pylint: disable=W0703
0184       logger.error("parsing gun options:\n%s\nException: %s " % (self, e))
0185       exit(1)