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
from
AnaAlgorithm.Logging
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
return
f
"[`{A}::{B}`]({url})"
158
else
:
159
# TODO: any other cases to handle?
160
return
f
"`{A}::{B}`"
161
162
return
re.sub(pattern, replace_match, info)
163
164
165
def
link_jira_tickets
(info: str) -> str:
166
"""
167
Convert JIRA ticket references in a string to Markdown links.
168
169
- JIRA tickets are of the form: all-caps letters, a dash, then digits (e.g., ATLASG-2358)
170
- Converted to Markdown links: [ATLASG-2358](https://its.cern.ch/jira/browse/ATLASG-2358)
171
172
Args:
173
info: Input string that may contain JIRA tickets.
174
175
Returns:
176
The string with JIRA tickets converted to Markdown links.
177
"""
178
if
not
info:
179
return
info
180
181
# Regex pattern: one or more uppercase letters, dash, one or more digits
182
pattern =
r"\b([A-Z]+-\d+)\b"
183
184
def
replace_match(match):
185
ticket = match.group(1)
186
url = f
"https://its.cern.ch/jira/browse/{ticket}"
187
return
f
"[{ticket}]({url})"
188
189
return
re.sub(pattern, replace_match, info)
190
191
192
def
extract_from_classes
(
193
block_classes: List[Type], output_vars_yaml: Optional[Union[str, List[str]]] =
None
194
) -> List[Dict[str, Any]]:
195
"""
196
Extract options information from a list of ConfigBlock classes and merge
197
with output variables metadata.
198
199
Args:
200
block_classes: List of classes that inherit from ConfigBlock
201
output_vars_yaml: Optional path to YAML file or list of paths to YAML files
202
containing output variables. If multiple files provided,
203
their contents will be merged. Files are located using
204
find_datafile.
205
206
Returns:
207
List of dictionaries, each containing information about a block class
208
"""
209
# Load output variables if YAML file(s) provided
210
output_vars_map = {}
211
if
output_vars_yaml:
212
# Normalize to list for uniform processing
213
yaml_files = (
214
[output_vars_yaml]
215
if
isinstance(output_vars_yaml, str)
216
else
output_vars_yaml
217
)
218
219
# Load and merge all YAML files
220
for
yaml_file
in
yaml_files:
221
# Locate the file
222
resolved_path = find_datafile(yaml_file)
223
if
resolved_path
is
None
:
224
raise
FileNotFoundError(f
"Could not locate YAML file: {yaml_file}"
)
225
226
file_vars =
load_output_variables
(resolved_path)
227
# Merge with existing map (later files can override earlier ones)
228
for
class_name, variables
in
file_vars.items():
229
if
class_name
in
output_vars_map:
230
# Merge variable lists, avoiding duplicates if needed
231
output_vars_map[class_name].extend(variables)
232
else
:
233
output_vars_map[class_name] = variables
234
235
results = []
236
for
block_class
in
block_classes:
237
info =
extract_block_options
(block_class)
238
# Merge output variables if available
239
class_name = block_class.__name__
240
info[
"output_variables"
] = output_vars_map.get(class_name, [])
241
242
results.append(info)
243
244
return
results
245
246
247
def
save_as_yaml
(data: List[Dict[str, Any]], filepath: str) ->
None
:
248
"""Save extracted data as YAML."""
249
with
open(filepath,
"w"
)
as
f:
250
yaml.dump(data, f, default_flow_style=
False
, sort_keys=
False
)
251
logger.info(f
"Saved YAML to {filepath}"
)
252
253
254
def
generate_block_markdown
(block_info: Dict[str, Any]) -> str:
255
"""
256
Generate Markdown documentation for a single block.
257
258
Args:
259
block_info: Dictionary containing block information with keys:
260
- class: Block class name
261
- module: Module containing the block
262
- options: List of option dictionaries
263
- output_variables: List of output variable dictionaries
264
265
Returns:
266
Markdown string for this block
267
"""
268
markdown =
""
269
270
# Options section
271
if
block_info.get(
"options"
):
272
for
opt
in
block_info[
"options"
]:
273
name = opt[
"label"
]
274
275
# Skip these settings unless they are True
276
if
name
in
[
"skipOnData"
,
"skipOnMC"
,
"skipWithSystematics"
]:
277
if
not
opt[
"default"
]
is
True
:
278
continue
279
# Skip these settings unless they are set
280
if
name
in
[
"onlyForDSIDs"
]:
281
if
opt[
"default"
] == []:
282
continue
283
284
# Option label with type, expert flag, required flag
285
label = f
"`{opt['label']}` ({opt['type']})"
286
if
opt[
"expertMode"
]
is
not
None
:
287
expertOptions = list(opt[
"expertMode"
])
288
label += f
" **[expert-only options: {','.join(['`' + str(x) + '`' for x in expertOptions])}]**"
289
if
opt[
"required"
]
is
True
or
opt[
"noneAction"
] !=
"ignore"
:
290
label +=
" **[REQUIRED]**"
291
292
markdown += f
"{label}\n"
293
info_string = opt[
"info"
]
294
info_string =
process_info_links
(info_string)
295
info_string =
link_jira_tickets
(info_string)
296
markdown += f
": {info_string}"
297
298
if
opt.get(
"default"
) !=
""
:
299
default_val = opt[
"default"
]
300
default_str = repr(default_val)
301
302
# Add unit information if available
303
unit = opt.get(
"physicalUnit"
)
304
if
unit
is
None
or
default_val
is
None
:
305
default_display = f
"`{default_str}`"
306
elif
unit ==
"GeV"
:
307
default_display = f
"`{default_str}` GeV"
308
elif
unit ==
"MeV"
:
309
# Convert MeV to GeV for simplified display
310
try
:
311
if
isinstance(default_val, (list, tuple)):
312
converted = [float(x) / 1000
for
x
in
default_val]
313
converted_str = (
314
"["
+
", "
.join(f
"{x}"
for
x
in
converted) +
"]"
315
)
316
else
:
317
converted = float(default_val) / 1000
318
converted_str = f
"{converted}"
319
except
(TypeError, ValueError):
320
converted_str =
"?"
321
default_display = f
"`{default_str}` MeV (`{converted_str}` GeV)"
322
else
:
323
default_display = f
"`{default_str}` {unit}"
324
325
markdown += f
" Default: {default_display}."
326
327
markdown +=
"\n\n"
328
329
# Output variables section
330
if
block_info.get(
"output_variables"
):
331
# Separate variables into always-saved and toggled
332
always_saved = []
333
toggled_vars = {}
# toggled_by condition -> list of variables
334
335
for
var
in
block_info[
"output_variables"
]:
336
if
var.get(
"toggled_by"
):
337
condition = var[
"toggled_by"
]
338
if
condition
not
in
toggled_vars:
339
toggled_vars[condition] = []
340
toggled_vars[condition].append(var)
341
else
:
342
always_saved.append(var)
343
344
# Always-saved variables section
345
if
always_saved:
346
markdown +=
'!!! success "Registers the following variables:"\n'
347
for
var
in
always_saved:
348
var_name = var.get(
"name"
,
"N/A"
)
349
var_desc = var.get(
"description"
,
""
)
350
markdown += f
" - `{var_name}`: {var_desc}\n"
351
markdown +=
"\n"
352
353
# Toggled variables sections
354
for
condition, vars_list
in
toggled_vars.items():
355
markdown += (
356
f
'!!! success "Additional variables toggled by `{condition}`:"\n'
357
)
358
for
var
in
vars_list:
359
var_name = var.get(
"name"
,
"N/A"
)
360
var_desc = var.get(
"description"
,
""
)
361
markdown += f
" - `{var_name}`: {var_desc}\n"
362
markdown +=
"\n"
363
else
:
364
logger.warning(
365
f
"Block {block_info.get('class')} didn't register any output variables."
366
)
367
368
return
markdown
369
370
371
def
process_markdown_with_autogen
(
372
input_filepath: str, output_filepath: str, block_data: List[Dict[str, Any]]
373
) ->
None
:
374
"""
375
Process an input markdown file, replacing AUTOGEN<BlockName> markers
376
with generated block documentation.
377
378
Looks for lines of the form "AUTOGEN<BlockName>" and replaces them
379
with the generated markdown for that block. All other content is
380
left untouched.
381
382
Args:
383
input_filepath: Path to the input markdown file
384
output_filepath: Path to write the processed output
385
block_data: List of extracted block information dictionaries
386
387
Raises:
388
FileNotFoundError: If input file does not exist
389
ValueError: If a referenced block is not found in block_data
390
"""
391
# Create a mapping of block names to their markdown
392
block_markdown_map = {
393
block[
"class"
]:
generate_block_markdown
(block)
for
block
in
block_data
394
}
395
396
# Read input file
397
with
open(input_filepath,
"r"
)
as
f:
398
lines = f.readlines()
399
400
output_lines = []
401
for
line
in
lines:
402
stripped = line.strip()
403
404
# Check if this line is an AUTOGEN marker
405
if
stripped.startswith(
"AUTOGEN<"
)
and
stripped.endswith(
">"
):
406
# Extract block name from AUTOGEN<BlockName>
407
block_name = stripped[8:-1]
# Remove 'AUTOGEN<' and '>'
408
409
if
block_name
in
block_markdown_map:
410
output_lines.append(block_markdown_map[block_name])
411
else
:
412
raise
ValueError(
413
f
"Block '{block_name}' not found in extracted block data"
414
)
415
else
:
416
output_lines.append(line)
417
418
# Write output file
419
with
open(output_filepath,
"w"
)
as
f:
420
f.writelines(output_lines)
421
422
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:165
python.AutogenDocumentation.save_as_yaml
None save_as_yaml(List[Dict[str, Any]] data, str filepath)
Definition
AutogenDocumentation.py:247
python.AutogenDocumentation.generate_block_markdown
str generate_block_markdown(Dict[str, Any] block_info)
Definition
AutogenDocumentation.py:254
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:373
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:194
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