All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
icarusalg/icarusalg/gallery/helpers/python/galleryUtils.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 
3 from __future__ import print_function
4 
5 __doc__ = """
6 Collection of utilities to interface gallery with python.
7 
8 This module requires ROOT.
9 
10 An example of a interactive session counting the number of muons
11 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
12 import galleryUtils
13 import ROOT
14 sampleEvents = galleryUtils.makeEvent("data.root")
15 LArG4tag = ROOT.art.InputTag("largeant")
16 
17 getParticleHandle \\
18  = galleryUtils.make_getValidHandle("std::vector<simb::MCParticle>", sampleEvents)
19 for event in galleryUtils.forEach(sampleEvents):
20  particles = getParticleHandle(LArG4tag).product()
21 
22  nMuons = sum(1 for part in particles if abs(part.PdgCode()) == 13)
23  print("%s: %d muons" % (event.eventAuxiliary().id(), nMuons))
24 
25 # for all events
26 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
27 
28 
29 """
30 
31 __all__ = [
32  'readHeader', # imported from `cppUtils`
33  'SourceCode', # imported from `cppUtils`
34  'make_getValidHandle',
35  'makeFileList',
36  'forEach',
37  'eventLoop',
38  'findFHiCL',
39  'loadConfiguration',
40  'ConfigurationClass',
41  'startMessageFacility',
42  'ServiceRegistryClass',
43  'ConfigurationHelper',
44  ]
45 
46 import sys, os
47 from ROOTutils import ROOT, expandFileList
48 import cppUtils
49 import warnings
50 
51 
52 ################################################################################
53 ### Pass-through
54 ###
55 SourceCode = cppUtils.SourceCode
56 readHeader = cppUtils.readHeader
57 
58 
59 ################################################################################
60 ### gallery
61 ################################################################################
62 
63 # override conversion of art::EventID into a string (used by `print()`)
64 ROOT.art.EventID.__str__ \
65  = lambda self: "R:%d S:%d E:%d" % (self.run(), self.subRun(), self.event())
66 
67 
69  """Make the ROOT C++ jit compiler instantiate the
70  Event::getValidHandle member template for template parameter klass.
71 
72  Needs to keep track of what was done already, because Cling will protest if
73  the same class is asked twice.
74 
75  See the documentation of `__call__` for examples of usage of instances of
76  this class.
77 
78  """
80  def __init__(self, klass):
81  self.handleVectorClass = ROOT.std.vector[f'gallery::Handle<{klass}>']
82  self.getHandles = ROOT.gallery.Event.getManyByType[klass]
83  def __call__(self, event):
84  handles = self.handleVectorClass()
85  self.getHandles(event, handles)
86  return handles
87  # ManyByTypeProc
88 
89 
90  def validHandle(self,
91  klass: "data product class to prepare for (as a C++ class name string)",
92  event: "(optional) the event object to retrieve the data products from " = None,
93  ) -> "if `event` is specified, bound `getValidHandle<klass>`, unbound otherwise":
94  # this has been tested with ROOT 6.22;
95  # big improvements in cppyy make this **way** simpler than it used to be
96  getHandle = ROOT.gallery.Event.getValidHandle[klass]
97  return (lambda tag: getHandle(event, tag)) if event else getHandle
98  # validHandle()
99 
100  def manyByType(self,
101  klass: "data product class to prepare for (as a C++ class name string)",
102  event: "(optional) the event object to retrieve the data products from " = None,
103  ) -> """if `event` is specified, a list (std::vector) of handles,
104  otherwise a callable that when called on an event returns such list""":
105  getManyByType = HandleMaker.ManyByTypeProc(klass)
106  return getManyByType(event) if event else (lambda event: getManyByType(event))
107  # manyByType()
108 
109  def __call__(self,
110  klass: "data product class to prepare for (as a C++ class name string)",
111  event: "(optional) the event object to retrieve the data products from " = None,
112  ) -> "if `event` is specified, bound `getValidHandle<klass>`, unbound otherwise":
113  """Prepares for reading data products of the specified class.
114 
115  This function causes the instantiation of the `gallery::Event::getValidHandle()`
116  method specialized to read the specified `klass`.
117  It also returns a bound or unbound function to actually retrieve data products.
118 
119  If an `event` instance is specified, the returned value is a bound function
120  that can be used directly.
121 
122  from galleryUtils import makeEvent, make_getValidHandle
123  import ROOT
124 
125  event = makeEvent("test.root")
126  inputTag = ROOT.art.InputTag("largeant")
127 
128  getParticlesFrom = make_getValidHandle("std::vector<simb::MCParticle>")
129  # note: the following is curretly mostly broken (see below)
130  particles1 = getParticlesFrom(event, inputTag).product()
131 
132  getParticles = make_getValidHandle("std::vector<simb::MCParticle>", event)
133  particles2 = getParticles(inputTag).product()
134 
135  Both `particles1` and `particles2` point to the same data product.
136 
137  Exception (`RuntimeError`) is raised on error.
138  """
139  return self.validHandle(klass, event)
140  # __call__()
141 
142 # class HandleMaker
143 make_getValidHandle = HandleMaker()
144 
145 
146 def makeFileList(
147  *filePaths: "a list of input files"
148  ) -> "a list of files suitable to construct a `gallery.Event object":
149  """Creates a file list suitable for `gallery::Event`.
150 
151  If a file ends with `.root`, it is added directly to the list.
152  Otherwise, it is interpreted as a file list and treated as such
153  (see `ROOTutils.expandFileList()`).
154  File list recursion is disabled.
155  """
156  files = ROOT.vector(ROOT.string)()
157  for path in filePaths:
158  entries = [ path ] if path.endswith('.root') else expandFileList(path)
159  for entry in entries: files.push_back(entry)
160  # for
161  return files
162 # makeFileList()
163 
164 
165 def makeEvent(
166  files: "files or file lists, as supported by `ROOTutils.makeFileList()`",
167  **kwargs: "additional arguments to `gallery::Event` constructor"
168  ) -> "a gallery.Event object reading the specified input files":
169  return ROOT.gallery.Event(makeFileList(files), **kwargs)
170 # makeEvent()
171 
172 
173 def forEach(event):
174  """
175  Simplifies sequential event looping.
176 
177  This function handles the state of `event`, moving the current event around.
178  Therefore, it has side effects.
179  The function returns `event` itself, after it has set to the next available
180  event.
181 
182  This function is actually a generator.
183 
184  Example of loop:
185 
186  for iEvent, event in enumerate(forEach(event)):
187  ...
188 
189 
190  """
191  while (not event.atEnd()):
192  yield event
193  event.next()
194  # while
195 # forEach()
196 
197 
199  """
200  Iterator for an event sequence.
201 
202  It can be used directly as:
203 
204  for event in EventIterator(event): ...
205 
206  or it can be plugged in directly in the gallery.Event class, making the latter
207  be iterable:
208 
209  for event in event: ...
210 
211  (or `for evt in event: ...`).
212  """
213  def __init__(self, event):
214  self._event = event
215  self._index = None
216  def __iter__(self):
217  self._index = None
218  return self
219  def next(self):
220  if self._index is not None:
221  self._event.next()
222  self._index += 1
223  if self._event.atEnd(): raise StopIteration
224  return self._event
225  # __next__
226 
227 # class EventIterator
228 
229 
230 def eventLoop(inputFiles,
231  process,
232  options = {},
233  ):
234  """
235  Applies the `process` function to each and every event from the specified
236  input files, in sequence.
237 
238  The `inputFiles` list may be a single file, or a list (or any iterable object)
239  of files, or a `std::vector<std::string>` object.
240 
241  The `process` callable is executed with two arguments: the number of argument
242  in the loop, and the event itself. No information on which file the event is
243  taken from is provided. If a call returns exactly `False`, it is considered
244  to have failed and an error counter is incremented. Exceptions raised in
245  `process` are not handled.
246 
247  The error counter is returned at the end of the execution.
248 
249  Options:
250  - 'nEvents': number of events to be processed (does not include skipped ones)
251  - 'nSkip': number of events from the beginning of the sample to be skipped
252  """
253 
254  # option reading
255  nSkip = options.get('nSkip', 0)
256  nEvents = options.get('nEvents', None)
257 
258  # make sure the input file list is in the right format
259  if not isinstance(inputFiles, ROOT.vector(ROOT.string)):
260  if isinstance(inputFiles, str): inputFiles = [ inputFiles, ]
261  inputFiles = makeFileList(*inputFiles)
262  # if
263 
264  event = ROOT.gallery.Event(inputFiles)
265 
266  # ROOT.gStyle.SetOptStat(0)
267 
268  iEvent = 0
269  nProcessedEvents = 0
270  nErrors = 0
271 
272  iFile = None
273  for iEvent, event in enumerate(forEach(event)):
274 
275  if iFile != event.fileEntry():
276  iFile = event.fileEntry()
277  print("Opening: '%s'" % inputFiles[iFile])
278  # if new file
279 
280  # event flow control
281  if iEvent < nSkip: continue
282  if (nEvents is not None) and (nProcessedEvents >= nEvents): break
283  nProcessedEvents += 1
284 
285  ###
286  ###
287  ###
288  res = process(event, iEvent)
289  if isinstance(res, bool) and not res: nErrors += 1
290 
291  ###
292  ### all done
293  ###
294 
295  # for
296  if nErrors > 0:
297  print("Encountered %d/%d errors." % (nErrors, nProcessedEvents),file=sys.stderr)
298  return nErrors
299 # eventLoop()
300 
301 
302 
303 # this does not really work...
304 # ROOT.gallery.Event.__iter__ = lambda self: EventIterator(self)
305 
306 
307 ################################################################################
308 ### Infrastructure
309 ################################################################################
310 
311 def findFHiCL(configRelPath, extraDirs = []):
312 
313  if os.path.isfile(configRelPath):
314  return os.path.join(os.getcwd(), str(configRelPath))
315  for path in extraDirs + os.environ.get('FHICL_FILE_PATH', "").split(':'):
316  candidate = os.path.join(path, str(configRelPath))
317  if os.path.isfile(candidate): return candidate
318  else: return None
319 
320 # findFHiCL()
321 
322 
324  pset: "fhicl.ParameterSet object containing the desired configuration table",
325  key: "name of the table to be extracted",
326  defValue: "value returned if the key is not present (None by default)" = None,
327  type_: "type to look for (instead of ROOT.fhicl.ParameterSet)" = None,
328  ) -> "ParameterSet with `key` in `pset`, or `defValue` if not present":
329 
330  try: return pset.get(type_ if type_ else ROOT.fhicl.ParameterSet)(key)
331  except Exception: return defValue
332 
333 # getTableIfPresent()
334 
335 
336 ################################################################################
338  """
339  Provides more user-friendly interface for the configuration access.
340 
341  While FHiCL C++ interface is friendly enough, some of that is lost in Python
342  bindings.
343 
344  This helper class keeps a reference to the FHiCL configuration object, and
345  provides some `get()` overloads to make the interface easier.
346  """
347  def __init__(self, config):
348  self.config = config
349  def get(self, key, default=None, klass=None):
350  """
351  Returns the value associated with the specified key.
352 
353  Parameters
354  -----------
355 
356  key _(string)_
357  the key of the configuration parameter to be read
358  default
359  the value to provide if no key is present; if default is `None`,
360  no default is present and if key is missing a `KeyError` exception will
361  be raised
362  klass _(class type)_
363  type of object returned (if `None`, it's taken from `default` if
364  specified)
365 
366  Returns
367  --------
368 
369  An object of type `klass`, initialised with the value associated to `key`
370  or with `default`.
371 
372  Raises
373  -------
374 
375  `KeyError` if the `key` is not present in the configuration and `default` is
376  specified as `None`.
377 
378  """
379  if klass is None:
380  assert default is not None
381  klass = default.__class__
382  # first try to directly return the key
383  try: return self.config.get[klass](key)
384  except Exception: pass
385  # if that failed, act depending on the default value
386  if default is None: raise KeyError(key)
387  return klass(default)
388  # get()
389 
390  def has(self, key):
391  """
392  Returns whether there is a value associated with the specified key.
393 
394  Parameters
395  -----------
396 
397  key _(string)_
398  the key of the configuration parameter to be read
399 
400  """
401  return self.config.has(klass)
402  # has()
403 
404  __call__ = get # alias
405 
406  def pset(self): return self.config
407 
408 # class ConfigurationHelper
409 
410 
411 def loadConfiguration(configSpec):
412  # this utility actually relies on generic utilities that while not LArSoft
413  # specific, are nevertheless distributed with LArSoft (`larcorealg`).
414  SourceCode.loadHeaderFromUPS("larcorealg/Geometry/StandaloneBasicSetup.h")
415 
416  if isinstance(configSpec, ConfigurationString):
417  import tempfile
418  configFile = tempfile.NamedTemporaryFile("w+")
419  configFile.write(str(configSpec))
420  configFile.flush()
421  configPath = configFile.name
422  else:
423  configFile = None
424  configPath = configSpec
425  # if
426 
427  fullPath = findFHiCL(configPath)
428  if not fullPath:
429  raise RuntimeError("Couldn't find configuration file '%s'" % configPath)
430  return ROOT.lar.standalone.ParseConfiguration(fullPath)
431 # loadConfiguration()
432 
433 
435  """Wrapper to a string that should be considered configuration text."""
436  def __init__(self, config): self.config = config
437  def __str__(self): return self.config
438 # class ConfigurationString
439 
440 
442  def __init__(self, configPath):
443  self.config = loadConfiguration(configPath)
444  def paramsFor(self, FHiCLpath):
445  return self.config.get[ROOT.fhicl.ParameterSet](FHiCLpath)
446  def service(self, serviceName):
447  return self.paramsFor("services." + serviceName)
448  def producer(self, moduleName):
449  return self.paramsFor("physics.producers." + moduleName)
450 # class ConfigurationClass
451 
452 
453 ################################################################################
455  def __init__(self, config):
456  self.fullConfig = config if isinstance(config, ConfigurationClass) else ConfigurationClass(config)
457  self.services = {}
458  # __init__(self)
459 
460  def config(self, serviceName): return self.fullConfig.service(serviceName)
461 
462  def has(self, serviceName): return serviceName in self.services
463 
464  def register(self, serviceName, service):
465  self.services[serviceName] = service
466  return service
467 
468  def registeredServiceNames(self): return self.services.keys()
469 
470  def registry(self): return self # behave like a manager (LArSoftUtils concept)
471 
472  def get(self, serviceName): return self.services[serviceName]
473  def __call__(self, serviceName): return self.get(serviceName)
474 
475  def create(self, serviceName, serviceClass, *otherServiceConstructorArgs):
476  serviceConfig = self.config(serviceName)
477  if not serviceConfig:
478  raise RuntimeError("Couldn't find the configuration for service '%s'" % serviceName)
479  service = serviceClass(serviceConfig, *otherServiceConstructorArgs)
480  if not service:
481  raise RuntimeError("Failed to create service '%s' (type: %s)" % (serviceName, serviceClass.__class__.__name__))
482  return self.register(serviceName, service)
483  # create()
484 
485 # class ServiceRegistryClass
486 
487 
488 ################################################################################
490  """Use it as a function: `startMessageFacility(config, applName)`.
491 
492  `config` either is `ConfigurationClass` object, or it is a parameter set
493  with the configuration for the service.
494  """
495  Init = False
496  def __init__(self, config, applName = None):
497  if not startMessageFacility.Init: self.init(config, applName)
498  def init(self, config, applName):
499  if not applName: applName = os.path.basename(sys.argv[0])
500  if isinstance(config, ConfigurationClass): config = config.service("message")
501  print("Starting message facility for %s..." % applName)
502  ROOT.mf.StartMessageFacility(config, applName)
503  startMessageFacility.Init = True
504  # init()
505 # class startMessageFacility
506 
507 
508 ################################################################################
do one_file $F done echo for F in find $TOP name CMakeLists txt print
auto enumerate(Iterables &&...iterables)
Range-for loop helper tracking the number of iteration.
Definition: enumerate.h:69
std::vector< std::string > expandFileList(std::string const &listPath)
Expands the content of a file list into a vector of file paths (recursive).