1"""! Tools that can be used in HPSMC jobs."""
10from subprocess
import PIPE
18 Run the SLIC Geant4 simulation.
20 Optional parameters are: **nevents**, **macros**, **run_number**, **disable_particle_table** \n
21 Required parameters are: **detector** \n
22 Required configurations are: **slic_dir**, **detector_dir**
38 self, name=
"slic", command=
"slic", output_ext=
".slcio", **kwargs
43 Setup command arguments.
44 @return list of arguments
47 raise Exception(
"No inputs given for SLIC.")
62 args.extend([
"-m",
"run_number.mac"])
66 if os.path.exists(tbl):
67 args.extend([
"-P", tbl])
69 raise Exception(
"SLIC particle.tbl does not exist: %s" % tbl)
74 if macro ==
"run_number.mac":
75 raise Exception(
"Macro name '%s' is not allowed." % macro)
76 if not os.path.isabs(macro):
77 raise Exception(
"Macro '%s' is not an absolute path." % macro)
78 args.extend([
"-m", macro])
85 """! Return path to detector file."""
89 """! Return path to particle table."""
90 return os.path.join(self.
slic_dir,
"share",
"particle.tbl")
93 """! Configure SLIC component."""
99 raise Exception(
"Failed to find valid detector_dir")
101 "Using detector_dir from install: {}".format(self.
detector_dir)
105 """! Setup SLIC component."""
106 if not os.path.exists(self.
slic_dir):
107 raise Exception(
"slic_dir does not exist: %s" % self.
slic_dir)
111 raise Exception(
"SLIC setup script does not exist: %s" % self.
namename)
114 run_number_cmd =
"/lcio/runNumber %d" % self.
run_number
115 run_number_mac = open(
"run_number.mac",
"w")
116 run_number_mac.write(run_number_cmd)
117 run_number_mac.close()
121 Return list of optional parameters.
123 Optional parameters are: **nevents**, **macros**, **run_number**
124 @return list of optional parameters
126 return [
"nevents",
"macros",
"run_number",
"disable_particle_table"]
130 Return list of required parameters.
132 Required parameters are: **detector**
133 @return list of required parameters
139 Return list of required configurations.
141 Required configurations are: **slic_dir**, **detector_dir**
142 @return list of required configurations
144 return [
"slic_dir",
"detector_dir"]
148 Execute SLIC component.
150 Component is executed by creating command line input
151 from command and command arguments.
152 @return return code of process
155 cl =
'bash -c ". %s && %s %s"' % (
162 proc = subprocess.Popen(cl, shell=
True, stdout=log_out, stderr=log_err)
166 return proc.returncode
171 Copy the SQLite database file to the desired location.
176 Initialize SQLiteProc to copy the SQLite file.
178 This component simply copies source_file to destination_file (see execute); it does not run a command,
179 so no command arguments are assembled here. Logging is deferred until after Component.__init__ has run,
180 as required by the Component base class.
186 Component.__init__(self, name=
"sqlite_file_copy", **kwargs)
190 Return dummy command arguments to satisfy the parent class.
192 cmd_args = [
"(no-command-needed)"]
194 if not all(isinstance(arg, str)
for arg
in cmd_args):
195 raise ValueError(
"All arguments must be strings.")
201 Execute the file copy operation.
208 f
"Copying file from {self.source_file} to {self.destination_file}"
213 os.makedirs(
"tmp", exist_ok=
True)
216 self.
logger.info(f
"Successfully copied file to {self.destination_file}")
220 except Exception
as e:
221 self.
logger.error(f
"Error during file copy: {e}")
227 Run the hps-java JobManager class.
229 Input files have slcio format.
231 Required parameters are: **steering_files** \n
232 Optional parameters are: **detector**, **run_number**, **defs**
264 if "overlay_file" in kwargs:
273 description=
"HPS Java Job Manager",
282 "Append token for '%s' automatically set to '%s' from steering key."
287 """! Configure JobManager component."""
291 if os.getenv(
"HPS_JAVA_BIN_JAR",
None)
is not None:
294 "Set HPS_JAVA_BIN_JAR from environment: {}".format(
300 "hps_java_bin_jar not set in environment or config file!"
303 if os.getenv(
"CONDITIONS_URL",
None)
is not None:
306 "Set CONDITIONS_URL from environment: {}".format(
313 Return list of required configurations.
315 Required configurations are: **hps_java_bin_jar**
316 @retun list of required configurations.
318 return [
"hps_java_bin_jar"]
321 """! Setup JobManager component."""
323 raise Exception(
"No inputs provided to hps-java.")
333 Setup command arguments.
334 @return list of arguments
339 self.
logger.debug(
"Setting java_args from config: %s" % self.
java_args)
360 self.
logger.debug(
"Setting conditions_password from config (not shown)")
386 args.append(
"outputFile=" + os.path.splitext(self.
output_files()[0])[0])
389 for k, v
in self.
defs.items():
391 args.append(k +
"=" + str(v))
396 "Steering does not exist at '%s' so assuming it is a resource."
402 "Steering looks like a file but is not an abs path: %s"
413 args.append(input_file)
417 args.append(
"overlayFile=" + os.path.splitext(self.
overlay_file)[0])
423 Return list of required parameters.
425 Required parameters are: **steering_files**
426 @return list of required parameters
428 return [
"steering_files"]
432 Return list of optional parameters.
434 Optional parameters are: **detector**, **run_number**, **defs**
435 @return list of optional parameters
437 return [
"detector",
"run_number",
"defs",
"nevents"]
442 Run the make_mini_dst command on the input file.
444 Required parameters are: **input_file**
445 Required configs are: **minidst_install_dir**
450 Initialize ProcessMiniDst with default input file and the command to run.
455 Component.__init__(self, name=
'make_mini_dst',
456 command=
'make_mini_dst',
457 description=
'Create the MiniDST ROOT file',
462 """! Setup the MiniDST component."""
465 raise Exception(
"No input files provided to make_mini_dst.")
472 Return list of required parameters.
474 Required parameters are only the standard "input_files".
475 @return list of required parameters
481 Return list of optional parameters.
483 There are currently no optional parameters.
484 @return list of optional parameters
490 Return list of required configs.
492 Required configs are: **minidst_install_dir**
493 @return list of required configs
495 return [
"minidst_install_dir"]
498 """! Adjust names of output files."""
502 print(f
"Set outputs to: {self.outputs}")
508 Setup command arguments for make_mini_dst.
509 @return list of arguments
513 print(
"===== Make MiniDST with input files: ", end=
" ")
515 print(f
"{self.input_files()[i]}", end=
", ")
516 print(f
" ==> {self.output_files()}")
528 Run the hpstr analysis tool.
530 Required parameters are: **config_files** \n
531 Optional parameters are: **year**, **is_data**, **nevents** \n
532 Required configs are: **hpstr_install_dir**, **hpstr_base**
535 def __init__(self, cfg=None, is_data=0, year=None, tracking=None, **kwargs):
548 Component.__init__(self, name=
"hpstr", command=
"hpstr", **kwargs)
551 """! Setup HPSTR component."""
566 if len(os.path.dirname(config_file)):
568 if os.path.isabs(config_file):
573 "The config has a directory but is not an abs path: %s" % self.
cfg
578 self.
hpstr_base,
"processors",
"config", config_file
583 if os.path.splitext(self.
input_files()[0])[1] ==
".root":
591 Return list of required parameters.
593 Required parameters are: **config_files**
594 @return list of required parameters
596 return [
"config_files"]
600 Return list of optional parameters.
602 Optional parameters are: **year**, **is_data**, **nevents**
603 @return list of optional parameters
605 return [
"year",
"is_data",
"nevents",
"tracking"]
609 Return list of required configs.
611 Required configs are: **hpstr_install_dir**, **hpstr_base**
612 @return list of required configs
614 return [
"hpstr_install_dir",
"hpstr_base"]
618 Setup command arguments.
619 @return list of arguments
632 if self.
year is not None:
633 args.extend([
"-y", str(self.
year)])
635 args.extend([
"-w", str(self.
tracking)])
639 """! Adjust names of output files."""
642 return [
"%s.root" % f]
649 """! Execute HPSTR component."""
651 cl =
'bash -c ". %s && %s %s"' % (
658 proc = subprocess.Popen(cl, shell=
True, stdout=log_out, stderr=log_err)
662 return proc.returncode
670 Generic class for StdHep tools.
678 "lhe_tridents_displacetime",
679 "lhe_tridents_displaceuni",
687 Component.__init__(self, name=name, command=
"stdhep_" + name, **kwargs)
691 Setup command arguments.
692 @return list of arguments
696 if self.
name in StdHepTool.seed_names:
702 raise Exception(
"Too many outputs specified for StdHepTool.")
704 raise Exception(
"No outputs specified for StdHepTool.")
707 for i
in self.
inputs[::-1]:
710 raise Exception(
"No inputs specified for StdHepTool.")
717 Transform StdHep events into beam coordinates.
719 Optional parameters are: **beam_sigma_x**, **beam_sigma_y**, **beam_rot_x**,
720 **beam_rot_y**, **beam_rot_z**, **target_x**, **target_y**, **target_z**
742 StdHepTool.__init__(self, name=
"beam_coords", append_tok=
"rot", **kwargs)
746 Setup command arguments.
747 @return list of arguments
749 args = StdHepTool.cmd_args(self)
764 args.extend([
"-X", str(self.
target_x)])
766 args.extend([
"-Y", str(self.
target_y)])
768 args.extend([
"-Z", str(self.
target_z)])
774 Return list of optional parameters.
776 Optional parameters are: **beam_sigma_x**, **beam_sigma_y**, **beam_rot_x**,
777 **beam_rot_y**, **beam_rot_z**, **target_x**, **target_y**, **target_z**
778 @return list of optional parameters
794 Randomly sample StdHep events into a new file.
796 Optional parameters are: **nevents**, **mu**
800 StdHepTool.__init__(self, name=
"random_sample", append_tok=
"sampled", **kwargs)
806 Setup command arguments.
807 @return list of arguments
811 if self.
name in StdHepTool.seed_names:
814 args.extend([
"-N", str(1)])
819 if self.
mu is not None:
820 args.extend([
"-m", str(self.
mu)])
824 args.insert(0, os.path.splitext(self.
output_files()[0])[0])
826 raise Exception(
"Too many outputs specified for RandomSample.")
828 raise Exception(
"No outputs specified for RandomSample.")
831 for i
in self.
inputs[::-1]:
834 raise Exception(
"No inputs were provided.")
840 Return list of optional parameters.
842 Optional parameters are: **nevents**, **mu**
843 @return list of optional parameters
845 return [
"nevents",
"mu"]
848 """! Execute RandomSample component"""
849 returncode = Component.execute(self, log_out, log_err)
852 src =
"%s_1.stdhep" % os.path.splitext(self.
output_files()[0])[0]
853 dest =
"%s.stdhep" % os.path.splitext(self.
output_files()[0])[0]
854 self.
logger.debug(
"Moving '%s' to '%s'" % (src, dest))
855 shutil.move(src, dest)
862 Convert LHE files to StdHep.
868 StdHepTool.__init__(self,
870 output_ext=
'.stdhep',
875 Setup command arguments.
876 @return list of arguments
878 args = StdHepTool.cmd_args(self)
884 Convert LHE files to StdHep, displacing the time by given ctau.
886 Optional parameters are: **ctau**
893 self, name=
"lhe_tridents_displacetime", output_ext=
".stdhep", **kwargs
898 Setup command arguments.
899 @return list of arguments
901 args = StdHepTool.cmd_args(self)
902 if self.
ctau is not None:
903 args.extend([
"-l", str(self.
ctau)])
908 Return list of optional parameters.
910 Optional parameters are: **ctau**
911 @return list of optional parameters
918 Convert LHE files to StdHep, displacing the time by given ctau.
920 Optional parameters are: **ctau**
927 self, name=
"lhe_tridents_displaceuni", output_ext=
".stdhep", **kwargs
932 Setup command arguments.
933 @return list of arguments
935 args = StdHepTool.cmd_args(self)
936 if self.
ctau is not None:
937 args.extend([
"-l", str(self.
ctau)])
942 Return list of optional parameters.
944 Optional parameters are: **ctau**
945 @return list of optional parameters
952 Add mother particles for physics samples.
956 StdHepTool.__init__(self, name=
"add_mother", append_tok=
"mom", **kwargs)
960 """! Add full truth mother particles for physics samples"""
964 self,
"add_mother_full_truth", append_tok=
"mom_full_truth", **kwargs
968 "Must have 2 input files: a stdhep file and a lhe file in order"
973 raise Exception(
"The first input file must be a stdhep file")
977 raise Exception(
"The second input file must be a lhe file")
981 Setup command arguments.
982 @return list of arguments
989 Merge StdHep files, applying poisson sampling.
991 Required parameters are: **target_thickness**, **num_electrons**
1002 StdHepTool.__init__(self, name=
"merge_poisson", append_tok=
"sampled", **kwargs)
1005 """! Setup MergePoisson component."""
1009 raise Exception(
"Cross section is missing.")
1010 self.
logger.info(
"mu is %f", self.
mu)
1014 Return list of required parameters.
1016 Required parameters are: **target_thickness**, **num_electrons**
1017 @return list of required parameters
1019 return [
"target_thickness",
"num_electrons"]
1023 Setup command arguments.
1024 @return list of arguments
1027 if self.
name in StdHepTool.seed_names:
1034 args.insert(0, os.path.splitext(self.
output_files()[0])[0])
1036 raise Exception(
"Too many outputs specified for MergePoisson.")
1038 raise Exception(
"No outputs specified for MergePoisson.")
1041 for i
in self.
inputs[::-1]:
1044 raise Exception(
"No inputs were provided.")
1049 """! Execute MergePoisson component."""
1050 returncode = Component.execute(self, log_out, log_err)
1053 src =
"%s_1.stdhep" % os.path.splitext(self.
output_files()[0])[0]
1054 dest =
"%s.stdhep" % os.path.splitext(self.
output_files()[0])[0]
1055 self.
logger.debug(
"Moving '%s' to '%s'" % (src, dest))
1056 shutil.move(src, dest)
1065 Optional parameters are: none \n
1066 Required parameters are: none
1070 StdHepTool.__init__(self, name=
"merge_files", **kwargs)
1074 Return list of optional parameters.
1076 Optional parameters are: none
1077 @return list of optional parameters
1083 Return list of required parameters.
1085 Required parameters are: none
1086 @return list of required parameters
1093 Count number of events in a StdHep file.
1098 self, name=
"stdhep_count", command=
"stdhep_count.sh", **kwargs
1103 Setup command arguments.
1104 @return list of arguments
1110 """! Execute StdHepCount component."""
1113 proc = subprocess.Popen(cl, stdout=PIPE)
1114 (output, err) = proc.communicate()
1116 nevents = int(output.split()[1])
1117 print(
"StdHep file '%s' has %d events." % (self.
input_files()[0], nevents))
1119 return proc.returncode
1124 Generic base class for Java based tools.
1134 Component.__init__(self, name,
"java", **kwargs)
1138 Return list of required config.
1140 Required config are: **hps_java_bin_jar**
1141 @return list of required config
1143 return [
"hps_java_bin_jar"]
1147 Setup command arguments.
1148 @return list of arguments
1152 self.
logger.debug(
"Setting java_args from config: %s" + self.
java_args)
1170 Convert EVIO events to LCIO using the hps-java EvioToLcio command line tool.
1172 Input files have evio format (format used by DAQ system).
1174 Required parameters are: **detector**, **steering_files** \n
1175 Optional parameters are: **run_number**, **skip_events**, **nevents**, **event_print_interval**
1192 name=
"evio_to_lcio",
1193 java_class=
"org.hps.evio.EvioToLcio",
1194 output_ext=
".slcio",
1200 Return list of required parameters.
1202 Required parameters are: **detector**, **steering_files**
1203 @return list of required parameters
1205 return [
"detector",
"steering_files"]
1209 Return list of optional parameters.
1211 Optional parameters are: **run_number**, **skip_events**, **nevents**, **event_print_interval**
1212 @return list of optional parameters
1214 return [
"run_number",
"skip_events",
"nevents",
"event_print_interval"]
1217 """! Setup EvioToLcio component."""
1227 Setup command arguments.
1228 @return list of arguments
1230 args = JavaTool.cmd_args(self)
1232 raise Exception(
"No output files were provided.")
1235 args.append(
"-Djava.io.tmpdir=./tmp")
1236 args.append(
"-DoutputFile=%s" % os.path.splitext(output_file)[0])
1241 args.append(
"-Dorg.hps.conditions.url=jdbc:sqlite:./hps_local_conditions.db")
1251 "Steering does not exist at '%s' so assuming it is a resource."
1257 "Steering looks like a file but is not an abs path: %s"
1268 args.append(inputfile)
1278 Space MC events and apply energy filters to process before readout.
1280 Optional parameters are: **filter_ecal_hit_ecut**, **filter_event_interval**,
1281 **filter_nevents_read**, **filter_nevents_write**, **filter_no_cuts** \n
1282 Required config are: **hps_java_bin_jar**
1286 if "filter_no_cuts" in kwargs:
1292 if "filter_ecal_pairs" in kwargs:
1297 if "filter_ecal_hit_ecut" in kwargs:
1304 if "filter_event_interval" in kwargs:
1310 if "filter_nevents_read" in kwargs:
1316 if "filter_nevents_write" in kwargs:
1326 name=
"filter_bunches",
1327 java_class=
"org.hps.util.FilterMCBunches",
1333 """! Configure FilterBunches component."""
1336 if os.getenv(
"HPS_JAVA_BIN_JAR",
None)
is not None:
1339 "Set HPS_JAVA_BIN_JAR from environment: {}".format(
1346 Setup command arguments.
1347 @return list of arguments
1349 args = JavaTool.cmd_args(self)
1372 Return list of optional parameters.
1374 Optional parameters are: **filter_ecal_hit_ecut**, **filter_event_interval**,
1375 **filter_nevents_read**, **filter_nevents_write**, **filter_no_cuts** \n
1376 @return list of optional parameters
1379 "filter_ecal_hit_ecut",
1380 "filter_event_interval",
1381 "filter_nevents_read",
1382 "filter_nevents_write",
1388 Return list of required config.
1390 Required config are: **hps_java_bin_jar**
1391 @return list of required config
1393 return [
"hps_java_bin_jar"]
1398 Apply hodo-hit filter and space MC events to process before readout.
1400 The nevents parameter is not settable from JSON in this class. It should
1401 be supplied as an init argument in the job script if it needs to be
1402 customized (the default nevents and event_interval used to apply spacing
1403 should usually not need to be changed by the user). \n
1405 Optional parameters are: **num_hodo_hits**, **event_interval**
1409 if "num_hodo_hits" in kwargs:
1414 if "event_interval" in kwargs:
1421 name=
"filter_events",
1422 java_class=
"org.hps.util.ExtractEventsWithHitAtHodoEcal",
1429 Setup command arguments.
1430 @return list of arguments
1432 args = JavaTool.cmd_args(self)
1448 Return list of optional parameters.
1450 Optional parameters are: **num_hodo_hits**, **event_interval**
1451 @return list of optional parameters
1453 return [
"num_hodo_hits",
"event_interval"]
1458 Unzip the input files to outputs.
1462 Component.__init__(self, name=
"unzip", command=
"gunzip", **kwargs)
1465 """! Return list of output files."""
1468 return [os.path.splitext(i)[0]
for i
in self.
input_files()]
1471 """! Execute Unzip component."""
1475 with gzip.open(inputfile,
"rb")
as in_file, open(
1478 shutil.copyfileobj(in_file, out_file)
1479 self.
logger.debug(
"Unzipped '%s' to '%s'" % (inputfile, outputfile))
1485 Dump LCIO event information.
1487 Required parameters are: none \n
1488 Required config are: **lcio_dir**
1494 Component.__init__(self, name=
"lcio_dump_event", command=
"dumpevent", **kwargs)
1496 if "event_num" in kwargs:
1502 """! Configure LCIODumpEvent component."""
1508 """! Setup LCIODumpEvent component."""
1513 Setup command arguments.
1514 @return list of arguments
1517 raise Exception(
"Missing required inputs for LCIODumpEvent.")
1525 Return list of required config.
1527 Required config are: **lcio_dir**
1528 @return list of required config
1534 Return list of required parameters.
1536 Required parameters are: none
1537 @return list of required parameters
1544 Count events in an LHE file.
1547 def __init__(self, minevents=0, fail_on_underflow=False, **kwargs):
1549 Component.__init__(self, name=
"lhe_count", **kwargs)
1552 """! Setup LHECount component."""
1554 raise Exception(
"Missing at least one input file.")
1558 Check if command exists.
1559 @return True if command exists
1564 """! Execute LHECount component."""
1566 with gzip.open(i,
"rb")
as in_file:
1567 lines = in_file.readlines()
1571 if "<event>" in line:
1574 print(
"LHE file '%s' has %d events." % (i, nevents))
1577 msg =
"LHE file '%s' does not contain the minimum %d events." % (
1581 if self.fail_on_underflow:
1582 raise Exception(msg)
1590 Tar files into an archive.
1594 Component.__init__(self, name=
"tar_files", **kwargs)
1598 Check if command exists.
1599 @return True if command exists
1604 """! Execute TarFiles component."""
1605 self.
logger.debug(
"Opening '%s' for writing ..." % self.
outputs[0])
1606 tar = tarfile.open(self.
outputs[0],
"w")
1608 self.
logger.debug(
"Adding '%s' to archive" % i)
1617 Move input files to new locations.
1621 Component.__init__(self, name=
"move_files", **kwargs)
1625 Check if command exists.
1626 @return True if command exists
1631 """! Execute TarFiles component."""
1633 raise Exception(
"Input and output lists are not the same length!")
1637 self.
logger.info(
"Moving %s -> %s" % (src, dest))
1638 shutil.move(src, dest)
1644 Generic component for LCIO tools.
1646 Required parameters are: none \n
1647 Required config are: **lcio_bin_jar**
1653 Component.__init__(self, name, command=
"java", **kwargs)
1656 """! Configure LCIOTool component."""
1663 Setup command arguments.
1664 @return list of arguments
1667 raise Exception(
"Name required to write cmd args for LCIOTool.")
1672 Return list of required config.
1674 Required config are: **lcio_bin_jar**
1675 @return list of required config
1677 return [
"lcio_bin_jar"]
1681 Return list of required parameters.
1683 Required parameters are: none
1684 @return list of required parameters
1691 Concatenate LCIO files together.
1695 LCIOTool.__init__(self, name=
"concat", **kwargs)
1699 Setup command arguments.
1700 @return list of arguments
1702 args = LCIOTool.cmd_args(self)
1704 raise Exception(
"Missing at least one input file.")
1706 raise Exception(
"Missing an output file.")
1708 args.extend([
"-f", i])
1709 args.extend([
"-o", self.
outputs[0]])
1715 Count events in LCIO files.
1717 Required parameters are: none \n
1718 Optional parameters are: none
1722 LCIOTool.__init__(self, name=
"count", **kwargs)
1726 Setup command arguments.
1727 @return list of arguments
1729 args = LCIOTool.cmd_args(self)
1731 raise Exception(
"Missing an input file.")
1737 Return list of required parameters.
1739 Required parameters are: none
1740 @return list of required parameters
1746 Return list of optional parameters.
1748 Optional parameters are: none
1749 @return list of optional parameters
1760 LCIOTool.__init__(self, name=
"merge", **kwargs)
1764 Setup command arguments.
1765 @return list of arguments
1767 args = LCIOTool.cmd_args(self)
1769 raise Exception(
"Missing at least one input file.")
1771 raise Exception(
"Missing an output file.")
1773 args.extend([
"-f", i])
1774 args.extend([
"-o", self.
outputs[0]])
1781MergeROOT tool for hps-mc
1782Merges ROOT files using hadd with validation
1788 Merge ROOT files using hadd with event count validation.
1790 This component uses ROOT's hadd utility to merge multiple ROOT files
1791 into a single output file, and validates that all events are preserved.
1796 Initialize MergeROOT component.
1801 List of input ROOT files to merge
1803 List containing the output merged ROOT file name
1804 force : bool, optional
1805 Force overwrite of output file (default: True)
1806 compression : int, optional
1807 Compression level for output file (0-9, default: None uses hadd default)
1808 validate : bool, optional
1809 Validate event counts after merge (default: True)
1810 write_stats : bool, optional
1811 Write JSON stats file after merge (default: True when validate=True)
1812 job_id : int, optional
1813 Job ID to include in stats output
1815 Component.__init__(self, **kwargs)
1822 if not hasattr(self,
"force"):
1826 if not hasattr(self,
"compression"):
1830 if not hasattr(self,
"validate"):
1834 if not hasattr(self,
"write_stats"):
1838 if not hasattr(self,
"job_id"):
1850 Build command line arguments for hadd.
1855 List of command arguments
1858 sys.stderr.write(
"MergeROOT DEBUG: cmd_args() called\n")
1859 sys.stderr.write(
" self.force=%s, self.compression=%s\n" % (self.
force, self.
compression))
1878 sys.stderr.write(
"MergeROOT DEBUG: ERROR - No output file specified!\n")
1880 raise RuntimeError(
"MergeROOT: No output file specified")
1886 sys.stderr.write(
"MergeROOT DEBUG: ERROR - No input files specified!\n")
1888 raise RuntimeError(
"MergeROOT: No input files specified")
1890 sys.stderr.write(
"MergeROOT DEBUG: cmd_args() returning: %s\n" % args)
1896 Scan a ROOT file and extract TTree event counts.
1902 log_out : file, optional
1903 Log file for output (used to report multiple key cycles)
1908 Dictionary mapping tree names to entry counts
1914 "MergeROOT: PyROOT is required for validation but not available"
1921 root_file = ROOT.TFile.Open(filename,
"READ")
1922 if not root_file
or root_file.IsZombie():
1923 raise RuntimeError(
"MergeROOT: Cannot open ROOT file: %s" % filename)
1926 for key
in root_file.GetListOfKeys():
1930 if obj.InheritsFrom(
"TTree"):
1931 tree_name = obj.GetName()
1932 cycle = key.GetCycle()
1933 num_entries = obj.GetEntries()
1935 if tree_name
not in tree_cycles:
1936 tree_cycles[tree_name] = []
1937 tree_cycles[tree_name].append((cycle, num_entries))
1942 for tree_name, cycles
in tree_cycles.items():
1945 cycles.sort(key=
lambda x: x[0], reverse=
True)
1946 highest_cycle, highest_entries = cycles[0]
1948 log_out.write(
" WARNING: Multiple key cycles found for tree '%s':\n" % tree_name)
1949 for cyc, ent
in cycles:
1950 marker =
" <-- using" if cyc == highest_cycle
else ""
1951 log_out.write(
" Cycle %d: %d entries%s\n" % (cyc, ent, marker))
1952 tree_counts[tree_name] = highest_entries
1954 tree_counts[tree_name] = cycles[0][1]
1960 Scan all input files and store tree event counts.
1967 log_out.write(
"\n" +
"=" * 70 +
"\n")
1968 log_out.write(
"MergeROOT: Scanning input files for TTrees\n")
1969 log_out.write(
"=" * 70 +
"\n")
1972 if not os.path.exists(input_file):
1973 raise RuntimeError(
"MergeROOT: Input file not found: %s" % input_file)
1975 log_out.write(
"\nScanning: %s\n" % input_file)
1979 log_out.write(
" WARNING: No TTrees found in this file\n")
1981 for tree_name, count
in tree_counts.items():
1982 log_out.write(
" Tree '%s': %d events\n" % (tree_name, count))
1986 log_out.write(
"\n" +
"=" * 70 +
"\n")
1991 Scan output file and store tree event counts.
2000 log_out.write(
"\n" +
"=" * 70 +
"\n")
2001 log_out.write(
"MergeROOT: Scanning output file for TTrees\n")
2002 log_out.write(
"=" * 70 +
"\n")
2003 log_out.write(
"\nScanning: %s\n" % output_file)
2008 log_out.write(
" WARNING: No TTrees found in output file\n")
2011 log_out.write(
" Tree '%s': %d events\n" % (tree_name, count))
2013 log_out.write(
"\n" +
"=" * 70 +
"\n")
2018 Validate that event counts match between input and output files.
2028 True if validation passes, False otherwise
2030 log_out.write(
"\n" +
"=" * 70 +
"\n")
2031 log_out.write(
"MergeROOT: Validating merge results\n")
2032 log_out.write(
"=" * 70 +
"\n\n")
2035 total_input_counts = {}
2038 for tree_name, count
in tree_counts.items():
2039 if tree_name
not in total_input_counts:
2040 total_input_counts[tree_name] = 0
2041 total_input_counts[tree_name] += count
2046 if not total_input_counts:
2047 log_out.write(
"WARNING: No TTrees found in input files\n")
2050 log_out.write(
"Event count validation:\n")
2051 log_out.write(
"-" * 70 +
"\n")
2053 "%-30s %15s %15s %10s\n"
2054 % (
"Tree Name",
"Input Events",
"Output Events",
"Status")
2056 log_out.write(
"-" * 70 +
"\n")
2058 for tree_name, input_count
in sorted(total_input_counts.items()):
2061 if output_count == input_count:
2068 "%-30s %15d %15d %10s\n"
2069 % (tree_name, input_count, output_count, status)
2074 total_input_counts.keys()
2077 log_out.write(
"\nWARNING: Output contains trees not found in inputs:\n")
2078 for tree_name
in extra_trees:
2080 " - %s: %d events\n"
2084 log_out.write(
"-" * 70 +
"\n")
2087 log_out.write(
"\n✓ VALIDATION PASSED: All event counts match!\n")
2089 log_out.write(
"\n✗ VALIDATION FAILED: Event count mismatch detected!\n")
2091 log_out.write(
"=" * 70 +
"\n\n")
2098 Print a summary of the merge operation.
2105 log_out.write(
"\n" +
"=" * 70 +
"\n")
2106 log_out.write(
"MergeROOT: Summary\n")
2107 log_out.write(
"=" * 70 +
"\n")
2108 log_out.write(
"Input files: %d\n" % len(self.
inputsinputs))
2111 log_out.write(
" %d. %s\n" % (i, input_file))
2115 "Compression level: %s\n"
2121 log_out.write(
"\nTotal events in merged file:\n")
2123 log_out.write(
" %-30s: %d events\n" % (tree_name, count))
2125 log_out.write(
"=" * 70 +
"\n")
2130 Get the stats JSON filename based on the output ROOT filename.
2135 Path to stats JSON file (e.g., 'merged_X_job1.root' -> 'merged_X_job1_stats.json')
2140 base, _ = os.path.splitext(output_file)
2141 return base +
"_stats.json"
2145 Write merge statistics to a JSON file.
2151 validation_passed : bool
2152 Whether the validation passed
2155 if stats_file
is None:
2156 log_out.write(
"WARNING: Cannot determine stats filename, skipping stats output\n")
2159 log_out.write(
"\n" +
"=" * 70 +
"\n")
2160 log_out.write(
"MergeROOT: Writing stats to %s\n" % stats_file)
2161 log_out.write(
"=" * 70 +
"\n")
2164 total_input_events = {}
2166 for tree_name, count
in tree_counts.items():
2167 if tree_name
not in total_input_events:
2168 total_input_events[tree_name] = 0
2169 total_input_events[tree_name] += count
2172 input_files_list = []
2175 input_files_list.append({
2177 "events": tree_counts
2185 "input_files": input_files_list,
2186 "total_input_events": total_input_events,
2187 "validation_passed": validation_passed,
2192 with open(stats_file,
'w')
as f:
2193 json.dump(stats, f, indent=2)
2195 log_out.write(
"Stats written successfully\n")
2196 log_out.write(
"=" * 70 +
"\n")
2201 Execute MergeROOT component using hadd.
2213 Return code from hadd command
2216 log_out.write(
"\n" +
"=" * 70 +
"\n")
2217 log_out.write(
"MergeROOT: DEBUG - Entering execute()\n")
2218 log_out.write(
"=" * 70 +
"\n")
2219 log_out.write(
"DEBUG: self.command = %s\n" % self.
commandcommand)
2220 log_out.write(
"DEBUG: self.inputs = %s\n" % self.
inputsinputs)
2221 log_out.write(
"DEBUG: self.outputs = %s\n" % self.
outputsoutputs)
2222 log_out.write(
"DEBUG: self.force = %s\n" % self.
force)
2223 log_out.write(
"DEBUG: self.compression = %s\n" % self.
compression)
2224 log_out.write(
"DEBUG: self.validate = %s\n" % self.
validate)
2228 log_out.write(
"\nDEBUG: Checking if hadd command exists...\n")
2231 raise RuntimeError(
"MergeROOT: hadd command not found in PATH")
2232 log_out.write(
"DEBUG: hadd command found\n")
2236 log_out.write(
"\nDEBUG: Checking input files exist...\n")
2239 log_out.write(
"DEBUG: Checking: %s\n" % input_file)
2241 if not os.path.exists(input_file):
2242 raise RuntimeError(
"MergeROOT: Input file not found: %s" % input_file)
2243 log_out.write(
"DEBUG: -> exists (size: %d bytes)\n" % os.path.getsize(input_file))
2247 log_out.write(
"\nDEBUG: Validation enabled = %s\n" % self.
validate)
2251 log_out.write(
"DEBUG: Starting input file scan...\n")
2254 log_out.write(
"DEBUG: Input file scan complete\n")
2256 except Exception
as e:
2257 log_out.write(
"\nWARNING: Could not scan input files: %s\n" % str(e))
2258 log_out.write(
"Proceeding with merge without validation.\n")
2262 log_out.write(
"\nDEBUG: Building command arguments...\n")
2265 log_out.write(
"DEBUG: cmd_args() returned: %s\n" % self.
cmd_argscmd_args())
2269 log_out.write(
"\n" +
"=" * 70 +
"\n")
2270 log_out.write(
"MergeROOT: Executing hadd\n")
2271 log_out.write(
"=" * 70 +
"\n")
2272 log_out.write(
"Command: %s\n" %
" ".join(cmd))
2273 log_out.write(
"=" * 70 +
"\n\n")
2277 log_out.write(
"DEBUG: About to call subprocess.Popen...\n")
2279 proc = subprocess.Popen(cmd, stdout=log_out, stderr=log_err)
2280 log_out.write(
"DEBUG: Popen returned, PID = %s\n" % proc.pid)
2282 log_out.write(
"DEBUG: Waiting for process to complete...\n")
2285 log_out.write(
"DEBUG: Process completed, returncode = %d\n" % proc.returncode)
2289 if proc.returncode != 0:
2290 log_out.write(
"DEBUG: hadd FAILED with return code %d\n" % proc.returncode)
2293 "MergeROOT: hadd failed with return code %d" % proc.returncode
2297 log_out.write(
"DEBUG: Checking if output file exists: %s\n" % self.
outputsoutputs[0])
2301 "MergeROOT: Output file was not created: %s" % self.
outputsoutputs[0]
2303 log_out.write(
"DEBUG: Output file exists, size = %d bytes\n" % os.path.getsize(self.
outputsoutputs[0]))
2306 log_out.write(
"\n✓ hadd completed successfully\n")
2310 log_out.write(
"\nDEBUG: Post-merge validation check, self.validate = %s\n" % self.
validate)
2312 validation_passed =
True
2315 log_out.write(
"DEBUG: Starting output file scan...\n")
2318 log_out.write(
"DEBUG: Output file scan complete\n")
2320 log_out.write(
"DEBUG: Starting merge validation...\n")
2324 log_out.write(
"DEBUG: Merge validation complete, passed = %s\n" % validation_passed)
2327 if not validation_passed:
2328 raise RuntimeError(
"MergeROOT: Event count validation failed!")
2330 except Exception
as e:
2331 log_out.write(
"\nERROR during validation: %s\n" % str(e))
2336 log_out.write(
"\nDEBUG: write_stats = %s\n" % self.
write_stats)
2341 except Exception
as e:
2342 log_out.write(
"\nWARNING: Could not write stats JSON: %s\n" % str(e))
2346 log_out.write(
"\nDEBUG: Printing summary...\n")
2350 log_out.write(
"\nDEBUG: MergeROOT.execute() returning %d\n" % proc.returncode)
2352 return proc.returncode
2356 Return list of output files.
2361 List containing the merged output ROOT file and optionally the stats JSON
2366 if stats_file
and stats_file
not in files:
2367 files.append(stats_file)
2372 Return list of required configuration parameters.
2377 List of required config parameters (empty for MergeROOT)
Base class for components in a job.
output_files(self)
Return a list of output files created by this component.
config_from_environ(self)
Configure component from environment variables which are just upper case versions of the required con...
cmd_exists(self)
Check if the component's assigned command exists.
cmd_args(self)
Return the command arguments of this component.
input_files(self)
Get a list of input files for this component.
Miscellaneous math functions.