Analysis Software
Documentation for sPHENIX simulation software
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
material_mapping_optimisation.py
Go to the documentation of this file. Or view the newest version in sPHENIX GitHub for file material_mapping_optimisation.py
1 #!/usr/bin/env python3
2 import os
3 import argparse
4 import string
5 import math
6 import time
7 from types import FunctionType
8 
9 from orion.client import build_experiment
10 from orion.storage.base import get_storage
11 
12 from acts.examples import (
13  Sequencer,
14  WhiteBoard,
15  AlgorithmContext,
16  ProcessCode,
17  RootMaterialTrackReader,
18  RootMaterialTrackWriter,
19  MaterialMapping,
20  JsonMaterialWriter,
21  JsonFormat,
22 )
23 
24 import acts
25 from acts import (
26  Vector4,
27  UnitConstants as u,
28  SurfaceMaterialMapper,
29  VolumeMaterialMapper,
30  Navigator,
31  Propagator,
32  StraightLineStepper,
33  MaterialMapJsonConverter,
34 )
35 
36 from common import getOpenDataDetectorDirectory
37 from acts.examples.odd import getOpenDataDetector
38 from datetime import datetime
39 
40 
42  trackingGeometry,
43  decorators,
44  outputDir,
45  inputDir,
46  mapName="material-map",
47  mapSurface=True,
48  mapVolume=True,
49  format=JsonFormat.Json,
50  readCachedSurfaceInformation=False,
51  s=None,
52 ):
53  """
54  Implementation of the material mapping that doesn't write the material tracks.
55  Used to create the material map that will then be used to compute the material variance.
56 
57  trackingGeometry : The tracking geometry
58  decorators : The decorators for the tracking geometry
59  outputDir : Output directory for the material map
60  inputDir : Input directory containing the Material track
61  mapName : Name of the material map
62  mapSurface : Is material being mapped onto surfaces ?
63  mapVolume : Is material being mapped onto volumes ?
64  format : Json format used to write the material map (json, cbor, ...)
65  readCachedSurfaceInformation : If set to true it will be assumed that the surface has already been associated with each material interaction in the input file.
66  """
67 
68  s = s or Sequencer(numThreads=1)
69  for decorator in decorators:
70  s.addContextDecorator(decorator)
71  wb = WhiteBoard(acts.logging.INFO)
72  context = AlgorithmContext(0, 0, wb)
73  for decorator in decorators:
74  assert decorator.decorate(context) == ProcessCode.SUCCESS
75 
76  # Read material step information from a ROOT TTRee
77  s.addReader(
78  RootMaterialTrackReader(
79  level=acts.logging.INFO,
80  collection="material-tracks",
81  fileList=[
82  os.path.join(
83  inputDir,
84  "optimised-material-map_tracks.root"
85  if readCachedSurfaceInformation
86  else "geant4_material_tracks.root",
87  )
88  ],
89  readCachedSurfaceInformation=readCachedSurfaceInformation,
90  )
91  )
92 
93  stepper = StraightLineStepper()
94  mmAlgCfg = MaterialMapping.Config(context.geoContext, context.magFieldContext)
95  mmAlgCfg.trackingGeometry = trackingGeometry
96  mmAlgCfg.collection = "material-tracks"
97 
98  if mapSurface:
99  navigator = Navigator(
100  trackingGeometry=trackingGeometry,
101  resolveSensitive=True,
102  resolveMaterial=True,
103  resolvePassive=True,
104  )
105  propagator = Propagator(stepper, navigator)
106  mapper = SurfaceMaterialMapper(level=acts.logging.INFO, propagator=propagator)
107  mmAlgCfg.materialSurfaceMapper = mapper
108 
109  if mapVolume:
110  navigator = Navigator(
111  trackingGeometry=trackingGeometry,
112  )
113  propagator = Propagator(stepper, navigator)
114  mapper = VolumeMaterialMapper(
115  level=acts.logging.INFO, propagator=propagator, mappingStep=999
116  )
117  mmAlgCfg.materialVolumeMapper = mapper
118 
119  jmConverterCfg = MaterialMapJsonConverter.Config(
120  processSensitives=True,
121  processApproaches=True,
122  processRepresenting=True,
123  processBoundaries=True,
124  processVolumes=True,
125  context=context.geoContext,
126  )
127 
128  jmw = JsonMaterialWriter(
129  level=acts.logging.VERBOSE,
130  converterCfg=jmConverterCfg,
131  fileName=os.path.join(outputDir, mapName),
132  writeFormat=format,
133  )
134 
135  mmAlgCfg.materialWriters = [jmw]
136 
137  s.addAlgorithm(MaterialMapping(level=acts.logging.INFO, config=mmAlgCfg))
138 
139  return s
140 
141 
143  binMap,
144  events,
145  job,
146  inputPath,
147  pathExp,
148  pipeResult,
149  readCachedSurfaceInformation=False,
150 ):
151  """
152  Run the material mapping and compute the variance for each bin of each surface
153  Return a dict with the GeometryId value of the surface as a key that stores
154  a list of pairs corresponding to the variance and number of tracks associated with each bin of the surface
155 
156  binMap : Map containing the binning for each surface
157  events : Number of event to use in the mapping
158  job : ID of the job
159  inputPath : Directory containing the input geantino track and the json geometry
160  pathExp : Material mapping optimisation path
161  pipeResult : Pipe to send back the score to the main python instance
162  readCachedSurfaceInformation : Are surface information stored in the material track. Switch to true if the mapping was already performed to improve the speed.
163  """
164  print(
165  datetime.now().strftime("%H:%M:%S") + " Start mapping for job " + str(job),
166  flush=True,
167  )
168  mapName = "material-map-" + str(job)
169  mapSurface = True
170  mapVolume = False
171 
172  # Create a MappingMaterialDecorator based on the tracking geometry
173  matDeco = acts.IMaterialDecorator.fromFile(
174  str(os.path.join(inputPath, "geometry-map.json"))
175  )
176  detectorTemp, trackingGeometryTemp, decoratorsTemp = getOpenDataDetector(
178  )
179  matMapDeco = acts.MappingMaterialDecorator(
180  tGeometry=trackingGeometryTemp, level=acts.logging.ERROR
181  )
182  # Update the binning using the bin map corresponding to this trial
183  matMapDeco.setBinningMap(binMap)
184 
185  del detectorTemp
186  del trackingGeometryTemp
187  del decoratorsTemp
188 
189  # Decorate the detector with the MappingMaterialDecorator
190  detector, trackingGeometry, decorators = getOpenDataDetector(
191  getOpenDataDetectorDirectory(), matMapDeco
192  )
193 
194  # Sequence for the mapping, only use one thread when mapping material
196  events=events, numThreads=1, logLevel=acts.logging.INFO
197  )
198 
199  # Run the material mapping without writing the material track (as they take too much space)
201  trackingGeometry,
202  decorators,
203  outputDir=pathExp,
204  inputDir=inputPath,
205  mapName=mapName,
206  format=JsonFormat.Cbor,
207  mapVolume=mapVolume,
208  readCachedSurfaceInformation=readCachedSurfaceInformation,
209  s=sMap,
210  )
211  sMap.run()
212  del sMap # Need to be deleted to write the material map to cbor
213  del detector
214  del trackingGeometry
215  del decorators
216 
217  # Compute the variance by rerunning the mapping
218  print(
219  datetime.now().strftime("%H:%M:%S")
220  + " Job "
221  + str(job)
222  + ": second pass to compute the variance",
223  flush=True,
224  )
225  # Use the material map from the previous mapping as an input
226  cborMap = os.path.join(pathExp, (mapName + ".cbor"))
227  matDecoVar = acts.IMaterialDecorator.fromFile(cborMap)
228  detectorVar, trackingGeometryVar, decoratorsVar = getOpenDataDetector(
229  getOpenDataDetectorDirectory(), matDecoVar
230  )
231  s = acts.examples.Sequencer(events=events, numThreads=1, logLevel=acts.logging.INFO)
232  for decorator in decoratorsVar:
233  s.addContextDecorator(decorator)
234  wb = WhiteBoard(acts.logging.ERROR)
235  context = AlgorithmContext(0, 0, wb)
236  for decorator in decoratorsVar:
237  assert decorator.decorate(context) == ProcessCode.SUCCESS
238 
239  # Read material step information from a ROOT TTRee
240  reader = RootMaterialTrackReader(
241  level=acts.logging.ERROR,
242  collection="material-tracks",
243  fileList=[
244  os.path.join(
245  inputPath,
246  "optimised-material-map_tracks.root"
247  if readCachedSurfaceInformation
248  else "geant4_material_tracks.root",
249  )
250  ],
251  readCachedSurfaceInformation=readCachedSurfaceInformation,
252  )
253  s.addReader(reader)
254  stepper = StraightLineStepper()
255  mmAlgCfg = MaterialMapping.Config(context.geoContext, context.magFieldContext)
256  mmAlgCfg.trackingGeometry = trackingGeometryVar
257  mmAlgCfg.collection = "material-tracks"
258 
259  if mapSurface:
260  navigator = Navigator(
261  trackingGeometry=trackingGeometryVar,
262  resolveSensitive=True,
263  resolveMaterial=True,
264  resolvePassive=True,
265  )
266  propagator = Propagator(stepper, navigator)
267  surfaceCfg = SurfaceMaterialMapper.Config(
268  computeVariance=True
269  ) # Don't forget to turn the `computeVariance` to true
270  mapper = SurfaceMaterialMapper(
271  config=surfaceCfg, level=acts.logging.ERROR, propagator=propagator
272  )
273  mmAlgCfg.materialSurfaceMapper = mapper
274 
275  if mapVolume:
276  navigator = Navigator(
277  trackingGeometry=trackingGeometryVar,
278  )
279  propagator = Propagator(stepper, navigator)
280  mapper = VolumeMaterialMapper(
281  level=acts.logging.ERROR, propagator=propagator, mappingStep=999
282  )
283  mmAlgCfg.materialVolumeMapper = mapper
284 
285  mapping = MaterialMapping(level=acts.logging.ERROR, config=mmAlgCfg)
286  s.addAlgorithm(mapping)
287  s.run()
288 
289  # Compute the scoring parameters
290  score = dict()
291  for key in binMap:
292  objective = 0
293  nonZero = 0
294  binParameters = mapping.scoringParameters(key)
295  # Objective : Sum of variance in all bin divided by the number of bin
296  # The variance is scaled by (1.0 + 1.0/(number of hits in the bin)) to encourage larger bin at equal score
297  for parameters in binParameters:
298  if parameters[1] != 0:
299  objective += parameters[0] * (1.0 + 1.0 / parameters[1])
300  nonZero += 1
301  if nonZero != 0:
302  objective = objective / nonZero
303  score[key] = [dict(name="surface_score", type="objective", value=objective)]
304  print(
305  datetime.now().strftime("%H:%M:%S")
306  + " Mapping over for job "
307  + str(job)
308  + " : now sending score",
309  flush=True,
310  )
311  pipeResult.send(score)
312 
313  del mapping
314  del s
315  del detectorVar
316  del trackingGeometryVar
317  del decoratorsVar
318  os.remove(cborMap)
319 
320 
321 def surfaceExperiment(key, nbJobs, pathDB, pathResult, pipeBin, pipeResult, doPloting):
322  """
323  This function create an experiment for a given single surface
324  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.
325 
326  key : Id of the surface corresponding to this experiment
327  nbJobs : Total number of jobs to be executed simultaneously
328  pathDB : Path to the databases
329  pathResult : Path to write the result of the optimisation
330  pipeBin : Pipe use to send the experiment binning to the main python instance
331  pipeResult : Pipe to receive the result of the optimisation
332  doPloting : true if we want to plot the result of the optimisation and obtain the optimal material map
333  """
334  # Create the database
335  storage = {
336  "database": {
337  "name": "database_" + str(key),
338  "type": "pickleddb",
339  "host": os.path.join(pathDB, "database_" + str(key) + ".pkl"),
340  "timeout": 43200,
341  },
342  }
343  # Create the search space, the range of the binning can be chosen here
344  # x represent X or phi depending on the type of surface
345  # y represent Y, R or Z depending on the type of surface
346  space = {
347  "x": "uniform(1, 240, discrete=True)",
348  "y": "uniform(1, 240, discrete=True)",
349  }
350  # Build the experiment
351  experiments = build_experiment(
352  "s_" + str(key),
353  version="1",
354  space=space,
355  algorithms="random",
356  storage=storage,
357  max_idle_time=43200,
358  )
359  # Clean trial that haven't been completed
360  store = get_storage()
361  store.delete_trials(uid=experiments.id, where={"status": {"$ne": "completed"}})
362  store.release_algorithm_lock(uid=experiments.id)
363 
364  # Suggest one binning per job and then send them via the pipe
365  trials = dict()
366  binMap = dict()
367  for job in range(nbJobs):
368  trials[job] = experiments.suggest()
369  binMap[job] = (trials[job].params["x"], trials[job].params["y"])
370  print(
371  datetime.now().strftime("%H:%M:%S")
372  + " Binning for job "
373  + str(job)
374  + " and surface "
375  + str(key)
376  + " has been selected",
377  flush=True,
378  )
379  pipeBin.send(binMap[job])
380  print(
381  datetime.now().strftime("%H:%M:%S")
382  + " All binning for surface "
383  + str(key)
384  + " has been sent",
385  flush=True,
386  )
387  # Store the score resulting for the jobs in the database
388  for job in range(nbJobs):
389  score = pipeResult.recv()
390  print(
391  datetime.now().strftime("%H:%M:%S")
392  + " Received score for job "
393  + str(job)
394  + " and surface "
395  + str(key),
396  flush=True,
397  )
398  experiments.observe(trials[job], score)
399  print(
400  datetime.now().strftime("%H:%M:%S")
401  + " Score for job "
402  + str(job)
403  + " and surface "
404  + str(key)
405  + " has been written",
406  flush=True,
407  )
408 
409  # Create some performances plots for each surface
410  if doPloting:
411  print(
412  datetime.now().strftime("%H:%M:%S")
413  + " All the jobs are over. Now creating the optimisation plots",
414  flush=True,
415  )
416 
417  pathExpSurface = os.path.join(pathResult, "b_" + str(key))
418  if not os.path.isdir(pathExpSurface):
419  os.makedirs(pathExpSurface)
420 
421  regret = experiments.plot.regret()
422  regret.write_html(pathExpSurface + "/regret.html")
423 
424  parallel_coordinates = experiments.plot.parallel_coordinates()
425  parallel_coordinates.write_html(pathExpSurface + "/parallel_coordinates.html")
426 
427  lpi = experiments.plot.lpi()
428  lpi.write_html(pathExpSurface + "/lpi.html")
429 
430  partial_dependencies = experiments.plot.partial_dependencies()
431  partial_dependencies.write_html(pathExpSurface + "/partial_dependencies.html")
432 
433  # Select the optimal binning and send it via the pipe
434  df = experiments.to_pandas()
435  best = df.iloc[df.objective.idxmin()]
436  print(
437  datetime.now().strftime("%H:%M:%S")
438  + " Best score for surface "
439  + str(key)
440  + " : "
441  + str(best),
442  flush=True,
443  )
444  resultBinMap = (best.x, best.y)
445  pipeBin.send(resultBinMap)
446 
447 
448 if "__main__" == __name__:
449 
450  print(datetime.now().strftime("%H:%M:%S") + " Starting")
451  # Optimiser arguments
452  parser = argparse.ArgumentParser()
453  parser.add_argument(
454  "--numberOfJobs", nargs="?", default=2, type=int
455  ) # number of parallele jobs
456  parser.add_argument(
457  "--topNumberOfEvents", nargs="?", default=100, type=int
458  ) # number of events per trials
459  parser.add_argument(
460  "--inputPath", nargs="?", default=os.getcwd(), type=str
461  ) # path to the input
462  parser.add_argument(
463  "--outputPath", nargs="?", default="", type=str
464  ) # path to the output
465  parser.add_argument(
466  "--doPloting", action="store_true"
467  ) # Return the optimisation plot and create the optimal material map
468  parser.add_argument(
469  "--readCachedSurfaceInformation", action="store_true"
470  ) # Use surface information from the material track
471  parser.set_defaults(doPloting=False)
472  parser.set_defaults(readCachedSurfaceInformation=False)
473  args = parser.parse_args()
474 
475  # Define the useful path and create them if they do not exist
476  pathExp = os.path.join(args.outputPath, "Mapping")
477  pathDB = os.path.join(pathExp, "Database")
478  pathResult = os.path.join(pathExp, "Result")
479  if not os.path.isdir(pathExp):
480  os.makedirs(pathExp)
481  if not os.path.isdir(pathDB):
482  os.makedirs(pathDB)
483  if not os.path.isdir(pathResult):
484  os.makedirs(pathResult)
485 
486  # Create the tracking geometry, uses the json file to configure the proto-surfaces
487  matDeco = acts.IMaterialDecorator.fromFile(
488  str(os.path.join(args.inputPath, "geometry-map.json"))
489  )
490  detector, trackingGeometry, decorators = getOpenDataDetector(
492  )
493 
494  # Use the MappingMaterialDecorator to create a binning map that can be optimised
495  matMapDeco = acts.MappingMaterialDecorator(
496  tGeometry=trackingGeometry, level=acts.logging.WARNING
497  )
498  binDict = matMapDeco.binningMap()
499  del detector, decorators
500 
501  # Create the pipes that will be used to transfer data to/from the jobs
502  from multiprocessing import Process, Pipe
503 
504  binPipes_child = dict()
505  resultPipes_child = dict()
506  scorePipes_child = dict()
507 
508  binPipes_parent = dict()
509  resultPipes_parent = dict()
510  scorePipes_parent = dict()
511 
512  expJob = dict()
513  OptiJob = dict()
514 
515  # Build one experiment per surface
516  # The binning of the surfaces are independent so we split
517  # an optimisation problem with a large number of variable into a lot of optimisation with 2
518  for key in binDict:
519  binPipes_parent[key], binPipes_child[key] = Pipe()
520  scorePipes_parent[key], scorePipes_child[key] = Pipe()
521  expJob[key] = Process(
522  target=surfaceExperiment,
523  args=(
524  key,
525  args.numberOfJobs,
526  pathDB,
527  pathResult,
528  binPipes_child[key],
529  scorePipes_child[key],
530  args.doPloting,
531  ),
532  )
533  expJob[key].start()
534 
535  # Prepare `args.numberOfJobs` material mapping jobs
536  for job in range(args.numberOfJobs):
537  resultPipes_parent[job], resultPipes_child[job] = Pipe()
538  binMap = dict()
539  # Collect the binning for all the surfaces and create a bin map
540  for key in binDict:
541  binMap[key] = binPipes_parent[key].recv()
542  print(
543  datetime.now().strftime("%H:%M:%S")
544  + " Binning for job "
545  + str(job)
546  + " have been selected, now running the mapping",
547  flush=True,
548  )
549  # Launch the material mapping with the bin map
550  OptiJob[job] = Process(
551  target=runMaterialMappingVariance,
552  args=(
553  binMap,
554  args.topNumberOfEvents,
555  job,
556  args.inputPath,
557  pathExp,
558  resultPipes_child[job],
559  args.readCachedSurfaceInformation,
560  ),
561  )
562  OptiJob[job].start()
563 
564  # Collect the score from the material mapping, this pauses the script until all the jobs have been completed
565  for job in range(args.numberOfJobs):
566  scores = resultPipes_parent[job].recv()
567  print(
568  datetime.now().strftime("%H:%M:%S")
569  + " Retried score for job "
570  + str(job),
571  flush=True,
572  )
573  for key in binDict:
574  score = scores[key]
575  scorePipes_parent[key].send(score)
576  print(
577  datetime.now().strftime("%H:%M:%S")
578  + " Job number "
579  + str(job)
580  + " is over",
581  flush=True,
582  )
583 
584  if args.doPloting:
585  # The optimal binning has been found.
586  # Run the material mapping one last to obtain a usable material map
587  print(
588  datetime.now().strftime("%H:%M:%S")
589  + " Running the material mapping to obtain the optimised material map",
590  flush=True,
591  )
592  resultBinMap = dict()
593  for key in binDict:
594  resultBinMap[key] = binPipes_parent[key].recv()
595  matMapDeco.setBinningMap(resultBinMap)
596 
597  # Decorate the detector with the MappingMaterialDecorator
598  resultDetector, resultTrackingGeometry, resultDecorators = getOpenDataDetector(
599  getOpenDataDetectorDirectory(), matMapDeco
600  )
601 
602  # Sequence for the mapping, only use one thread when mapping material
604  events=args.topNumberOfEvents, numThreads=1, logLevel=acts.logging.INFO
605  )
606 
607  # Run the material mapping
608  from material_mapping import runMaterialMapping
609 
611  resultTrackingGeometry,
612  resultDecorators,
613  outputDir=args.outputPath,
614  inputDir=args.inputPath,
615  mapName="optimised-material-map",
616  format=JsonFormat.Json,
617  mapVolume=False,
618  s=rMap,
619  )
620  rMap.run()
621  del rMap # Need to be deleted to write the material map to cbor
622  del resultDetector, resultTrackingGeometry, resultDecorators
623  print(
624  datetime.now().strftime("%H:%M:%S")
625  + " Waiting for all the score to have been stored",
626  flush=True,
627  )
628 
629  # Create a timer that will be used to terminate the Process
630  deadline = time.time() + 600
631 
632  for key in binDict:
633  timeout = max(deadline - time.time(), 0)
634  expJob[key].join(timeout=timeout)
635  expJob[key].terminate()
636  print(
637  datetime.now().strftime("%H:%M:%S")
638  + " Experiment for surface "
639  + str(key)
640  + " is over",
641  flush=True,
642  )