sdf-toolkit 0.1.0.post3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sdf_toolkit/__init__.py +71 -0
- sdf_toolkit/__main__.py +6 -0
- sdf_toolkit/analysis/__init__.py +52 -0
- sdf_toolkit/analysis/diff.py +327 -0
- sdf_toolkit/analysis/export.py +70 -0
- sdf_toolkit/analysis/pathgraph.py +704 -0
- sdf_toolkit/analysis/query.py +154 -0
- sdf_toolkit/analysis/report.py +177 -0
- sdf_toolkit/analysis/stats.py +118 -0
- sdf_toolkit/analysis/validate.py +285 -0
- sdf_toolkit/cli.py +967 -0
- sdf_toolkit/core/__init__.py +77 -0
- sdf_toolkit/core/builder.py +682 -0
- sdf_toolkit/core/model.py +595 -0
- sdf_toolkit/core/utils.py +90 -0
- sdf_toolkit/io/__init__.py +23 -0
- sdf_toolkit/io/annotate.py +722 -0
- sdf_toolkit/io/sdfparse.py +101 -0
- sdf_toolkit/io/templates/delay.j2 +42 -0
- sdf_toolkit/io/templates/macros.j2 +15 -0
- sdf_toolkit/io/templates/sdf.j2 +54 -0
- sdf_toolkit/io/templates/specify.j2 +5 -0
- sdf_toolkit/io/templates/timingcheck.j2 +26 -0
- sdf_toolkit/io/templates/timingenv.j2 +11 -0
- sdf_toolkit/io/writer.py +82 -0
- sdf_toolkit/parser/__init__.py +15 -0
- sdf_toolkit/parser/parser.py +72 -0
- sdf_toolkit/parser/sdf.lark +181 -0
- sdf_toolkit/parser/transformers.py +525 -0
- sdf_toolkit/transform/__init__.py +12 -0
- sdf_toolkit/transform/merge.py +207 -0
- sdf_toolkit/transform/normalize.py +90 -0
- sdf_toolkit-0.1.0.post3.dist-info/METADATA +781 -0
- sdf_toolkit-0.1.0.post3.dist-info/RECORD +38 -0
- sdf_toolkit-0.1.0.post3.dist-info/WHEEL +4 -0
- sdf_toolkit-0.1.0.post3.dist-info/entry_points.txt +2 -0
- sdf_toolkit-0.1.0.post3.dist-info/licenses/AUTHORS +8 -0
- sdf_toolkit-0.1.0.post3.dist-info/licenses/LICENSE +202 -0
sdf_toolkit/__init__.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""sdf_toolkit -- parse and emit Standard Delay Format (SDF) timing files."""
|
|
2
|
+
|
|
3
|
+
from sdf_toolkit.analysis import (
|
|
4
|
+
DiffEntry,
|
|
5
|
+
DiffResult,
|
|
6
|
+
EndpointResult,
|
|
7
|
+
LintIssue,
|
|
8
|
+
RankedPath,
|
|
9
|
+
SDFStats,
|
|
10
|
+
TimingEdge,
|
|
11
|
+
TimingGraph,
|
|
12
|
+
VerificationResult,
|
|
13
|
+
batch_endpoint_analysis,
|
|
14
|
+
compute_slack,
|
|
15
|
+
compute_stats,
|
|
16
|
+
critical_path,
|
|
17
|
+
decompose_delay,
|
|
18
|
+
diff,
|
|
19
|
+
generate_report,
|
|
20
|
+
query,
|
|
21
|
+
rank_paths,
|
|
22
|
+
to_dot,
|
|
23
|
+
validate,
|
|
24
|
+
verify_path,
|
|
25
|
+
)
|
|
26
|
+
from sdf_toolkit.core import CellBuilder, SDFBuilder, SDFFile, SDFHeader
|
|
27
|
+
from sdf_toolkit.io import annotate_verilog, emit, emit_sdf, parse
|
|
28
|
+
from sdf_toolkit.parser import parse_sdf, parse_sdf_file
|
|
29
|
+
from sdf_toolkit.transform import ConflictStrategy, merge, normalize_delays
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
# core
|
|
33
|
+
"CellBuilder",
|
|
34
|
+
"SDFBuilder",
|
|
35
|
+
"SDFFile",
|
|
36
|
+
"SDFHeader",
|
|
37
|
+
# parser
|
|
38
|
+
"parse_sdf",
|
|
39
|
+
"parse_sdf_file",
|
|
40
|
+
# io
|
|
41
|
+
"annotate_verilog",
|
|
42
|
+
"emit",
|
|
43
|
+
"emit_sdf",
|
|
44
|
+
"parse",
|
|
45
|
+
# analysis
|
|
46
|
+
"DiffEntry",
|
|
47
|
+
"DiffResult",
|
|
48
|
+
"EndpointResult",
|
|
49
|
+
"LintIssue",
|
|
50
|
+
"RankedPath",
|
|
51
|
+
"SDFStats",
|
|
52
|
+
"TimingEdge",
|
|
53
|
+
"TimingGraph",
|
|
54
|
+
"VerificationResult",
|
|
55
|
+
"batch_endpoint_analysis",
|
|
56
|
+
"compute_slack",
|
|
57
|
+
"compute_stats",
|
|
58
|
+
"critical_path",
|
|
59
|
+
"decompose_delay",
|
|
60
|
+
"diff",
|
|
61
|
+
"generate_report",
|
|
62
|
+
"query",
|
|
63
|
+
"rank_paths",
|
|
64
|
+
"to_dot",
|
|
65
|
+
"validate",
|
|
66
|
+
"verify_path",
|
|
67
|
+
# transform
|
|
68
|
+
"ConflictStrategy",
|
|
69
|
+
"merge",
|
|
70
|
+
"normalize_delays",
|
|
71
|
+
]
|
sdf_toolkit/__main__.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Analysis modules for SDF timing data."""
|
|
2
|
+
|
|
3
|
+
from sdf_toolkit.analysis.diff import DiffEntry, DiffResult, diff
|
|
4
|
+
from sdf_toolkit.analysis.export import to_dot
|
|
5
|
+
from sdf_toolkit.analysis.pathgraph import (
|
|
6
|
+
EndpointResult,
|
|
7
|
+
RankedPath,
|
|
8
|
+
TimingEdge,
|
|
9
|
+
TimingGraph,
|
|
10
|
+
VerificationResult,
|
|
11
|
+
batch_endpoint_analysis,
|
|
12
|
+
compute_slack,
|
|
13
|
+
critical_path,
|
|
14
|
+
decompose_delay,
|
|
15
|
+
rank_paths,
|
|
16
|
+
verify_path,
|
|
17
|
+
)
|
|
18
|
+
from sdf_toolkit.analysis.query import query
|
|
19
|
+
from sdf_toolkit.analysis.report import generate_report
|
|
20
|
+
from sdf_toolkit.analysis.stats import SDFStats, compute_stats
|
|
21
|
+
from sdf_toolkit.analysis.validate import LintIssue, validate
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
# diff
|
|
25
|
+
"DiffEntry",
|
|
26
|
+
"DiffResult",
|
|
27
|
+
"diff",
|
|
28
|
+
# export
|
|
29
|
+
"to_dot",
|
|
30
|
+
# pathgraph
|
|
31
|
+
"EndpointResult",
|
|
32
|
+
"RankedPath",
|
|
33
|
+
"TimingEdge",
|
|
34
|
+
"TimingGraph",
|
|
35
|
+
"VerificationResult",
|
|
36
|
+
"batch_endpoint_analysis",
|
|
37
|
+
"compute_slack",
|
|
38
|
+
"critical_path",
|
|
39
|
+
"decompose_delay",
|
|
40
|
+
"rank_paths",
|
|
41
|
+
"verify_path",
|
|
42
|
+
# query
|
|
43
|
+
"query",
|
|
44
|
+
# report
|
|
45
|
+
"generate_report",
|
|
46
|
+
# stats
|
|
47
|
+
"SDFStats",
|
|
48
|
+
"compute_stats",
|
|
49
|
+
# validate
|
|
50
|
+
"LintIssue",
|
|
51
|
+
"validate",
|
|
52
|
+
]
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""Compare two SDF files and report differences."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from sdf_toolkit.core.model import DelayPaths, SDFFile, Values
|
|
6
|
+
from sdf_toolkit.transform.normalize import normalize_delays
|
|
7
|
+
|
|
8
|
+
_HEADER_FIELDS: tuple[str, ...] = (
|
|
9
|
+
"sdfversion",
|
|
10
|
+
"design",
|
|
11
|
+
"vendor",
|
|
12
|
+
"program",
|
|
13
|
+
"version",
|
|
14
|
+
"divider",
|
|
15
|
+
"date",
|
|
16
|
+
"voltage",
|
|
17
|
+
"process",
|
|
18
|
+
"temperature",
|
|
19
|
+
"timescale",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
_VALUES_FIELDS: tuple[str, ...] = ("min", "avg", "max")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class DiffEntry:
|
|
27
|
+
"""A single value difference between two SDF files.
|
|
28
|
+
|
|
29
|
+
Attributes
|
|
30
|
+
----------
|
|
31
|
+
cell_type : str
|
|
32
|
+
The cell type name.
|
|
33
|
+
instance : str
|
|
34
|
+
The cell instance name.
|
|
35
|
+
entry_name : str
|
|
36
|
+
The timing entry name within the cell.
|
|
37
|
+
field : str
|
|
38
|
+
The dotted field path, e.g. ``"nominal.min"`` or ``"slow.max"``.
|
|
39
|
+
value_a : float | None
|
|
40
|
+
The value from the first SDF file.
|
|
41
|
+
value_b : float | None
|
|
42
|
+
The value from the second SDF file.
|
|
43
|
+
delta : float | None
|
|
44
|
+
``value_b - value_a`` when both are present, else None.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
cell_type: str
|
|
48
|
+
instance: str
|
|
49
|
+
entry_name: str
|
|
50
|
+
field: str
|
|
51
|
+
value_a: float | None
|
|
52
|
+
value_b: float | None
|
|
53
|
+
delta: float | None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class DiffResult:
|
|
58
|
+
"""Complete comparison result between two SDF files.
|
|
59
|
+
|
|
60
|
+
Attributes
|
|
61
|
+
----------
|
|
62
|
+
only_in_a : list[tuple[str, str, str]]
|
|
63
|
+
Entries present only in the first file, as
|
|
64
|
+
``(cell_type, instance, entry_name)`` tuples.
|
|
65
|
+
only_in_b : list[tuple[str, str, str]]
|
|
66
|
+
Entries present only in the second file, as
|
|
67
|
+
``(cell_type, instance, entry_name)`` tuples.
|
|
68
|
+
value_diffs : list[DiffEntry]
|
|
69
|
+
Per-field value differences for entries present in both files.
|
|
70
|
+
header_diffs : dict[str, tuple[str | None, str | None]]
|
|
71
|
+
Header field differences, mapping field name to
|
|
72
|
+
``(value_in_a, value_in_b)``.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
only_in_a: list[tuple[str, str, str]] = field(default_factory=list)
|
|
76
|
+
only_in_b: list[tuple[str, str, str]] = field(default_factory=list)
|
|
77
|
+
value_diffs: list[DiffEntry] = field(default_factory=list)
|
|
78
|
+
header_diffs: dict[str, tuple[str | None, str | None]] = field(
|
|
79
|
+
default_factory=dict,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _build_entry_keys(sdf: SDFFile) -> set[tuple[str, str, str]]:
|
|
84
|
+
"""Build the set of ``(cell_type, instance, entry_name)`` keys from an SDF file.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
sdf : SDFFile
|
|
89
|
+
The SDF file to extract keys from.
|
|
90
|
+
|
|
91
|
+
Returns
|
|
92
|
+
-------
|
|
93
|
+
set[tuple[str, str, str]]
|
|
94
|
+
All entry keys present in the file.
|
|
95
|
+
"""
|
|
96
|
+
keys: set[tuple[str, str, str]] = set()
|
|
97
|
+
for cell_type, instances in sdf.cells.items():
|
|
98
|
+
for instance, entries in instances.items():
|
|
99
|
+
for entry_name in entries:
|
|
100
|
+
keys.add((cell_type, instance, entry_name))
|
|
101
|
+
return keys
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _compare_values(
|
|
105
|
+
values_a: Values | None,
|
|
106
|
+
values_b: Values | None,
|
|
107
|
+
cell_type: str,
|
|
108
|
+
instance: str,
|
|
109
|
+
entry_name: str,
|
|
110
|
+
field_name: str,
|
|
111
|
+
tolerance: float,
|
|
112
|
+
) -> list[DiffEntry]:
|
|
113
|
+
"""Compare two Values triples and return diff entries for any differences.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
values_a : Values | None
|
|
118
|
+
Values from the first SDF file.
|
|
119
|
+
values_b : Values | None
|
|
120
|
+
Values from the second SDF file.
|
|
121
|
+
cell_type : str
|
|
122
|
+
The cell type name for context.
|
|
123
|
+
instance : str
|
|
124
|
+
The cell instance name for context.
|
|
125
|
+
entry_name : str
|
|
126
|
+
The timing entry name for context.
|
|
127
|
+
field_name : str
|
|
128
|
+
The delay path field name (e.g. ``"nominal"``, ``"slow"``).
|
|
129
|
+
tolerance : float
|
|
130
|
+
Absolute tolerance for floating-point comparison.
|
|
131
|
+
|
|
132
|
+
Returns
|
|
133
|
+
-------
|
|
134
|
+
list[DiffEntry]
|
|
135
|
+
A list of diff entries, one per metric that differs.
|
|
136
|
+
"""
|
|
137
|
+
diffs: list[DiffEntry] = []
|
|
138
|
+
|
|
139
|
+
for metric in _VALUES_FIELDS:
|
|
140
|
+
val_a: float | None = (
|
|
141
|
+
getattr(values_a, metric) if values_a is not None else None
|
|
142
|
+
)
|
|
143
|
+
val_b: float | None = (
|
|
144
|
+
getattr(values_b, metric) if values_b is not None else None
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if val_a is None and val_b is None:
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
delta: float | None = None
|
|
151
|
+
if val_a is not None and val_b is not None:
|
|
152
|
+
delta = val_b - val_a
|
|
153
|
+
if abs(delta) <= tolerance:
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
diffs.append(
|
|
157
|
+
DiffEntry(
|
|
158
|
+
cell_type=cell_type,
|
|
159
|
+
instance=instance,
|
|
160
|
+
entry_name=entry_name,
|
|
161
|
+
field=f"{field_name}.{metric}",
|
|
162
|
+
value_a=val_a,
|
|
163
|
+
value_b=val_b,
|
|
164
|
+
delta=delta,
|
|
165
|
+
),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return diffs
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _compare_delay_paths(
|
|
172
|
+
dp_a: DelayPaths | None,
|
|
173
|
+
dp_b: DelayPaths | None,
|
|
174
|
+
cell_type: str,
|
|
175
|
+
instance: str,
|
|
176
|
+
entry_name: str,
|
|
177
|
+
tolerance: float,
|
|
178
|
+
) -> list[DiffEntry]:
|
|
179
|
+
"""Compare two DelayPaths and return diff entries for all differences.
|
|
180
|
+
|
|
181
|
+
Parameters
|
|
182
|
+
----------
|
|
183
|
+
dp_a : DelayPaths | None
|
|
184
|
+
Delay paths from the first SDF file.
|
|
185
|
+
dp_b : DelayPaths | None
|
|
186
|
+
Delay paths from the second SDF file.
|
|
187
|
+
cell_type : str
|
|
188
|
+
The cell type name for context.
|
|
189
|
+
instance : str
|
|
190
|
+
The cell instance name for context.
|
|
191
|
+
entry_name : str
|
|
192
|
+
The timing entry name for context.
|
|
193
|
+
tolerance : float
|
|
194
|
+
Absolute tolerance for floating-point comparison.
|
|
195
|
+
|
|
196
|
+
Returns
|
|
197
|
+
-------
|
|
198
|
+
list[DiffEntry]
|
|
199
|
+
A list of diff entries for each metric that differs.
|
|
200
|
+
"""
|
|
201
|
+
diffs: list[DiffEntry] = []
|
|
202
|
+
|
|
203
|
+
for field_name in DelayPaths._FIELD_NAMES: # noqa: SLF001
|
|
204
|
+
values_a: Values | None = (
|
|
205
|
+
getattr(dp_a, field_name) if dp_a is not None else None
|
|
206
|
+
)
|
|
207
|
+
values_b: Values | None = (
|
|
208
|
+
getattr(dp_b, field_name) if dp_b is not None else None
|
|
209
|
+
)
|
|
210
|
+
diffs.extend(
|
|
211
|
+
_compare_values(
|
|
212
|
+
values_a,
|
|
213
|
+
values_b,
|
|
214
|
+
cell_type,
|
|
215
|
+
instance,
|
|
216
|
+
entry_name,
|
|
217
|
+
field_name,
|
|
218
|
+
tolerance,
|
|
219
|
+
),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return diffs
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def diff(
|
|
226
|
+
a: SDFFile,
|
|
227
|
+
b: SDFFile,
|
|
228
|
+
tolerance: float = 1e-9,
|
|
229
|
+
normalize_first: bool = False,
|
|
230
|
+
target_timescale: str = "1ps",
|
|
231
|
+
) -> DiffResult:
|
|
232
|
+
"""Compare two SDF files and return a structured diff result.
|
|
233
|
+
|
|
234
|
+
Parameters
|
|
235
|
+
----------
|
|
236
|
+
a : SDFFile
|
|
237
|
+
The first (reference) SDF file.
|
|
238
|
+
b : SDFFile
|
|
239
|
+
The second (comparison) SDF file.
|
|
240
|
+
tolerance : float, optional
|
|
241
|
+
Absolute tolerance for floating-point value comparison, by default
|
|
242
|
+
1e-9.
|
|
243
|
+
normalize_first : bool, optional
|
|
244
|
+
If True, normalize both files to ``target_timescale`` before
|
|
245
|
+
comparing, by default False.
|
|
246
|
+
target_timescale : str, optional
|
|
247
|
+
The timescale to normalize to when ``normalize_first`` is True,
|
|
248
|
+
by default ``"1ps"``.
|
|
249
|
+
|
|
250
|
+
Returns
|
|
251
|
+
-------
|
|
252
|
+
DiffResult
|
|
253
|
+
The complete comparison result including header diffs, entries
|
|
254
|
+
only in one file, and per-field value differences.
|
|
255
|
+
|
|
256
|
+
Examples
|
|
257
|
+
--------
|
|
258
|
+
>>> from sdf_toolkit.core.builder import SDFBuilder
|
|
259
|
+
>>> from sdf_toolkit.analysis.diff import diff
|
|
260
|
+
>>> a = (
|
|
261
|
+
... SDFBuilder()
|
|
262
|
+
... .set_header(timescale="1ps")
|
|
263
|
+
... .add_cell("BUF", "b0")
|
|
264
|
+
... .add_iopath("A", "Y", {
|
|
265
|
+
... "nominal": {"min": 1.0, "avg": 2.0, "max": 3.0},
|
|
266
|
+
... })
|
|
267
|
+
... .build()
|
|
268
|
+
... )
|
|
269
|
+
>>> b = (
|
|
270
|
+
... SDFBuilder()
|
|
271
|
+
... .set_header(timescale="1ps")
|
|
272
|
+
... .add_cell("BUF", "b0")
|
|
273
|
+
... .add_iopath("A", "Y", {
|
|
274
|
+
... "nominal": {"min": 1.0, "avg": 2.0, "max": 5.0},
|
|
275
|
+
... })
|
|
276
|
+
... .add_cell("INV", "i0")
|
|
277
|
+
... .add_iopath("A", "Y", {
|
|
278
|
+
... "nominal": {"min": 1.0, "avg": 2.0, "max": 3.0},
|
|
279
|
+
... })
|
|
280
|
+
... .build()
|
|
281
|
+
... )
|
|
282
|
+
>>> result = diff(a, b)
|
|
283
|
+
>>> len(result.only_in_b)
|
|
284
|
+
1
|
|
285
|
+
>>> result.value_diffs[0].field
|
|
286
|
+
'nominal.max'
|
|
287
|
+
>>> result.value_diffs[0].delta
|
|
288
|
+
2.0
|
|
289
|
+
"""
|
|
290
|
+
if normalize_first:
|
|
291
|
+
a = normalize_delays(a, target_timescale)
|
|
292
|
+
b = normalize_delays(b, target_timescale)
|
|
293
|
+
|
|
294
|
+
result = DiffResult()
|
|
295
|
+
|
|
296
|
+
# Compare headers
|
|
297
|
+
for hdr_field in _HEADER_FIELDS:
|
|
298
|
+
val_a: str | None = getattr(a.header, hdr_field)
|
|
299
|
+
val_b: str | None = getattr(b.header, hdr_field)
|
|
300
|
+
if val_a != val_b:
|
|
301
|
+
result.header_diffs[hdr_field] = (val_a, val_b)
|
|
302
|
+
|
|
303
|
+
# Build entry key sets
|
|
304
|
+
keys_a = _build_entry_keys(a)
|
|
305
|
+
keys_b = _build_entry_keys(b)
|
|
306
|
+
|
|
307
|
+
result.only_in_a = sorted(keys_a - keys_b)
|
|
308
|
+
result.only_in_b = sorted(keys_b - keys_a)
|
|
309
|
+
|
|
310
|
+
# Compare shared entries
|
|
311
|
+
common_keys = keys_a & keys_b
|
|
312
|
+
for cell_type, instance, entry_name in sorted(common_keys):
|
|
313
|
+
entry_a = a.cells[cell_type][instance][entry_name]
|
|
314
|
+
entry_b = b.cells[cell_type][instance][entry_name]
|
|
315
|
+
|
|
316
|
+
result.value_diffs.extend(
|
|
317
|
+
_compare_delay_paths(
|
|
318
|
+
entry_a.delay_paths,
|
|
319
|
+
entry_b.delay_paths,
|
|
320
|
+
cell_type,
|
|
321
|
+
instance,
|
|
322
|
+
entry_name,
|
|
323
|
+
tolerance,
|
|
324
|
+
),
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
return result
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""DOT/Graphviz export for timing graphs."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from sdf_toolkit.analysis.pathgraph import RankedPath, TimingGraph
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def to_dot(
|
|
8
|
+
graph: TimingGraph,
|
|
9
|
+
highlight_path: RankedPath | None = None,
|
|
10
|
+
cluster_by_instance: bool = False,
|
|
11
|
+
field: str = "slow",
|
|
12
|
+
metric: str = "max",
|
|
13
|
+
) -> str:
|
|
14
|
+
"""Export a timing graph to DOT format.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
graph : TimingGraph
|
|
19
|
+
The timing graph to export.
|
|
20
|
+
highlight_path : RankedPath | None
|
|
21
|
+
If provided, highlight these edges in red with bold penwidth.
|
|
22
|
+
cluster_by_instance : bool
|
|
23
|
+
If True, group nodes into subgraph clusters by instance prefix.
|
|
24
|
+
field : str
|
|
25
|
+
Delay field for edge labels.
|
|
26
|
+
metric : str
|
|
27
|
+
Metric for edge labels.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
str
|
|
32
|
+
The DOT-format string.
|
|
33
|
+
"""
|
|
34
|
+
highlight_edges: set[tuple[str, str]] = (
|
|
35
|
+
{(edge.source, edge.sink) for edge in highlight_path.edges}
|
|
36
|
+
if highlight_path is not None
|
|
37
|
+
else set()
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
lines: list[str] = ["digraph timing {", " rankdir=LR;"]
|
|
41
|
+
|
|
42
|
+
nodes = graph.nodes()
|
|
43
|
+
|
|
44
|
+
if cluster_by_instance:
|
|
45
|
+
clusters: dict[str, list[str]] = {}
|
|
46
|
+
for node in sorted(nodes):
|
|
47
|
+
parts = node.rsplit("/", 1)
|
|
48
|
+
instance = parts[0] if len(parts) > 1 else ""
|
|
49
|
+
clusters.setdefault(instance, []).append(node)
|
|
50
|
+
for i, (instance, members) in enumerate(sorted(clusters.items())):
|
|
51
|
+
label = instance or "(top)"
|
|
52
|
+
lines.append(f" subgraph cluster_{i} {{")
|
|
53
|
+
lines.append(f' label="{label}";')
|
|
54
|
+
for node in sorted(members):
|
|
55
|
+
lines.append(f' "{node}";')
|
|
56
|
+
lines.append(" }")
|
|
57
|
+
else:
|
|
58
|
+
for node in sorted(nodes):
|
|
59
|
+
lines.append(f' "{node}";')
|
|
60
|
+
|
|
61
|
+
for edge in graph.edges():
|
|
62
|
+
scalar = edge.delay.get_scalar(field, metric)
|
|
63
|
+
label = f"{scalar:.3f}" if scalar is not None else "?"
|
|
64
|
+
attrs = f'label="{label}"'
|
|
65
|
+
if (edge.source, edge.sink) in highlight_edges:
|
|
66
|
+
attrs += ', color="red", penwidth=2.0'
|
|
67
|
+
lines.append(f' "{edge.source}" -> "{edge.sink}" [{attrs}];')
|
|
68
|
+
|
|
69
|
+
lines.append("}")
|
|
70
|
+
return "\n".join(lines)
|