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