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.
Files changed (38) hide show
  1. sdf_toolkit/__init__.py +71 -0
  2. sdf_toolkit/__main__.py +6 -0
  3. sdf_toolkit/analysis/__init__.py +52 -0
  4. sdf_toolkit/analysis/diff.py +327 -0
  5. sdf_toolkit/analysis/export.py +70 -0
  6. sdf_toolkit/analysis/pathgraph.py +704 -0
  7. sdf_toolkit/analysis/query.py +154 -0
  8. sdf_toolkit/analysis/report.py +177 -0
  9. sdf_toolkit/analysis/stats.py +118 -0
  10. sdf_toolkit/analysis/validate.py +285 -0
  11. sdf_toolkit/cli.py +967 -0
  12. sdf_toolkit/core/__init__.py +77 -0
  13. sdf_toolkit/core/builder.py +682 -0
  14. sdf_toolkit/core/model.py +595 -0
  15. sdf_toolkit/core/utils.py +90 -0
  16. sdf_toolkit/io/__init__.py +23 -0
  17. sdf_toolkit/io/annotate.py +722 -0
  18. sdf_toolkit/io/sdfparse.py +101 -0
  19. sdf_toolkit/io/templates/delay.j2 +42 -0
  20. sdf_toolkit/io/templates/macros.j2 +15 -0
  21. sdf_toolkit/io/templates/sdf.j2 +54 -0
  22. sdf_toolkit/io/templates/specify.j2 +5 -0
  23. sdf_toolkit/io/templates/timingcheck.j2 +26 -0
  24. sdf_toolkit/io/templates/timingenv.j2 +11 -0
  25. sdf_toolkit/io/writer.py +82 -0
  26. sdf_toolkit/parser/__init__.py +15 -0
  27. sdf_toolkit/parser/parser.py +72 -0
  28. sdf_toolkit/parser/sdf.lark +181 -0
  29. sdf_toolkit/parser/transformers.py +525 -0
  30. sdf_toolkit/transform/__init__.py +12 -0
  31. sdf_toolkit/transform/merge.py +207 -0
  32. sdf_toolkit/transform/normalize.py +90 -0
  33. sdf_toolkit-0.1.0.post3.dist-info/METADATA +781 -0
  34. sdf_toolkit-0.1.0.post3.dist-info/RECORD +38 -0
  35. sdf_toolkit-0.1.0.post3.dist-info/WHEEL +4 -0
  36. sdf_toolkit-0.1.0.post3.dist-info/entry_points.txt +2 -0
  37. sdf_toolkit-0.1.0.post3.dist-info/licenses/AUTHORS +8 -0
  38. sdf_toolkit-0.1.0.post3.dist-info/licenses/LICENSE +202 -0
@@ -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
+ ]
@@ -0,0 +1,6 @@
1
+ """Allow running sdf_toolkit as ``python -m sdf_toolkit``."""
2
+
3
+ from sdf_toolkit.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -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)