Belle II Software  release-06-01-15
misc.py
1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*-
3 
4 
11 
12 """
13 Miscellaneous utility functions for skim experts.
14 """
15 
16 import subprocess
17 import json
18 import re
19 from pathlib import Path
20 
21 from skim.registry import Registry
22 
23 
24 def get_file_metadata(filename):
25  """
26  Retrieve the metadata for a file using ``b2file-metadata-show``.
27 
28  Parameters:
29  metadata (str): File to get number of events from.
30 
31  Returns:
32  dict: Metadata of file in dict format.
33  """
34  if not Path(filename).exists():
35  raise FileNotFoundError(f"Could not find file {filename}")
36 
37  proc = subprocess.run(
38  ["b2file-metadata-show", "--json", str(filename)],
39  stdout=subprocess.PIPE,
40  check=True,
41  )
42  metadata = json.loads(proc.stdout.decode("utf-8"))
43  return metadata
44 
45 
46 def get_eventN(filename):
47  """
48  Retrieve the number of events in a file using ``b2file-metadata-show``.
49 
50  Parameters:
51  filename (str): File to get number of events from.
52 
53  Returns:
54  int: Number of events in the file.
55  """
56  return int(get_file_metadata(filename)["nEvents"])
57 
58 
59 def resolve_skim_modules(SkimsOrModules, *, LocalModule=None):
60  """
61  Produce an ordered list of skims, by expanding any Python skim module names into a
62  list of skims in that module. Also produce a dict of skims grouped by Python module.
63 
64  Raises:
65  RuntimeError: Raised if a skim is listed twice.
66  ValueError: Raised if ``LocalModule`` is passed and skims are normally expected
67  from more than one module.
68  """
69  skims = []
70 
71  for name in SkimsOrModules:
72  if name in Registry.names:
73  skims.append(name)
74  elif name in Registry.modules:
75  skims.extend(Registry.get_skims_in_module(name))
76 
77  duplicates = set([skim for skim in skims if skims.count(skim) > 1])
78  if duplicates:
79  raise RuntimeError(
80  f"Skim{'s'*(len(duplicates)>1)} requested more than once: {', '.join(duplicates)}"
81  )
82 
83  modules = sorted({Registry.get_skim_module(skim) for skim in skims})
84  if LocalModule:
85  if len(modules) > 1:
86  raise ValueError(
87  f"Local module {LocalModule} specified, but the combined skim expects "
88  "skims from more than one module. No steering file written."
89  )
90  modules = {LocalModule.rstrip(".py"): sorted(skims)}
91  else:
92  modules = {
93  module: sorted(
94  [skim for skim in skims if Registry.get_skim_module(skim) == module]
95  )
96  for module in modules
97  }
98 
99  return skims, modules
100 
101 
102 class _hashable_list(list):
103  def __hash__(self):
104  return hash(tuple(self))
105 
106 
107 def _sphinxify_decay(decay_string):
108  """Format the given decay string by using LaTeX commands instead of plain-text.
109  Output is formatted for use with Sphinx (ReStructured Text).
110 
111  This is a utility function for autogenerating skim documentation.
112 
113  Parameters:
114  decay_string (str): A decay descriptor.
115 
116  Returns:
117  sphinxed_string (str): LaTeX version of the decay descriptor.
118  """
119 
120  decay_string = re.sub("^(B.):generic", "\\1_{\\\\text{had}}", decay_string)
121  decay_string = decay_string.replace(":generic", "")
122  decay_string = decay_string.replace(":semileptonic", "_{\\text{SL}}")
123  decay_string = decay_string.replace(":FSP", "_{FSP}")
124  decay_string = decay_string.replace(":V0", "_{V0}")
125  decay_string = re.sub("_[0-9]+", "", decay_string)
126  # Note: these are applied from top to bottom, so if you have
127  # both B0 and anti-B0, put anti-B0 first.
128  substitutes = [
129  ("==>", "\\to"),
130  ("->", "\\to"),
131  ("gamma", "\\gamma"),
132  ("p+", "p"),
133  ("anti-p-", "\\bar{p}"),
134  ("pi+", "\\pi^+"),
135  ("pi-", "\\pi^-"),
136  ("pi0", "\\pi^0"),
137  ("K_S0", "K^0_S"),
138  ("K_L0", "K^0_L"),
139  ("mu+", "\\mu^+"),
140  ("mu-", "\\mu^-"),
141  ("tau+", "\\tau^+"),
142  ("tau-", "\\tau^-"),
143  ("nu", "\\nu"),
144  ("K+", "K^+"),
145  ("K-", "K^-"),
146  ("e+", "e^+"),
147  ("e-", "e^-"),
148  ("J/psi", "J/\\psi"),
149  ("anti-Lambda_c-", "\\Lambda^{-}_{c}"),
150  ("anti-Sigma+", "\\overline{\\Sigma}^{+}"),
151  ("anti-Lambda0", "\\overline{\\Lambda}^{0}"),
152  ("anti-D0*", "\\overline{D}^{0*}"),
153  ("anti-D*0", "\\overline{D}^{0*}"),
154  ("anti-D0", "\\overline{D}^0"),
155  ("anti-B0", "\\overline{B}^0"),
156  ("Sigma+", "\\Sigma^{+}"),
157  ("Lambda_c+", "\\Lambda^{+}_{c}"),
158  ("Lambda0", "\\Lambda^{0}"),
159  ("D+", "D^+"),
160  ("D-", "D^-"),
161  ("D0", "D^0"),
162  ("D*+", "D^{+*}"),
163  ("D*-", "D^{-*}"),
164  ("D*0", "D^{0*}"),
165  ("D_s+", "D^+_s"),
166  ("D_s-", "D^-_s"),
167  ("D_s*+", "D^{+*}_s"),
168  ("D_s*-", "D^{-*}_s"),
169  ("B+", "B^+"),
170  ("B-", "B^-"),
171  ("B0", "B^0"),
172  ("B_s0", "B^0_s"),
173  ("K*0", "K^{0*}"),
174  ]
175  tex_string = decay_string
176  for (key, value) in substitutes:
177  tex_string = tex_string.replace(key, value)
178  return f":math:`{tex_string}`"
179 
180 
181 def fancy_skim_header(SkimClass):
182  """Decorator to generate a fancy header to skim documentation and prepend it to the
183  docstring. Add this just above the definition of a skim.
184 
185  Also ensures the documentation of the template functions like `BaseSkim.build_lists`
186  is not repeated in every skim documentation.
187 
188  .. code-block:: python
189 
190  @fancy_skim_header
191  class MySkimName(BaseSkim):
192  # docstring here describing your skim, and explaining cuts.
193  """
194  SkimName = SkimClass.__name__
195  SkimCode = Registry.encode_skim_name(SkimName)
196  authors = SkimClass.__authors__ or ["(no authors listed)"]
197  description = SkimClass.__description__ or "(no description)"
198  contact = SkimClass.__contact__ or "(no contact listed)"
199  category = SkimClass.__category__ or "(no category listed)"
200 
201  if isinstance(authors, str):
202  # If we were given a string, split it up at: commas, "and", "&", and newlines
203  authors = re.split(
204  r",\s+and\s+|\s+and\s+|,\s+&\s+|\s+&\s+|,\s+|\s*\n\s*", authors
205  )
206  # Strip any remaining whitespace either side of an author's name
207  authors = [re.sub(r"^\s+|\s+$", "", author) for author in authors]
208 
209  if isinstance(category, list):
210  category = ", ".join(category)
211 
212  # If the contact is of the form "NAME <EMAIL>" or "NAME (EMAIL)", then make it a link
213  match = re.match("([^<>()`]+) [<(]([^<>()`]+@[^<>()`]+)[>)]", contact)
214  if match:
215  name, email = match[1], match[2]
216  contact = f"`{name} <mailto:{email}>`_"
217 
218  header = f"""
219  Note:
220  * **Skim description**: {description}
221  * **Skim name**: {SkimName}
222  * **Skim LFN code**: {SkimCode}
223  * **Category**: {category}
224  * **Author{"s"*(len(authors) > 1)}**: {", ".join(authors)}
225  * **Contact**: {contact}
226  """
227 
228  if SkimClass.ApplyHLTHadronCut:
229  HLTLine = "*This skim includes a selection on the HLT flag* ``hlt_hadron``."
230  header = f"{header.rstrip()}\n\n {HLTLine}\n"
231 
232  if SkimClass.__doc__:
233  SkimClass.__doc__ = header + "\n\n" + SkimClass.__doc__.lstrip("\n")
234  else:
235  # Handle case where docstring is empty, or was not redefined
236  SkimClass.__doc__ = header
237 
238  # If documentation of template functions not redefined, make sure BaseSkim docstring is not repeated
239  SkimClass.load_standard_lists.__doc__ = SkimClass.load_standard_lists.__doc__ or ""
240  SkimClass.build_lists.__doc__ = SkimClass.build_lists.__doc__ or ""
241  SkimClass.validation_histograms.__doc__ = (
242  SkimClass.validation_histograms.__doc__ or ""
243  )
244  SkimClass.additional_setup.__doc__ = SkimClass.additional_setup.__doc__ or ""
245 
246  return SkimClass
247 
248 
249 def dry_run_steering_file(SteeringFile):
250  """
251  Check if the steering file at the given path can be run with the "--dry-run" option.
252  """
253  proc = subprocess.run(
254  ["basf2", "--dry-run", "-i", "i.root", "-o", "o.root", str(SteeringFile)],
255  stderr=subprocess.PIPE,
256  stdout=subprocess.PIPE,
257  )
258 
259  if proc.returncode != 0:
260  stdout = proc.stdout.decode("utf-8")
261  stderr = proc.stderr.decode("utf-8")
262 
263  raise RuntimeError(
264  f"An error occured while dry-running steering file {SteeringFile}\n"
265  f"Script output:\n{stdout}\n{stderr}"
266  )