ATLAS Offline Software
Loading...
Searching...
No Matches
athenaEF.py
Go to the documentation of this file.
1#!/bin/sh
2# -*- mode: python -*-
3#
4# Copyright (C) 2002-2026 CERN for the benefit of the ATLAS collaboration
5#
6# athenaEF.py - A modified version of athenaHLT.py that runs the HLT configuration
7# directly without using HLTMPPy/HLTMPPU. It creates the configuration like
8# athenaHLT but executes it like athena.py does.
9#
10"""date"
11
12# defaults
13export USETCMALLOC=1
14export USEIMF=1
15
16# parse command line arguments
17for a in ${@}
18do
19 case "$a" in
20 --stdcmalloc) USETCMALLOC=0;;
21 --tcmalloc) USETCMALLOC=1;;
22 --stdcmath) USEIMF=0;;
23 --imf) USEIMF=1;;
24 --preloadlib*) export ATHENA_ADD_PRELOAD=${a#*=};;
25 --no-ers-signal-handlers) export TDAQ_ERS_NO_SIGNAL_HANDLERS=1;;
26 esac
27done
28
29# Do the actual preloading via LD_PRELOAD
30source `which athena_preload.sh `
31
32# Now resurrect ourselves as python script
33python_path=`which python`
34"exec" "$python_path" "-tt" "$0" "$@";
35
36"""
37
38import sys
39import os
40import argparse
41import json
42import pickle
43import traceback
44from datetime import datetime as dt
45
46from TrigConfStorage.TriggerCrestUtil import TriggerCrestUtil
47
48# Use single-threaded oracle client library to avoid extra
49# threads when forking (see ATR-21890, ATDBOPS-115)
50os.environ["CORAL_ORA_NO_OCI_THREADED"] = "1"
51
52from TrigCommon import AthHLT
53from AthenaCommon.Logging import logging
54log = logging.getLogger('athenaEF')
55
56
57# =============================================================================
58# Run Parameters Configuration
59# =============================================================================
60# Default values for run parameters used by prepareForStart.
61# These can be overridden by command-line arguments or fetched from IS.
62
64 """
65 Container for run parameters needed by HltEventLoopMgr::prepareForStart().
66
67 This class centralizes all run parameter defaults or their retrieval from IS.
68 """
69
70 # Default values
71 DEFAULT_RUN_NUMBER = 0
72 DEFAULT_LB_NUMBER = 0
73 DEFAULT_DETECTOR_MASK = 'f' * 32 # All detectors enabled
74 DEFAULT_SOR_TIME = None # Will use 'now' if not set
75 DEFAULT_SOLENOID_CURRENT = 7730.0 # (nominal)
76 DEFAULT_TOROIDS_CURRENT = 20400.0 # (nominal)
77 DEFAULT_BEAM_TYPE = 0
78 DEFAULT_BEAM_ENERGY = 0
79 DEFAULT_RUN_TYPE = "Physics"
80 DEFAULT_TRIGGER_TYPE = 0
81 DEFAULT_RECORDING_ENABLED = False
82
83 def __init__(self,
84 run_number=None,
85 lb_number=None,
86 detector_mask=None,
87 sor_time=None,
88 solenoid_current=None,
89 toroids_current=None,
90 beam_type=None,
91 beam_energy=None,
92 run_type=None,
93 trigger_type=None,
94 recording_enabled=None,
95 conditions_run=None,
96 T0_project_tag='',
97 stream='',
98 lumiblock=0):
99 """Initialize run parameters with defaults for any unspecified values."""
100 self.run_number = run_number if run_number is not None else self.DEFAULT_RUN_NUMBER
101 self.lb_number = lb_number if lb_number is not None else self.DEFAULT_LB_NUMBER
102 self.detector_mask = detector_mask if detector_mask is not None else self.DEFAULT_DETECTOR_MASK
103 self.sor_time = sor_time if sor_time is not None else dt.now().strftime('%d/%m/%y %H:%M:%S.%f')
104 self.solenoid_current = solenoid_current
105 self.toroids_current = toroids_current
106 self.beam_type = beam_type if beam_type is not None else self.DEFAULT_BEAM_TYPE
107 self.beam_energy = beam_energy if beam_energy is not None else self.DEFAULT_BEAM_ENERGY
108 self.run_type = run_type if run_type is not None else self.DEFAULT_RUN_TYPE
109 self.trigger_type = trigger_type if trigger_type is not None else self.DEFAULT_TRIGGER_TYPE
110 self.recording_enabled = recording_enabled if recording_enabled is not None else self.DEFAULT_RECORDING_ENABLED
111 self.conditions_run = conditions_run # Reference run for conditions lookup (None = use run_number)
112 self.T0_project_tag = T0_project_tag
113 self.stream = stream
114 self.lumiblock = lumiblock
115
116 def to_dict(self):
117 """Return run parameters as a dictionary for prepareForStart."""
118 return {
119 'run_number': self.run_number,
120 'lb_number': self.lb_number,
121 'detector_mask': self.detector_mask,
122 'sor_time': self.sor_time,
123 'solenoid_current': self.solenoid_current,
124 'toroids_current': self.toroids_current,
125 'beam_type': self.beam_type,
126 'beam_energy': self.beam_energy,
127 'run_type': self.run_type,
128 'trigger_type': self.trigger_type,
129 'recording_enabled': self.recording_enabled,
130 'conditions_run': self.conditions_run,
131 'T0_project_tag': self.T0_project_tag,
132 'stream': self.stream,
133 'lumiblock': self.lumiblock,
134 }
135
136 @classmethod
137 def from_args(cls, args):
138 """Create RunParams from argparse args, using defaults for unset values."""
139 return cls(
140 run_number=args.run_number,
141 lb_number=args.lb_number,
142 detector_mask=args.detector_mask,
143 sor_time=args.sor_time,
144 solenoid_current=getattr(args, 'solenoid_current', None),
145 toroids_current=getattr(args, 'toroids_current', None),
146 beam_type=getattr(args, 'beam_type', None),
147 beam_energy=getattr(args, 'beam_energy', None),
148 conditions_run=getattr(args, 'conditions_run', None),
149 T0_project_tag=getattr(args, 'T0_project_tag', ''),
150 stream=getattr(args, 'stream', ''),
151 lumiblock=getattr(args, 'lumiblock', 0),
152 )
153
154 @classmethod
155 def from_is(cls, partition=None, webdaq_base=None, strict=False,
156 solenoid_current_override=None, toroids_current_override=None):
157 """
158 Create RunParams by reading from IS via the WEBDAQ REST API.
159
160 This uses the webis_server REST API to fetch run parameters, avoiding
161 direct dependencies on TDAQ libraries. The API endpoint is determined by:
162 1. The webdaq_base parameter if provided
163 2. The TDAQ_WEBDAQ_BASE environment variable
164
165 The IS objects accessed are:
166 - RunParams.RunParams: run_number, det_mask, timeSOR, trigger_type, etc.
167 - Magnets.Magnets: SolenoidCurrent, ToroidsCurrent
168
169 Args:
170 partition: The partition name (default: from TDAQ_PARTITION env var)
171 webdaq_base: Base URL for webis_server (default: from TDAQ_WEBDAQ_BASE env var)
172 strict: If True, raise an exception if IS read fails (for --online-environment)
173 solenoid_current_override: If provided, skip IS fetch for solenoid (command-line override)
174 toroids_current_override: If provided, skip IS fetch for toroids (command-line override)
175
176 Returns:
177 RunParams instance with values from IS, or defaults if unavailable
178
179 Raises:
180 RuntimeError: If strict=True and IS read fails
181 """
182 import requests
183
184 # Determine the base URL
185 if webdaq_base is None:
186 webdaq_base = os.environ.get('TDAQ_WEBDAQ_BASE')
187
188 if not webdaq_base:
189 msg = "TDAQ_WEBDAQ_BASE not set, cannot read from IS"
190 if strict:
191 raise RuntimeError(msg + " (required for --online-environment)")
192 log.warning(msg + ". Using defaults.")
193 return cls()
194
195 # Determine partition
196 if partition is None:
197 partition = os.environ.get('TDAQ_PARTITION', 'ATLAS')
198
199 log.info("Reading run parameters from IS via WEBDAQ: %s (partition=%s)", webdaq_base, partition)
200
201 params = {}
202
203 # Fetch RunParams from IS
204 # API: GET /info/current/{partition}/is/{server}/{server}.{name}?format=compact
205 # Response format: [name, type, timestamp, data] - we need element [3]
206 try:
207 url = f"{webdaq_base}/info/current/{partition}/is/RunParams/RunParams.RunParams?format=compact"
208 log.debug("Fetching RunParams from: %s", url)
209
210 response = requests.get(url, timeout=10)
211 if response.status_code == 200:
212 response_data = response.json()
213 log.debug("RunParams response from IS: %s", response_data)
214
215 # Response is [name, type, timestamp, data]
216 if isinstance(response_data, list) and len(response_data) >= 4:
217 runparams = response_data[3]
218 else:
219 runparams = response_data
220
221 log.debug("RunParams data: %s", runparams)
222
223 # Map IS fields to our RunParams fields
224 if 'run_number' in runparams:
225 params['run_number'] = int(runparams['run_number'])
226 if 'lumiblock' in runparams:
227 params['lb_number'] = int(runparams['lumiblock'])
228 if 'det_mask' in runparams:
229 params['detector_mask'] = runparams['det_mask']
230 if 'timeSOR' in runparams:
231 sor_time = runparams['timeSOR']
232 # Ensure microseconds are present (TrigSORFromPtreeHelper expects format with .%f)
233 if '.' not in sor_time:
234 sor_time += '.000000'
235 params['sor_time'] = sor_time
236 if 'beam_type' in runparams:
237 params['beam_type'] = int(runparams['beam_type'])
238 if 'beam_energy' in runparams:
239 params['beam_energy'] = int(runparams['beam_energy'])
240 if 'run_type' in runparams:
241 params['run_type'] = runparams['run_type']
242 if 'trigger_type' in runparams:
243 params['trigger_type'] = int(runparams['trigger_type'])
244 if 'recording_enabled' in runparams:
245 params['recording_enabled'] = runparams['recording_enabled'] in ('1', 'true', 'True', True, 1)
246
247 log.info("Got run parameters from IS: run=%s, lb=%s",
248 params.get('run_number'), params.get('lb_number'))
249 else:
250 msg = f"Failed to fetch RunParams from IS: HTTP {response.status_code}"
251 if strict:
252 raise RuntimeError(msg + " (required for --online-environment)")
253 log.warning(msg)
254
255 except requests.exceptions.RequestException as e:
256 msg = f"Error fetching RunParams from IS: {e}"
257 if strict:
258 raise RuntimeError(msg + " (required for --online-environment)")
259 log.warning(msg)
260 except (ValueError, KeyError) as e:
261 msg = f"Error parsing RunParams from IS: {e}"
262 if strict:
263 raise RuntimeError(msg + " (required for --online-environment)")
264 log.warning(msg)
265
266 # Fetch Magnets from IS
267 # In strict mode (online environment), magnets are required unless provided via command line
268 # If command-line overrides are provided, use those instead of fetching from IS
269 have_solenoid_override = solenoid_current_override is not None
270 have_toroids_override = toroids_current_override is not None
271
272 if have_solenoid_override:
273 params['solenoid_current'] = solenoid_current_override
274 log.info("Using solenoid_current=%.1f from command line override", solenoid_current_override)
275 if have_toroids_override:
276 params['toroids_current'] = toroids_current_override
277 log.info("Using toroids_current=%.1f from command line override", toroids_current_override)
278
279 # Only fetch from IS if we need at least one value
280 if not (have_solenoid_override and have_toroids_override):
281 try:
282 url = f"{webdaq_base}/info/current/{partition}/is/Magnets/Magnets.Magnets?format=compact"
283 log.debug("Fetching Magnets from: %s", url)
284
285 response = requests.get(url, timeout=10)
286 if response.status_code == 200:
287 response_data = response.json()
288 log.debug("Magnets response from IS: %s", response_data)
289
290 magnets = response_data[3] if isinstance(response_data, list) and len(response_data) >= 4 else response_data
291 log.debug("Magnets data: %s", magnets)
292
293 # Magnets structure: { "SolenoidCurrent": {"value": ..., "ts": ...},
294 # "ToroidsCurrent": {"value": ..., "ts": ...} }
295 if not have_solenoid_override:
296 params['solenoid_current'] = float(magnets['SolenoidCurrent']['value'])
297 if not have_toroids_override:
298 params['toroids_current'] = float(magnets['ToroidsCurrent']['value'])
299
300 log.info("Got magnet currents from IS: solenoid=%s, toroids=%s",
301 params.get('solenoid_current'), params.get('toroids_current'))
302 elif strict:
303 raise RuntimeError(f"Magnets not available from IS: HTTP {response.status_code} "
304 "(required for --online-environment, use --solenoid-current and --toroids-current to override)")
305 else:
306 log.debug("Magnets not available from IS: HTTP %d", response.status_code)
307
308 except requests.exceptions.RequestException as e:
309 if strict:
310 raise RuntimeError(f"Error fetching Magnets from IS: {e} "
311 "(required for --online-environment, use --solenoid-current and --toroids-current to override)")
312 log.debug("Error fetching Magnets from IS: %s", e)
313 except (ValueError, KeyError, TypeError) as e:
314 if strict:
315 raise RuntimeError(f"Error parsing Magnets from IS: {e} "
316 "(required for --online-environment, use --solenoid-current and --toroids-current to override)")
317 log.debug("Error parsing Magnets from IS: %s", e)
318
319 # In strict mode, verify we got at least run_number from IS
320 if strict and 'run_number' not in params:
321 raise RuntimeError("Failed to get run_number from IS (required for --online-environment)")
322
323 return cls(**params)
324
325
326def get_trigconf_keys_from_oks(partition=None, webdaq_base=None, strict=False):
327 """
328 Read trigger configuration keys (SMK, L1PSK, HLTPSK) and DB info from OKS via WEBDAQ REST API.
329
330 This reads the keys from the partition's TriggerConfiguration object and its
331 related L1TriggerConfiguration and TriggerDBConnection objects.
332
333 OKS Structure:
334 - Partition -> TriggerConfiguration -> L1TriggerConfiguration (Lvl1PrescaleKey)
335 - Partition -> TriggerConfiguration -> TriggerDBConnection (SuperMasterKey)
336 - Partition -> TriggerConfiguration -> HLTImplementationDB (hltPrescaleKey)
337
338 Args:
339 partition: The partition name (default: from TDAQ_PARTITION env var)
340 webdaq_base: Base URL for webis_server (default: from TDAQ_WEBDAQ_BASE env var)
341 strict: If True, raise an exception if OKS read fails (for --online-environment)
342
343 Returns:
344 dict with keys: SMK, L1PSK, HLTPSK, db_alias (values may be None if not found)
345
346 Raises:
347 RuntimeError: If strict=True and OKS read fails
348 """
349 import requests
350
351 # Determine the base URL
352 if webdaq_base is None:
353 webdaq_base = os.environ.get('TDAQ_WEBDAQ_BASE')
354
355 if not webdaq_base:
356 msg = "TDAQ_WEBDAQ_BASE not set, cannot read from OKS"
357 if strict:
358 raise RuntimeError(msg + " (required for --online-environment)")
359 log.warning(msg)
360 return {'SMK': None, 'L1PSK': None, 'HLTPSK': None, 'db_alias': None}
361
362 # Determine partition
363 if partition is None:
364 partition = os.environ.get('TDAQ_PARTITION', 'ATLAS')
365
366 log.info("Reading trigger configuration keys from OKS via WEBDAQ: %s (partition=%s)",
367 webdaq_base, partition)
368
369 result = {'SMK': None, 'L1PSK': None, 'HLTPSK': None, 'db_alias': None}
370
371 def extract_oks_data(response_json):
372 """
373 Extract data from OKS compact format: [name, type, attributes, relationships]
374 Returns tuple (attributes_dict, relationships_dict)
375 """
376 if isinstance(response_json, list) and len(response_json) >= 4:
377 return response_json[2], response_json[3] # attributes, relationships
378 elif isinstance(response_json, list) and len(response_json) >= 3:
379 return response_json[2], {} # attributes only
380 return response_json, {} # fallback
381
382 def get_ref_id(ref):
383 """Extract object ID from a relationship reference."""
384 if isinstance(ref, list) and len(ref) >= 2:
385 return ref[0] # [id, class] format
386 elif isinstance(ref, dict) and 'id' in ref:
387 return ref['id']
388 elif isinstance(ref, str):
389 return ref
390 return None
391
392 # OKS API: GET /info/current/{partition}/oks/{class}/{name}?format=compact
393 # Response format: [name, type, attributes, relationships]
394 # - attributes: dict of simple values (strings, ints, etc.)
395 # - relationships: dict of references to other objects
396 try:
397 url = f"{webdaq_base}/info/current/{partition}/oks/Partition/{partition}?format=compact"
398 log.debug("Fetching Partition from OKS: %s", url)
399
400 response = requests.get(url, timeout=10)
401 if response.status_code == 200:
402 part_attrs, part_rels = extract_oks_data(response.json())
403 log.debug("Partition attributes: %s", part_attrs)
404 log.debug("Partition relationships: %s", part_rels)
405
406 # Get TriggerConfiguration reference from relationships
407 trig_conf_id = None
408 if 'TriggerConfiguration' in part_rels:
409 trig_conf_id = get_ref_id(part_rels['TriggerConfiguration'])
410
411 if trig_conf_id:
412 log.debug("TriggerConfiguration ID: %s", trig_conf_id)
413
414 # Get TriggerConfiguration object
415 url = f"{webdaq_base}/info/current/{partition}/oks/TriggerConfiguration/{trig_conf_id}?format=compact"
416 response = requests.get(url, timeout=10)
417 if response.status_code == 200:
418 trig_attrs, trig_rels = extract_oks_data(response.json())
419 log.debug("TriggerConfiguration attributes: %s", trig_attrs)
420 log.debug("TriggerConfiguration relationships: %s", trig_rels)
421
422 # Get L1TriggerConfiguration for L1PSK (relationship 'l1')
423 if 'l1' in trig_rels:
424 l1_id = get_ref_id(trig_rels['l1'])
425 if l1_id:
426 url = f"{webdaq_base}/info/current/{partition}/oks/L1TriggerConfiguration/{l1_id}?format=compact"
427 resp = requests.get(url, timeout=10)
428 if resp.status_code == 200:
429 l1_attrs, _ = extract_oks_data(resp.json())
430 log.debug("L1TriggerConfiguration attributes: %s", l1_attrs)
431 if 'Lvl1PrescaleKey' in l1_attrs:
432 result['L1PSK'] = int(l1_attrs['Lvl1PrescaleKey'])
433 log.info("Got L1PSK=%d from OKS", result['L1PSK'])
434
435 # Get TriggerDBConnection for SMK and db_alias (relationship 'TriggerDBConnection')
436 if 'TriggerDBConnection' in trig_rels:
437 db_id = get_ref_id(trig_rels['TriggerDBConnection'])
438 if db_id:
439 url = f"{webdaq_base}/info/current/{partition}/oks/TriggerDBConnection/{db_id}?format=compact"
440 resp = requests.get(url, timeout=10)
441 if resp.status_code == 200:
442 db_attrs, _ = extract_oks_data(resp.json())
443 log.debug("TriggerDBConnection attributes: %s", db_attrs)
444 if 'SuperMasterKey' in db_attrs:
445 result['SMK'] = int(db_attrs['SuperMasterKey'])
446 log.info("Got SMK=%d from OKS", result['SMK'])
447 if 'Alias' in db_attrs:
448 result['db_alias'] = db_attrs['Alias']
449 log.info("Got db_alias=%s from OKS", result['db_alias'])
450
451 # Get HLTImplementationDB for HLTPSK (relationship 'hlt')
452 if 'hlt' in trig_rels:
453 hlt_id = get_ref_id(trig_rels['hlt'])
454 if hlt_id:
455 url = f"{webdaq_base}/info/current/{partition}/oks/HLTImplementationDB/{hlt_id}?format=compact"
456 resp = requests.get(url, timeout=10)
457 if resp.status_code == 200:
458 hlt_attrs, _ = extract_oks_data(resp.json())
459 log.debug("HLTImplementationDB attributes: %s", hlt_attrs)
460 if 'hltPrescaleKey' in hlt_attrs:
461 result['HLTPSK'] = int(hlt_attrs['hltPrescaleKey'])
462 log.info("Got HLTPSK=%d from OKS", result['HLTPSK'])
463 else:
464 msg = f"Failed to fetch Partition from OKS: HTTP {response.status_code}"
465 if strict:
466 raise RuntimeError(msg + " (required for --online-environment)")
467 log.warning(msg)
468
469 except requests.exceptions.RequestException as e:
470 msg = f"Error fetching trigger keys from OKS: {e}"
471 if strict:
472 raise RuntimeError(msg + " (required for --online-environment)")
473 log.warning(msg)
474 except (ValueError, KeyError, TypeError) as e:
475 msg = f"Error parsing trigger keys from OKS: {e}"
476 if strict:
477 raise RuntimeError(msg + " (required for --online-environment)")
478 log.warning(msg)
479
480 # In strict mode, verify we got the required keys from OKS
481 if strict:
482 missing = [k for k in ['SMK', 'L1PSK', 'HLTPSK'] if result.get(k) is None]
483 if missing:
484 raise RuntimeError(f"Failed to get {', '.join(missing)} from OKS (required for --online-environment)")
485
486 return result
487
488
489def get_run_params(args=None, from_is=False, partition=None, webdaq_base=None, strict=False,
490 solenoid_current_override=None, toroids_current_override=None):
491 """
492 Get run parameters from the appropriate source.
493
494 This is the main entry point for obtaining run parameters. It provides
495 a single place to modify when adding new sources (like WEBDAQ).
496
497 Args:
498 args: argparse Namespace with command-line arguments (optional)
499 from_is: If True, try to read from WEBDAQ first
500 partition: Partition name for IS access (defaults to TDAQ_PARTITION env var)
501 webdaq_base: WEBDAQ base URL (defaults to TDAQ_WEBDAQ_BASE env var)
502 strict: If True, raise an exception if IS read fails (for --online-environment)
503 solenoid_current_override: Command-line override for solenoid current
504 toroids_current_override: Command-line override for toroids current
505
506 Returns:
507 RunParams instance
508
509 Raises:
510 RuntimeError: If strict=True and IS read fails
511 """
512 if from_is:
513 return RunParams.from_is(partition=partition, webdaq_base=webdaq_base, strict=strict,
514 solenoid_current_override=solenoid_current_override,
515 toroids_current_override=toroids_current_override)
516 elif args is not None:
517 return RunParams.from_args(args)
518 else:
519 return RunParams()
520
521
523 """
524 Runner class that executes Gaudi configuration from JSON file or database.
525 Uses TrigConf::JobOptionsSvc with TYPE="FILE" or TYPE="DB" to load configuration.
526 Same approach used by PSC (Psc.cxx) - it sets JobOptionsType and
527 JobOptionsPath on the ApplicationMgr, and TrigConf::JobOptionsSvc handles both
528 FILE and DB modes transparently.
529 """
530 def __init__(self, job_options_type, job_options_path, run_params=None,
531 properties=None, db_server=None, smk=None,
532 num_threads=1, num_slots=1, ef_overrides=None):
533 """
534 Args:
535 job_options_type: "FILE" or "DB"
536 job_options_path: JSON file path (for FILE) or DB connection string (for DB)
537 run_params: Run parameters dict for prepareForStart
538 properties: Pre-loaded properties dict (optional, for FILE mode)
539 db_server: DB server alias (for store() in DB mode)
540 smk: Super Master Key (for store() in DB mode)
541 num_threads: Number of threads for AvalancheSchedulerSvc.ThreadPoolSize
542 num_slots: Number of event slots for EventDataSvc.NSlots
543 ef_overrides: EFInterfaceSvc properties overriding the DB/JSON configuration
544 """
545 self.job_options_type = job_options_type
546 self.job_options_path = job_options_path
547 self.run_params = run_params or {}
548 self.properties = properties
549 self.db_server = db_server # For store() in DB mode
550 self.smk = smk # For store() in DB mode
551 self.num_threads = num_threads
552 self.num_slots = num_slots
553 self.ef_overrides = ef_overrides or {} # CLI overrides for EFInterfaceSvc
554 self._app = None
555
556 @classmethod
557 def from_json(cls, json_file, run_params=None, properties=None,
558 num_threads=1, num_slots=1, ef_overrides=None):
559 """Create runner for JSON file (TYPE=FILE)"""
560 return cls("FILE", os.path.abspath(json_file), run_params, properties,
561 num_threads=num_threads, num_slots=num_slots, ef_overrides=ef_overrides)
562
563 @classmethod
564 def from_database(cls, db_server, smk, l1psk=None, hltpsk=None, run_params=None,
565 num_threads=1, num_slots=1, ef_overrides=None):
566 """Create runner for database (TYPE=DB)"""
567 # Build the DB connection string: server=X;smkey=Y;lvl1key=Z;hltkey=W
568 db_path = f"server={db_server};smkey={smk}"
569 if l1psk is not None:
570 db_path += f";lvl1key={l1psk}"
571 if hltpsk is not None:
572 db_path += f";hltkey={hltpsk}"
573 return cls("DB", db_path, run_params, db_server=db_server, smk=smk,
574 num_threads=num_threads, num_slots=num_slots, ef_overrides=ef_overrides)
575
576 def run(self, maxEvents=None):
577 """
578 This follows the same pattern as PSC (Psc.cxx):
579 1. Create ApplicationMgr via BootstrapHelper
580 2. Set JobOptionsSvcType, JobOptionsType, JobOptionsPath
581 3. configure() -> initialize() -> prepareForStart() -> start() ->
582 hltUpdateAfterFork() -> run() -> stop() -> finalize() -> terminate()
583 """
584 from Gaudi.Main import BootstrapHelper
585
586 # For FILE mode, load properties from JSON if not already provided
587 if self.job_options_type == "FILE" and self.properties is None:
588 with open(self.job_options_path, 'r') as f:
589 jocat = json.load(f)
590 self.properties = jocat.get('properties', {})
591
592 bsh = BootstrapHelper()
593 app = bsh.createApplicationMgr()
594 self._app = app
595
596 # For FILE mode, set ApplicationMgr properties from JSON before configure
597 if self.job_options_type == "FILE" and self.properties:
598 app_props = self.properties.get('ApplicationMgr', {})
599 for k, v in app_props.items():
600 if k not in ('JobOptionsSvcType', 'JobOptionsType', 'JobOptionsPath'):
601 log.debug("Setting ApplicationMgr.%s = %s", k, v)
602 app.setProperty(k, str(v) if not isinstance(v, str) else v)
603
604 # Set JobOptionsSvc properties like PSC does in Psc.cxx
605 log.info("Configuring TrigConf::JobOptionsSvc with TYPE=%s, PATH=%s",
607 app.setProperty("JobOptionsSvcType", "TrigConf::JobOptionsSvc")
608 app.setProperty("JobOptionsType", self.job_options_type)
609 app.setProperty("JobOptionsPath", self.job_options_path)
610
611 # Configure the application - TrigConf::JobOptionsSvc will load from FILE or DB
612 app.configure()
613
614 # Override EvtMax AFTER configure() only if explicitly specified by user
615 # Otherwise use whatever value is in the DB
616 if maxEvents is not None:
617 log.info("Setting EvtMax=%d (overriding DB value)", maxEvents)
618 app.setProperty('EvtMax', str(maxEvents))
619
620 # All property overrides below use iProperty and must be done after configure()
621 # but before initialize() - this is the same pattern as PSC (Psc.cxx)
622 from GaudiPython.Bindings import iProperty
623
624 # Set threading configuration
625 log.info("Setting threading: ThreadPoolSize=%d, NSlots=%d", self.num_threads, self.num_slots)
626 iProperty("AvalancheSchedulerSvc").ThreadPoolSize = self.num_threads
627 iProperty("EventDataSvc").NSlots = self.num_slots
628
629 # Override EFInterfaceSvc properties explicitly given on the command line.
630 ef_svc = iProperty("EFInterfaceSvc")
631 for prop, value in self.ef_overrides.items():
632 log.info("Overriding EFInterfaceSvc.%s = %s (from command line)", prop, value)
633 setattr(ef_svc, prop, value)
634
635 # If HLT PSK is set on command line, read it from DB instead of COOL (ATR-25974)
636 # This is the same logic as TrigPSCPythonDbSetup.py
637 from TrigPSC import PscConfig
638 if PscConfig.forcePSK:
639 log.info("PscConfig.forcePSK is set - configuring HLTPrescaleCondAlg to read from DB instead of COOL")
640 iProperty("HLTPrescaleCondAlg").Source = "DB"
641
642 # Set forceRunNumber on HltEventLoopMgr if conditions_run is specified
643 # This overrides the run number used for IOV lookup in conditions loading
644 conditions_run = self.run_params.get('conditions_run')
645 if conditions_run is not None:
646 log.info("Setting HltEventLoopMgr.forceRunNumber=%d for conditions lookup", conditions_run)
647 iProperty("HltEventLoopMgr").forceRunNumber = conditions_run
648
649 # Initialize
650 sc = app.initialize()
651 if not sc.isSuccess():
652 log.error("Failed to initialize AppMgr")
653 return sc
654
655 # Initialize TrigServicesHelper for lifecycle calls (prepareForStart, prepareForRun, hltUpdateAfterFork)
656 try:
657 from TrigServices.TrigServicesHelper import TrigServicesHelper
658 helper = TrigServicesHelper()
659 except ImportError as e:
660 log.error("TrigServicesHelper not available: %s", e)
661 log.error("Cannot proceed without TrigServicesHelper - required for HLTEventLoopMgr lifecycle")
662 raise RuntimeError("TrigServicesHelper not available") from e
663
664 # Call prepareForStart to set up ByteStreamMetadata (like PSC does)
665 try:
666 run_number = self.run_params['run_number']
667 det_mask = self.run_params['detector_mask']
668 sor_time = self.run_params['sor_time']
669 solenoid_current = self.run_params['solenoid_current']
670 toroids_current = self.run_params['toroids_current']
671 beam_type = self.run_params['beam_type']
672 beam_energy = self.run_params['beam_energy']
673 lb_number = self.run_params['lb_number']
674
675 log.info("Calling prepareForStart with run=%d, det_mask=0x%s, sor_time=%s",
676 run_number, det_mask, sor_time)
677
678 success = helper.prepareForStart(
679 run_number=run_number,
680 det_mask=det_mask,
681 sor_time=sor_time,
682 lb_number=lb_number,
683 beam_type=beam_type,
684 beam_energy=beam_energy,
685 solenoid_current=solenoid_current,
686 toroids_current=toroids_current
687 )
688 if not success:
689 log.error("prepareForStart failed")
690 raise RuntimeError("prepareForStart failed")
691 log.info("prepareForStart completed successfully")
692 except Exception as e:
693 log.error("Error calling prepareForStart: %s", e)
694 traceback.print_exc()
695 raise
696
697 # Start
698 sc = app.start()
699 if not sc.isSuccess():
700 log.error("Failed to start AppMgr")
701 return sc
702
703 # prepareForRun initializes COOL folder helper - must be called after start()
704 # which fires the start incident
705 try:
706 log.info("Calling prepareForRun to initialize COOL folder helper")
707 success = helper.prepareForRun()
708 if not success:
709 log.error("prepareForRun failed")
710 raise RuntimeError("prepareForRun failed")
711 log.info("prepareForRun completed successfully")
712 except Exception as e:
713 log.error("Error calling prepareForRun: %s", e)
714 traceback.print_exc()
715 raise
716
717 # hltUpdateAfterFork initializes the scheduler (like PSC does after fork)
718 # worker_id=1 for single-worker, non-forked mode
719 try:
720 log.info("Calling hltUpdateAfterFork to initialize scheduler (worker_id=1)")
721 success = helper.hltUpdateAfterFork(worker_id=1)
722 if not success:
723 log.error("hltUpdateAfterFork failed")
724 raise RuntimeError("hltUpdateAfterFork failed")
725 log.info("hltUpdateAfterFork completed successfully")
726 except Exception as e:
727 log.error("Error calling hltUpdateAfterFork: %s", e)
728 traceback.print_exc()
729 raise
730
731 # Run the event loop
732 # Note: Python signal handlers won't work during C++ execution.
733 nevt = maxEvents if maxEvents is not None else -1
734 sc = app.run(nevt)
735
736 if not sc.isSuccess():
737 log.error("Failure running application")
738 return sc
739
740 # Stop
741 sc = app.stop()
742 if not sc.isSuccess():
743 log.error("Failed to stop AppMgr")
744 return sc
745
746 # Finalize
747 sc = app.finalize()
748 if not sc.isSuccess():
749 log.error("Failed to finalize AppMgr")
750 return sc
751
752 # Terminate
753 sc = app.terminate()
754 return sc
755
756
757def load_from_json(json_file, run_params=None, num_threads=1, num_slots=1, ef_overrides=None):
758 """
759 Load configuration from a Gaudi joboptions JSON file.
760
761 Returns a ConfigRunner with a run() method that executes the configuration
762 using TrigConf::JobOptionsSvc with TYPE="FILE".
763 """
764 with open(json_file, 'r') as f:
765 jocat = json.load(f)
766
767 if jocat.get('filetype') != 'joboptions':
768 raise ValueError(f"Invalid JSON file type: {jocat.get('filetype')}, expected 'joboptions'")
769
770 properties = jocat.get('properties', {})
771 return ConfigRunner.from_json(json_file, run_params, properties,
772 num_threads=num_threads,
773 num_slots=num_slots,
774 ef_overrides=ef_overrides)
775
776
777def load_from_database(db_server, smk, l1psk=None, hltpsk=None, run_params=None,
778 num_threads=1, num_slots=1, ef_overrides=None):
779 """
780 Load configuration from trigger database using the Super Master Key (SMK).
781
782 Returns a ConfigRunner that uses TrigConf::JobOptionsSvc with TYPE="DB"
783 to load configuration directly from the database, same as athenaHLT.
784 """
785 log.info("Loading job options from database %s with SMK %d", db_server, smk)
786 return ConfigRunner.from_database(db_server, smk, l1psk, hltpsk, run_params,
787 num_threads=num_threads,
788 num_slots=num_slots,
789 ef_overrides=ef_overrides)
790
791
792
795def arg_sor_time(s) -> str:
796 """Convert possible SOR time arguments to an OWLTime compatible string"""
797 fmt = '%d/%m/%y %H:%M:%S.%f'
798 if s=='now': return dt.now().strftime(fmt)
799 elif s.isdigit(): return dt.fromtimestamp(float(s)/1e9).strftime(fmt)
800 else: return s
801
802
804 """Convert detector mask to format expected by eformat"""
805 if s=='all':
806 return RunParams.DEFAULT_DETECTOR_MASK
807 dmask = hex(int(s,16)) # Normalize input to hex-string
808 dmask = dmask.lower().replace('0x', '').replace('l', '') # remove markers
809 return '0' * (32 - len(dmask)) + dmask # (pad with 0s)
810
811
812def check_args(parser, args):
813 """Consistency check of command line arguments (same as athenaHLT.py)"""
814
815 if not args.jobOptions and not args.use_database:
816 parser.error("No job options file specified")
817
818 if (not args.file and not args.dump_config_exit
819 and (args.efdf_interface_library or 'TrigDFEmulator') == 'TrigDFEmulator'):
820 parser.error("--file is required unless using --dump-config-exit or online efdf-interface-library")
821
822 if args.use_crest and not args.use_database:
823 parser.error("--use-crest requires --use-database")
824
825 if args.oh_monitoring and args.online_environment:
826 parser.error("--oh-monitoring (-M) and --online-environment are mutually exclusive.")
827
828def update_run_params(args, flags):
829 """Update run parameters from IS, file, or conditions DB"""
830
831 # If --online-environment is specified, try to read from Information Service first
832 if getattr(args, 'online_environment', False):
833 log.info("Reading run parameters from Information Service via WEBDAQ")
834 # Pass command-line magnet values as overrides (if provided)
835 # strict=True ensures we fail if IS read fails, rather than falling back to defaults
836 # But if user provided magnet values on command line, those take precedence over IS
837 solenoid_override = getattr(args, 'solenoid_current', None)
838 toroids_override = getattr(args, 'toroids_current', None)
839
840 run_params = get_run_params(from_is=True,
841 partition=getattr(args, 'partition', None),
842 webdaq_base=getattr(args, 'webdaq_base', None),
843 strict=True,
844 solenoid_current_override=solenoid_override,
845 toroids_current_override=toroids_override)
846 # Update args with values from IS (if not already set on command line)
847 if args.run_number is None and run_params.run_number is not None:
848 args.run_number = run_params.run_number
849 log.info("Using run_number=%d from IS", args.run_number)
850 if args.lb_number is None and run_params.lb_number is not None:
851 args.lb_number = run_params.lb_number
852 log.info("Using lb_number=%d from IS", args.lb_number)
853 if args.sor_time is None and run_params.sor_time is not None:
854 args.sor_time = run_params.sor_time
855 log.info("Using sor_time=%s from IS", args.sor_time)
856 if args.detector_mask is None and run_params.detector_mask is not None:
857 args.detector_mask = run_params.detector_mask
858 log.info("Using detector_mask=%s from IS", args.detector_mask)
859 # Update magnet currents from IS (run_params already has command-line overrides if provided)
860 args.solenoid_current = run_params.solenoid_current
861 args.toroids_current = run_params.toroids_current
862 args.beam_type = run_params.beam_type
863 args.beam_energy = run_params.beam_energy
864
865 if (args.run_number is not None and args.lb_number is None) or (args.run_number is None and args.lb_number is not None):
866 log.error("Both or neither of the options -R (--run-number) and -L (--lb-number) have to be specified")
867
868 # Read metadata from input file (like HLTMPPy/runner.py getRunParamsFromFile)
869 if args.file:
870 from eformat import EventStorage
871 dr = EventStorage.pickDataReader(args.file[0])
872 if args.run_number is None:
873 args.run_number = dr.runNumber()
874 args.lb_number = dr.lumiblockNumber()
875 args.T0_project_tag = dr.projectTag()
876 args.beam_type = dr.beamType()
877 args.beam_energy = dr.beamEnergy()
878 args.trigger_type = dr.triggerType()
879 args.stream = dr.stream()
880 args.lumiblock = dr.lumiblockNumber()
881 args.file_detector_mask = "{:032x}".format(dr.detectorMask())
882 else:
883 args.T0_project_tag = getattr(args, 'T0_project_tag', '')
884 args.beam_type = getattr(args, 'beam_type', 0)
885 args.beam_energy = getattr(args, 'beam_energy', 0)
886 args.trigger_type = getattr(args, 'trigger_type', 0)
887 args.stream = getattr(args, 'stream', '')
888 args.lumiblock = getattr(args, 'lumiblock', 0)
889 args.file_detector_mask = getattr(args, 'file_detector_mask', '00000000000000000000000000000000')
890
891 sor_params = None
892 if (args.sor_time is None or args.detector_mask is None) and args.run_number is not None:
893 sor_params = AthHLT.get_sor_params(args.run_number)
894 log.debug('SOR parameters: %s', sor_params)
895 if sor_params is None:
896 log.error("Run %d does not exist. If you want to use this run-number specify "
897 "remaining run parameters, e.g.: --sor-time=now --detector-mask=all", args.run_number)
898 sys.exit(1)
899
900 if args.sor_time is None and sor_params is not None:
901 args.sor_time = arg_sor_time(str(sor_params['SORTime']))
902
903 if args.detector_mask is None and sor_params is not None:
904 dmask = sor_params['DetectorMask']
905 if args.run_number < AthHLT.CondDB._run2:
906 dmask = hex(dmask)
907 args.detector_mask = arg_detector_mask(dmask)
908
909 if args.dump_config_exit and not args.run_number:
910 args.run_number = 0
911
912 # Apply defaults for magnet currents if not set (offline mode only)
913 # In online mode, magnets must come from IS or command line (handled above)
914 if getattr(args, 'solenoid_current', None) is None:
915 args.solenoid_current = RunParams.DEFAULT_SOLENOID_CURRENT
916 log.debug("Using default solenoid_current=%.1f", args.solenoid_current)
917 if getattr(args, 'toroids_current', None) is None:
918 args.toroids_current = RunParams.DEFAULT_TOROIDS_CURRENT
919 log.debug("Using default toroids_current=%.1f", args.toroids_current)
920
921
922def update_trigconf_keys(args, flags):
923 """Update trigger configuration keys from OKS, COOL, or CREST.
924
925 Priority order:
926 1. Command-line arguments (always take precedence)
927 2. OKS via WEBDAQ (if --online-environment is set)
928 3. CREST (if --use-crest is set)
929 4. COOL (default)
930 """
931
932 if args.smk is None or args.l1psk is None or args.hltpsk is None:
933 trigconf = None
934
935 # Try OKS first if --online-environment is set
936 if getattr(args, 'online_environment', False):
937 log.info("Reading trigger configuration keys from OKS (online environment)")
938 # strict=True ensures we fail if OKS read fails, rather than falling back to COOL
940 partition=getattr(args, 'partition', None),
941 webdaq_base=getattr(args, 'webdaq_base', None),
942 strict=True
943 )
944 log.info("Retrieved trigger keys from OKS: %s", oks_keys)
945
946 # With strict=True, we're guaranteed to have all keys or an exception was raised
947 trigconf = {
948 'SMK': oks_keys.get('SMK'),
949 'LVL1PSK': oks_keys.get('L1PSK'),
950 'HLTPSK': oks_keys.get('HLTPSK')
951 }
952 # Also update db_server if provided by OKS and not set on command line
953 if oks_keys.get('db_alias') and args.db_server == 'TRIGGERDB_RUN3':
954 args.db_server = oks_keys['db_alias']
955 log.info("Using db_server=%s from OKS", args.db_server)
956
957 # Fall back to CREST or COOL only if NOT in online-environment mode
958 if trigconf is None:
959 if args.use_crest:
960 crest_server = args.crest_server or flags.Trigger.crestServer
961 log.info("Reading trigger configuration keys from CREST for run %s", args.run_number)
962 trigconf = AthHLT.get_trigconf_keys_crest(args.run_number, args.lb_number, crest_server)
963 log.info("Retrieved trigger keys from CREST: %s", trigconf)
964 else:
965 log.info("Reading trigger configuration keys from COOL for run %s", args.run_number)
966 trigconf = AthHLT.get_trigconf_keys(args.run_number, args.lb_number)
967 log.info("Retrieved trigger keys from COOL: %s", trigconf)
968
969 try:
970 if args.smk is None:
971 args.smk = trigconf['SMK']
972 log.debug("Using SMK=%d from conditions DB/OKS", args.smk)
973 else:
974 log.debug("Using SMK=%d from command line (ignoring DB/OKS value %s)", args.smk, trigconf.get('SMK'))
975 if args.l1psk is None:
976 args.l1psk = trigconf['LVL1PSK']
977 log.debug("Using L1PSK=%d from conditions DB/OKS", args.l1psk)
978 else:
979 log.debug("Using L1PSK=%d from command line (ignoring DB/OKS value %s)", args.l1psk, trigconf.get('LVL1PSK'))
980 if args.hltpsk is None:
981 args.hltpsk = trigconf['HLTPSK']
982 log.debug("Using HLTPSK=%d from conditions DB/OKS", args.hltpsk)
983 else:
984 log.debug("Using HLTPSK=%d from command line (ignoring DB/OKS value %s)", args.hltpsk, trigconf.get('HLTPSK'))
985 except KeyError:
986 log.error("Cannot read trigger configuration keys from the conditions database for run %d", args.run_number)
987 sys.exit(1)
988 else:
989 log.info("Using trigger configuration keys from command line: SMK=%d, L1PSK=%d, HLTPSK=%d",
990 args.smk, args.l1psk, args.hltpsk)
991
993 """Start a private TDAQ infrastructure (offline test of OH publication)."""
994 import shutil, socket, signal, subprocess, time
995
996 infra_script = shutil.which('athenaEF_tdaq_infra.py')
997 if infra_script is None:
998 log.error("athenaEF_tdaq_infra.py not found on PATH (required for -M)")
999 sys.exit(1)
1000
1001 partition = args.partition or 'athenaEF'
1002 host = 'localhost'
1003 # Get a free port for webis
1004 s = socket.socket()
1005 s.bind((host, 0))
1006 port = s.getsockname()[1]
1007 s.close()
1008 oh_server = 'Histogramming' # WebdaqHistSvc.OHServerName default
1009 run_number = args.run_number if args.run_number is not None else 0
1010
1011 # Export variables for the -M case
1012 os.environ['TDAQ_PARTITION'] = partition
1013 os.environ['TDAQ_WEBDAQ_BASE'] = f'http://{host}:{port}'
1014 os.environ['TDAQ_OH_SERVER'] = oh_server
1015
1016 log.info("Starting private OH infrastructure: partition=%s, webdaq=%s, oh_server=%s",
1017 partition, os.environ['TDAQ_WEBDAQ_BASE'], oh_server)
1018
1019 logfile = open('athenaEF_oh_infra.log', 'w')
1020
1021 # PR_SET_PDEATHSIG so the infrastructure is torn down (SIGTERM -> oh_cp +
1022 # ipc_rm) if athenaEF dies unexpectedly, e.g. segfaults mid-run.
1023 from ctypes import cdll
1024 PR_SET_PDEATHSIG = 1
1025 def _pdeathsig():
1026 cdll['libc.so.6'].prctl(PR_SET_PDEATHSIG, signal.SIGTERM)
1027
1028 proc = subprocess.Popen(
1029 [infra_script,
1030 '--partition', partition,
1031 '--webdaq-port', str(port),
1032 '--oh-server', oh_server,
1033 '--run-number', str(run_number)],
1034 stdout=logfile, stderr=subprocess.STDOUT,
1035 preexec_fn=_pdeathsig, close_fds=True)
1036
1037 # Wait for the readiness marker (or early failure / timeout)
1038 timeout = 120
1039 deadline = time.time() + timeout
1040 while time.time() < deadline:
1041 if proc.poll() is not None:
1042 log.error("OH infrastructure exited early (code %s); see %s", proc.returncode, logfile.name)
1043 sys.exit(1)
1044 with open(logfile.name) as f:
1045 if 'ATHENAEF_INFRA_READY' in f.read():
1046 log.info("OH infrastructure is ready")
1047 return proc
1048 time.sleep(1)
1049
1050 log.error("OH infrastructure did not become ready within %d s; see %s", timeout, logfile.name)
1051 proc.terminate()
1052 sys.exit(1)
1053
1054
1056 """Terminate the private TDAQ infrastructure (SIGTERM triggers oh_cp + ipc_rm)."""
1057 if proc is None or proc.poll() is not None:
1058 return
1059 import signal
1060 log.info("Stopping OH infrastructure")
1061 proc.send_signal(signal.SIGTERM)
1062 try:
1063 proc.wait(timeout=60)
1064 except Exception:
1065 proc.kill()
1066
1067
1068class MyHelp(argparse.Action):
1069 """Custom help to hide/show expert groups"""
1070 def __call__(self, parser, namespace, values, option_string=None):
1071
1072 for g in parser.expert_groups:
1073 for a in g._group_actions:
1074 if values!='all':
1075 a.help = argparse.SUPPRESS
1076
1077 parser.print_help()
1078 if values!='all':
1079 print('\nUse --help=all to show all (expert) options')
1080 sys.exit(0)
1081
1082
1083def main():
1084 parser = argparse.ArgumentParser(prog='athenaEF.py', formatter_class=
1085 lambda prog : argparse.ArgumentDefaultsHelpFormatter(prog, max_help_position=32, width=100),
1086 usage = '%(prog)s [OPTION]... -f FILE jobOptions',
1087 add_help=False)
1088 parser.expert_groups = [] # Keep list of expert option groups
1089
1090
1091 g = parser.add_argument_group('Options')
1092 g.add_argument('jobOptions', nargs='?', help='job options: CA module (package.module:function), pickle file (.pkl), or JSON file (.json)')
1093 g.add_argument('--threads', metavar='N', type=int, default=1, help='number of threads')
1094 g.add_argument('--concurrent-events', metavar='N', type=int, help='number of concurrent events if different from --threads')
1095 g.add_argument('--log-level', '-l', metavar='LVL', default='INFO', help='OutputLevel of athena')
1096 g.add_argument('--precommand', '-c', metavar='CMD', action='append', default=[],
1097 help='Python commands executed before job options')
1098 g.add_argument('--postcommand', '-C', metavar='CMD', action='append', default=[],
1099 help='Python commands executed after job options')
1100 g.add_argument('--interactive', '-i', action='store_true', help='interactive mode')
1101 g.add_argument('--help', '-h', nargs='?', choices=['all'], action=MyHelp, help='show help')
1102
1103 g = parser.add_argument_group('Input/Output')
1104 g.add_argument('--file', '--filesInput', '-f', action='append', help='input RAW file')
1105 g.add_argument('--save-output', '-o', metavar='FILE', help='output file name')
1106 g.add_argument('--number-of-events', '--evtMax', '-n', metavar='N', type=int, default=None,
1107 help='processes N events (default: from DB/config, -1 means all)')
1108 g.add_argument('--skip-events', '--skipEvents', '-k', metavar='N', type=int, default=None,
1109 help='skip N first events')
1110 g.add_argument('--loop-files', action=argparse.BooleanOptionalAction, default=None,
1111 help='loop over input files if no more events')
1112 g.add_argument('--efdf-interface-library', metavar='LIB', default=None,
1113 help='name of the EFDF interface shared library to load (default: TrigDFEmulator)')
1114
1115
1116 g = parser.add_argument_group('Performance and debugging')
1117 g.add_argument('--perfmon', action='store_true', help='enable PerfMon')
1118 g.add_argument('--tcmalloc', action='store_true', default=True, help='use tcmalloc')
1119 g.add_argument('--stdcmalloc', action='store_true', help='use stdcmalloc')
1120 g.add_argument('--stdcmath', action='store_true', help='use stdcmath library')
1121 g.add_argument('--imf', action='store_true', default=True, help='use Intel math library')
1122 g.add_argument('--show-includes', '-s', action='store_true', help='show printout of included files')
1123
1124
1125 g = parser.add_argument_group('Conditions')
1126 g.add_argument('--run-number', '-R', metavar='RUN', type=int,
1127 help='run number (if None, read from first event)')
1128 g.add_argument('--lb-number', '-L', metavar='LBN', type=int,
1129 help='lumiblock number (if None, read from first event)')
1130 g.add_argument('--conditions-run', metavar='RUN', type=int, default=None,
1131 help='reference run number for conditions lookup (use when IS run number has no COOL data)')
1132 g.add_argument('--sor-time', type=arg_sor_time,
1133 help='The Start Of Run time. Three formats are accepted: '
1134 '1) the string "now", for current time; '
1135 '2) the number of nanoseconds since epoch (e.g. 1386355338658000000 or int(time.time() * 1e9)); '
1136 '3) human-readable "20/11/18 17:40:42.3043". If not specified the sor-time is read from the conditions DB')
1137 g.add_argument('--detector-mask', metavar='MASK', type=arg_detector_mask,
1138 help='detector mask (if None, read from the conditions DB), use string "all" to enable all detectors')
1139
1140
1141 g = parser.add_argument_group('Database')
1142 g.add_argument('--use-database', '-b', action='store_true',
1143 help='configure from trigger database using SMK')
1144 g.add_argument('--db-server', metavar='DB', default='TRIGGERDB_RUN3', help='DB server name (alias)')
1145 g.add_argument('--smk', type=int, default=None, help='Super Master Key')
1146 g.add_argument('--l1psk', type=int, default=None, help='L1 prescale key')
1147 g.add_argument('--hltpsk', type=int, default=None, help='HLT prescale key')
1148 g.add_argument('--use-crest', action='store_true', default=False,
1149 help='Use CREST for trigger configuration')
1150 g.add_argument('--crest-server', metavar='URL', default=None,
1151 help='CREST server URL (defaults to flags.Trigger.crestServer)')
1152 g.add_argument('--dump-config', action='store_true', help='Dump joboptions JSON file')
1153 g.add_argument('--dump-config-exit', action='store_true', help='Dump joboptions JSON file and exit')
1154
1155
1156 g = parser.add_argument_group('Magnets')
1157 g.add_argument('--solenoid-current', type=float, default=None,
1158 help='Solenoid current in Amperes (default: nominal current for offline running, required from IS online)')
1159 g.add_argument('--toroids-current', type=float, default=None,
1160 help='Toroids current in Amperes (default: nominal current for offline running, required from IS online)')
1161
1162
1163 g = parser.add_argument_group('Online')
1164 g.add_argument('--online-environment', action='store_true',
1165 help='Enable online environment: read run parameters from IS and trigger '
1166 'configuration keys (SMK, L1PSK, HLTPSK) from OKS via WEBDAQ REST API')
1167 g.add_argument('--partition', metavar='NAME', default=None,
1168 help='TDAQ partition name (defaults to TDAQ_PARTITION environment variable)')
1169 g.add_argument('--webdaq-base', metavar='URL', default=None,
1170 help='WEBDAQ base URL (defaults to TDAQ_WEBDAQ_BASE environment variable)')
1171
1172
1173 g = parser.add_argument_group('Online Histogramming')
1174 g.add_argument('--oh-monitoring', '-M', action='store_true', default=False,
1175 help='enable online histogram publishing via WebdaqHistSvc')
1176
1177
1178 g = parser.add_argument_group('Expert')
1179 parser.expert_groups.append(g)
1180 (args, unparsed_args) = parser.parse_known_args()
1181 check_args(parser, args)
1182
1183 # set ROOT to batch mode (ATR-21890)
1184 from PyUtils.Helpers import ROOTSetup
1185 ROOTSetup(batch=True)
1186
1187 # Enable ROOT thread safety
1188 import ROOT
1189 ROOT.ROOT.EnableThreadSafety()
1190
1191 # set default Python OutputLevel and file inclusion
1192 import AthenaCommon.Logging
1193 AthenaCommon.Logging.log.setLevel(getattr(logging, args.log_level))
1194 AthenaCommon.Logging.log.setFormat("%(asctime)s Py:%(name)-31s %(levelname)7s %(message)s")
1195 if args.show_includes:
1196 from AthenaCommon.Include import include
1197 include.setShowIncludes( True )
1198
1199 # consistency checks for arguments
1200 if not args.concurrent_events:
1201 args.concurrent_events = args.threads
1202
1203 # Update args and set athena flags
1204 from TrigPSC import PscConfig
1205 from TrigPSC.PscDefaultFlags import defaultOnlineFlags
1206
1207 # Get flags with online defaults (same as athenaHLT)
1208 flags = defaultOnlineFlags()
1209
1210 # set MessageSvc OutputLevel
1211 from AthenaCommon import Constants
1212 flags.Exec.OutputLevel = getattr(Constants, args.log_level)
1213
1214 # Enable WebdaqHistSvc for online histogram publishing if requested
1215 if args.oh_monitoring:
1216 flags.Trigger.Online.useOnlineWebdaqHistSvc = True
1217 log.info("Enabled WebdaqHistSvc for online histogram publishing")
1218
1219 # CREST configuration (same as athenaHLT)
1220 log.info("Using CREST for trigger configuration: %s", args.use_crest)
1221 if args.use_crest:
1222 flags.Trigger.useCrest = True
1223 if args.crest_server:
1224 flags.Trigger.crestServer = args.crest_server
1225 else:
1226 args.crest_server = flags.Trigger.crestServer
1227
1228 update_run_params(args, flags)
1229
1230 if args.use_database:
1231 # If HLTPSK was given on the command line OR from OKS (--online-environment),
1232 # we ignore what is stored in COOL and use the specified key directly from the DB.
1233 # This is needed because COOL may point to a different HLTPSK for the forced run number.
1234 PscConfig.forcePSK = (args.hltpsk is not None) or args.online_environment
1235 # Read trigger config keys from COOL/OKS if not specified
1236 update_trigconf_keys(args, flags)
1237
1238 # Fill flags from command line (if not running from DB/JSON)
1239 if not args.use_database and args.jobOptions and not args.jobOptions.endswith('.json'):
1240 PscConfig.unparsedArguments = unparsed_args
1241 for flag_arg in unparsed_args:
1242 flags.fillFromString(flag_arg)
1243
1244 PscConfig.interactive = args.interactive
1245 PscConfig.exitAfterDump = args.dump_config_exit
1246
1247 # NOTE: Do NOT set flags.Input.Files here!
1248 # Like athenaHLT, we keep Input.Files=[] during configuration to ensure the
1249 # configuration is portable and doesn't depend on specific input file metadata.
1250 # Input files are passed to EFInterface for runtime use only.
1251
1252 # Set conditions run number override (for test partitions with fake run numbers)
1253 if args.conditions_run is not None:
1254 log.info("Using conditions from reference run %d (overriding run %s for IOV lookup)",
1255 args.conditions_run, args.run_number)
1256 flags.Input.ConditionsRunNumber = args.conditions_run
1257
1258 # Set number of events
1259 if args.number_of_events is not None and args.number_of_events > 0:
1260 flags.Exec.MaxEvents = args.number_of_events
1261
1262 # Set skip events
1263 if args.skip_events is not None and args.skip_events > 0:
1264 flags.Exec.SkipEvents = args.skip_events
1265
1266 # NOTE: Do NOT set flags.Concurrency.NumThreads or NumConcurrentEvents here.
1267 # Threading is set at runtime via iProperty after configure() - see ConfigRunner.run()
1268
1269 # Enable PerfMon if requested
1270 flags.PerfMon.doFastMonMT = args.perfmon
1271
1272 # Configure EF ByteStream services (mandatory to run without HLTMPPU)
1273 # This provides the data flow interface that would normally come from HLTMPPU
1274 flags.Trigger.Online.useEFByteStreamSvc = True
1275 # EFInterfaceSvc settings from the command line.
1276 # Only options explicitly given are collected, anything else keeps the value from the DB/jobOptions configuration
1277 ef_files = args.file if args.file else []
1278 ef_overrides = {}
1279 if ef_files:
1280 ef_overrides['Files'] = ef_files
1281 # Metadata read from the input file - always more accurate than DB values
1282 ef_overrides.update({
1283 'T0ProjectTag' : args.T0_project_tag,
1284 'BeamType' : args.beam_type,
1285 'BeamEnergy' : args.beam_energy,
1286 'TriggerType' : args.trigger_type,
1287 'Stream' : args.stream,
1288 'Lumiblock' : args.lumiblock,
1289 'DetMask' : args.file_detector_mask,
1290 })
1291 if args.run_number is not None: # from -R, IS, or the input file
1292 ef_overrides['RunNumber'] = args.run_number
1293 if args.save_output is not None:
1294 ef_overrides['OutputFileName'] = args.save_output
1295 if args.loop_files is not None:
1296 ef_overrides['LoopOverFiles'] = args.loop_files
1297 if args.number_of_events is not None:
1298 ef_overrides['NumEvents'] = args.number_of_events
1299 if args.skip_events is not None:
1300 ef_overrides['SkipEvents'] = args.skip_events
1301 if args.efdf_interface_library is not None:
1302 ef_overrides['EFDFInterfaceLibraryName'] = args.efdf_interface_library
1303
1304 # Apply to the flags for the CA-module path (getEFInterfaceSvc reads these)
1305 _prop2flag = {'Files': 'Files', 'OutputFileName': 'OutputFileName',
1306 'LoopOverFiles': 'LoopFiles', 'NumEvents': 'NumEvents',
1307 'SkipEvents': 'SkipEvents', 'RunNumber': 'RunNumber',
1308 'T0ProjectTag': 'T0ProjectTag', 'BeamType': 'BeamType',
1309 'BeamEnergy': 'BeamEnergy', 'TriggerType': 'TriggerType',
1310 'Stream': 'Stream', 'Lumiblock': 'Lumiblock', 'DetMask': 'DetMask',
1311 'EFDFInterfaceLibraryName': 'LibraryName'}
1312 for prop, value in ef_overrides.items():
1313 setattr(flags.Trigger.Online.EFInterface, _prop2flag[prop], value)
1314
1315 # Execute precommands
1316 if args.precommand:
1317 log.info("Executing precommand(s)")
1318 for cmd in args.precommand:
1319 log.info(" %s", cmd)
1320 exec(cmd, globals(), {'flags': flags})
1321
1322 # Determine input type
1323 is_database = args.use_database
1324 is_pickle = False
1325 is_json = False
1326
1327 if not is_database and args.jobOptions:
1328 jobOptions = args.jobOptions
1329 is_pickle = jobOptions.endswith('.pkl')
1330 is_json = jobOptions.endswith('.json')
1331
1332 if is_database:
1333 # Load configuration from trigger database
1334 # Handle CREST vs standard DB access (same as athenaHLT)
1335 if args.use_crest:
1336 crestconn = TriggerCrestUtil.getCrestConnection(args.db_server)
1337 db_alias = f"{args.crest_server}/{crestconn}"
1338 log.info("Loading configuration via CREST from %s with SMK %d", db_alias, args.smk)
1339 else:
1340 db_alias = args.db_server
1341 log.info("Loading configuration from database %s with SMK %d", db_alias, args.smk)
1342
1343 # Get run parameters for prepareForStart
1344 run_params = get_run_params(args).to_dict()
1345 acc = load_from_database(db_alias, args.smk, args.l1psk, args.hltpsk, run_params,
1346 num_threads=args.threads, num_slots=args.concurrent_events,
1347 ef_overrides=ef_overrides)
1348 log.info("Configuration loaded from database")
1349
1350 elif is_pickle:
1351 # Load ComponentAccumulator from pickle file
1352 log.info("Loading configuration from pickle file: %s", jobOptions)
1353 with open(jobOptions, 'rb') as f:
1354 acc = pickle.load(f)
1355 log.info("Configuration loaded from pickle")
1356
1357 elif is_json:
1358 # Load configuration from JSON file
1359 log.info("Loading configuration from JSON file: %s", jobOptions)
1360 # Get run parameters for prepareForStart
1361 run_params = get_run_params(args).to_dict()
1362 acc = load_from_json(jobOptions, run_params,
1363 num_threads=args.threads, num_slots=args.concurrent_events,
1364 ef_overrides=ef_overrides)
1365 log.info("Configuration loaded from JSON")
1366
1367 else:
1368 # Load from CA module - follow the same pattern as athenaHLT/TrigPSCPythonCASetup:
1369 # 1. Build the full configuration with services
1370 # 2. Dump to JSON file
1371 # 3. Use AthHLT.reload_from_json to re-exec and reload from JSON
1372 # This preserves the ability to use the same JobOptionsSvc as athenaHLT
1373 log.info("Loading CA configuration from: %s", jobOptions)
1374
1375 # Clone and lock flags for services configuration (as done in TrigPSCPythonCASetup)
1376 from AthenaConfiguration.ComponentAccumulator import ComponentAccumulator
1377 from AthenaConfiguration.MainServicesConfig import addMainSequences
1378 from TrigServices.TriggerUnixStandardSetup import commonServicesCfg
1379 from AthenaConfiguration.ComponentFactory import CompFactory
1380
1381 locked_flags = flags.clone()
1382 locked_flags.lock()
1383
1384 # Create base CA with framework services (like TrigPSCPythonCASetup)
1385 cfg = ComponentAccumulator(CompFactory.AthSequencer("AthMasterSeq", Sequential=True))
1386 cfg.setAppProperty('ExtSvcCreates', False)
1387 cfg.setAppProperty("MessageSvcType", "TrigMessageSvc")
1388 cfg.setAppProperty("JobOptionsSvcType", "TrigConf::JobOptionsSvc")
1389
1390 # Add main sequences and common services (includes TrigServicesCfg)
1391 addMainSequences(locked_flags, cfg)
1392 cfg.merge(commonServicesCfg(locked_flags))
1393
1394 # Now merge user CA config (with unlocked flags, as in TrigPSCPythonCASetup)
1395 cfg_func = AthHLT.getCACfg(jobOptions)
1396 cfg.merge(cfg_func(flags))
1397
1398 # Execute postcommands before dumping (like TrigPSCPythonCASetup)
1399 if args.postcommand:
1400 log.info("Executing postcommand(s)")
1401 for cmd in args.postcommand:
1402 log.info(" %s", cmd)
1403 exec(cmd, globals(), {'flags': flags, 'cfg': cfg})
1404 args.postcommand = [] # Clear so we don't run them again later
1405
1406 # Dump configuration to JSON (like TrigPSCPythonCASetup)
1407 fname = "HLTJobOptions"
1408 log.info("Dumping configuration to %s.pkl and %s.json", fname, fname)
1409 with open(f"{fname}.pkl", "wb") as f:
1410 cfg.store(f)
1411
1412 from TrigConfIO.JsonUtils import create_joboptions_json
1413 create_joboptions_json(f"{fname}.pkl", f"{fname}.json")
1414
1415 # Check for dump-and-exit
1416 if args.dump_config_exit:
1417 log.info("Configuration dumped to %s.json. Exiting...", fname)
1418 sys.exit(0)
1419
1420 # Re-exec from the JSON (same as athenaHLT TrigPSCPythonCASetup -> AthHLT.reload_from_json -> os.execvp).
1421 # This replaces the process image freeing up the configuration heap
1422 log.info("Configuration dumped to %s.json. Re-exec...", fname)
1423 AthHLT.reload_from_json(f"{fname}.json", suppress_args=PscConfig.unparsedArguments + ['--dump-config'], jobOptions=args.jobOptions)
1424
1425 # Execute postcommands
1426 if args.postcommand:
1427 log.info("Executing postcommand(s)")
1428 for cmd in args.postcommand:
1429 log.info(" %s", cmd)
1430 exec(cmd, globals(), {'flags': flags, 'acc': acc})
1431
1432 # Dump configuration if requested
1433 if args.dump_config or args.dump_config_exit:
1434 fname = "HLTJobOptions"
1435
1436 if is_database:
1437 # For DB mode, fetch properties via Python API
1438 from TrigConfIO.HLTTriggerConfigAccess import HLTJobOptionsAccess
1439 log.info("Fetching configuration from database for dump...")
1440 jo_access = HLTJobOptionsAccess(dbalias=acc.db_server, smkey=acc.smk)
1441 props = jo_access.algorithms()
1442
1443 log.info("Dumping configuration to %s.json", fname)
1444 hlt_json = {'filetype': 'joboptions', 'properties': props}
1445 with open(f"{fname}.json", "w") as f:
1446 json.dump(hlt_json, f, indent=4, sort_keys=True, ensure_ascii=True)
1447
1448 elif is_json:
1449 # For JSON mode, properties were already loaded
1450 props = acc.properties
1451 if props:
1452 log.info("Dumping configuration to %s.json", fname)
1453 hlt_json = {'filetype': 'joboptions', 'properties': props}
1454 with open(f"{fname}.json", "w") as f:
1455 json.dump(hlt_json, f, indent=4, sort_keys=True, ensure_ascii=True)
1456 else:
1457 log.warning("No properties available to dump")
1458
1459 elif is_pickle:
1460 # For pickle-loaded ComponentAccumulator, gather properties
1461 app_props, msg_props, comp_props = acc.gatherProps()
1462 props = {"ApplicationMgr": app_props, "MessageSvc": msg_props}
1463 for comp, name, value in comp_props:
1464 props.setdefault(comp, {})[name] = value
1465
1466 log.info("Dumping configuration to %s.json", fname)
1467 hlt_json = {'filetype': 'joboptions', 'properties': props}
1468 with open(f"{fname}.json", "w") as f:
1469 json.dump(hlt_json, f, indent=4, sort_keys=True, ensure_ascii=True)
1470
1471 # Note: For CA module, dumping is already handled earlier
1472 # before converting to ConfigRunner
1473
1474 if args.dump_config_exit:
1475 log.info("Configuration dumped. Exiting...")
1476 sys.exit(0)
1477
1478 # Run the application directly (like athena.py does)
1479 log.info("Starting Athena execution...")
1480
1481 # Create worker directory structure that HLT services expect
1482 # (normally created by HLTMPPU/PSC). Worker ID 1 means single-worker, non-forked mode
1483 # and must match what we pass to hltUpdateAfterFork(worker_id=1) in ConfigRunner.run()
1484 worker_dir = os.path.join(os.getcwd(), "athenaHLT_workers", "athenaHLT-01")
1485 if not os.path.exists(worker_dir):
1486 log.info("Creating worker directory: %s", worker_dir)
1487 os.makedirs(worker_dir, exist_ok=True)
1488
1489 # Start the private TDAQ infrastructure for -M
1490 oh_infra = start_oh_infrastructure(args) if args.oh_monitoring else None
1491
1492 if args.interactive:
1493 log.info("Interactive mode - call acc.run() to execute")
1494 import code
1495 code.interact(local={'acc': acc, 'flags': flags})
1496 else:
1497 # Run the application
1498 from AthenaCommon import ExitCodes
1499 exitcode = 0
1500 try:
1501 # Pass maxEvents if explicitly set (including -1 for all events)
1502 sc = acc.run(args.number_of_events)
1503 if sc.isFailure():
1504 exitcode = ExitCodes.EXE_ALG_FAILURE
1505 except SystemExit as e:
1506 exitcode = ExitCodes.EXE_ALG_FAILURE if e.code == 1 else e.code
1507 except Exception:
1508 traceback.print_exc()
1509 exitcode = ExitCodes.UNKNOWN_EXCEPTION
1510 finally:
1511 stop_oh_infrastructure(oh_infra)
1512
1513 log.info('Leaving with code %d: "%s"', exitcode, ExitCodes.what(exitcode))
1514 sys.exit(exitcode)
1515
1516
1517if "__main__" in __name__:
1518 sys.exit(main())
void print(char *figname, TCanvas *c1)
Helper class to call ITrigEventLoopMgr methods from Python.
__init__(self, job_options_type, job_options_path, run_params=None, properties=None, db_server=None, smk=None, num_threads=1, num_slots=1, ef_overrides=None)
Definition athenaEF.py:532
from_database(cls, db_server, smk, l1psk=None, hltpsk=None, run_params=None, num_threads=1, num_slots=1, ef_overrides=None)
Definition athenaEF.py:565
run(self, maxEvents=None)
Definition athenaEF.py:576
from_json(cls, json_file, run_params=None, properties=None, num_threads=1, num_slots=1, ef_overrides=None)
Definition athenaEF.py:558
__call__(self, parser, namespace, values, option_string=None)
Definition athenaEF.py:1070
from_is(cls, partition=None, webdaq_base=None, strict=False, solenoid_current_override=None, toroids_current_override=None)
Definition athenaEF.py:156
from_args(cls, args)
Definition athenaEF.py:137
bool DEFAULT_RECORDING_ENABLED
Definition athenaEF.py:81
str DEFAULT_DETECTOR_MASK
Definition athenaEF.py:73
__init__(self, run_number=None, lb_number=None, detector_mask=None, sor_time=None, solenoid_current=None, toroids_current=None, beam_type=None, beam_energy=None, run_type=None, trigger_type=None, recording_enabled=None, conditions_run=None, T0_project_tag='', stream='', lumiblock=0)
Definition athenaEF.py:98
std::string replace(std::string s, const std::string &s2, const std::string &s3)
Definition hcg.cxx:312
T * get(TKey *tobj)
get a TObject* from a TKey* (why can't a TObject be a TKey?)
Definition hcg.cxx:132
arg_detector_mask(s)
Definition athenaEF.py:803
get_trigconf_keys_from_oks(partition=None, webdaq_base=None, strict=False)
Definition athenaEF.py:326
str arg_sor_time(s)
The following arg_* methods are used as custom types in argparse.
Definition athenaEF.py:795
load_from_json(json_file, run_params=None, num_threads=1, num_slots=1, ef_overrides=None)
Definition athenaEF.py:757
start_oh_infrastructure(args)
Definition athenaEF.py:992
load_from_database(db_server, smk, l1psk=None, hltpsk=None, run_params=None, num_threads=1, num_slots=1, ef_overrides=None)
Definition athenaEF.py:778
update_run_params(args, flags)
Definition athenaEF.py:828
check_args(parser, args)
Definition athenaEF.py:812
update_trigconf_keys(args, flags)
Definition athenaEF.py:922
get_run_params(args=None, from_is=False, partition=None, webdaq_base=None, strict=False, solenoid_current_override=None, toroids_current_override=None)
Definition athenaEF.py:490
stop_oh_infrastructure(proc)
Definition athenaEF.py:1055