|
| 1 | +import argparse |
| 2 | +import os |
| 3 | +import re |
| 4 | +import shutil |
| 5 | +import subprocess |
| 6 | +import sys |
| 7 | +import time |
| 8 | +from datetime import datetime |
| 9 | +from concurrent.futures import ThreadPoolExecutor, as_completed |
| 10 | +import Configuration.Geometry.defaultPhase2ConditionsEra_cff as _settings |
| 11 | +_PH2_GLOBAL_TAG, _PH2_ERA = _settings.get_era_and_conditions(_settings.DEFAULT_VERSION) |
| 12 | + |
| 13 | +# Automatically generate the default geometry from DEFAULT_VERSION |
| 14 | +_PH2_GEOMETRY = f"Extended{_settings.DEFAULT_VERSION}" |
| 15 | + |
| 16 | +# Get the actual era name from the version key |
| 17 | +_PH2_ERA_NAME = _settings.properties[2026][_settings.DEFAULT_VERSION]['Era'] |
| 18 | + |
| 19 | +# Function to display help information |
| 20 | +def print_help(): |
| 21 | + help_text = """ |
| 22 | + This script runs HLT test configurations for the CMS Phase2 upgrade. |
| 23 | +
|
| 24 | + Arguments: |
| 25 | + --globaltag : GlobalTag for the CMS conditions (required) |
| 26 | + --geometry : Geometry setting for the CMS process (required) |
| 27 | + --events : Number of events to process (default: 1) |
| 28 | + --threads : Number of threads to use (default: 1) |
| 29 | +
|
| 30 | + Example usage: |
| 31 | + python script.py --globaltag auto:phase2_realistic_T33 --geometry Extended2026D110 --events 10 --threads 4 |
| 32 | + """ |
| 33 | + print(help_text) |
| 34 | + |
| 35 | +# Function to run a shell command and handle errors |
| 36 | +def run_command(command, log_file=None, workdir=None): |
| 37 | + try: |
| 38 | + print(f"Running command: {command}") |
| 39 | + with open(log_file, "w") as log: |
| 40 | + subprocess.run(command, shell=True, check=True, cwd=workdir, stdout=log, stderr=log) |
| 41 | + except subprocess.CalledProcessError as e: |
| 42 | + print(f"Error running command: {e}") |
| 43 | + return e.returncode # Return the error code |
| 44 | + |
| 45 | + return 0 # Return 0 for success |
| 46 | + |
| 47 | +# Argument Parser for command-line configuration |
| 48 | +parser = argparse.ArgumentParser(description="Run HLT Test Configurations") |
| 49 | +parser.add_argument("--globaltag", default=_PH2_GLOBAL_TAG, help="GlobalTag for the CMS conditions") |
| 50 | +parser.add_argument("--geometry", default=_PH2_GEOMETRY, help="Geometry setting for the CMS process") # Auto-generated geometry default |
| 51 | +parser.add_argument("--era", default=_PH2_ERA_NAME, help="Era setting for the CMS process") # Convert _PH2_ERA to string |
| 52 | +parser.add_argument("--events", type=int, default=10, help="Number of events to process") |
| 53 | +parser.add_argument("--parallelJobs", type=int, default=4, help="Number of parallel cmsRun HLT jobs") |
| 54 | +parser.add_argument("--threads", type=int, default=1, help="Number of threads to use") |
| 55 | + |
| 56 | +# Step 0: Capture the start time and print the start timestamp |
| 57 | +start_time = time.time() |
| 58 | +start_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") |
| 59 | +print("------------------------------------------------------------") |
| 60 | +print(f"Script started at {start_timestamp}") |
| 61 | +print("------------------------------------------------------------") |
| 62 | + |
| 63 | +# Parse arguments |
| 64 | +try: |
| 65 | + args = parser.parse_args() |
| 66 | +except SystemExit: |
| 67 | + print_help() |
| 68 | + sys.exit(0) |
| 69 | + |
| 70 | +global_tag = args.globaltag |
| 71 | +era = args.era |
| 72 | +geometry = args.geometry |
| 73 | +num_events = args.events |
| 74 | +num_threads = args.threads |
| 75 | +num_parallel_jobs = args.parallelJobs |
| 76 | + |
| 77 | +# Print the values in a nice formatted manner |
| 78 | +print(f"{'Configuration Summary':^40}") |
| 79 | +print("=" * 40) |
| 80 | +print(f"Global Tag: {global_tag}") |
| 81 | +print(f"Geometry: {geometry}") |
| 82 | +print(f"Era: {era}") |
| 83 | +print(f"Num Events: {num_events}") |
| 84 | +print(f"Num Threads: {num_threads}") |
| 85 | +print(f"Num Par. Jobs: {num_parallel_jobs}") |
| 86 | +print("=" * 40) |
| 87 | + |
| 88 | +# Directory where all test configurations will be stored |
| 89 | +output_dir = "hlt_test_configs" |
| 90 | +# If the directory exists, remove it first |
| 91 | +if os.path.exists(output_dir): |
| 92 | + shutil.rmtree(output_dir) |
| 93 | + |
| 94 | +# Create the directory |
| 95 | +os.makedirs(output_dir) |
| 96 | + |
| 97 | +# Define the cmsDriver.py command to create the base configuration |
| 98 | +base_cmsdriver_command = ( |
| 99 | + f"cmsDriver.py Phase2 -s L1P2GT,HLT:75e33_timing " |
| 100 | + f"--conditions {global_tag} -n {num_events} --eventcontent FEVTDEBUGHLT " |
| 101 | + f"--geometry {geometry} --era {era} --filein file:{output_dir}/step1.root --fileout {output_dir}/hlt.root --no_exec " |
| 102 | + f"--nThreads {num_threads} " |
| 103 | + f"--customise SLHCUpgradeSimulations/Configuration/aging.customise_aging_1000 " |
| 104 | + f'--customise_commands "process.options.wantSummary=True"' |
| 105 | +) |
| 106 | + |
| 107 | +# The base configuration file and the dumped configuration file |
| 108 | +base_config_file = os.path.join(output_dir, "Phase2_L1P2GT_HLT.py") |
| 109 | +dumped_config_file = os.path.join(output_dir, "Phase2_dump.py") |
| 110 | +log_file = os.path.join(output_dir, "hlt.log") |
| 111 | + |
| 112 | +# Step 1: Run the cmsDriver.py command to generate the base configuration in the output directory |
| 113 | +print(f"Running cmsDriver.py to generate the base config: {base_config_file}") |
| 114 | +subprocess.run(base_cmsdriver_command, shell=True, cwd=output_dir) |
| 115 | + |
| 116 | +# Step 2: Use edmConfigDump to dump the full configuration |
| 117 | +print(f"Dumping the full configuration using edmConfigDump to {dumped_config_file}") |
| 118 | +with open(dumped_config_file, "w") as dump_file, open(log_file, "w") as log: |
| 119 | + subprocess.run(f"edmConfigDump {base_config_file}", shell=True, stdout=dump_file, stderr=log) |
| 120 | + |
| 121 | +# Step 3: Extract the list of HLT paths from the dumped configuration |
| 122 | +print(f"Extracting HLT paths from {dumped_config_file}...") |
| 123 | + |
| 124 | +# Read the dumped configuration to extract HLT paths |
| 125 | +with open(dumped_config_file, "r") as f: |
| 126 | + config_content = f.read() |
| 127 | + |
| 128 | +# Use regex to find all HLT and L1T paths defined in process.schedule |
| 129 | +unsorted_hlt_paths = re.findall(r"process\.(HLT_[A-Za-z0-9_]+|L1T_[A-Za-z0-9_]+)", config_content) |
| 130 | + |
| 131 | +# Remove duplicates and sort alphabetically |
| 132 | +hlt_paths = sorted(set(unsorted_hlt_paths)) |
| 133 | + |
| 134 | +if not hlt_paths: |
| 135 | + print("No HLT paths found in the schedule!") |
| 136 | + exit(1) |
| 137 | + |
| 138 | +print(f"Found {len(hlt_paths)} HLT paths.") |
| 139 | + |
| 140 | +# Step 4: Broadened Regex for Matching process.schedule |
| 141 | +schedule_match = re.search( |
| 142 | + r"(process\.schedule\s*=\s*cms\.Schedule\(\*?\s*\[)([\s\S]+?)(\]\s*\))", |
| 143 | + config_content |
| 144 | +) |
| 145 | + |
| 146 | +if not schedule_match: |
| 147 | + print("No schedule match found after tweaking regex! Exiting...") |
| 148 | + exit(1) |
| 149 | +else: |
| 150 | + print(f"Matched schedule section.") |
| 151 | + |
| 152 | +# Step 5: Generate N configurations by modifying the dumped config to keep only one path at a time |
| 153 | +for path_name in hlt_paths: |
| 154 | + # Create a new configuration file for this path |
| 155 | + config_filename = os.path.join(output_dir, f"Phase2_{path_name}.py") |
| 156 | + |
| 157 | + # Define regex to find all HLT paths in the cms.Schedule and replace them |
| 158 | + def replace_hlt_paths(match): |
| 159 | + all_paths = match.group(2).split(", ") |
| 160 | + # Keep non-HLT/L1T paths and include only the current HLT or L1T path |
| 161 | + filtered_paths = [path for path in all_paths if not re.match(r"process\.(HLT_|L1T_)", path) or f"process.{path_name}" in path] |
| 162 | + return match.group(1) + ", ".join(filtered_paths) + match.group(3) |
| 163 | + |
| 164 | + # Apply the regex to remove all HLT and L1T paths except the current one |
| 165 | + modified_content = re.sub( |
| 166 | + r"(process\.schedule\s*=\s*cms\.Schedule\(\*?\s*\[)([\s\S]+?)(\]\s*\))", |
| 167 | + replace_hlt_paths, |
| 168 | + config_content |
| 169 | + ) |
| 170 | + |
| 171 | + # Modify the fileout parameter to save a unique root file for each path |
| 172 | + modified_content = re.sub( |
| 173 | + r"fileName = cms\.untracked\.string\('.*'\)", |
| 174 | + f"fileName = cms.untracked.string('{output_dir}/{path_name}.root')", |
| 175 | + modified_content |
| 176 | + ) |
| 177 | + |
| 178 | + # Write the new config to a file |
| 179 | + with open(config_filename, "w") as new_config: |
| 180 | + new_config.write(modified_content) |
| 181 | + |
| 182 | + print(f"Generated config: {config_filename}") |
| 183 | + |
| 184 | +print(f"Generated {len(hlt_paths)} configuration files in the {output_dir} directory.") |
| 185 | + |
| 186 | +# Step 6: Run cmsDriver.py for TTbar GEN,SIM steps and save the output in output_dir |
| 187 | +ttbar_config_file = os.path.join(output_dir, "TTbar_GEN_SIM_step.py") |
| 188 | +ttbar_command = ( |
| 189 | + f"cmsDriver.py TTbar_14TeV_TuneCP5_cfi -s GEN,SIM,DIGI:pdigi_valid,L1TrackTrigger,L1,L1P2GT,DIGI2RAW -n {num_events} " |
| 190 | + f"--conditions {global_tag} --beamspot DBrealisticHLLHC --datatier GEN-SIM-DIGI-RAW " |
| 191 | + f"--eventcontent FEVTDEBUG --geometry {geometry} --era {era} " |
| 192 | + f"--relval 9000,100 --fileout {output_dir}/step1.root --nThreads {num_threads} " |
| 193 | + f"--customise SLHCUpgradeSimulations/Configuration/aging.customise_aging_1000 " |
| 194 | + f"--python_filename {ttbar_config_file}" |
| 195 | +) |
| 196 | + |
| 197 | +print("Running TTbar GEN,SIM step...") |
| 198 | +run_command(ttbar_command, log_file=os.path.join(output_dir, "ttbar_gen_sim.log")) |
| 199 | + |
| 200 | +# Directory containing HLT test configurations |
| 201 | +hlt_configs_dir = output_dir |
| 202 | + |
| 203 | +# Check if the directory exists |
| 204 | +if not os.path.exists(hlt_configs_dir): |
| 205 | + print(f"Directory {hlt_configs_dir} not found! Exiting...") |
| 206 | + exit(1) |
| 207 | + |
| 208 | +# Step 7: Function to run cmsRun on a given HLT config file and save the output |
| 209 | +def run_cmsrun(config_file): |
| 210 | + # Extract the HLT path name from the config file (e.g., "Phase2_HLT_IsoMu24_FromL1TkMuon.py") |
| 211 | + base_name = os.path.basename(config_file).replace("Phase2_", "").replace(".py", "") |
| 212 | + log_file = os.path.join(output_dir, f"{base_name}.log") |
| 213 | + |
| 214 | + # Run the cmsRun command and log the output |
| 215 | + cmsrun_command = f"cmsRun {config_file}" |
| 216 | + result_code = run_command(cmsrun_command, log_file=log_file) |
| 217 | + |
| 218 | + if result_code != 0: |
| 219 | + print(f"cmsRun failed for {config_file} with exit code {result_code}. Check {log_file} for details.") |
| 220 | + return result_code # Return the error code |
| 221 | + |
| 222 | + print(f"cmsRun completed for {config_file}") |
| 223 | + return 0 # Return 0 for success |
| 224 | + |
| 225 | +# Step 8: Loop through all files in hlt_test_configs and run cmsRun on each in parallel |
| 226 | +config_files = [f for f in os.listdir(hlt_configs_dir) if f.endswith(".py") and f.startswith("Phase2_")] |
| 227 | +print(f"Found {len(config_files)} configuration files in {hlt_configs_dir}.") |
| 228 | + |
| 229 | +# Run cmsRun on all config files in parallel and handle errors |
| 230 | +error_occurred = False |
| 231 | +with ThreadPoolExecutor(max_workers=num_parallel_jobs) as executor: |
| 232 | + futures = {executor.submit(run_cmsrun, os.path.join(output_dir, config_file)): config_file for config_file in config_files} |
| 233 | + |
| 234 | + for future in as_completed(futures): |
| 235 | + config_file = futures[future] |
| 236 | + try: |
| 237 | + result_code = future.result() |
| 238 | + if result_code != 0: |
| 239 | + error_occurred = True |
| 240 | + print(f"cmsRun for {config_file} exited with code {result_code}") |
| 241 | + except Exception as exc: |
| 242 | + error_occurred = True |
| 243 | + print(f"cmsRun for {config_file} generated an exception: {exc}") |
| 244 | + |
| 245 | +if error_occurred: |
| 246 | + print("One or more cmsRun jobs failed. Exiting with failure.") |
| 247 | + exit(1) |
| 248 | + |
| 249 | +print("All cmsRun jobs submitted.") |
| 250 | + |
| 251 | +# Step 9: Compare all HLT root files using hltDiff |
| 252 | +def compare_hlt_results(): |
| 253 | + # List all root files starting with "HLT_" in the output directory |
| 254 | + root_files = [f for f in os.listdir(output_dir) if f.endswith(".root") and (f.startswith("HLT_") or f.startswith("L1T_"))] |
| 255 | + |
| 256 | + # Base file (hltrun output) to compare against |
| 257 | + base_root_file = os.path.join(output_dir, "hlt.root") |
| 258 | + |
| 259 | + # Check if base_root_file exists |
| 260 | + if not os.path.exists(base_root_file): |
| 261 | + print(f"Base root file {base_root_file} not found! Exiting...") |
| 262 | + exit(1) |
| 263 | + |
| 264 | + # Compare each root file with the base using hltDiff |
| 265 | + for root_file in root_files: |
| 266 | + root_path = os.path.join(output_dir, root_file) |
| 267 | + print(f"Comparing {root_path} with {base_root_file} using hltDiff...") |
| 268 | + |
| 269 | + # Run the hltDiff command |
| 270 | + hlt_diff_command = f"hltDiff -o {base_root_file} -n {root_path}" |
| 271 | + result = subprocess.run(hlt_diff_command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| 272 | + |
| 273 | + # Decode and process the output |
| 274 | + output = result.stdout.decode("utf-8") |
| 275 | + print(output) # Print for debug purposes |
| 276 | + |
| 277 | + # Use a dynamic check based on the number of events configured |
| 278 | + expected_match_string = f"Found {num_events} matching events, out of which 0 have different HLT results" |
| 279 | + |
| 280 | + # Check if the output contains the expected match string |
| 281 | + if expected_match_string not in output: |
| 282 | + print(f"Error: {root_file} has different HLT results!") |
| 283 | + exit(1) # Exit with failure if differences are found |
| 284 | + |
| 285 | + print("All HLT comparisons passed with no differences.") |
| 286 | + |
| 287 | +# Step 10: Once all cmsRun jobs are completed, perform the hltDiff comparisons |
| 288 | +print("Performing HLT result comparisons...") |
| 289 | +compare_hlt_results() |
| 290 | + |
| 291 | +# Step 11: Capture the end time and print the end timestamp |
| 292 | +end_time = time.time() |
| 293 | +end_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") |
| 294 | +print("------------------------------------------------------------") |
| 295 | +print(f"Script ended at {end_timestamp}") |
| 296 | +print("------------------------------------------------------------") |
| 297 | + |
| 298 | +# Step 12: Calculate the total execution time and print it |
| 299 | +total_time = end_time - start_time |
| 300 | +formatted_total_time = time.strftime("%H:%M:%S", time.gmtime(total_time)) |
| 301 | +print("------------------------------------------------------------") |
| 302 | +print(f"Total execution time: {formatted_total_time}") |
| 303 | +print("------------------------------------------------------------") |
| 304 | + |
| 305 | +print("All steps completed successfully.") |
0 commit comments