ATLAS Offline Software
Toggle main menu visibility
Loading...
Searching...
No Matches
PhysicsAnalysis
Algorithms
AnalysisAlgorithmsConfig
python
AutogenDocumentation.py
Go to the documentation of this file.
1
# Copyright (C) 2002-2026 CERN for the benefit of the ATLAS collaboration
2
#
3
# @author Baptiste Ravina
4
"""
5
Core methods to extract options information from ConfigBlock classes and merge with
6
output variables metadata from a YAML file.
7
"""
8
9
import
inspect
10
import
yaml
11
import
re
12
import
logging
13
from
typing
import
Any, Dict, List, Type, Optional, Union
14
15
from
AthenaCommon.Utils.unixtools
import
find_datafile
16
17
logger = logging.getLogger(
"AutogenDocumentation"
)
18
19
def
load_output_variables
(yaml_filepath: str) -> Dict[str, List[Dict[str, Any]]]:
20
"""
21
Load output variables metadata from YAML file.
22
23
Expected YAML format:
24
BlockClassName:
25
- name: variable_name
26
description: Variable description
27
toggled_by: Optional condition description
28
29
Args:
30
yaml_filepath: Path to the YAML file
31
32
Returns:
33
Dictionary mapping block class names to their output variables
34
"""
35
with
open(yaml_filepath,
"r"
)
as
f:
36
data = yaml.safe_load(f)
37
return
data
if
data
else
{}
38
39
40
def
extract_block_options
(block_class: Type) -> Dict[str, Any]:
41
"""
42
Extract options information from a ConfigBlock subclass.
43
44
Args:
45
block_class: A class that inherits from ConfigBlock
46
47
Returns:
48
A dictionary containing the class name and its options
49
"""
50
# Create a temporary instance to access the options
51
instance = block_class()
52
53
# Get the options dictionary
54
options_dict = instance.getOptions()
55
56
# Extract information for each option
57
options_list = []
58
for
option_name, option_obj
in
options_dict.items():
59
# skip some specific options
60
if
option_name
in
[
"groupName"
,
"propertyOverrides"
,
"ignoreDependencies"
]:
61
continue
62
option_info = {
63
"label"
: option_name,
64
"type"
: option_obj.type.__name__
if
option_obj.type
is
not
None
else
"None"
,
65
"default"
: option_obj.default,
66
"info"
: option_obj.info,
67
"required"
: option_obj.required,
68
"noneAction"
: option_obj.noneAction,
69
"physicalUnit"
:
interpret_physical_unit
(option_obj.info),
70
"meta"
: option_obj.meta,
71
}
72
# Check if this option has expert mode settings
73
if
(
74
hasattr(instance,
"_expertModeSettings"
)
75
and
option_name
in
instance._expertModeSettings
76
):
77
expert_rule = instance._expertModeSettings[option_name]
78
if
not
isinstance(expert_rule, list):
79
expert_rule = [expert_rule]
80
else
:
81
expert_rule =
None
82
option_info[
"expertMode"
] = expert_rule
83
options_list.append(option_info)
84
85
return
{
86
"class"
: block_class.__name__,
87
"module"
: block_class.__module__,
88
"docstring"
: inspect.getdoc(block_class),
89
"options"
: options_list,
90
}
91
92
93
def
interpret_physical_unit
(info: str) -> Optional[str]:
94
"""
95
Extract a physical unit from an info string.
96
Currently looks for energy units like MeV or GeV.
97
98
Args:
99
info: The information string from an option.
100
101
Returns:
102
The detected unit as a string ("MeV", "GeV", etc.) or None if no unit is found.
103
"""
104
if
not
info:
105
return
None
106
107
# Check for specific units
108
patterns = [
109
r"\[MeV\]"
,
110
r"\(MeV\)"
,
111
r"\(in MeV\)"
,
112
r"\[in MeV\]"
,
113
r"\[GeV\]"
,
114
r"\(GeV\)"
,
115
r"\(in GeV\)"
,
116
r"\[in GeV\]"
,
117
r"\[mm\]"
,
118
r"\(mm\)"
,
119
r"\(in mm\)"
,
120
r"\[in mm\]"
,
121
]
122
for
pattern
in
patterns:
123
if
re.search(pattern, info):
124
if
"MeV"
in
pattern:
125
return
"MeV"
126
elif
"GeV"
in
pattern:
127
return
"GeV"
128
elif
"mm"
in
pattern:
129
return
"mm"
130
return
None
131
132
133
def
process_info_links
(info: str) -> str:
134
"""
135
Scan an info string for backtick-enclosed substrings of the form `A::B`.
136
Turn them into a link to the appropriate module/files.
137
138
Args:
139
info: The input info string.
140
141
Returns:
142
The processed string with Markdown links where applicable.
143
"""
144
if
not
info:
145
return
info
146
147
# Regex to match `A::B` inside backticks
148
pattern =
r"`([^`]+)::([^`]+)`"
149
150
def
replace_match(match):
151
A, B = match.group(1), match.group(2)
152
if
A ==
"CP"
or
A ==
"ORUtils"
:
153
url = f
"https://acode-browser1.usatlas.bnl.gov/lxr/search?%21v=head&_filestring=**{B}**&_string="
154
return
f
"[`{A}::{B}`]({url})"
155
elif
A ==
"xAOD"
or
A ==
"AthOnnx"
:
156
url = f
"https://acode-browser1.usatlas.bnl.gov/lxr/ident?v=head&_i={B}&_identdefonly=1&_remember=1"
157
else
:
158
# TODO: any other cases to handle?
159
return
f
"`{A}::{B}`"
160
161
return
re.sub(pattern, replace_match, info)
162
163
164
def
link_jira_tickets
(info: str) -> str:
165
"""
166
Convert JIRA ticket references in a string to Markdown links.
167
168
- JIRA tickets are of the form: all-caps letters, a dash, then digits (e.g., ATLASG-2358)
169
- Converted to Markdown links: [ATLASG-2358](https://its.cern.ch/jira/browse/ATLASG-2358)
170
171
Args:
172
info: Input string that may contain JIRA tickets.
173
174
Returns:
175
The string with JIRA tickets converted to Markdown links.
176
"""
177
if
not
info:
178
return
info
179
180
# Regex pattern: one or more uppercase letters, dash, one or more digits
181
pattern =
r"\b([A-Z]+-\d+)\b"
182
183
def
replace_match(match):
184
ticket = match.group(1)
185
url = f
"https://its.cern.ch/jira/browse/{ticket}"
186
return
f
"[{ticket}]({url})"
187
188
return
re.sub(pattern, replace_match, info)
189
190
191
def
extract_from_classes
(
192
block_classes: List[Type], output_vars_yaml: Optional[Union[str, List[str]]] =
None
193
) -> List[Dict[str, Any]]:
194
"""
195
Extract options information from a list of ConfigBlock classes and merge
196
with output variables metadata.
197
198
Args:
199
block_classes: List of classes that inherit from ConfigBlock
200
output_vars_yaml: Optional path to YAML file or list of paths to YAML files
201
containing output variables. If multiple files provided,
202
their contents will be merged. Files are located using
203
find_datafile.
204
205
Returns:
206
List of dictionaries, each containing information about a block class
207
"""
208
# Load output variables if YAML file(s) provided
209
output_vars_map = {}
210
if
output_vars_yaml:
211
# Normalize to list for uniform processing
212
yaml_files = (
213
[output_vars_yaml]
214
if
isinstance(output_vars_yaml, str)
215
else
output_vars_yaml
216
)
217
218
# Load and merge all YAML files
219
for
yaml_file
in
yaml_files:
220
# Locate the file
221
resolved_path = find_datafile(yaml_file)
222
if
resolved_path
is
None
:
223
raise
FileNotFoundError(f
"Could not locate YAML file: {yaml_file}"
)
224
225
file_vars =
load_output_variables
(resolved_path)
226
# Merge with existing map (later files can override earlier ones)
227
for
class_name, variables
in
file_vars.items():
228
if
class_name
in
output_vars_map:
229
# Merge variable lists, avoiding duplicates if needed
230
output_vars_map[class_name].extend(variables)
231
else
:
232
output_vars_map[class_name] = variables
233
234
results = []
235
for
block_class
in
block_classes:
236
info =
extract_block_options
(block_class)
237
# Merge output variables if available
238
class_name = block_class.__name__
239
info[
"output_variables"
] = output_vars_map.get(class_name, [])
240
241
results.append(info)
242
243
return
results
244
245
246
def
save_as_yaml
(data: List[Dict[str, Any]], filepath: str) ->
None
:
247
"""Save extracted data as YAML."""
248
with
open(filepath,
"w"
)
as
f:
249
yaml.dump(data, f, default_flow_style=
False
, sort_keys=
False
)
250
logger.info(f
"Saved YAML to {filepath}"
)
251
252
253
def
generate_block_markdown
(block_info: Dict[str, Any]) -> str:
254
"""
255
Generate Markdown documentation for a single block.
256
257
Args:
258
block_info: Dictionary containing block information with keys:
259
- class: Block class name
260
- module: Module containing the block
261
- options: List of option dictionaries
262
- output_variables: List of output variable dictionaries
263
264
Returns:
265
Markdown string for this block
266
"""
267
markdown =
""
268
269
# Options section
270
if
block_info.get(
"options"
):
271
for
opt
in
block_info[
"options"
]:
272
name = opt[
"label"
]
273
274
# Skip these settings unless they are True
275
if
name
in
[
"skipOnData"
,
"skipOnMC"
,
"skipWithSystematics"
]:
276
if
not
opt[
"default"
]
is
True
:
277
continue
278
# Skip these settings unless they are set
279
if
name
in
[
"onlyForDSIDs"
]:
280
if
not
opt[
"default"
]
is
[]:
281
continue
282
283
# Option label with type, expert flag, required flag
284
label = f
"`{opt['label']}` ({opt['type']})"
285
if
opt[
"expertMode"
]
is
not
None
:
286
expertOptions = list(opt[
"expertMode"
])
287
label += f
" **[expert-only options: {','.join(['`' + str(x) + '`' for x in expertOptions])}]**"
288
if
opt[
"required"
]
is
True
or
opt[
"noneAction"
] !=
"ignore"
:
289
label +=
" **[REQUIRED]**"
290
291
markdown += f
"{label}\n"
292
info_string = opt[
"info"
]
293
info_string =
process_info_links
(info_string)
294
info_string =
link_jira_tickets
(info_string)
295
markdown += f
": {info_string}"
296
297
if
opt.get(
"default"
) !=
""
:
298
default_val = opt[
"default"
]
299
default_str = repr(default_val)
300
301
# Add unit information if available
302
unit = opt.get(
"physicalUnit"
)
303
if
unit
is
None
or
default_val
is
None
:
304
default_display = f
"`{default_str}`"
305
elif
unit ==
"GeV"
:
306
default_display = f
"`{default_str}` GeV"
307
elif
unit ==
"MeV"
:
308
# Convert MeV to GeV for simplified display
309
try
:
310
if
isinstance(default_val, (list, tuple)):
311
converted = [float(x) / 1000
for
x
in
default_val]
312
converted_str = (
313
"["
+
", "
.join(f
"{x}"
for
x
in
converted) +
"]"
314
)
315
else
:
316
converted = float(default_val) / 1000
317
converted_str = f
"{converted}"
318
except
(TypeError, ValueError):
319
converted_str =
"?"
320
default_display = f
"`{default_str}` MeV (`{converted_str}` GeV)"
321
else
:
322
default_display = f
"`{default_str}` {unit}"
323
324
markdown += f
" Default: {default_display}."
325
326
markdown +=
"\n\n"
327
328
# Output variables section
329
if
block_info.get(
"output_variables"
):
330
# Separate variables into always-saved and toggled
331
always_saved = []
332
toggled_vars = {}
# toggled_by condition -> list of variables
333
334
for
var
in
block_info[
"output_variables"
]:
335
if
var.get(
"toggled_by"
):
336
condition = var[
"toggled_by"
]
337
if
condition
not
in
toggled_vars:
338
toggled_vars[condition] = []
339
toggled_vars[condition].append(var)
340
else
:
341
always_saved.append(var)
342
343
# Always-saved variables section
344
if
always_saved:
345
markdown +=
'!!! success "Registers the following variables:"\n'
346
for
var
in
always_saved:
347
var_name = var.get(
"name"
,
"N/A"
)
348
var_desc = var.get(
"description"
,
""
)
349
markdown += f
" - `{var_name}`: {var_desc}\n"
350
markdown +=
"\n"
351
352
# Toggled variables sections
353
for
condition, vars_list
in
toggled_vars.items():
354
markdown += (
355
f
'!!! success "Additional variables toggled by `{condition}`:"\n'
356
)
357
for
var
in
vars_list:
358
var_name = var.get(
"name"
,
"N/A"
)
359
var_desc = var.get(
"description"
,
""
)
360
markdown += f
" - `{var_name}`: {var_desc}\n"
361
markdown +=
"\n"
362
else
:
363
logger.warning(
364
f
"Block {block_info.get('class')} didn't register any output variables."
365
)
366
367
return
markdown
368
369
370
def
process_markdown_with_autogen
(
371
input_filepath: str, output_filepath: str, block_data: List[Dict[str, Any]]
372
) ->
None
:
373
"""
374
Process an input markdown file, replacing AUTOGEN<BlockName> markers
375
with generated block documentation.
376
377
Looks for lines of the form "AUTOGEN<BlockName>" and replaces them
378
with the generated markdown for that block. All other content is
379
left untouched.
380
381
Args:
382
input_filepath: Path to the input markdown file
383
output_filepath: Path to write the processed output
384
block_data: List of extracted block information dictionaries
385
386
Raises:
387
FileNotFoundError: If input file does not exist
388
ValueError: If a referenced block is not found in block_data
389
"""
390
# Create a mapping of block names to their markdown
391
block_markdown_map = {
392
block[
"class"
]:
generate_block_markdown
(block)
for
block
in
block_data
393
}
394
395
# Read input file
396
with
open(input_filepath,
"r"
)
as
f:
397
lines = f.readlines()
398
399
output_lines = []
400
for
line
in
lines:
401
stripped = line.strip()
402
403
# Check if this line is an AUTOGEN marker
404
if
stripped.startswith(
"AUTOGEN<"
)
and
stripped.endswith(
">"
):
405
# Extract block name from AUTOGEN<BlockName>
406
block_name = stripped[8:-1]
# Remove 'AUTOGEN<' and '>'
407
408
if
block_name
in
block_markdown_map:
409
output_lines.append(block_markdown_map[block_name])
410
else
:
411
raise
ValueError(
412
f
"Block '{block_name}' not found in extracted block data"
413
)
414
else
:
415
output_lines.append(line)
416
417
# Write output file
418
with
open(output_filepath,
"w"
)
as
f:
419
f.writelines(output_lines)
420
421
logger.info(f
"Processed markdown saved to {output_filepath}."
)
python.AutogenDocumentation.load_output_variables
Dict[str, List[Dict[str, Any]]] load_output_variables(str yaml_filepath)
Definition
AutogenDocumentation.py:19
python.AutogenDocumentation.link_jira_tickets
str link_jira_tickets(str info)
Definition
AutogenDocumentation.py:164
python.AutogenDocumentation.save_as_yaml
None save_as_yaml(List[Dict[str, Any]] data, str filepath)
Definition
AutogenDocumentation.py:246
python.AutogenDocumentation.generate_block_markdown
str generate_block_markdown(Dict[str, Any] block_info)
Definition
AutogenDocumentation.py:253
python.AutogenDocumentation.process_markdown_with_autogen
None process_markdown_with_autogen(str input_filepath, str output_filepath, List[Dict[str, Any]] block_data)
Definition
AutogenDocumentation.py:372
python.AutogenDocumentation.extract_block_options
Dict[str, Any] extract_block_options(Type block_class)
Definition
AutogenDocumentation.py:40
python.AutogenDocumentation.interpret_physical_unit
Optional[str] interpret_physical_unit(str info)
Definition
AutogenDocumentation.py:93
python.AutogenDocumentation.extract_from_classes
List[Dict[str, Any]] extract_from_classes(List[Type] block_classes, Optional[Union[str, List[str]]] output_vars_yaml=None)
Definition
AutogenDocumentation.py:193
python.AutogenDocumentation.process_info_links
str process_info_links(str info)
Definition
AutogenDocumentation.py:133
Generated on
for ATLAS Offline Software by
1.17.0