Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-12-16 09:24:05

0001 #!/usr/bin/env python3
0002 
0003 import os
0004 import argparse
0005 import time
0006 from datetime import datetime
0007 
0008 from orion.client import build_experiment
0009 from orion.storage.base import get_storage
0010 
0011 import acts
0012 from acts import (
0013     SurfaceMaterialMapper,
0014     VolumeMaterialMapper,
0015     Navigator,
0016     Propagator,
0017     StraightLineStepper,
0018 )
0019 
0020 from acts.json import MaterialMapJsonConverter
0021 
0022 from acts.examples import (
0023     Sequencer,
0024     WhiteBoard,
0025     AlgorithmContext,
0026     ProcessCode,
0027     MaterialMapping,
0028 )
0029 
0030 from acts.examples.root import (
0031     RootMaterialTrackReader,
0032     RootMaterialTrackWriter,
0033 )
0034 
0035 from acts.examples.json import (
0036     JsonMaterialWriter,
0037     JsonFormat,
0038 )
0039 
0040 from acts.examples.odd import getOpenDataDetector
0041 
0042 
0043 def runMaterialMappingNoTrack(
0044     trackingGeometry,
0045     decorators,
0046     outputDir,
0047     inputDir,
0048     mapName="material-map",
0049     mapSurface=True,
0050     mapVolume=True,
0051     format=JsonFormat.Json,
0052     readCachedSurfaceInformation=False,
0053     s=None,
0054 ):
0055     """
0056     Implementation of the material mapping that doesn't write the material tracks.
0057     Used to create the material map that will then be used to compute the material variance.
0058 
0059     trackingGeometry : The tracking geometry
0060     decorators : The decorators for the tracking geometry
0061     outputDir : Output directory for the material map
0062     inputDir : Input directory containing the Material track
0063     mapName : Name of the material map
0064     mapSurface : Is material being mapped onto surfaces ?
0065     mapVolume : Is material being mapped onto volumes ?
0066     format : Json format used to write the material map (json, cbor, ...)
0067     readCachedSurfaceInformation : If set to true it will be assumed that the surface has already been associated with each material interaction in the input file.
0068     """
0069 
0070     s = s or Sequencer(numThreads=1)
0071     for decorator in decorators:
0072         s.addContextDecorator(decorator)
0073     wb = WhiteBoard(acts.logging.INFO)
0074     context = AlgorithmContext(0, 0, wb)
0075     for decorator in decorators:
0076         assert decorator.decorate(context) == ProcessCode.SUCCESS
0077 
0078     # Read material step information from a ROOT TTRee
0079     s.addReader(
0080         RootMaterialTrackReader(
0081             level=acts.logging.INFO,
0082             outputMaterialTracks="material-tracks",
0083             fileList=[
0084                 os.path.join(
0085                     inputDir,
0086                     (
0087                         "optimised-material-map_tracks.root"
0088                         if readCachedSurfaceInformation
0089                         else "geant4_material_tracks.root"
0090                     ),
0091                 )
0092             ],
0093             readCachedSurfaceInformation=readCachedSurfaceInformation,
0094         )
0095     )
0096 
0097     stepper = StraightLineStepper()
0098     mmAlgCfg = MaterialMapping.Config(context.geoContext, context.magFieldContext)
0099     mmAlgCfg.trackingGeometry = trackingGeometry
0100     mmAlgCfg.inputMaterialTracks = "material-tracks"
0101 
0102     if mapSurface:
0103         navigator = Navigator(
0104             trackingGeometry=trackingGeometry,
0105             resolveSensitive=True,
0106             resolveMaterial=True,
0107             resolvePassive=True,
0108         )
0109         propagator = Propagator(stepper, navigator)
0110         mapper = SurfaceMaterialMapper(level=acts.logging.INFO, propagator=propagator)
0111         mmAlgCfg.materialSurfaceMapper = mapper
0112 
0113     if mapVolume:
0114         navigator = Navigator(
0115             trackingGeometry=trackingGeometry,
0116         )
0117         propagator = Propagator(stepper, navigator)
0118         mapper = VolumeMaterialMapper(
0119             level=acts.logging.INFO, propagator=propagator, mappingStep=999
0120         )
0121         mmAlgCfg.materialVolumeMapper = mapper
0122 
0123     jmConverterCfg = MaterialMapJsonConverter.Config(
0124         processSensitives=True,
0125         processApproaches=True,
0126         processRepresenting=True,
0127         processBoundaries=True,
0128         processVolumes=True,
0129         context=context.geoContext,
0130     )
0131 
0132     jmw = JsonMaterialWriter(
0133         level=acts.logging.VERBOSE,
0134         converterCfg=jmConverterCfg,
0135         fileName=os.path.join(outputDir, mapName),
0136         writeFormat=format,
0137     )
0138 
0139     mmAlgCfg.materialWriters = [jmw]
0140 
0141     s.addAlgorithm(MaterialMapping(level=acts.logging.INFO, config=mmAlgCfg))
0142 
0143     return s
0144 
0145 
0146 def runMaterialMappingVariance(
0147     binMap,
0148     events,
0149     job,
0150     inputPath,
0151     pathExp,
0152     pipeResult,
0153     readCachedSurfaceInformation=False,
0154 ):
0155     """
0156     Run the material mapping and compute the variance for each bin of each surface
0157     Return a dict with the GeometryId value of the surface as a key that stores
0158     a list of pairs corresponding to the variance and number of tracks associated with each bin of the surface
0159 
0160     binMap : Map containing the binning for each surface
0161     events : Number of event to use in the mapping
0162     job : ID of the job
0163     inputPath : Directory containing the input geantino track and the json geometry
0164     pathExp : Material mapping optimisation path
0165     pipeResult : Pipe to send back the score to the main python instance
0166     readCachedSurfaceInformation : Are surface information stored in the material track. Switch to true if the mapping was already performed to improve the speed.
0167     """
0168     print(
0169         datetime.now().strftime("%H:%M:%S") + "    Start mapping for job " + str(job),
0170         flush=True,
0171     )
0172     mapName = "material-map-" + str(job)
0173     mapSurface = True
0174     mapVolume = False
0175 
0176     # Create a MappingMaterialDecorator based on the tracking geometry
0177     matDeco = acts.IMaterialDecorator.fromFile(
0178         str(os.path.join(inputPath, "geometry-map.json"))
0179     )
0180     detectorTemp = getOpenDataDetector(matDeco)
0181     trackingGeometryTemp = detectorTemp.trackingGeometry()
0182     matMapDeco = acts.examples.MappingMaterialDecorator(
0183         tGeometry=trackingGeometryTemp, level=acts.logging.ERROR
0184     )
0185     # Update the binning using the bin map corresponding to this trial
0186     matMapDeco.setBinningMap(binMap)
0187 
0188     # Decorate the detector with the MappingMaterialDecorator
0189     detector = getOpenDataDetector(matMapDeco)
0190     trackingGeometry = detector.trackingGeometry()
0191     decorators = detector.contextDecorators()
0192 
0193     # Sequence for the mapping, only use one thread when mapping material
0194     sMap = acts.examples.Sequencer(
0195         events=events, numThreads=1, logLevel=acts.logging.INFO
0196     )
0197 
0198     # Run the material mapping without writing the material track (as they take too much space)
0199     runMaterialMappingNoTrack(
0200         trackingGeometry,
0201         decorators,
0202         outputDir=pathExp,
0203         inputDir=inputPath,
0204         mapName=mapName,
0205         format=JsonFormat.Cbor,
0206         mapVolume=mapVolume,
0207         readCachedSurfaceInformation=readCachedSurfaceInformation,
0208         s=sMap,
0209     )
0210     sMap.run()
0211 
0212     # Compute the variance by rerunning the mapping
0213     print(
0214         datetime.now().strftime("%H:%M:%S")
0215         + "    Job "
0216         + str(job)
0217         + ": second pass to compute the variance",
0218         flush=True,
0219     )
0220     # Use the material map from the previous mapping as an input
0221     cborMap = os.path.join(pathExp, (mapName + ".cbor"))
0222     matDecoVar = acts.IMaterialDecorator.fromFile(cborMap)
0223     detectorVar = getOpenDataDetector(matDecoVar)
0224     trackingGeometryVar = detectorVar.trackingGeometry()
0225     decoratorsVar = detectorVar.contextDecorators()
0226     s = acts.examples.Sequencer(events=events, numThreads=1, logLevel=acts.logging.INFO)
0227     for decorator in decoratorsVar:
0228         s.addContextDecorator(decorator)
0229     wb = WhiteBoard(acts.logging.ERROR)
0230     context = AlgorithmContext(0, 0, wb)
0231     for decorator in decoratorsVar:
0232         assert decorator.decorate(context) == ProcessCode.SUCCESS
0233 
0234     # Read material step information from a ROOT TTRee
0235     reader = RootMaterialTrackReader(
0236         level=acts.logging.ERROR,
0237         outputMaterialTracks="material-tracks",
0238         fileList=[
0239             os.path.join(
0240                 inputPath,
0241                 (
0242                     "optimised-material-map_tracks.root"
0243                     if readCachedSurfaceInformation
0244                     else "geant4_material_tracks.root"
0245                 ),
0246             )
0247         ],
0248         readCachedSurfaceInformation=readCachedSurfaceInformation,
0249     )
0250     s.addReader(reader)
0251     stepper = StraightLineStepper()
0252     mmAlgCfg = MaterialMapping.Config(context.geoContext, context.magFieldContext)
0253     mmAlgCfg.trackingGeometry = trackingGeometryVar
0254     mmAlgCfg.inputMaterialTracks = "material-tracks"
0255 
0256     if mapSurface:
0257         navigator = Navigator(
0258             trackingGeometry=trackingGeometryVar,
0259             resolveSensitive=True,
0260             resolveMaterial=True,
0261             resolvePassive=True,
0262         )
0263         propagator = Propagator(stepper, navigator)
0264         surfaceCfg = SurfaceMaterialMapper.Config(
0265             computeVariance=True
0266         )  # Don't forget to turn the `computeVariance` to true
0267         mapper = SurfaceMaterialMapper(
0268             config=surfaceCfg, level=acts.logging.ERROR, propagator=propagator
0269         )
0270         mmAlgCfg.materialSurfaceMapper = mapper
0271 
0272     if mapVolume:
0273         navigator = Navigator(
0274             trackingGeometry=trackingGeometryVar,
0275         )
0276         propagator = Propagator(stepper, navigator)
0277         mapper = VolumeMaterialMapper(
0278             level=acts.logging.ERROR, propagator=propagator, mappingStep=999
0279         )
0280         mmAlgCfg.materialVolumeMapper = mapper
0281 
0282     mapping = MaterialMapping(level=acts.logging.ERROR, config=mmAlgCfg)
0283     s.addAlgorithm(mapping)
0284     s.run()
0285 
0286     # Compute the scoring parameters
0287     score = dict()
0288     for key in binMap:
0289         objective = 0
0290         nonZero = 0
0291         binParameters = mapping.scoringParameters(key)
0292         # Objective : Sum of variance in all bin divided by the number of bin
0293         # The variance is scaled by (1.0 + 1.0/(number of hits in the bin)) to encourage larger bin at equal score
0294         for parameters in binParameters:
0295             if parameters[1] != 0:
0296                 objective += parameters[0] * (1.0 + 1.0 / parameters[1])
0297                 nonZero += 1
0298         if nonZero != 0:
0299             objective = objective / nonZero
0300         score[key] = [dict(name="surface_score", type="objective", value=objective)]
0301     print(
0302         datetime.now().strftime("%H:%M:%S")
0303         + "    Mapping over for job "
0304         + str(job)
0305         + " : now sending score",
0306         flush=True,
0307     )
0308     pipeResult.send(score)
0309 
0310     os.remove(cborMap)
0311 
0312 
0313 def surfaceExperiment(key, nbJobs, pathDB, pathResult, pipeBin, pipeResult, doPloting):
0314     """
0315     This function create an experiment for a given single surface
0316     Due to how Orion is implemented only one DB can exist per job, this thus need to be call using pythons multiprocessing to circumvent the issue.
0317 
0318     key : Id of the surface corresponding to this experiment
0319     nbJobs : Total number of jobs to be executed simultaneously
0320     pathDB : Path to the databases
0321     pathResult : Path to write the result of the optimisation
0322     pipeBin : Pipe use to send the experiment binning to the main python instance
0323     pipeResult : Pipe to receive the result of the optimisation
0324     doPloting : true if we want to plot the result of the optimisation and obtain the optimal material map
0325     """
0326     # Create the database
0327     storage = {
0328         "database": {
0329             "name": "database_" + str(key),
0330             "type": "pickleddb",
0331             "host": os.path.join(pathDB, "database_" + str(key) + ".pkl"),
0332             "timeout": 43200,
0333         },
0334     }
0335     # Create the search space, the range of the binning can be chosen here
0336     # x represent X or phi depending on the type of surface
0337     # y represent Y, R or Z depending on the type of surface
0338     space = {
0339         "x": "uniform(1, 240, discrete=True)",
0340         "y": "uniform(1, 240, discrete=True)",
0341     }
0342     # Build the experiment
0343     experiments = build_experiment(
0344         "s_" + str(key),
0345         version="1",
0346         space=space,
0347         algorithms="random",
0348         storage=storage,
0349         max_idle_time=43200,
0350     )
0351     # Clean trial that haven't been completed
0352     store = get_storage()
0353     store.delete_trials(uid=experiments.id, where={"status": {"$ne": "completed"}})
0354     store.release_algorithm_lock(uid=experiments.id)
0355 
0356     # Suggest one binning per job and then send them via the pipe
0357     trials = dict()
0358     binMap = dict()
0359     for job in range(nbJobs):
0360         trials[job] = experiments.suggest()
0361         binMap[job] = (trials[job].params["x"], trials[job].params["y"])
0362         print(
0363             datetime.now().strftime("%H:%M:%S")
0364             + "    Binning for job "
0365             + str(job)
0366             + " and surface "
0367             + str(key)
0368             + " has been selected",
0369             flush=True,
0370         )
0371         pipeBin.send(binMap[job])
0372     print(
0373         datetime.now().strftime("%H:%M:%S")
0374         + "    All binning for surface "
0375         + str(key)
0376         + " has been sent",
0377         flush=True,
0378     )
0379     # Store the score resulting for the jobs in the database
0380     for job in range(nbJobs):
0381         score = pipeResult.recv()
0382         print(
0383             datetime.now().strftime("%H:%M:%S")
0384             + "    Received score for job "
0385             + str(job)
0386             + " and surface "
0387             + str(key),
0388             flush=True,
0389         )
0390         experiments.observe(trials[job], score)
0391         print(
0392             datetime.now().strftime("%H:%M:%S")
0393             + "    Score for job "
0394             + str(job)
0395             + " and surface "
0396             + str(key)
0397             + " has been written",
0398             flush=True,
0399         )
0400 
0401     # Create some performances plots for each surface
0402     if doPloting:
0403         print(
0404             datetime.now().strftime("%H:%M:%S")
0405             + "    All the jobs are over. Now creating the optimisation plots",
0406             flush=True,
0407         )
0408 
0409         pathExpSurface = os.path.join(pathResult, "b_" + str(key))
0410         if not os.path.isdir(pathExpSurface):
0411             os.makedirs(pathExpSurface)
0412 
0413         regret = experiments.plot.regret()
0414         regret.write_html(pathExpSurface + "/regret.html")
0415 
0416         parallel_coordinates = experiments.plot.parallel_coordinates()
0417         parallel_coordinates.write_html(pathExpSurface + "/parallel_coordinates.html")
0418 
0419         lpi = experiments.plot.lpi()
0420         lpi.write_html(pathExpSurface + "/lpi.html")
0421 
0422         partial_dependencies = experiments.plot.partial_dependencies()
0423         partial_dependencies.write_html(pathExpSurface + "/partial_dependencies.html")
0424 
0425         # Select the optimal binning and send it via the pipe
0426         df = experiments.to_pandas()
0427         best = df.iloc[df.objective.idxmin()]
0428         print(
0429             datetime.now().strftime("%H:%M:%S")
0430             + "    Best score for surface "
0431             + str(key)
0432             + " : "
0433             + str(best),
0434             flush=True,
0435         )
0436         resultBinMap = (best.x, best.y)
0437         pipeBin.send(resultBinMap)
0438 
0439 
0440 if "__main__" == __name__:
0441     print(datetime.now().strftime("%H:%M:%S") + "    Starting")
0442     # Optimiser arguments
0443     parser = argparse.ArgumentParser()
0444     parser.add_argument(
0445         "--numberOfJobs", nargs="?", default=2, type=int
0446     )  # number of parallele jobs
0447     parser.add_argument(
0448         "--topNumberOfEvents", nargs="?", default=100, type=int
0449     )  # number of events per trials
0450     parser.add_argument(
0451         "--inputPath", nargs="?", default=os.getcwd(), type=str
0452     )  # path to the input
0453     parser.add_argument(
0454         "--outputPath", nargs="?", default="", type=str
0455     )  # path to the output
0456     parser.add_argument(
0457         "--doPloting", action="store_true"
0458     )  # Return the optimisation plot and create the optimal material map
0459     parser.add_argument(
0460         "--readCachedSurfaceInformation", action="store_true"
0461     )  # Use surface information from the material track
0462     parser.set_defaults(doPloting=False)
0463     parser.set_defaults(readCachedSurfaceInformation=False)
0464     args = parser.parse_args()
0465 
0466     # Define the useful path and create them if they do not exist
0467     pathExp = os.path.join(args.outputPath, "Mapping")
0468     pathDB = os.path.join(pathExp, "Database")
0469     pathResult = os.path.join(pathExp, "Result")
0470     if not os.path.isdir(pathExp):
0471         os.makedirs(pathExp)
0472     if not os.path.isdir(pathDB):
0473         os.makedirs(pathDB)
0474     if not os.path.isdir(pathResult):
0475         os.makedirs(pathResult)
0476 
0477     # Create the tracking geometry, uses the json file to configure the proto-surfaces
0478     matDeco = acts.IMaterialDecorator.fromFile(
0479         str(os.path.join(args.inputPath, "geometry-map.json"))
0480     )
0481     detector = getOpenDataDetector(matDeco)
0482     trackingGeometry = detector.trackingGeometry()
0483     decorators = detector.contextDecorators()
0484 
0485     # Use the MappingMaterialDecorator to create a binning map that can be optimised
0486     matMapDeco = acts.examples.MappingMaterialDecorator(
0487         tGeometry=trackingGeometry, level=acts.logging.WARNING
0488     )
0489     binDict = matMapDeco.binningMap()
0490 
0491     # Create the pipes that will be used to transfer data to/from the jobs
0492     from multiprocessing import Process, Pipe
0493 
0494     binPipes_child = dict()
0495     resultPipes_child = dict()
0496     scorePipes_child = dict()
0497 
0498     binPipes_parent = dict()
0499     resultPipes_parent = dict()
0500     scorePipes_parent = dict()
0501 
0502     expJob = dict()
0503     OptiJob = dict()
0504 
0505     # Build one experiment per surface
0506     # The binning of the surfaces are independent so we split
0507     # an optimisation problem with a large number of variable into a lot of optimisation with 2
0508     for key in binDict:
0509         binPipes_parent[key], binPipes_child[key] = Pipe()
0510         scorePipes_parent[key], scorePipes_child[key] = Pipe()
0511         expJob[key] = Process(
0512             target=surfaceExperiment,
0513             args=(
0514                 key,
0515                 args.numberOfJobs,
0516                 pathDB,
0517                 pathResult,
0518                 binPipes_child[key],
0519                 scorePipes_child[key],
0520                 args.doPloting,
0521             ),
0522         )
0523         expJob[key].start()
0524 
0525     # Prepare `args.numberOfJobs` material mapping jobs
0526     for job in range(args.numberOfJobs):
0527         resultPipes_parent[job], resultPipes_child[job] = Pipe()
0528         binMap = dict()
0529         # Collect the binning for all the surfaces and create a bin map
0530         for key in binDict:
0531             binMap[key] = binPipes_parent[key].recv()
0532         print(
0533             datetime.now().strftime("%H:%M:%S")
0534             + "    Binning for job "
0535             + str(job)
0536             + " have been selected, now running the mapping",
0537             flush=True,
0538         )
0539         # Launch the material mapping with the bin map
0540         OptiJob[job] = Process(
0541             target=runMaterialMappingVariance,
0542             args=(
0543                 binMap,
0544                 args.topNumberOfEvents,
0545                 job,
0546                 args.inputPath,
0547                 pathExp,
0548                 resultPipes_child[job],
0549                 args.readCachedSurfaceInformation,
0550             ),
0551         )
0552         OptiJob[job].start()
0553 
0554     # Collect the score from the material mapping, this pauses the script until all the jobs have been completed
0555     for job in range(args.numberOfJobs):
0556         scores = resultPipes_parent[job].recv()
0557         print(
0558             datetime.now().strftime("%H:%M:%S")
0559             + "    Retried score for job "
0560             + str(job),
0561             flush=True,
0562         )
0563         for key in binDict:
0564             score = scores[key]
0565             scorePipes_parent[key].send(score)
0566         print(
0567             datetime.now().strftime("%H:%M:%S")
0568             + "    Job number "
0569             + str(job)
0570             + " is over",
0571             flush=True,
0572         )
0573 
0574     if args.doPloting:
0575         # The optimal binning has been found.
0576         # Run the material mapping one last to obtain a usable material map
0577         print(
0578             datetime.now().strftime("%H:%M:%S")
0579             + "    Running the material mapping to obtain the optimised material map",
0580             flush=True,
0581         )
0582         resultBinMap = dict()
0583         for key in binDict:
0584             resultBinMap[key] = binPipes_parent[key].recv()
0585         matMapDeco.setBinningMap(resultBinMap)
0586 
0587         # Decorate the detector with the MappingMaterialDecorator
0588         resultDetector, resultTrackingGeometry, resultDecorators = getOpenDataDetector(
0589             matMapDeco
0590         )
0591 
0592         # Sequence for the mapping, only use one thread when mapping material
0593         rMap = acts.examples.Sequencer(
0594             events=args.topNumberOfEvents, numThreads=1, logLevel=acts.logging.INFO
0595         )
0596 
0597         # Run the material mapping
0598         from material_mapping import runMaterialMapping
0599 
0600         runMaterialMapping(
0601             resultTrackingGeometry,
0602             resultDecorators,
0603             outputDir=args.outputPath,
0604             inputDir=args.inputPath,
0605             mapName="optimised-material-map",
0606             format=JsonFormat.Json,
0607             mapVolume=False,
0608             s=rMap,
0609         )
0610         rMap.run()
0611     print(
0612         datetime.now().strftime("%H:%M:%S")
0613         + "    Waiting for all the score to have been stored",
0614         flush=True,
0615     )
0616 
0617     # Create a timer that will be used to terminate the Process
0618     deadline = time.time() + 600
0619 
0620     for key in binDict:
0621         timeout = max(deadline - time.time(), 0)
0622         expJob[key].join(timeout=timeout)
0623         expJob[key].terminate()
0624         print(
0625             datetime.now().strftime("%H:%M:%S")
0626             + "    Experiment for surface "
0627             + str(key)
0628             + " is over",
0629             flush=True,
0630         )