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