dau-sim 0.1.0__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.
- dau_sim/__init__.py +9 -0
- dau_sim/adapters/__init__.py +6 -0
- dau_sim/adapters/selectors.py +127 -0
- dau_sim/adapters/vcd.py +375 -0
- dau_sim/api.py +94 -0
- dau_sim/backends/__init__.py +1 -0
- dau_sim/backends/cocotb_backend.py +955 -0
- dau_sim/benchmarks/__init__.py +1 -0
- dau_sim/benchmarks/bench_compile_partitioning.py +10 -0
- dau_sim/benchmarks/bench_cross_simulators.py +216 -0
- dau_sim/benchmarks/bench_selective_settle.py +11 -0
- dau_sim/benchmarks/compile_partitioning.py +43 -0
- dau_sim/benchmarks/results/Darwin-CPython-3.12-64bit/0001_cross-runtime-500k.json +195 -0
- dau_sim/benchmarks/results/Darwin-CPython-3.12-64bit/0002_cross-runtime-500k.json +195 -0
- dau_sim/benchmarks/results/cross-runtime-500k.json +223 -0
- dau_sim/benchmarks/selective_settle.py +60 -0
- dau_sim/cli.py +123 -0
- dau_sim/compiler/__init__.py +2 -0
- dau_sim/compiler/codegen.py +841 -0
- dau_sim/compiler/compile.py +1660 -0
- dau_sim/compiler/depanalysis.py +340 -0
- dau_sim/compiler/eval.py +194 -0
- dau_sim/compiler/eval4.py +282 -0
- dau_sim/compiler/flatten.py +200 -0
- dau_sim/compiler/resolve.py +129 -0
- dau_sim/compiler/rewrite.py +86 -0
- dau_sim/extension/cdn/index.js +1 -0
- dau_sim/extension/cdn/index.js.map +7 -0
- dau_sim/extension/css/index.css +0 -0
- dau_sim/extension/esm/index.js +1 -0
- dau_sim/extension/esm/index.js.map +7 -0
- dau_sim/extension/html/index.html +13 -0
- dau_sim/extension/index.html +13 -0
- dau_sim/frontends/__init__.py +15 -0
- dau_sim/frontends/amaranth_frontend.py +614 -0
- dau_sim/frontends/pyslang_frontend.py +555 -0
- dau_sim/integrations/__init__.py +12 -0
- dau_sim/integrations/verilator.py +90 -0
- dau_sim/integrations/verilator_profiles.py +50 -0
- dau_sim/ir/__init__.py +4 -0
- dau_sim/ir/expr.py +153 -0
- dau_sim/ir/module.py +172 -0
- dau_sim/ir/printer.py +183 -0
- dau_sim/ir/stmt.py +105 -0
- dau_sim/ir/types.py +150 -0
- dau_sim/ir/validate.py +166 -0
- dau_sim/perf.py +141 -0
- dau_sim/runtime/__init__.py +1 -0
- dau_sim/testbench.py +336 -0
- dau_sim/tests/cocotb_benches/__init__.py +1 -0
- dau_sim/tests/cocotb_benches/ready_valid_sum_tb.py +59 -0
- dau_sim/tests/sv/ready_valid_sum.sv +39 -0
- dau_sim/tests/sv/ready_valid_sum_tb.sv +102 -0
- dau_sim/tests/test_api.py +53 -0
- dau_sim/tests/test_cli.py +70 -0
- dau_sim/tests/test_cocotb_backend.py +945 -0
- dau_sim/tests/test_cocotb_examples.py +739 -0
- dau_sim/tests/test_compiler.py +608 -0
- dau_sim/tests/test_eval.py +365 -0
- dau_sim/tests/test_frontend_amaranth.py +1384 -0
- dau_sim/tests/test_frontend_sv.py +691 -0
- dau_sim/tests/test_ir.py +327 -0
- dau_sim/tests/test_nonsynthesizable.py +693 -0
- dau_sim/tests/test_perf.py +100 -0
- dau_sim/tests/test_ready_valid_sum_cocotb.py +29 -0
- dau_sim/tests/test_selectors.py +188 -0
- dau_sim/tests/test_sim_comb.py +570 -0
- dau_sim/tests/test_sim_seq.py +657 -0
- dau_sim/tests/test_testbench.py +781 -0
- dau_sim/tests/test_vcd.py +500 -0
- dau_sim/tests/test_verilator_integration.py +29 -0
- dau_sim/tests/test_verilator_profiles.py +50 -0
- dau_sim-0.1.0.dist-info/METADATA +123 -0
- dau_sim-0.1.0.dist-info/RECORD +77 -0
- dau_sim-0.1.0.dist-info/WHEEL +4 -0
- dau_sim-0.1.0.dist-info/entry_points.txt +2 -0
- dau_sim-0.1.0.dist-info/licenses/LICENSE +201 -0
dau_sim/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""CSP adapters for I/O (VCD output, stimulus injection, etc.)."""
|
|
2
|
+
|
|
3
|
+
from dau_sim.adapters.selectors import match_signals, select_signals
|
|
4
|
+
from dau_sim.adapters.vcd import traces_to_vcd, write_vcd
|
|
5
|
+
|
|
6
|
+
__all__ = ["match_signals", "select_signals", "traces_to_vcd", "write_vcd"]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Signal selectors: glob/regex patterns for filtering simulation traces.
|
|
2
|
+
|
|
3
|
+
Use signal selectors to choose which signals to include in VCD output
|
|
4
|
+
or testbench observation, avoiding the overhead of tracing everything
|
|
5
|
+
in large designs.
|
|
6
|
+
|
|
7
|
+
Usage::
|
|
8
|
+
|
|
9
|
+
from dau_sim.adapters.selectors import select_signals
|
|
10
|
+
|
|
11
|
+
# Glob patterns
|
|
12
|
+
filtered = select_signals(traces, include=["count", "en"])
|
|
13
|
+
filtered = select_signals(traces, include=["*clk*"])
|
|
14
|
+
filtered = select_signals(traces, exclude=["rst", "*internal*"])
|
|
15
|
+
|
|
16
|
+
# Regex patterns
|
|
17
|
+
filtered = select_signals(traces, include=[r"data_\\d+"], regex=True)
|
|
18
|
+
|
|
19
|
+
# With CompiledModule
|
|
20
|
+
traces = cm.run(cycles=100, inputs={"en": 1})
|
|
21
|
+
cm.write_vcd("out.vcd", traces, signals=["count", "en"])
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import fnmatch
|
|
27
|
+
import re
|
|
28
|
+
|
|
29
|
+
__all__ = ["select_signals", "match_signals"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def match_signals(
|
|
33
|
+
names: list[str] | set[str],
|
|
34
|
+
*,
|
|
35
|
+
include: list[str] | None = None,
|
|
36
|
+
exclude: list[str] | None = None,
|
|
37
|
+
regex: bool = False,
|
|
38
|
+
) -> list[str]:
|
|
39
|
+
"""Return signal names matching the include/exclude filters.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
names
|
|
44
|
+
All available signal names.
|
|
45
|
+
include
|
|
46
|
+
Patterns to include. If ``None``, all names are included.
|
|
47
|
+
exclude
|
|
48
|
+
Patterns to exclude. Applied after include filtering.
|
|
49
|
+
regex
|
|
50
|
+
If ``True``, patterns are Python regexes.
|
|
51
|
+
If ``False`` (default), patterns are Unix glob patterns.
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
list[str]
|
|
56
|
+
Matching signal names, in the order they appear in *names*.
|
|
57
|
+
"""
|
|
58
|
+
result = list(names)
|
|
59
|
+
|
|
60
|
+
if include is not None:
|
|
61
|
+
result = _apply_patterns(result, include, regex, mode="include")
|
|
62
|
+
|
|
63
|
+
if exclude is not None:
|
|
64
|
+
excluded = set(_apply_patterns(result, exclude, regex, mode="include"))
|
|
65
|
+
result = [n for n in result if n not in excluded]
|
|
66
|
+
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def select_signals(
|
|
71
|
+
traces: dict[str, list],
|
|
72
|
+
*,
|
|
73
|
+
include: list[str] | None = None,
|
|
74
|
+
exclude: list[str] | None = None,
|
|
75
|
+
regex: bool = False,
|
|
76
|
+
) -> dict[str, list]:
|
|
77
|
+
"""Filter a traces dict to only include matching signals.
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
traces
|
|
82
|
+
Signal name → trace list mapping (from ``CompiledModule.run()``).
|
|
83
|
+
include
|
|
84
|
+
Glob or regex patterns for signals to keep.
|
|
85
|
+
exclude
|
|
86
|
+
Glob or regex patterns for signals to remove.
|
|
87
|
+
regex
|
|
88
|
+
Interpret patterns as regex (default: glob).
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
dict[str, list]
|
|
93
|
+
Filtered traces dict.
|
|
94
|
+
"""
|
|
95
|
+
matched = match_signals(
|
|
96
|
+
list(traces.keys()),
|
|
97
|
+
include=include,
|
|
98
|
+
exclude=exclude,
|
|
99
|
+
regex=regex,
|
|
100
|
+
)
|
|
101
|
+
return {name: traces[name] for name in matched}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _apply_patterns(
|
|
105
|
+
names: list[str],
|
|
106
|
+
patterns: list[str],
|
|
107
|
+
regex: bool,
|
|
108
|
+
mode: str,
|
|
109
|
+
) -> list[str]:
|
|
110
|
+
"""Return names that match any of the given patterns."""
|
|
111
|
+
matched = []
|
|
112
|
+
seen = set()
|
|
113
|
+
|
|
114
|
+
for pattern in patterns:
|
|
115
|
+
if regex:
|
|
116
|
+
compiled = re.compile(pattern)
|
|
117
|
+
for name in names:
|
|
118
|
+
if name not in seen and compiled.search(name):
|
|
119
|
+
matched.append(name)
|
|
120
|
+
seen.add(name)
|
|
121
|
+
else:
|
|
122
|
+
for name in names:
|
|
123
|
+
if name not in seen and fnmatch.fnmatch(name, pattern):
|
|
124
|
+
matched.append(name)
|
|
125
|
+
seen.add(name)
|
|
126
|
+
|
|
127
|
+
return matched
|
dau_sim/adapters/vcd.py
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"""VCD (Value Change Dump) writer for dau-sim simulation traces.
|
|
2
|
+
|
|
3
|
+
Produces IEEE 1364-2001 compliant VCD files from the simulation trace
|
|
4
|
+
format (``dict[str, list[tuple[datetime, int]]]``) produced by
|
|
5
|
+
``CompiledModule.run()``.
|
|
6
|
+
|
|
7
|
+
Usage::
|
|
8
|
+
|
|
9
|
+
from dau_sim.adapters.vcd import write_vcd, traces_to_vcd
|
|
10
|
+
|
|
11
|
+
# From a CompiledModule run:
|
|
12
|
+
traces = compiled.run(cycles=100, inputs={"d": 1})
|
|
13
|
+
write_vcd("out.vcd", traces, module=compiled.module)
|
|
14
|
+
|
|
15
|
+
# Or get VCD as a string:
|
|
16
|
+
vcd_str = traces_to_vcd(traces, module=compiled.module)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import io
|
|
22
|
+
import string
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import TextIO
|
|
26
|
+
|
|
27
|
+
from dau_sim.ir.module import Module
|
|
28
|
+
from dau_sim.ir.types import EdgePolarity, Shape
|
|
29
|
+
|
|
30
|
+
_ID_CHARS = string.printable[:-5] # printable minus whitespace
|
|
31
|
+
|
|
32
|
+
__all__ = (
|
|
33
|
+
"traces_to_vcd",
|
|
34
|
+
"write_vcd",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _make_id(index: int) -> str:
|
|
39
|
+
"""Generate a short VCD identifier code from an integer index.
|
|
40
|
+
|
|
41
|
+
VCD identifiers are one or more printable ASCII characters (33–126).
|
|
42
|
+
"""
|
|
43
|
+
if index < 0:
|
|
44
|
+
raise ValueError("index must be non-negative")
|
|
45
|
+
result = []
|
|
46
|
+
base = len(_ID_CHARS)
|
|
47
|
+
while True:
|
|
48
|
+
result.append(_ID_CHARS[index % base])
|
|
49
|
+
index = index // base
|
|
50
|
+
if index == 0:
|
|
51
|
+
break
|
|
52
|
+
index -= 1 # shift so 0→first char, not 1→first char on next digit
|
|
53
|
+
return "".join(result)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _datetime_to_timescale_ticks(
|
|
57
|
+
timestamps: list[datetime],
|
|
58
|
+
timescale_ns: int = 1,
|
|
59
|
+
) -> list[int]:
|
|
60
|
+
"""Convert datetime timestamps to integer VCD time ticks.
|
|
61
|
+
|
|
62
|
+
All timestamps are relative to the first timestamp in the list.
|
|
63
|
+
"""
|
|
64
|
+
if not timestamps:
|
|
65
|
+
return []
|
|
66
|
+
base = timestamps[0]
|
|
67
|
+
ticks = []
|
|
68
|
+
for ts in timestamps:
|
|
69
|
+
delta = ts - base
|
|
70
|
+
ns = int(delta.total_seconds() * 1_000_000_000)
|
|
71
|
+
ticks.append(ns // timescale_ns)
|
|
72
|
+
return ticks
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _format_value(value: int, width: int) -> str:
|
|
76
|
+
"""Format an integer value as a VCD binary literal."""
|
|
77
|
+
if width == 1:
|
|
78
|
+
return str(value & 1)
|
|
79
|
+
# Multi-bit: binary string, MSB first, padded to width
|
|
80
|
+
if value < 0:
|
|
81
|
+
# Two's complement representation for signed values
|
|
82
|
+
value = value & ((1 << width) - 1)
|
|
83
|
+
bits = bin(value)[2:] # strip '0b'
|
|
84
|
+
return "b" + bits.zfill(width)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _write_header(
|
|
88
|
+
out: TextIO,
|
|
89
|
+
module_name: str,
|
|
90
|
+
signals: dict[str, tuple[str, int, str]],
|
|
91
|
+
timescale: str = "1ns",
|
|
92
|
+
date: str | None = None,
|
|
93
|
+
) -> None:
|
|
94
|
+
"""Write the VCD header section.
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
out : TextIO
|
|
99
|
+
Output stream.
|
|
100
|
+
module_name : str
|
|
101
|
+
Top-level scope name.
|
|
102
|
+
signals : dict
|
|
103
|
+
Maps signal name → (vcd_id, width, var_type).
|
|
104
|
+
var_type is "wire", "reg", etc.
|
|
105
|
+
timescale : str
|
|
106
|
+
VCD timescale string (e.g. "1ns", "1ps").
|
|
107
|
+
date : str | None
|
|
108
|
+
Date string for $date section.
|
|
109
|
+
"""
|
|
110
|
+
if date:
|
|
111
|
+
out.write(f"$date\n {date}\n$end\n")
|
|
112
|
+
out.write("$version dau-sim 0.1.0 $end\n")
|
|
113
|
+
out.write(f"$timescale {timescale} $end\n")
|
|
114
|
+
|
|
115
|
+
# Scope
|
|
116
|
+
out.write(f"$scope module {module_name} $end\n")
|
|
117
|
+
for sig_name, (vcd_id, width, var_type) in signals.items():
|
|
118
|
+
out.write(f"$var {var_type} {width} {vcd_id} {sig_name} $end\n")
|
|
119
|
+
out.write("$upscope $end\n")
|
|
120
|
+
|
|
121
|
+
out.write("$enddefinitions $end\n")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _write_initial_values(
|
|
125
|
+
out: TextIO,
|
|
126
|
+
signals: dict[str, tuple[str, int, str]],
|
|
127
|
+
initial_values: dict[str, int],
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Write the $dumpvars section with initial values."""
|
|
130
|
+
out.write("$dumpvars\n")
|
|
131
|
+
for sig_name, (vcd_id, width, _) in signals.items():
|
|
132
|
+
val = initial_values.get(sig_name, 0)
|
|
133
|
+
formatted = _format_value(val, width)
|
|
134
|
+
if width == 1:
|
|
135
|
+
out.write(f"{formatted}{vcd_id}\n")
|
|
136
|
+
else:
|
|
137
|
+
out.write(f"{formatted} {vcd_id}\n")
|
|
138
|
+
out.write("$end\n")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _write_changes(
|
|
142
|
+
out: TextIO,
|
|
143
|
+
signals: dict[str, tuple[str, int, str]],
|
|
144
|
+
traces: dict[str, list[tuple[datetime, int]]],
|
|
145
|
+
timescale_ns: int = 1,
|
|
146
|
+
base_time: datetime | None = None,
|
|
147
|
+
) -> None:
|
|
148
|
+
"""Write time-stamped value changes."""
|
|
149
|
+
# Collect all events across all signals, sorted by time
|
|
150
|
+
events: list[tuple[int, str, int]] = [] # (tick, signal_name, value)
|
|
151
|
+
|
|
152
|
+
# Find the global base time if not provided
|
|
153
|
+
if base_time is None:
|
|
154
|
+
for sig_name, trace in traces.items():
|
|
155
|
+
if trace and (base_time is None or trace[0][0] < base_time):
|
|
156
|
+
base_time = trace[0][0]
|
|
157
|
+
|
|
158
|
+
if base_time is None:
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
for sig_name, trace in traces.items():
|
|
162
|
+
if sig_name not in signals:
|
|
163
|
+
continue
|
|
164
|
+
for ts, val in trace:
|
|
165
|
+
delta = ts - base_time
|
|
166
|
+
ns = int(delta.total_seconds() * 1_000_000_000)
|
|
167
|
+
tick = ns // timescale_ns
|
|
168
|
+
events.append((tick, sig_name, val))
|
|
169
|
+
|
|
170
|
+
# Sort by tick, then by signal name for determinism
|
|
171
|
+
events.sort(key=lambda e: (e[0], e[1]))
|
|
172
|
+
|
|
173
|
+
current_tick = -1
|
|
174
|
+
for tick, sig_name, value in events:
|
|
175
|
+
if tick != current_tick:
|
|
176
|
+
out.write(f"#{tick}\n")
|
|
177
|
+
current_tick = tick
|
|
178
|
+
vcd_id, width, _ = signals[sig_name]
|
|
179
|
+
formatted = _format_value(value, width)
|
|
180
|
+
if width == 1:
|
|
181
|
+
out.write(f"{formatted}{vcd_id}\n")
|
|
182
|
+
else:
|
|
183
|
+
out.write(f"{formatted} {vcd_id}\n")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def traces_to_vcd(
|
|
187
|
+
traces: dict[str, list[tuple[datetime, int]]],
|
|
188
|
+
*,
|
|
189
|
+
module: Module | None = None,
|
|
190
|
+
timescale: str = "1ns",
|
|
191
|
+
scope: str | None = None,
|
|
192
|
+
) -> str:
|
|
193
|
+
"""Convert simulation traces to a VCD string.
|
|
194
|
+
|
|
195
|
+
Parameters
|
|
196
|
+
----------
|
|
197
|
+
traces : dict
|
|
198
|
+
Signal traces from ``CompiledModule.run()``.
|
|
199
|
+
module : Module | None
|
|
200
|
+
IR module (used for signal widths and port directions).
|
|
201
|
+
If None, all signals are assumed 1-bit wire.
|
|
202
|
+
timescale : str
|
|
203
|
+
VCD timescale (e.g. "1ns", "1ps", "10ns").
|
|
204
|
+
scope : str | None
|
|
205
|
+
Top-level scope name. Defaults to module name or "top".
|
|
206
|
+
|
|
207
|
+
Returns
|
|
208
|
+
-------
|
|
209
|
+
str
|
|
210
|
+
Complete VCD file content.
|
|
211
|
+
"""
|
|
212
|
+
buf = io.StringIO()
|
|
213
|
+
_traces_to_vcd_stream(buf, traces, module=module, timescale=timescale, scope=scope)
|
|
214
|
+
return buf.getvalue()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def write_vcd(
|
|
218
|
+
path: str | Path,
|
|
219
|
+
traces: dict[str, list[tuple[datetime, int]]],
|
|
220
|
+
*,
|
|
221
|
+
module: Module | None = None,
|
|
222
|
+
timescale: str = "1ns",
|
|
223
|
+
scope: str | None = None,
|
|
224
|
+
) -> None:
|
|
225
|
+
"""Write simulation traces to a VCD file.
|
|
226
|
+
|
|
227
|
+
Parameters
|
|
228
|
+
----------
|
|
229
|
+
path : str or Path
|
|
230
|
+
Output file path.
|
|
231
|
+
traces : dict
|
|
232
|
+
Signal traces from ``CompiledModule.run()``.
|
|
233
|
+
module : Module | None
|
|
234
|
+
IR module for signal metadata.
|
|
235
|
+
timescale : str
|
|
236
|
+
VCD timescale string.
|
|
237
|
+
scope : str | None
|
|
238
|
+
Top-level scope name.
|
|
239
|
+
"""
|
|
240
|
+
with open(path, "w") as f:
|
|
241
|
+
_traces_to_vcd_stream(f, traces, module=module, timescale=timescale, scope=scope)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _traces_to_vcd_stream(
|
|
245
|
+
out: TextIO,
|
|
246
|
+
traces: dict[str, list[tuple[datetime, int]]],
|
|
247
|
+
*,
|
|
248
|
+
module: Module | None = None,
|
|
249
|
+
timescale: str = "1ns",
|
|
250
|
+
scope: str | None = None,
|
|
251
|
+
) -> None:
|
|
252
|
+
"""Core implementation: write VCD to a stream."""
|
|
253
|
+
# Parse timescale to get ns multiplier
|
|
254
|
+
timescale_ns = _parse_timescale_ns(timescale)
|
|
255
|
+
|
|
256
|
+
# Build signal metadata
|
|
257
|
+
shapes: dict[str, Shape] = {}
|
|
258
|
+
var_types: dict[str, str] = {}
|
|
259
|
+
|
|
260
|
+
if module is not None:
|
|
261
|
+
scope_name = scope or module.name
|
|
262
|
+
for p in module.ports:
|
|
263
|
+
shapes[p.name] = p.shape
|
|
264
|
+
var_types[p.name] = "wire"
|
|
265
|
+
for s in module.signals:
|
|
266
|
+
shapes[s.name] = s.shape
|
|
267
|
+
var_types[s.name] = "reg"
|
|
268
|
+
else:
|
|
269
|
+
scope_name = scope or "top"
|
|
270
|
+
|
|
271
|
+
# Build VCD signal table: name → (id, width, type)
|
|
272
|
+
signal_table: dict[str, tuple[str, int, str]] = {}
|
|
273
|
+
idx = 0
|
|
274
|
+
for sig_name in sorted(traces.keys()):
|
|
275
|
+
shape = shapes.get(sig_name, Shape(1, False))
|
|
276
|
+
vtype = var_types.get(sig_name, "wire")
|
|
277
|
+
signal_table[sig_name] = (_make_id(idx), shape.width, vtype)
|
|
278
|
+
idx += 1
|
|
279
|
+
|
|
280
|
+
# Write header
|
|
281
|
+
_write_header(out, scope_name, signal_table, timescale=timescale)
|
|
282
|
+
|
|
283
|
+
# Clock synthesis setup
|
|
284
|
+
# Detect clock signals and their active/inactive values from the module.
|
|
285
|
+
# The simulation engine only emits trace data on active clock edges,
|
|
286
|
+
# so the VCD writer must synthesize the inactive (opposite) edges to
|
|
287
|
+
# produce a proper square-wave clock in the output.
|
|
288
|
+
clock_info: dict[str, tuple[int, int]] = {} # {clk_name: (active, inactive)}
|
|
289
|
+
if module is not None:
|
|
290
|
+
for cd in module.clock_domains:
|
|
291
|
+
if cd.clk in signal_table:
|
|
292
|
+
if cd.edge == EdgePolarity.POSEDGE:
|
|
293
|
+
clock_info[cd.clk] = (1, 0)
|
|
294
|
+
else:
|
|
295
|
+
clock_info[cd.clk] = (0, 1)
|
|
296
|
+
|
|
297
|
+
# Infer half-period from the gap between consecutive trace timestamps.
|
|
298
|
+
half_period_td = None
|
|
299
|
+
if clock_info:
|
|
300
|
+
for trace in traces.values():
|
|
301
|
+
if len(trace) >= 2:
|
|
302
|
+
half_period_td = (trace[1][0] - trace[0][0]) / 2
|
|
303
|
+
break
|
|
304
|
+
|
|
305
|
+
synthesize_clocks = bool(clock_info and half_period_td is not None)
|
|
306
|
+
|
|
307
|
+
# Initial values
|
|
308
|
+
initial_values: dict[str, int] = {}
|
|
309
|
+
if synthesize_clocks:
|
|
310
|
+
# With clock synthesis the VCD timeline is shifted back by one
|
|
311
|
+
# half-period so that $dumpvars represents the state *before* the
|
|
312
|
+
# first active edge. Clocks start at their inactive level and
|
|
313
|
+
# non-clock signals start at zero (reset state).
|
|
314
|
+
for sig_name in signal_table:
|
|
315
|
+
if sig_name in clock_info:
|
|
316
|
+
initial_values[sig_name] = clock_info[sig_name][1]
|
|
317
|
+
else:
|
|
318
|
+
initial_values[sig_name] = 0
|
|
319
|
+
else:
|
|
320
|
+
for sig_name, trace in traces.items():
|
|
321
|
+
initial_values[sig_name] = trace[0][1] if trace else 0
|
|
322
|
+
|
|
323
|
+
# Write initial values
|
|
324
|
+
_write_initial_values(out, signal_table, initial_values)
|
|
325
|
+
|
|
326
|
+
# Compute the base time from the full (untrimmed) traces
|
|
327
|
+
base_time: datetime | None = None
|
|
328
|
+
for trace in traces.values():
|
|
329
|
+
if trace and (base_time is None or trace[0][0] < base_time):
|
|
330
|
+
base_time = trace[0][0]
|
|
331
|
+
|
|
332
|
+
if synthesize_clocks and base_time is not None:
|
|
333
|
+
base_time = base_time - half_period_td
|
|
334
|
+
|
|
335
|
+
# Build value-change traces
|
|
336
|
+
trimmed: dict[str, list[tuple[datetime, int]]] = {}
|
|
337
|
+
if synthesize_clocks:
|
|
338
|
+
# Include ALL entries for non-clock signals (dumpvars has zeros,
|
|
339
|
+
# so the first trace entry is a real change at the first posedge).
|
|
340
|
+
for sig_name, trace in traces.items():
|
|
341
|
+
if sig_name in clock_info:
|
|
342
|
+
# Synthesize clock toggle events
|
|
343
|
+
active_val, inactive_val = clock_info[sig_name]
|
|
344
|
+
events: list[tuple[datetime, int]] = []
|
|
345
|
+
for i, (ts, _) in enumerate(trace):
|
|
346
|
+
events.append((ts, active_val))
|
|
347
|
+
if i < len(trace) - 1:
|
|
348
|
+
mid = ts + (trace[i + 1][0] - ts) / 2
|
|
349
|
+
events.append((mid, inactive_val))
|
|
350
|
+
elif half_period_td:
|
|
351
|
+
events.append((ts + half_period_td, inactive_val))
|
|
352
|
+
trimmed[sig_name] = events
|
|
353
|
+
else:
|
|
354
|
+
trimmed[sig_name] = list(trace)
|
|
355
|
+
else:
|
|
356
|
+
# Original behaviour: first entry is in $dumpvars, changes start
|
|
357
|
+
# from the second entry onward.
|
|
358
|
+
for sig_name, trace in traces.items():
|
|
359
|
+
trimmed[sig_name] = trace[1:] if len(trace) > 1 else []
|
|
360
|
+
|
|
361
|
+
_write_changes(out, signal_table, trimmed, timescale_ns=timescale_ns, base_time=base_time)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _parse_timescale_ns(timescale: str) -> int:
|
|
365
|
+
"""Parse a VCD timescale string to nanoseconds.
|
|
366
|
+
|
|
367
|
+
Supports: "1ns", "10ns", "100ns", "1us", "1ps", etc.
|
|
368
|
+
"""
|
|
369
|
+
ts = timescale.strip().lower()
|
|
370
|
+
units = {"ps": 0.001, "ns": 1, "us": 1000, "ms": 1_000_000, "s": 1_000_000_000}
|
|
371
|
+
for suffix, mult in units.items():
|
|
372
|
+
if ts.endswith(suffix):
|
|
373
|
+
num = int(ts[: -len(suffix)])
|
|
374
|
+
return max(1, int(num * mult))
|
|
375
|
+
return 1 # default to 1ns
|
dau_sim/api.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import timedelta
|
|
5
|
+
|
|
6
|
+
from dau_sim.compiler import compile_module
|
|
7
|
+
from dau_sim.compiler.compile import CompiledModule
|
|
8
|
+
from dau_sim.ir.module import Module
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class SimulationResult:
|
|
13
|
+
"""Container for simulation output with interactive helpers."""
|
|
14
|
+
|
|
15
|
+
module_name: str
|
|
16
|
+
traces: dict[str, list[tuple[object, int]]]
|
|
17
|
+
|
|
18
|
+
def latest(self, signal: str) -> int | None:
|
|
19
|
+
"""Return the most recent value for a signal, if available."""
|
|
20
|
+
values = self.traces.get(signal, [])
|
|
21
|
+
if not values:
|
|
22
|
+
return None
|
|
23
|
+
return values[-1][1]
|
|
24
|
+
|
|
25
|
+
def to_rows(self, *, signals: list[str] | None = None) -> list[dict[str, object]]:
|
|
26
|
+
"""Return trace points as row dictionaries for notebooks/data tools."""
|
|
27
|
+
selected = signals if signals is not None else sorted(self.traces.keys())
|
|
28
|
+
rows: list[dict[str, object]] = []
|
|
29
|
+
for name in selected:
|
|
30
|
+
for ts, value in self.traces.get(name, []):
|
|
31
|
+
rows.append({"signal": name, "timestamp": ts, "value": value})
|
|
32
|
+
return rows
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Simulator:
|
|
36
|
+
"""High-level interactive API for compiling and running designs."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, compiled: CompiledModule):
|
|
39
|
+
self._compiled = compiled
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def module(self) -> Module:
|
|
43
|
+
return self._compiled.module
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def compiled(self) -> CompiledModule:
|
|
47
|
+
return self._compiled
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def from_module(cls, module: Module, *, four_state: bool = False) -> "Simulator":
|
|
51
|
+
return cls(compile_module(module, four_state=four_state))
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def from_sv(cls, source: str, *, top: str | None = None, four_state: bool = False) -> "Simulator":
|
|
55
|
+
from dau_sim.frontends import parse_sv
|
|
56
|
+
|
|
57
|
+
module = parse_sv(source, top=top)
|
|
58
|
+
return cls.from_module(module, four_state=four_state)
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_sv_file(cls, path: str, *, top: str | None = None, four_state: bool = False) -> "Simulator":
|
|
62
|
+
from dau_sim.frontends import parse_sv_file
|
|
63
|
+
|
|
64
|
+
module = parse_sv_file(path, top=top)
|
|
65
|
+
return cls.from_module(module, four_state=four_state)
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def from_amaranth(cls, design, *, four_state: bool = False) -> "Simulator":
|
|
69
|
+
from dau_sim.frontends import from_amaranth
|
|
70
|
+
|
|
71
|
+
module = from_amaranth(design)
|
|
72
|
+
return cls.from_module(module, four_state=four_state)
|
|
73
|
+
|
|
74
|
+
def run(
|
|
75
|
+
self,
|
|
76
|
+
*,
|
|
77
|
+
cycles: int = 10,
|
|
78
|
+
clock_period: timedelta = timedelta(microseconds=1),
|
|
79
|
+
inputs: dict[str, int] | None = None,
|
|
80
|
+
clocks: dict[str, timedelta] | None = None,
|
|
81
|
+
) -> SimulationResult:
|
|
82
|
+
traces = self._compiled.run(cycles=cycles, clock_period=clock_period, inputs=inputs, clocks=clocks)
|
|
83
|
+
return SimulationResult(module_name=self._compiled.module.name, traces=traces)
|
|
84
|
+
|
|
85
|
+
def write_vcd(
|
|
86
|
+
self,
|
|
87
|
+
path: str,
|
|
88
|
+
result: SimulationResult,
|
|
89
|
+
*,
|
|
90
|
+
timescale: str = "1ns",
|
|
91
|
+
signals: list[str] | None = None,
|
|
92
|
+
exclude: list[str] | None = None,
|
|
93
|
+
) -> None:
|
|
94
|
+
self._compiled.write_vcd(path, result.traces, timescale=timescale, signals=signals, exclude=exclude)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Backends: simulator interface implementations (e.g. cocotb)."""
|