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