oscura 0.3.0__py3-none-any.whl → 0.5.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.
- oscura/__init__.py +1 -7
- oscura/acquisition/__init__.py +147 -0
- oscura/acquisition/file.py +255 -0
- oscura/acquisition/hardware.py +186 -0
- oscura/acquisition/saleae.py +340 -0
- oscura/acquisition/socketcan.py +315 -0
- oscura/acquisition/streaming.py +38 -0
- oscura/acquisition/synthetic.py +229 -0
- oscura/acquisition/visa.py +376 -0
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/digital/__init__.py +48 -0
- oscura/analyzers/digital/clock.py +9 -1
- oscura/analyzers/digital/edges.py +1 -1
- oscura/analyzers/digital/extraction.py +195 -0
- oscura/analyzers/digital/ic_database.py +498 -0
- oscura/analyzers/digital/timing.py +41 -11
- oscura/analyzers/digital/timing_paths.py +339 -0
- oscura/analyzers/digital/vintage.py +377 -0
- oscura/analyzers/digital/vintage_result.py +148 -0
- oscura/analyzers/protocols/__init__.py +22 -1
- oscura/analyzers/protocols/parallel_bus.py +449 -0
- oscura/analyzers/side_channel/__init__.py +52 -0
- oscura/analyzers/side_channel/power.py +690 -0
- oscura/analyzers/side_channel/timing.py +369 -0
- oscura/analyzers/signal_integrity/sparams.py +1 -1
- oscura/automotive/__init__.py +4 -2
- oscura/automotive/can/patterns.py +3 -1
- oscura/automotive/can/session.py +277 -78
- oscura/automotive/can/state_machine.py +5 -2
- oscura/builders/__init__.py +9 -11
- oscura/builders/signal_builder.py +99 -191
- oscura/core/exceptions.py +5 -1
- oscura/export/__init__.py +12 -0
- oscura/export/wavedrom.py +430 -0
- oscura/exporters/json_export.py +47 -0
- oscura/exporters/vintage_logic_csv.py +247 -0
- oscura/loaders/__init__.py +1 -0
- oscura/loaders/chipwhisperer.py +393 -0
- oscura/loaders/touchstone.py +1 -1
- oscura/reporting/__init__.py +7 -0
- oscura/reporting/vintage_logic_report.py +523 -0
- oscura/session/session.py +54 -46
- oscura/sessions/__init__.py +70 -0
- oscura/sessions/base.py +323 -0
- oscura/sessions/blackbox.py +640 -0
- oscura/sessions/generic.py +189 -0
- oscura/utils/autodetect.py +5 -1
- oscura/visualization/digital_advanced.py +718 -0
- oscura/visualization/figure_manager.py +156 -0
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/METADATA +86 -5
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/RECORD +54 -33
- oscura/automotive/dtc/data.json +0 -2763
- oscura/schemas/bus_configuration.json +0 -322
- oscura/schemas/device_mapping.json +0 -182
- oscura/schemas/packet_format.json +0 -418
- oscura/schemas/protocol_definition.json +0 -363
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/WHEEL +0 -0
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
"""Advanced digital logic visualizations.
|
|
2
|
+
|
|
3
|
+
State-of-the-art visualization tools for digital logic analysis including:
|
|
4
|
+
- Logic analyzer-style timeline displays
|
|
5
|
+
- Multi-channel bus views with bus decoding
|
|
6
|
+
- Timing diagram annotations
|
|
7
|
+
- IC timing validation overlays
|
|
8
|
+
- Eye diagrams for signal quality
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
>>> from oscura.visualization.digital_advanced import plot_logic_analyzer_view
|
|
12
|
+
>>> plot_logic_analyzer_view(channels, title="8-bit Data Bus")
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import TYPE_CHECKING, Any
|
|
19
|
+
|
|
20
|
+
import matplotlib.pyplot as plt
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from matplotlib.axes import Axes
|
|
25
|
+
from matplotlib.figure import Figure
|
|
26
|
+
from numpy.typing import NDArray
|
|
27
|
+
|
|
28
|
+
from oscura.core.types import DigitalTrace, WaveformTrace
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def plot_logic_analyzer_view(
|
|
32
|
+
channels: dict[str, WaveformTrace | DigitalTrace],
|
|
33
|
+
*,
|
|
34
|
+
title: str | None = None,
|
|
35
|
+
time_range: tuple[float, float] | None = None,
|
|
36
|
+
group_buses: dict[str, list[str]] | None = None,
|
|
37
|
+
show_hex: bool = True,
|
|
38
|
+
show_cursors: bool = True,
|
|
39
|
+
figsize: tuple[float, float] = (14, 8),
|
|
40
|
+
) -> tuple[Figure, Axes]:
|
|
41
|
+
"""Create logic analyzer-style timeline display.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
channels: Dictionary mapping channel names to traces.
|
|
45
|
+
title: Optional plot title.
|
|
46
|
+
time_range: Optional (start, end) time range in seconds.
|
|
47
|
+
group_buses: Optional dict mapping bus names to channel lists.
|
|
48
|
+
show_hex: Show hexadecimal values for buses.
|
|
49
|
+
show_cursors: Show timing cursors.
|
|
50
|
+
figsize: Figure size in inches.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Tuple of (figure, axes).
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
>>> channels = {f"D{i}": trace for i, trace in enumerate(data_lines)}
|
|
57
|
+
>>> group_buses = {"DATA": [f"D{i}" for i in range(8)]}
|
|
58
|
+
>>> fig, ax = plot_logic_analyzer_view(channels, group_buses=group_buses)
|
|
59
|
+
"""
|
|
60
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
61
|
+
|
|
62
|
+
# Determine time range
|
|
63
|
+
first_trace = next(iter(channels.values()))
|
|
64
|
+
time_base = first_trace.metadata.time_base
|
|
65
|
+
total_time = len(first_trace.data) * time_base
|
|
66
|
+
|
|
67
|
+
t_start: float
|
|
68
|
+
t_end: float
|
|
69
|
+
if time_range is None:
|
|
70
|
+
t_start, t_end = 0.0, total_time
|
|
71
|
+
else:
|
|
72
|
+
t_start, t_end = time_range
|
|
73
|
+
|
|
74
|
+
# Calculate display window
|
|
75
|
+
start_idx = int(t_start / time_base)
|
|
76
|
+
end_idx = int(t_end / time_base)
|
|
77
|
+
|
|
78
|
+
# Create time array
|
|
79
|
+
time_array = np.arange(start_idx, end_idx) * time_base
|
|
80
|
+
|
|
81
|
+
# Plot channels from bottom to top
|
|
82
|
+
y_offset = 0.0
|
|
83
|
+
channel_positions: dict[str, float] = {}
|
|
84
|
+
|
|
85
|
+
# Handle bus grouping
|
|
86
|
+
if group_buses:
|
|
87
|
+
for bus_name, bus_channels in group_buses.items():
|
|
88
|
+
# Combine bus channels into values
|
|
89
|
+
bus_values = _combine_bus_channels(
|
|
90
|
+
{name: channels[name] for name in bus_channels}, start_idx, end_idx
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Plot bus as combined signal with hex labels
|
|
94
|
+
_plot_bus_signal(ax, time_array, bus_values, y_offset, bus_name, show_hex=show_hex)
|
|
95
|
+
channel_positions[bus_name] = y_offset
|
|
96
|
+
y_offset += 1.5 # Extra spacing for buses
|
|
97
|
+
|
|
98
|
+
# Remove individual channels from main list
|
|
99
|
+
for ch_name in bus_channels:
|
|
100
|
+
channels.pop(ch_name, None)
|
|
101
|
+
else:
|
|
102
|
+
group_buses = {}
|
|
103
|
+
|
|
104
|
+
# Plot remaining individual channels
|
|
105
|
+
for ch_name, trace in channels.items():
|
|
106
|
+
# Extract data in window
|
|
107
|
+
data = np.asarray(trace.data[start_idx:end_idx])
|
|
108
|
+
|
|
109
|
+
# Convert to digital if needed
|
|
110
|
+
if hasattr(data, "dtype") and data.dtype == bool:
|
|
111
|
+
digital_data = data.astype(float)
|
|
112
|
+
else:
|
|
113
|
+
# Threshold analog signal
|
|
114
|
+
threshold = (np.max(data) + np.min(data)) / 2
|
|
115
|
+
digital_data = (data >= threshold).astype(float)
|
|
116
|
+
|
|
117
|
+
# Plot digital waveform
|
|
118
|
+
_plot_digital_waveform(ax, time_array, digital_data, y_offset, ch_name)
|
|
119
|
+
channel_positions[ch_name] = y_offset
|
|
120
|
+
y_offset += 1
|
|
121
|
+
|
|
122
|
+
# Style the plot
|
|
123
|
+
ax.set_xlim(t_start, t_end)
|
|
124
|
+
ax.set_ylim(-0.5, y_offset + 0.5)
|
|
125
|
+
ax.set_xlabel("Time (s)", fontsize=12, fontweight="bold")
|
|
126
|
+
ax.set_ylabel("Channel", fontsize=12, fontweight="bold")
|
|
127
|
+
|
|
128
|
+
if title:
|
|
129
|
+
ax.set_title(title, fontsize=14, fontweight="bold")
|
|
130
|
+
|
|
131
|
+
# Add grid
|
|
132
|
+
ax.grid(True, which="both", alpha=0.3, linestyle="--")
|
|
133
|
+
ax.set_yticks([])
|
|
134
|
+
|
|
135
|
+
# Format time axis
|
|
136
|
+
_format_time_axis(ax, t_start, t_end)
|
|
137
|
+
|
|
138
|
+
# Add cursors if requested
|
|
139
|
+
if show_cursors:
|
|
140
|
+
_add_timing_cursors(ax, t_start, t_end, y_offset)
|
|
141
|
+
|
|
142
|
+
plt.tight_layout()
|
|
143
|
+
return fig, ax
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def plot_timing_diagram_with_annotations(
|
|
147
|
+
signals: dict[str, WaveformTrace | DigitalTrace],
|
|
148
|
+
*,
|
|
149
|
+
timing_params: dict[str, tuple[float, float, str]] | None = None,
|
|
150
|
+
title: str | None = None,
|
|
151
|
+
reference_edges: dict[str, str] | None = None,
|
|
152
|
+
figsize: tuple[float, float] = (12, 6),
|
|
153
|
+
) -> tuple[Figure, Axes]:
|
|
154
|
+
"""Plot timing diagram with measurement annotations.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
signals: Dictionary mapping signal names to traces.
|
|
158
|
+
timing_params: Dict of {name: (start_time, end_time, label)}.
|
|
159
|
+
title: Optional plot title.
|
|
160
|
+
reference_edges: Dict mapping signal names to edge types.
|
|
161
|
+
figsize: Figure size.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Tuple of (figure, axes).
|
|
165
|
+
|
|
166
|
+
Example:
|
|
167
|
+
>>> signals = {"CLK": clk, "DATA": data}
|
|
168
|
+
>>> timing_params = {"t_su": (10e-9, 40e-9, "Setup = 30ns")}
|
|
169
|
+
>>> fig, ax = plot_timing_diagram_with_annotations(signals, timing_params=timing_params)
|
|
170
|
+
"""
|
|
171
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
172
|
+
|
|
173
|
+
# Plot signals
|
|
174
|
+
y_offset = 0
|
|
175
|
+
signal_positions = {}
|
|
176
|
+
|
|
177
|
+
for sig_name, trace in signals.items():
|
|
178
|
+
# Get time array
|
|
179
|
+
time_base = trace.metadata.time_base
|
|
180
|
+
time_array = np.arange(len(trace.data)) * time_base
|
|
181
|
+
data = np.asarray(trace.data)
|
|
182
|
+
|
|
183
|
+
# Convert to digital
|
|
184
|
+
if data.dtype == bool:
|
|
185
|
+
digital_data = data.astype(float)
|
|
186
|
+
else:
|
|
187
|
+
threshold = (np.max(data) + np.min(data)) / 2
|
|
188
|
+
digital_data = (data >= threshold).astype(float)
|
|
189
|
+
|
|
190
|
+
# Offset for stacked view
|
|
191
|
+
plot_data = digital_data + y_offset
|
|
192
|
+
|
|
193
|
+
# Plot with nice edges
|
|
194
|
+
ax.plot(time_array, plot_data, linewidth=2, color="royalblue")
|
|
195
|
+
ax.fill_between(time_array, y_offset, plot_data, alpha=0.2, color="royalblue")
|
|
196
|
+
|
|
197
|
+
# Add label
|
|
198
|
+
ax.text(
|
|
199
|
+
-time_array[-1] * 0.02,
|
|
200
|
+
y_offset + 0.5,
|
|
201
|
+
sig_name,
|
|
202
|
+
ha="right",
|
|
203
|
+
va="center",
|
|
204
|
+
fontweight="bold",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
signal_positions[sig_name] = y_offset
|
|
208
|
+
y_offset += 2
|
|
209
|
+
|
|
210
|
+
# Add timing annotations
|
|
211
|
+
if timing_params:
|
|
212
|
+
for t_start, t_end, label in timing_params.values():
|
|
213
|
+
_add_timing_annotation(ax, t_start, t_end, y_offset - 1, label)
|
|
214
|
+
|
|
215
|
+
# Style
|
|
216
|
+
ax.set_ylim(-0.5, y_offset + 0.5)
|
|
217
|
+
ax.set_xlabel("Time (s)", fontsize=12, fontweight="bold")
|
|
218
|
+
if title:
|
|
219
|
+
ax.set_title(title, fontsize=14, fontweight="bold")
|
|
220
|
+
|
|
221
|
+
ax.grid(True, alpha=0.3, linestyle="--")
|
|
222
|
+
ax.set_yticks([])
|
|
223
|
+
|
|
224
|
+
plt.tight_layout()
|
|
225
|
+
return fig, ax
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def plot_ic_timing_validation(
|
|
229
|
+
signals: dict[str, WaveformTrace | DigitalTrace],
|
|
230
|
+
ic_name: str,
|
|
231
|
+
measured_timings: dict[str, float],
|
|
232
|
+
*,
|
|
233
|
+
figsize: tuple[float, float] = (14, 8),
|
|
234
|
+
) -> tuple[Figure, Axes]:
|
|
235
|
+
"""Plot timing diagram with IC specification overlay.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
signals: Dictionary of signals.
|
|
239
|
+
ic_name: IC part number (e.g., "74LS74").
|
|
240
|
+
measured_timings: Measured timing parameters.
|
|
241
|
+
figsize: Figure size.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Tuple of (figure, axes).
|
|
245
|
+
|
|
246
|
+
Example:
|
|
247
|
+
>>> signals = {"CLK": clk, "D": data, "Q": output}
|
|
248
|
+
>>> measured = {"t_su": 25e-9, "t_h": 5e-9, "t_pd": 40e-9}
|
|
249
|
+
>>> fig, ax = plot_ic_timing_validation(signals, "74LS74", measured)
|
|
250
|
+
"""
|
|
251
|
+
from oscura.analyzers.digital.ic_database import validate_ic_timing
|
|
252
|
+
|
|
253
|
+
# Validate timings
|
|
254
|
+
validation = validate_ic_timing(ic_name, measured_timings)
|
|
255
|
+
|
|
256
|
+
# Plot timing diagram with custom figsize
|
|
257
|
+
fig, ax = plot_timing_diagram_with_annotations(
|
|
258
|
+
signals, title=f"{ic_name} Timing Validation", figsize=figsize
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Add validation results as text box
|
|
262
|
+
results_text = _format_validation_results(validation)
|
|
263
|
+
ax.text(
|
|
264
|
+
0.98,
|
|
265
|
+
0.98,
|
|
266
|
+
results_text,
|
|
267
|
+
transform=ax.transAxes,
|
|
268
|
+
fontsize=10,
|
|
269
|
+
verticalalignment="top",
|
|
270
|
+
horizontalalignment="right",
|
|
271
|
+
bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.8},
|
|
272
|
+
family="monospace",
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
return fig, ax
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def plot_multi_ic_timing_path(
|
|
279
|
+
ic_chain: list[tuple[str, dict[str, WaveformTrace | DigitalTrace]]],
|
|
280
|
+
*,
|
|
281
|
+
title: str = "Multi-IC Timing Path Analysis",
|
|
282
|
+
figsize: tuple[float, float] = (16, 10),
|
|
283
|
+
) -> tuple[Figure, Axes]:
|
|
284
|
+
"""Plot timing analysis for cascaded ICs.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
ic_chain: List of (ic_name, signals) tuples for each IC in chain.
|
|
288
|
+
title: Plot title.
|
|
289
|
+
figsize: Figure size.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Tuple of (figure, axes).
|
|
293
|
+
|
|
294
|
+
Example:
|
|
295
|
+
>>> chain = [
|
|
296
|
+
... ("74LS74", {"CLK": clk1, "Q": q1}),
|
|
297
|
+
... ("74LS00", {"A": q1, "Y": y1}),
|
|
298
|
+
... ("74LS74", {"D": y1, "Q": q2}),
|
|
299
|
+
... ]
|
|
300
|
+
>>> fig, ax = plot_multi_ic_timing_path(chain)
|
|
301
|
+
"""
|
|
302
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
303
|
+
|
|
304
|
+
y_offset = 0.0
|
|
305
|
+
|
|
306
|
+
for ic_name, signals in ic_chain:
|
|
307
|
+
# Plot this IC's signals
|
|
308
|
+
for sig_name, trace in signals.items():
|
|
309
|
+
time_base = trace.metadata.time_base
|
|
310
|
+
time_array = np.arange(len(trace.data)) * time_base
|
|
311
|
+
data = np.asarray(trace.data)
|
|
312
|
+
|
|
313
|
+
# Convert to digital
|
|
314
|
+
if data.dtype == bool:
|
|
315
|
+
digital_data = data.astype(float)
|
|
316
|
+
else:
|
|
317
|
+
threshold = (np.max(data) + np.min(data)) / 2
|
|
318
|
+
digital_data = (data >= threshold).astype(float)
|
|
319
|
+
|
|
320
|
+
# Plot
|
|
321
|
+
plot_data = digital_data + y_offset
|
|
322
|
+
ax.plot(time_array, plot_data, linewidth=2, label=f"{ic_name}.{sig_name}")
|
|
323
|
+
|
|
324
|
+
y_offset += 1.5
|
|
325
|
+
|
|
326
|
+
# Add IC boundary
|
|
327
|
+
ax.axhline(y_offset, color="gray", linestyle="--", alpha=0.5)
|
|
328
|
+
y_offset += 0.5
|
|
329
|
+
|
|
330
|
+
ax.set_xlabel("Time (s)", fontsize=12, fontweight="bold")
|
|
331
|
+
ax.set_title(title, fontsize=14, fontweight="bold")
|
|
332
|
+
ax.legend(loc="upper right")
|
|
333
|
+
ax.grid(True, alpha=0.3)
|
|
334
|
+
ax.set_yticks([])
|
|
335
|
+
|
|
336
|
+
plt.tight_layout()
|
|
337
|
+
return fig, ax
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def plot_bus_eye_diagram(
|
|
341
|
+
bus_traces: list[WaveformTrace],
|
|
342
|
+
*,
|
|
343
|
+
symbol_period: float,
|
|
344
|
+
num_symbols: int = 100,
|
|
345
|
+
title: str | None = None,
|
|
346
|
+
figsize: tuple[float, float] = (10, 6),
|
|
347
|
+
) -> tuple[Figure, Axes]:
|
|
348
|
+
"""Plot eye diagram for bus signal quality analysis.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
bus_traces: List of traces (one per bit).
|
|
352
|
+
symbol_period: Symbol period in seconds.
|
|
353
|
+
num_symbols: Number of symbols to overlay.
|
|
354
|
+
title: Optional plot title.
|
|
355
|
+
figsize: Figure size.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
Tuple of (figure, axes).
|
|
359
|
+
"""
|
|
360
|
+
fig, axes = plt.subplots(len(bus_traces), 1, figsize=figsize, sharex=True)
|
|
361
|
+
if len(bus_traces) == 1:
|
|
362
|
+
axes = [axes]
|
|
363
|
+
|
|
364
|
+
for idx, trace in enumerate(bus_traces):
|
|
365
|
+
ax = axes[idx]
|
|
366
|
+
|
|
367
|
+
# Extract eye diagram data
|
|
368
|
+
sample_rate = trace.metadata.sample_rate
|
|
369
|
+
samples_per_symbol = int(symbol_period * sample_rate)
|
|
370
|
+
|
|
371
|
+
data = np.asarray(trace.data)
|
|
372
|
+
|
|
373
|
+
# Overlay symbols
|
|
374
|
+
for i in range(num_symbols):
|
|
375
|
+
start = i * samples_per_symbol
|
|
376
|
+
end = start + samples_per_symbol * 2 # Two symbol periods
|
|
377
|
+
|
|
378
|
+
if end > len(data):
|
|
379
|
+
break
|
|
380
|
+
|
|
381
|
+
segment = data[start:end]
|
|
382
|
+
time_segment = np.arange(len(segment)) / sample_rate
|
|
383
|
+
|
|
384
|
+
ax.plot(time_segment * 1e9, segment, alpha=0.1, color="blue")
|
|
385
|
+
|
|
386
|
+
ax.set_ylabel(f"Bit {idx}", fontweight="bold")
|
|
387
|
+
ax.grid(True, alpha=0.3)
|
|
388
|
+
|
|
389
|
+
axes[-1].set_xlabel("Time (ns)", fontweight="bold")
|
|
390
|
+
|
|
391
|
+
if title:
|
|
392
|
+
fig.suptitle(title, fontsize=14, fontweight="bold")
|
|
393
|
+
|
|
394
|
+
plt.tight_layout()
|
|
395
|
+
return fig, axes
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
# =============================================================================
|
|
399
|
+
# Helper Functions
|
|
400
|
+
# =============================================================================
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _plot_digital_waveform(
|
|
404
|
+
ax: Axes, time: NDArray[np.float64], data: NDArray[np.float64], y_offset: float, label: str
|
|
405
|
+
) -> None:
|
|
406
|
+
"""Plot a single digital waveform."""
|
|
407
|
+
# Offset data
|
|
408
|
+
plot_data = data + y_offset
|
|
409
|
+
|
|
410
|
+
# Plot with thick lines
|
|
411
|
+
ax.plot(time, plot_data, linewidth=2, color="royalblue", drawstyle="steps-post")
|
|
412
|
+
|
|
413
|
+
# Fill area
|
|
414
|
+
ax.fill_between(time, y_offset, plot_data, alpha=0.2, color="royalblue", step="post")
|
|
415
|
+
|
|
416
|
+
# Add label
|
|
417
|
+
ax.text(
|
|
418
|
+
time[0] - (time[-1] - time[0]) * 0.02,
|
|
419
|
+
y_offset + 0.5,
|
|
420
|
+
label,
|
|
421
|
+
ha="right",
|
|
422
|
+
va="center",
|
|
423
|
+
fontweight="bold",
|
|
424
|
+
fontsize=10,
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _plot_bus_signal(
|
|
429
|
+
ax: Axes,
|
|
430
|
+
time: NDArray[np.float64],
|
|
431
|
+
values: NDArray[np.uint32],
|
|
432
|
+
y_offset: float,
|
|
433
|
+
label: str,
|
|
434
|
+
*,
|
|
435
|
+
show_hex: bool = True,
|
|
436
|
+
) -> None:
|
|
437
|
+
"""Plot a bus signal with hex value labels."""
|
|
438
|
+
# Plot as multi-level signal
|
|
439
|
+
plot_data = values.astype(float) / np.max(values) if np.max(values) > 0 else values
|
|
440
|
+
plot_data = plot_data + y_offset
|
|
441
|
+
|
|
442
|
+
ax.plot(time, plot_data, linewidth=2, color="green", drawstyle="steps-post")
|
|
443
|
+
ax.fill_between(time, y_offset, plot_data, alpha=0.2, color="green", step="post")
|
|
444
|
+
|
|
445
|
+
# Add bus label
|
|
446
|
+
ax.text(
|
|
447
|
+
time[0] - (time[-1] - time[0]) * 0.02,
|
|
448
|
+
y_offset + 0.5,
|
|
449
|
+
label,
|
|
450
|
+
ha="right",
|
|
451
|
+
va="center",
|
|
452
|
+
fontweight="bold",
|
|
453
|
+
fontsize=10,
|
|
454
|
+
color="green",
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
# Add hex values at transitions
|
|
458
|
+
if show_hex:
|
|
459
|
+
transitions = np.where(np.diff(values) != 0)[0]
|
|
460
|
+
for trans_idx in transitions[:10]: # Limit to first 10 transitions
|
|
461
|
+
value = values[trans_idx + 1]
|
|
462
|
+
ax.text(
|
|
463
|
+
time[trans_idx + 1],
|
|
464
|
+
y_offset + 0.5,
|
|
465
|
+
f"0x{value:02X}",
|
|
466
|
+
ha="left",
|
|
467
|
+
va="bottom",
|
|
468
|
+
fontsize=8,
|
|
469
|
+
color="green",
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _combine_bus_channels(
|
|
474
|
+
bus_channels: dict[str, WaveformTrace | DigitalTrace], start_idx: int, end_idx: int
|
|
475
|
+
) -> NDArray[np.uint32]:
|
|
476
|
+
"""Combine individual bus lines into values."""
|
|
477
|
+
# Sort channels by name (assume D0, D1, D2, etc.)
|
|
478
|
+
sorted_channels = sorted(bus_channels.items(), key=lambda x: x[0])
|
|
479
|
+
|
|
480
|
+
# Initialize result
|
|
481
|
+
sorted_channels[0][1]
|
|
482
|
+
num_samples = end_idx - start_idx
|
|
483
|
+
result = np.zeros(num_samples, dtype=np.uint32)
|
|
484
|
+
|
|
485
|
+
# Combine bits
|
|
486
|
+
for bit_idx, (_ch_name, trace) in enumerate(sorted_channels):
|
|
487
|
+
data = np.asarray(trace.data[start_idx:end_idx])
|
|
488
|
+
|
|
489
|
+
# Convert to digital
|
|
490
|
+
if data.dtype == bool:
|
|
491
|
+
digital_data = data.astype(np.uint32)
|
|
492
|
+
else:
|
|
493
|
+
threshold = (np.max(data) + np.min(data)) / 2
|
|
494
|
+
digital_data = (data >= threshold).astype(np.uint32)
|
|
495
|
+
|
|
496
|
+
result |= (digital_data << bit_idx).astype(np.uint32)
|
|
497
|
+
|
|
498
|
+
return result
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _format_time_axis(ax: Axes, t_start: float, t_end: float) -> None:
|
|
502
|
+
"""Format time axis with appropriate units."""
|
|
503
|
+
duration = t_end - t_start
|
|
504
|
+
|
|
505
|
+
# Get current tick locations
|
|
506
|
+
ticks = ax.get_xticks()
|
|
507
|
+
|
|
508
|
+
if duration < 1e-6: # Nanoseconds
|
|
509
|
+
ax.set_xlabel("Time (ns)", fontweight="bold")
|
|
510
|
+
ax.set_xticks(ticks) # Set ticks before labels
|
|
511
|
+
ax.set_xticklabels([f"{t * 1e9:.1f}" for t in ticks])
|
|
512
|
+
elif duration < 1e-3: # Microseconds
|
|
513
|
+
ax.set_xlabel("Time (μs)", fontweight="bold")
|
|
514
|
+
ax.set_xticks(ticks) # Set ticks before labels
|
|
515
|
+
ax.set_xticklabels([f"{t * 1e6:.1f}" for t in ticks])
|
|
516
|
+
elif duration < 1: # Milliseconds
|
|
517
|
+
ax.set_xlabel("Time (ms)", fontweight="bold")
|
|
518
|
+
ax.set_xticks(ticks) # Set ticks before labels
|
|
519
|
+
ax.set_xticklabels([f"{t * 1e3:.1f}" for t in ticks])
|
|
520
|
+
else: # Seconds
|
|
521
|
+
ax.set_xlabel("Time (s)", fontweight="bold")
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _add_timing_cursors(ax: Axes, t_start: float, t_end: float, y_max: float) -> None:
|
|
525
|
+
"""Add timing measurement cursors."""
|
|
526
|
+
# Add two cursors at 1/4 and 3/4 of time range
|
|
527
|
+
cursor1_time = t_start + (t_end - t_start) * 0.25
|
|
528
|
+
cursor2_time = t_start + (t_end - t_start) * 0.75
|
|
529
|
+
|
|
530
|
+
ax.axvline(cursor1_time, color="red", linestyle="--", alpha=0.7, linewidth=1.5)
|
|
531
|
+
ax.axvline(cursor2_time, color="red", linestyle="--", alpha=0.7, linewidth=1.5)
|
|
532
|
+
|
|
533
|
+
# Add delta time label
|
|
534
|
+
delta_t = cursor2_time - cursor1_time
|
|
535
|
+
mid_y = y_max / 2
|
|
536
|
+
|
|
537
|
+
ax.annotate(
|
|
538
|
+
"",
|
|
539
|
+
xy=(cursor2_time, mid_y),
|
|
540
|
+
xytext=(cursor1_time, mid_y),
|
|
541
|
+
arrowprops={"arrowstyle": "<->", "color": "red", "lw": 2},
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
ax.text(
|
|
545
|
+
(cursor1_time + cursor2_time) / 2,
|
|
546
|
+
mid_y + 0.3,
|
|
547
|
+
f"Δt = {delta_t * 1e9:.1f} ns",
|
|
548
|
+
ha="center",
|
|
549
|
+
fontweight="bold",
|
|
550
|
+
bbox={"boxstyle": "round", "facecolor": "yellow", "alpha": 0.8},
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _add_timing_annotation(
|
|
555
|
+
ax: Axes, t_start: float, t_end: float, y_pos: float, label: str
|
|
556
|
+
) -> None:
|
|
557
|
+
"""Add timing measurement annotation."""
|
|
558
|
+
ax.annotate(
|
|
559
|
+
"",
|
|
560
|
+
xy=(t_end, y_pos),
|
|
561
|
+
xytext=(t_start, y_pos),
|
|
562
|
+
arrowprops={"arrowstyle": "<->", "color": "red", "lw": 2},
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
ax.text(
|
|
566
|
+
(t_start + t_end) / 2,
|
|
567
|
+
y_pos + 0.3,
|
|
568
|
+
label,
|
|
569
|
+
ha="center",
|
|
570
|
+
fontweight="bold",
|
|
571
|
+
bbox={"boxstyle": "round", "facecolor": "yellow", "alpha": 0.8},
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def _format_validation_results(validation: dict[str, dict[str, Any]]) -> str:
|
|
576
|
+
"""Format IC timing validation results."""
|
|
577
|
+
lines = ["Timing Validation:\n"]
|
|
578
|
+
|
|
579
|
+
for param, result in validation.items():
|
|
580
|
+
passes = result.get("passes")
|
|
581
|
+
if passes is None:
|
|
582
|
+
continue
|
|
583
|
+
|
|
584
|
+
measured = result["measured"]
|
|
585
|
+
spec = result["spec"]
|
|
586
|
+
error = result["error"]
|
|
587
|
+
|
|
588
|
+
status = "✓ PASS" if passes else "✗ FAIL"
|
|
589
|
+
lines.append(
|
|
590
|
+
f"{param}: {status}\n Measured: {measured * 1e9:.1f}ns\n Spec: {spec * 1e9:.1f}ns\n Error: {error * 100:.1f}%\n"
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
return "".join(lines)
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def generate_all_vintage_logic_plots(
|
|
597
|
+
result: Any,
|
|
598
|
+
traces: dict[str, WaveformTrace | DigitalTrace],
|
|
599
|
+
*,
|
|
600
|
+
output_dir: str | Path | None = None,
|
|
601
|
+
save_formats: list[str] | None = None,
|
|
602
|
+
) -> dict[str, tuple[Figure, Axes] | Figure]:
|
|
603
|
+
"""Generate complete visualization suite for vintage logic analysis.
|
|
604
|
+
|
|
605
|
+
Creates all relevant plots based on analysis results. Optionally saves
|
|
606
|
+
figures to disk in multiple formats.
|
|
607
|
+
|
|
608
|
+
Args:
|
|
609
|
+
result: VintageLogicAnalysisResult object.
|
|
610
|
+
traces: Dictionary of channel names to traces.
|
|
611
|
+
output_dir: If provided, saves all figures to this directory.
|
|
612
|
+
save_formats: Formats to save ("png", "svg", "pdf"). Default: ["png"].
|
|
613
|
+
|
|
614
|
+
Returns:
|
|
615
|
+
Dictionary mapping plot names to Figure/Axes tuples.
|
|
616
|
+
|
|
617
|
+
Example:
|
|
618
|
+
>>> from oscura.analyzers.digital.vintage import analyze_vintage_logic
|
|
619
|
+
>>> result = analyze_vintage_logic(traces)
|
|
620
|
+
>>> plots = generate_all_vintage_logic_plots(result, traces, output_dir="./plots")
|
|
621
|
+
>>> # plots = {
|
|
622
|
+
>>> # "logic_analyzer": (fig, ax),
|
|
623
|
+
>>> # "timing_validation": (fig, ax),
|
|
624
|
+
>>> # ...
|
|
625
|
+
>>> # }
|
|
626
|
+
"""
|
|
627
|
+
from oscura.visualization.figure_manager import FigureManager
|
|
628
|
+
|
|
629
|
+
plots: dict[str, tuple[Figure, Axes] | Figure] = {}
|
|
630
|
+
|
|
631
|
+
# Initialize figure manager if output directory provided
|
|
632
|
+
if output_dir:
|
|
633
|
+
output_path = Path(output_dir)
|
|
634
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
635
|
+
fig_manager = FigureManager(output_path)
|
|
636
|
+
if save_formats is None:
|
|
637
|
+
save_formats = ["png"]
|
|
638
|
+
else:
|
|
639
|
+
fig_manager = None
|
|
640
|
+
save_formats = []
|
|
641
|
+
|
|
642
|
+
# 1. Logic analyzer view
|
|
643
|
+
try:
|
|
644
|
+
fig, ax = plot_logic_analyzer_view(
|
|
645
|
+
traces,
|
|
646
|
+
title=f"Logic Analyzer View - {result.detected_family}",
|
|
647
|
+
show_cursors=True,
|
|
648
|
+
)
|
|
649
|
+
plots["logic_analyzer"] = (fig, ax)
|
|
650
|
+
if fig_manager:
|
|
651
|
+
fig_manager.save_figure(fig, "logic_analyzer", formats=save_formats)
|
|
652
|
+
except Exception:
|
|
653
|
+
pass # Skip if plot fails
|
|
654
|
+
|
|
655
|
+
# 2. IC timing validation plots for each identified IC
|
|
656
|
+
for idx, ic_result in enumerate(result.identified_ics):
|
|
657
|
+
try:
|
|
658
|
+
# Create signals dictionary for timing validation
|
|
659
|
+
fig, ax = plot_ic_timing_validation(
|
|
660
|
+
signals=traces,
|
|
661
|
+
ic_name=ic_result.ic_name,
|
|
662
|
+
measured_timings=ic_result.timing_params,
|
|
663
|
+
)
|
|
664
|
+
plot_name = f"timing_validation_{ic_result.ic_name}_{idx}"
|
|
665
|
+
plots[plot_name] = (fig, ax)
|
|
666
|
+
if fig_manager:
|
|
667
|
+
fig_manager.save_figure(fig, plot_name, formats=save_formats)
|
|
668
|
+
except Exception:
|
|
669
|
+
pass # Skip if plot fails
|
|
670
|
+
|
|
671
|
+
# 3. Multi-IC timing path visualization
|
|
672
|
+
if result.timing_paths:
|
|
673
|
+
for idx, path_result in enumerate(result.timing_paths):
|
|
674
|
+
try:
|
|
675
|
+
fig, ax = plot_multi_ic_timing_path(path_result)
|
|
676
|
+
plot_name = f"timing_path_{idx}"
|
|
677
|
+
plots[plot_name] = (fig, ax)
|
|
678
|
+
if fig_manager:
|
|
679
|
+
fig_manager.save_figure(fig, plot_name, formats=save_formats)
|
|
680
|
+
except Exception:
|
|
681
|
+
pass # Skip if plot fails
|
|
682
|
+
|
|
683
|
+
# 4. Timing diagram with annotations (for first 2-3 channels)
|
|
684
|
+
if len(traces) >= 2:
|
|
685
|
+
try:
|
|
686
|
+
# Select first 2-3 channels
|
|
687
|
+
selected_traces = dict(list(traces.items())[: min(3, len(traces))])
|
|
688
|
+
|
|
689
|
+
# Create timing annotations from measurements
|
|
690
|
+
timing_params: dict[str, tuple[float, float, str]] = {}
|
|
691
|
+
for key, value in result.timing_measurements.items():
|
|
692
|
+
# Extract channel names from key like "CLK→DATA_t_pd"
|
|
693
|
+
if "→" in key:
|
|
694
|
+
label = key.split("_")[-1] # Get timing parameter
|
|
695
|
+
timing_params[label] = (0.0, float(value), label)
|
|
696
|
+
|
|
697
|
+
fig, ax = plot_timing_diagram_with_annotations(
|
|
698
|
+
selected_traces,
|
|
699
|
+
timing_params=timing_params or None,
|
|
700
|
+
title="Timing Diagram with Measurements",
|
|
701
|
+
)
|
|
702
|
+
plots["timing_diagram"] = (fig, ax)
|
|
703
|
+
if fig_manager:
|
|
704
|
+
fig_manager.save_figure(fig, "timing_diagram", formats=save_formats)
|
|
705
|
+
except Exception:
|
|
706
|
+
pass # Skip if plot fails
|
|
707
|
+
|
|
708
|
+
return plots
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
__all__ = [
|
|
712
|
+
"generate_all_vintage_logic_plots",
|
|
713
|
+
"plot_bus_eye_diagram",
|
|
714
|
+
"plot_ic_timing_validation",
|
|
715
|
+
"plot_logic_analyzer_view",
|
|
716
|
+
"plot_multi_ic_timing_path",
|
|
717
|
+
"plot_timing_diagram_with_annotations",
|
|
718
|
+
]
|