Skip to content

Commit c2eaeb6

Browse files
authored
Merge pull request #46110 from mmusich/mm_dev_phase2IntegrationTests
introduced a python script for Phase-2 HLT integration tests
2 parents 7869cfa + ac50279 commit c2eaeb6

File tree

1 file changed

+340
-0
lines changed

1 file changed

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

0 commit comments

Comments
 (0)