Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-01-18 09:27:05

0001 import pyhepmc
0002 from pyhepmc.io import WriterAscii
0003 import argparse
0004 
0005 import numpy as np
0006 import pandas as pd
0007 
0008 from datetime import datetime
0009 from operator import itemgetter
0010 import time
0011 import sys
0012 
0013 import psutil
0014 
0015 # =============================================================
0016 class signal_background_merger:
0017     """
0018     Combine signal and up to three background HEPMC files.
0019     
0020     Typical usage:
0021     python3 signal_background_merger --signalFile dis.hepmc --signalFreq 0 \
0022             --bg1File hgas.hepmc --bg1Freq 1852 \
0023             --bg2File egas.hepmc --bg2Freq 1852 \
0024             --bg3File sr.hepmc
0025     """
0026 
0027     # ============================================================================================
0028     def __init__(self):
0029         """ Parse arguments, print banner, open files, initialize rng"""
0030         
0031         self.digestArgs()
0032         self.rng=np.random.default_rng(seed=self.args.rngSeed)
0033         self.banner()
0034 
0035     # ============================================================================================
0036     def digestArgs(self):
0037         """Handle the command line tedium"""
0038         parser = argparse.ArgumentParser(description='Merge signal events with up to three background sources.')
0039         
0040         parser.add_argument('-i','--signalFile', default='pythia_DIS.hepmc',
0041                             help='Name of the HEPMC file with the signal events')
0042         parser.add_argument('-sf','--signalFreq', type=float, default=0.0,
0043                             help='Poisson-mu of the signal frequency in ns. Default is 0 to have exactly one signal event per slice. Set to the estimated DIS rate to randomize.')
0044 
0045     
0046         parser.add_argument('-bg1','--bg1File', default='pythia_PROTON.hepmc',
0047                             help='Name of the first HEPMC file with background events')
0048         parser.add_argument('-bf1','--bg1Freq', type=float, default=31347.0,
0049                             help='Poisson-mu of the first background frequency in ns. Default is the estimated hadron gas rate at 10x100. Set to 0 to use the weights in the corresponding input file.')
0050 
0051         parser.add_argument('-bg2','--bg2File', default='pythia_ELECTRON.hepmc',
0052                             help='Name of the second HEPMC file with background events')
0053         parser.add_argument('-bf2','--bg2Freq', type=float, default=333.0,
0054                             help='Poisson-mu of the second background frequency in ns. Default is the estimated electron  gas rate at 10x100. Set to 0 to use the weights in the corresponding input file.')
0055 
0056         parser.add_argument('-bg3','--bg3File', default='pythia_SR.hepmc',
0057                             help='Name of the third HEPMC file with background events')
0058         parser.add_argument('-bf3','--bg3Freq', type=float, default=0,
0059                             help='Poisson-mu of the third background frequency in ns. Default is 0 to use the weights in the corresponding input file. Set to a value >0 to specify a poisson mu instead.')
0060 
0061 
0062         parser.add_argument('-o','--outputFile', default='',
0063                             help='Specify the output file name. By default it will be auto-generated.')
0064 
0065         parser.add_argument('-w','--intWindow', type=float, default=2000.0,
0066                             help='Length of the integration window in nanoseconds. Default is 2000.')
0067         parser.add_argument('-N','--nSlices', type=int, default=-1,
0068                             help='Number of sampled time slices ("events"). Default is 10. If set to -1, all events in the signal file will be used and background files cycled as needed.')
0069         parser.add_argument('--squashTime', action='store_true',
0070                             help='Integration is performed but no time information is associated to vertices.')
0071 
0072         parser.add_argument('--rngSeed', action='store',type=int, default=None,
0073                             help='Random seed, default is None')
0074         parser.add_argument('--atLeastOne', action='store',type=bool, default=False,
0075                             help='Assert that at least one Background Event will always be admixed')
0076 
0077         parser.add_argument('-v','--verbose', action='store_true',
0078                             help='Display details for every slice.')
0079         self.args = parser.parse_args()
0080 
0081     # ============================================================================================
0082     def banner(self):
0083         """Print a banner."""
0084         print('==================================================================')
0085         print('=== EPIC HEPMC MERGER ===')
0086         print('authors: Benjamen Sterwerf* (bsterwerf@berkeley.edu), Kolja Kauder** (kkauder@bnl.gov), Reynier Cruz-Torres***')
0087         print('* University of California, Berkeley')
0088         print('** Brookhaven National Laboratory')
0089         print('*** formerly Lawrence Berkeley National Laboratory')
0090 
0091         print('\nFor more information, run \npython signal_background_merger.py --help')
0092         print('==================================================================\n')
0093 
0094         freqTerm = str(self.args.signalFreq) + " ns" if self.args.signalFreq> 0 else "(one event per time slice)"
0095         print('Signal events file and frequency:')
0096         print('\t- {} \t {}'.format( self.args.signalFile, freqTerm ))
0097         
0098         print('\nBackground files and their respective frequencies:')
0099         if self.args.bg1File != "" :
0100             freqTerm = str(self.args.bg1Freq) + " ns" if self.args.bg1Freq> 0 else "(from weights)"
0101             print('\t- {} \t {}'.format( self.args.bg1File, freqTerm ))
0102         if self.args.bg2File != "" :
0103             freqTerm = str(self.args.bg2Freq) + " ns" if self.args.bg2Freq> 0 else "(from weights)"
0104             print('\t- {} \t {}'.format( self.args.bg2File, freqTerm ))
0105         if self.args.bg3File != "" :
0106             freqTerm = str(self.args.bg3Freq) + " ns" if self.args.bg3Freq> 0 else "(from weights)"
0107             print('\t- {} \t {}'.format( self.args.bg3File, freqTerm ))
0108 
0109 
0110         print()
0111         print('Integration Window:',self.args.intWindow, ' ns')
0112         print('Number of time slices:', self.args.nSlices if self.args.nSlices>0 else ' (as many as needed to exhaust the signal file)')
0113         if self.args.squashTime:
0114             print('No timing information will be attached to vertices')
0115         print('The RNG is seeded to', self.args.rngSeed)
0116         print('==================================================================\n')
0117 
0118     # ============================================================================================
0119     def merge( self ):
0120         """Main method to steer the merging."""
0121 
0122         t0 = time.time()
0123         # Open signal file
0124         try :
0125             # sigFile=pyhepmc.io.ReaderAscii(self.args.signalFile)
0126             sigFile=pyhepmc.io.open(self.args.signalFile)
0127         except IOError as e:
0128             print ('Opening files failed: %s' % e.strerror)
0129             sys.exit()
0130 
0131         # Open input file readers, group them with the associated frequency,
0132         # then sort them into signal, frequency bg, or weight type bg
0133         self.sigDict=dict()
0134         self.freqDict=dict()
0135         self.weightDict=dict()
0136         self.makeDicts ( self.args.signalFile, self.args.signalFreq, signal=True )
0137         self.makeDicts ( self.args.bg1File, self.args.bg1Freq )
0138         #self.makeDicts ( self.args.bg2File, self.args.bg2Freq )
0139         #self.makeDicts ( self.args.bg3File, self.args.bg3Freq )
0140 
0141         # Open the output file
0142         if self.args.outputFile != "" :
0143             outputFileName = self.args.outputFile
0144         else :
0145             outputFileName = self.nameGen()
0146 
0147         t1 = time.time()        
0148         print('Initiation time:',np.round((t1-t0)/60.,2),'min')
0149                     
0150         # Process and write
0151         nSlices=self.args.nSlices
0152 
0153         print()
0154         print('==================================================================')
0155         print()
0156         i=0
0157         with WriterAscii(outputFileName) as f:
0158             while True :
0159                 if (i==nSlices) :
0160                     print ( "Finished all requested slices." )
0161                     break
0162                 # if  i % 10000 == 0 : print('Working on slice {}'.format( i+1 ))
0163                 if  i % 100 == 0 or self.args.verbose : self.squawk(i)
0164                 hepSlice = self.mergeSlice( i )
0165                 ### Arrgh, GenEvent==None throws an exception
0166                 try : 
0167                     if hepSlice==None : 
0168                         print ( "Exhausted signal source." )
0169                         break
0170                 except TypeError as e:
0171                     pass # just need to suppress the error
0172 
0173                 hepSlice.event_number=i
0174                 f.write_event(hepSlice)
0175                 i=i+1
0176 
0177         t2 = time.time()
0178         self.slicesDone=i
0179         print('Slice loop time:',np.round((t2-t1)/60.,2),'min')
0180         print(' -- ',np.round((t2-t1) / self.slicesDone ,3),'sec / slice')
0181         
0182 
0183         # Clean up, close all input files
0184         File, Freq = self.sigDict[self.args.signalFile]
0185         # File.close()
0186         for fileName in self.freqDict :
0187             File, Freq = self.freqDict[fileName]
0188             File.close()
0189 
0190     
0191     # ============================================================================================
0192     def makeDicts( self, fileName, freq, signal=False ):
0193         """Create background timeline chunks, open input file, sort into frequency or weight type"""
0194         if fileName == "" : return
0195 
0196         try :
0197             # Should use File=pyhepmc.open(fileName) but that doesn't currently work
0198             File=pyhepmc.open(fileName)
0199         except IOError as e:
0200             print ('Opening {} failed: {}', fileName, e.strerror)
0201             sys.exit()
0202             
0203         # For the signal only, we keep frequency even if it's 0
0204         if signal :
0205             self.sigDict[fileName] = [ File, freq ]
0206             return
0207             
0208         if freq<=0 :
0209             # file has its own weights
0210             # In this case, we will need to read in all events
0211             # because we need the distribution to draw from them
0212             print ( "Reading in all events from", fileName )
0213             # remove events with 0 weight - note that this does change avgRate = <weight> (by a little)
0214             container = [[event, event.weight()] for event in File if event.weight() > 0]
0215             # sorting may help for lookup, sampling
0216             container = sorted ( container, key=itemgetter(1) )            
0217             File.close()
0218             events  = np.array([ item[0] for item in container ])
0219             weights = np.array([ item[1] for item in container ])
0220             avgRate = np.average (weights) # I _think_ this is the total flux
0221             avgRate *= 1e-9 # convert to 1/ns == GHz
0222             print( "Average rate is", avgRate, "GHz")
0223             # np.Generator.choice expects normalized probabilities
0224             probs = weights / weights.sum()
0225             self.weightDict[fileName]=[ events, probs, avgRate ]
0226             return
0227 
0228         self.freqDict[fileName] = [ File, freq ]
0229 
0230         return
0231 
0232     # ============================================================================================
0233     def mergeSlice(self, i):
0234         """Arrange the composition of an individual time slice"""
0235         
0236         hepSlice = pyhepmc.GenEvent(pyhepmc.Units.GEV, pyhepmc.Units.MM)
0237         
0238         # Signal and frequency background are handled very similarly,
0239         # handle them in one method and just use a flag for what little is special
0240         try :
0241             hepSlice = self.addFreqEvents( self.args.signalFile, hepSlice, True )
0242         except EOFError :
0243             return
0244         
0245         # Treat frequency backgrounds very similarly 
0246         for fileName in self.freqDict :
0247             hepSlice = self.addFreqEvents( fileName, hepSlice )
0248 
0249         for fileName in self.weightDict :
0250             hepSlice = self.addWeightedEvents( fileName, hepSlice )
0251 
0252         return hepSlice
0253 
0254     # ============================================================================================
0255     def addFreqEvents( self, fileName, hepSlice, signal=False ):
0256         """Handles the signal as well as frequency-style backgrounds"""
0257 
0258         if signal :
0259             File, Freq = self.sigDict[fileName]
0260         else :
0261             File, Freq = self.freqDict[fileName]
0262 
0263 
0264         c_light  = 299.792458 # speed of light = 299.792458 mm/ns to get mm
0265         squash = self.args.squashTime
0266 
0267         # First, create a timeline
0268         intTime=self.args.intWindow
0269         # this could be offset by iSlice * intTime for continuous time throughout the file
0270 
0271         # Signals can be different
0272         if Freq==0:
0273             if not signal :
0274                 print( "frequency can't be 0 for background files" )
0275                 sys.exit()
0276             # exactly one signal event, at an arbigtrary point
0277             slice = np.array([ self.rng.uniform(low=0, high=intTime) ])
0278         else:
0279             # Generate poisson-distributed times to place events
0280             slice = self.poissonTimes( Freq, intTime )
0281 
0282         if self.args.verbose : print ( "Placing",slice.size,"events from", fileName )
0283         if slice.size == 0 : return hepSlice
0284 
0285         # Insert events at all specified locations
0286         for Time in slice :
0287             ### Arrgh, GenEvent==None throws an exception
0288             inevt = File.read()
0289             try : 
0290                 if inevt==None :
0291                     if signal :
0292                         # Exhausted signal events
0293                         raise EOFError
0294                     else :
0295                         # background file reached its end, reset to the start
0296                         print("Cycling back to the start of ", fileName )
0297                         File.close()
0298                         # File=pyhepmc.io.ReaderAscii(fileName)
0299                         File=pyhepmc.io.open(fileName)
0300                         # also update the dictionary
0301                         self.freqDict[fileName] = [File, Freq]
0302                         inevt = File.read()
0303             except TypeError as e:
0304                 pass # just need to suppress the error
0305 
0306             # Unit conversion
0307             TimeHepmc = c_light*Time
0308 
0309             particles, vertices = [], []
0310             # Stores the vertices of the event inside a vertex container. These vertices are in increasing order so we can index them with [abs(vertex_id)-1]
0311             for vertex in inevt.vertices:
0312                 position=vertex.position
0313                 if not squash:
0314                     position=position+pyhepmc.FourVector(x=0,y=0,z=0,t=TimeHepmc)
0315                 v1=pyhepmc.GenVertex(position)
0316                 vertices.append(v1)
0317             
0318             # copies the particles and attaches them to their corresponding vertices
0319             for particle in inevt.particles:
0320                 # no copy/clone operator...
0321                 momentum, status, pid = particle.momentum, particle.status, particle.pid                
0322                 p1 = pyhepmc.GenParticle(momentum=momentum, pid=pid, status=status)
0323                 p1.generated_mass = particle.generated_mass
0324                 particles.append(p1)
0325                 hepSlice.add_particle(p1)
0326                 
0327                 # since the beam particles do not have a production vertex they cannot be attached to a production vertex
0328                 if particle.production_vertex.id < 0:
0329                     production_vertex=particle.production_vertex.id
0330                     vertices[abs(production_vertex)-1].add_particle_out(p1)
0331                     
0332                 
0333                     
0334                 # Adds particles with an end vertex to their end vertices
0335                 if particle.end_vertex:
0336                     end_vertex = particle.end_vertex.id
0337                     vertices[abs(end_vertex)-1].add_particle_in(p1)
0338 
0339             # Adds the vertices with the attached particles to the event
0340             
0341                 
0342 
0343             for vertex in vertices:
0344                 hepSlice.add_vertex(vertex)
0345             
0346             
0347 
0348         return hepSlice
0349         
0350     # ============================================================================================
0351     def addWeightedEvents( self, fileName, hepSlice, signal=False ):
0352         """Handles weighted backgrounds"""
0353 
0354         events, probs, avgRate = self.weightDict[fileName]
0355 
0356         c_light  = 299.792458 # speed of light = 299.792458 mm/ns to get mm
0357         squash  = self.args.squashTime
0358         intTime = self.args.intWindow
0359         squash = self.args.squashTime
0360 
0361         # How many events? Assume Poisson distribution
0362         while True:
0363             nEvents = int( self.rng.exponential( intTime * avgRate ) )
0364             if nEvents > len(events) :
0365                 print("WARNING: Trying to place",nEvents,"events from", fileName,
0366                       "but the file doesn't have enough. Rerolling, but this is not physical.")
0367                 continue
0368             break
0369         if self.args.verbose : print ( "Placing",nEvents,"events from", fileName )
0370 
0371         # Get events 
0372         toPlace = self.rng.choice( a=events, size=nEvents, p=probs, replace=False )
0373 
0374         # Place at random times
0375         if not squash :
0376             times = list(self.rng.uniform(low=0, high=intTime, size=nEvents))        
0377 
0378         for inevt in toPlace :
0379             Time = 0
0380             if not squash :
0381                 Time = times.pop()
0382 
0383             # print ( " -- Placing at", time)
0384             
0385             particles, vertices = [], []
0386             # Stores the vertices of the event inside a vertex container. These vertices are in increasing order so we can index them with [abs(vertex_id)-1]
0387             for vertex in inevt.vertices:
0388                 position=vertex.position
0389                 if not squash :
0390                     # Unit conversion
0391                     TimeHepmc = c_light*Time
0392                     position=position+pyhepmc.FourVector(x=0,y=0,z=0,t=TimeHepmc)
0393                 v1=pyhepmc.GenVertex(position)
0394                 vertices.append(v1)
0395             
0396             # copies the particles and attaches them to their corresponding vertices
0397             for particle in inevt.particles:
0398                 # no copy/clone operator...
0399                 momentum, status, pid = particle.momentum, particle.status, particle.pid                
0400                 p1 = pyhepmc.GenParticle(momentum=momentum, pid=pid, status=status)
0401                 p1.generated_mass = particle.generated_mass
0402                 particles.append(p1)
0403                 
0404                 # since the beam particles do not have a production vertex they cannot be attached to a production vertex
0405                 if particle.production_vertex.id < 0:
0406                     production_vertex=particle.production_vertex.id
0407                     vertices[abs(production_vertex)-1].add_particle_out(p1)
0408                     hepSlice.add_particle(p1)
0409                 
0410                 
0411 
0412                 # Adds particles with an end vertex to their end vertices
0413                 if particle.end_vertex:
0414                     end_vertex = particle.end_vertex.id
0415                     vertices[abs(end_vertex)-1].add_particle_in(p1)
0416 
0417             # Adds the vertices with the attached particles to the event
0418             for vertex in vertices:
0419                 hepSlice.add_vertex(vertex)
0420 
0421         return hepSlice
0422 
0423     # ============================================================================================
0424     def poissonTimes( self, mu, endTime ):
0425         """Return an np.array of poisson-distributed times."""
0426         #Exponential distribution describes the time between poisson. We could start with an array of expected length
0427         #and then cut or fill as needed. Not very readable for very little gain.
0428         t = 0
0429         ret=np.array([])
0430         while True :
0431             delt = self.rng.exponential( mu )
0432             t = t + delt
0433             if t >= endTime :
0434                 break
0435             ret = np.append(ret,t)
0436         if self.args.atLeastOne:
0437             if ret.size==0:
0438                 while ret.size==0:
0439                     delt = self.rng.exponential( mu )
0440                     if delt<endTime:
0441                         ret=np.append(ret,delt)
0442         return ret
0443     
0444     # ============================================================================================
0445     def nameGen(self):
0446         # # datetime object containing current date and time
0447         # now = datetime.now()
0448         # dt_string = now.strftime("%Y_%m_%d_%H_%M_%S") # YY/mm/dd H:M:S        
0449         # return 'Sig_Back_Combo_{}_{}_event.hepmc'.format(dt_string,numEvents)
0450         name = self.args.signalFile
0451         if self.args.nSlices > 0 : 
0452             name = name.replace(".hepmc","_n_{}.hepmc".format(self.args.nSlices))
0453             
0454         name = "bgmerged_"+name
0455         return name        
0456 
0457     def squawk(self,i) :
0458         print('Working on slice {}'.format( i+1 ))
0459         print('Resident Memory',psutil.Process().memory_info().rss / 1024 / 1024,"MB")
0460     
0461     # ============================================================================================
0462     def fileWriter(combo_cont):
0463         numEvents=len(combo_cont)
0464         name=nameGen(numEvents)
0465         Event_ind=0
0466         with WriterAscii(name) as f:
0467             for i in combo_cont:
0468                 i.event_number=Event_ind
0469                 Event_ind=Event_ind+1
0470                 f.write_event(i)
0471         f.close()
0472 
0473     
0474 # ============================================================================================
0475 # Main function
0476 if __name__ == '__main__':
0477 
0478     t0 = time.time()
0479     sbm = signal_background_merger()
0480     sbm.merge()
0481     print()
0482     print('==================================================================')
0483     print('Overall running time:',np.round((time.time()-t0)/60.,2),'min')
0484 
0485 
0486