Belle II Software  release-08-01-10
test_support.py
1 
8 
9 import os
10 import random
11 import shutil
12 import subprocess
13 import sys
14 from glob import glob
15 
16 import b2test_utils
17 import basf2
18 import generators
19 from simulation import add_simulation
20 from rawdata import add_packers
21 from softwaretrigger import constants
22 from softwaretrigger.constants import DEFAULT_EXPRESSRECO_COMPONENTS, RAWDATA_OBJECTS, DEFAULT_HLT_COMPONENTS
23 from ROOT import Belle2
24 
25 
26 class CheckForCorrectHLTResults(basf2.Module):
27  """Test module for assuring correct data store content"""
28 
29  def event(self):
30  """reimplementation of Module::event()."""
31  sft_trigger = Belle2.PyStoreObj("SoftwareTriggerResult")
32 
33  if not sft_trigger.isValid():
34  basf2.B2FATAL("SoftwareTriggerResult object not created")
35  elif len(sft_trigger.getResults()) == 0:
36  basf2.B2FATAL("SoftwareTriggerResult exists but has no entries")
37 
38  if not Belle2.PyStoreArray("ROIs").isValid():
39  basf2.B2FATAL("ROIs are not present")
40 
41 
42 def get_file_name(base_path, run_type, location, passthrough, simulate_events_of_doom_buster):
43  mode = ""
44  if passthrough:
45  mode += "_passthrough"
46  if simulate_events_of_doom_buster:
47  mode += "_eodb"
48  return os.path.join(base_path, f"{location.name}_{run_type.name}{mode}.root")
49 
50 
51 def generate_input_file(run_type, location, output_file_name, exp_number, passthrough,
52  simulate_events_of_doom_buster):
53  """
54  Generate an input file for usage in the test.
55  Simulate uubar for "beam" and two muons for "cosmic" setting.
56 
57  Only raw data will be stored to the given output file.
58  :param run_type: Whether to simulate cosmic or beam
59  :param location: Whether to simulate expressreco (with ROIs) or hlt (no PXD)
60  :param output_file_name: where to store the result file
61  :param exp_number: which experiment number to simulate
62  :param passthrough: if true don't generate a trigger result in the input file
63  :param simulate_events_of_doom_buster: if true, simulate the effect of the
64  EventsOfDoomBuster module by inflating the number of CDC hits
65  """
66  if os.path.exists(output_file_name):
67  return 1
68 
69  basf2.set_random_seed(12345)
70 
71  path = basf2.Path()
72  path.add_module('EventInfoSetter', evtNumList=[4], expList=[exp_number])
73 
74  if run_type == constants.RunTypes.beam:
75  generators.add_continuum_generator(path, finalstate="uubar")
76  elif run_type == constants.RunTypes.cosmic:
77  # add something which looks a tiny bit like a cosmic generator. We
78  # cannot use the normal cosmic generator as that needs a bigger
79  # simulation top volume than the default geometry from the database.
80  path.add_module("ParticleGun", pdgCodes=[-13, 13], momentumParams=[10, 200])
81 
82  add_simulation(path, usePXDDataReduction=(location == constants.Location.expressreco))
83 
84  # inflate the number of CDC hits in order to later simulate the effect of the
85  # EventsOfDoomBuster module
86  if simulate_events_of_doom_buster:
87 
88  class InflateCDCHits(basf2.Module):
89  """Artificially inflate the number of CDC hits."""
90 
91  def initialize(self):
92  """Initialize."""
93  self.cdc_hits = Belle2.PyStoreArray("CDCHits")
94  self.cdc_hits.isRequired()
95  eodb_parameters = Belle2.PyDBObj("EventsOfDoomParameters")
96  if not eodb_parameters.isValid():
97  basf2.B2FATAL("EventsOfDoomParameters is not valid")
98  self.cdc_hits_threshold = eodb_parameters.getNCDCHitsMax() + 1
99 
100  def event(self):
101  """Event"""
102  if self.cdc_hits.isValid():
103  # Let's simply append a (default) CDC hit multiple times
104  for i in range(self.cdc_hits_threshold):
105  self.cdc_hits.appendNew()
106 
107  path.add_module(InflateCDCHits())
108 
109  if location == constants.Location.hlt:
110  components = DEFAULT_HLT_COMPONENTS
111  elif location == constants.Location.expressreco:
112  components = DEFAULT_EXPRESSRECO_COMPONENTS
113  else:
114  basf2.B2FATAL(f"Location {location.name} for test is not supported")
115 
116  components.append("TRG")
117 
118  add_packers(path, components=components)
119 
120  # express reco expects to have an HLT results, so lets add a fake one
121  if location == constants.Location.expressreco and not passthrough:
122  class FakeHLTResult(basf2.Module):
123  def initialize(self):
124  self.results = Belle2.PyStoreObj(Belle2.SoftwareTriggerResult.Class(), "SoftwareTriggerResult")
125  self.results.registerInDataStore()
126 
127  self.EventMetaData = Belle2.PyStoreObj("EventMetaData")
128 
129  def event(self):
130  self.results.create()
131  # First event: Add all the results that are used on express reco just to test all paths
132  if (self.EventMetaData.obj().getEvent() == 1):
133  self.results.addResult("software_trigger_cut&all&total_result", 1)
134  self.results.addResult("software_trigger_cut&skim&accept_mumutight", 1)
135  self.results.addResult("software_trigger_cut&skim&accept_dstar_1", 1)
136  self.results.addResult("software_trigger_cut&filter&L1_trigger", 1)
137  # Second event: No skim lines to replicate a HLT discared event with filter ON
138  elif (self.EventMetaData.obj().getEvent() == 2):
139  self.results.addResult("software_trigger_cut&all&total_result", 1)
140  self.results.addResult("software_trigger_cut&filter&L1_trigger", 1)
141  # Third event: Does not pass through L1 passthrough
142  elif (self.EventMetaData.obj().getEvent() == 3):
143  self.results.addResult("software_trigger_cut&all&total_result", 1)
144  self.results.addResult("software_trigger_cut&skim&accept_mumutight", 1)
145  self.results.addResult("software_trigger_cut&skim&accept_dstar_1", 1)
146  self.results.addResult("software_trigger_cut&filter&L1_trigger", 0)
147  # Fourth event: HLT discarded but passes HLT skims (possible in HLT filter OFF mode)
148  elif (self.EventMetaData.obj().getEvent() == 4):
149  self.results.addResult("software_trigger_cut&all&total_result", 0)
150  self.results.addResult("software_trigger_cut&skim&accept_mumutight", 1)
151  self.results.addResult("software_trigger_cut&skim&accept_dstar_1", 1)
152  self.results.addResult("software_trigger_cut&filter&L1_trigger", 0)
153 
154  path.add_module(FakeHLTResult())
155 
156  # remove everything but HLT input raw objects
157  branch_names = RAWDATA_OBJECTS + ["EventMetaData", "TRGSummary"]
158  if not passthrough:
159  branch_names += ["SoftwareTriggerResult"]
160  if location == constants.Location.hlt:
161  branch_names.remove("RawPXDs")
162  branch_names.remove("ROIs")
163 
164  # There is no packer for the following objects :(
165  branch_names.remove("RawTRGs")
166 
167  path.add_module("RootOutput", outputFileName=output_file_name, branchNames=branch_names)
168 
169  basf2.process(path)
170 
171  return 0
172 
173 
174 def test_script(script_location, input_file_name, temp_dir):
175  """
176  Test a script with the given file path using the given input file.
177  Raises an exception if the execution fails or if the needed output is not in
178  the output file.
179  The results are stored in the temporary directory location.
180 
181  :param script_location: the script to test
182  :param input_file_name: the file path of the input file
183  :param temp_dir: where to store and run
184  """
185  input_buffer = "UNUSED" # unused
186  output_buffer = "UNUSED" # unused
187  histo_port = 6666 # unused
188 
189  random_seed = "".join(random.choices("abcdef", k=4))
190 
191  histos_file_name = f"{random_seed}_histos.root"
192  output_file_name = os.path.join(temp_dir, f"{random_seed}_output.root")
193  # TODO: should we use the default global tag here?
194  globaltags = list(basf2.conditions.default_globaltags)
195  num_processes = 1
196 
197  # Because the script name is hard-coded in the run control GUI,
198  # we must jump into the script directory
199  cwd = os.getcwd()
200  os.chdir(os.path.dirname(script_location))
201  cmd1 = [sys.executable, script_location, "--central-db-tag"] + globaltags + [
202  "--input-file", os.path.abspath(input_file_name),
203  "--histo-output-file", os.path.join(temp_dir, f"{histos_file_name}"),
204  "--output-file", os.path.abspath(output_file_name),
205  "--number-processes", str(num_processes),
206  input_buffer, output_buffer, str(histo_port)
207  ]
208  subprocess.check_call(cmd1)
209 
210  # Move the output file with DQM histograms under the expected location:
211  # for reasons we don't want to know, they are saved under the current directory
212  # even if a valid and existing working directory is specified
213  if os.path.exists(histos_file_name):
214  final_histos_file_name = os.path.join(temp_dir, histos_file_name)
215  shutil.copy(histos_file_name, os.path.join(temp_dir, final_histos_file_name))
216  os.unlink(histos_file_name)
217 
218  # Go back to the original directory for safety
219  os.chdir(cwd)
220 
221  if "expressreco" not in script_location and "beam_reco" in script_location:
222  # Check the integrity of HLT result
223  test_path = basf2.Path()
224  test_path.add_module("RootInput", inputFileName=output_file_name)
225  test_path.add_module(CheckForCorrectHLTResults())
226  assert(b2test_utils.safe_process(test_path) == 0)
227  # Check the size of DQM histograms
228  cmd2 = ["hlt-check-dqm-size", final_histos_file_name]
229  subprocess.check_call(cmd2)
230 
231 
232 def test_folder(location, run_type, exp_number, phase, passthrough=False,
233  simulate_events_of_doom_buster=False):
234  """
235  Run all hlt operation scripts in a given folder
236  and test the outputs of the files.
237 
238  Will call the test_script function on all files in the folder given
239  by the location after having created a suitable input file with the given
240  experiment number.
241 
242  :param location: hlt or expressreco, depending on which operation files to run
243  and which input to simulate
244  :param run_type: cosmic or beam, depending on which operation files to run
245  and which input to simulate
246  :param exp_number: which experiment number to simulate
247  :param phase: where to look for the operation files (will search in the folder
248  hlt/operation/{phase}/global/{location}/evp_scripts/)
249  :param passthrough: only relevant for express reco: If true don't create a
250  software trigger result in the input file to test running
251  express reco if hlt is in passthrough mode
252  :param simulate_events_of_doom_buster: if true, simulate the effect of the
253  EventsOfDoomBuster module by inflating the number of CDC hits
254  """
255 
256  # The test is already run in a clean, temporary directory
257  temp_dir = os.getcwd()
258  prepare_path = os.environ["BELLE2_PREPARE_PATH"]
259  input_file_name = get_file_name(
260  prepare_path, run_type, location, passthrough, simulate_events_of_doom_buster)
261  # generate_input_file(run_type=run_type, location=location,
262  # output_file_name=output_file_name, exp_number=exp_number,
263  # passthrough=passthrough,
264  # simulate_events_of_doom_buster=simulate_events_of_doom_buster)
265 
266  script_dir = basf2.find_file(f"hlt/operation/{phase}/global/{location.name}/evp_scripts/")
267  run_at_least_one = False
268  for script_location in glob(os.path.join(script_dir, f"run_{run_type.name}*.py")):
269  run_at_least_one = True
270  test_script(script_location, input_file_name=input_file_name, temp_dir=temp_dir)
271 
272  assert run_at_least_one
Class to access a DBObjPtr from Python.
Definition: PyDBObj.h:50
A (simplified) python wrapper for StoreArray.
Definition: PyStoreArray.h:72
a (simplified) python wrapper for StoreObjPtr.
Definition: PyStoreObj.h:67
def safe_process(*args, **kwargs)
Definition: __init__.py:236
def add_continuum_generator(path, finalstate, userdecfile='', *skip_on_failure=True, eventType='')
Definition: generators.py:349