oscura 0.8.0__py3-none-any.whl → 0.11.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 +19 -19
- oscura/__main__.py +4 -0
- oscura/analyzers/__init__.py +2 -0
- oscura/analyzers/digital/extraction.py +2 -3
- oscura/analyzers/digital/quality.py +1 -1
- oscura/analyzers/digital/timing.py +1 -1
- oscura/analyzers/ml/signal_classifier.py +6 -0
- oscura/analyzers/patterns/__init__.py +66 -0
- oscura/analyzers/power/basic.py +3 -3
- oscura/analyzers/power/soa.py +1 -1
- oscura/analyzers/power/switching.py +3 -3
- oscura/analyzers/signal_classification.py +529 -0
- oscura/analyzers/signal_integrity/sparams.py +3 -3
- oscura/analyzers/statistics/basic.py +10 -7
- oscura/analyzers/validation.py +1 -1
- oscura/analyzers/waveform/measurements.py +200 -156
- oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
- oscura/analyzers/waveform/spectral.py +182 -84
- oscura/api/dsl/commands.py +15 -6
- oscura/api/server/templates/base.html +137 -146
- oscura/api/server/templates/export.html +84 -110
- oscura/api/server/templates/home.html +248 -267
- oscura/api/server/templates/protocols.html +44 -48
- oscura/api/server/templates/reports.html +27 -35
- oscura/api/server/templates/session_detail.html +68 -78
- oscura/api/server/templates/sessions.html +62 -72
- oscura/api/server/templates/waveforms.html +54 -64
- oscura/automotive/__init__.py +1 -1
- oscura/automotive/can/session.py +1 -1
- oscura/automotive/dbc/generator.py +638 -23
- oscura/automotive/dtc/data.json +17 -102
- oscura/automotive/flexray/fibex.py +9 -1
- oscura/automotive/uds/decoder.py +99 -6
- oscura/cli/analyze.py +8 -2
- oscura/cli/batch.py +36 -5
- oscura/cli/characterize.py +18 -4
- oscura/cli/export.py +47 -5
- oscura/cli/main.py +2 -0
- oscura/cli/onboarding/wizard.py +10 -6
- oscura/cli/pipeline.py +585 -0
- oscura/cli/visualize.py +6 -4
- oscura/convenience.py +400 -32
- oscura/core/measurement_result.py +286 -0
- oscura/core/progress.py +1 -1
- oscura/core/schemas/device_mapping.json +2 -8
- oscura/core/schemas/packet_format.json +4 -24
- oscura/core/schemas/protocol_definition.json +2 -12
- oscura/core/types.py +232 -239
- oscura/correlation/multi_protocol.py +1 -1
- oscura/export/legacy/__init__.py +11 -0
- oscura/export/legacy/wav.py +75 -0
- oscura/exporters/__init__.py +19 -0
- oscura/exporters/wireshark.py +809 -0
- oscura/hardware/acquisition/file.py +5 -19
- oscura/hardware/acquisition/saleae.py +10 -10
- oscura/hardware/acquisition/socketcan.py +4 -6
- oscura/hardware/acquisition/synthetic.py +1 -5
- oscura/hardware/acquisition/visa.py +6 -6
- oscura/hardware/security/side_channel_detector.py +5 -508
- oscura/inference/message_format.py +686 -1
- oscura/jupyter/display.py +2 -2
- oscura/jupyter/magic.py +3 -3
- oscura/loaders/__init__.py +17 -12
- oscura/loaders/binary.py +1 -1
- oscura/loaders/chipwhisperer.py +1 -2
- oscura/loaders/configurable.py +1 -1
- oscura/loaders/csv_loader.py +2 -2
- oscura/loaders/hdf5_loader.py +1 -1
- oscura/loaders/lazy.py +6 -1
- oscura/loaders/mmap_loader.py +0 -1
- oscura/loaders/numpy_loader.py +8 -7
- oscura/loaders/preprocessing.py +3 -5
- oscura/loaders/rigol.py +21 -7
- oscura/loaders/sigrok.py +2 -5
- oscura/loaders/tdms.py +3 -2
- oscura/loaders/tektronix.py +38 -32
- oscura/loaders/tss.py +20 -27
- oscura/loaders/validation.py +17 -10
- oscura/loaders/vcd.py +13 -8
- oscura/loaders/wav.py +1 -6
- oscura/pipeline/__init__.py +76 -0
- oscura/pipeline/handlers/__init__.py +165 -0
- oscura/pipeline/handlers/analyzers.py +1045 -0
- oscura/pipeline/handlers/decoders.py +899 -0
- oscura/pipeline/handlers/exporters.py +1103 -0
- oscura/pipeline/handlers/filters.py +891 -0
- oscura/pipeline/handlers/loaders.py +640 -0
- oscura/pipeline/handlers/transforms.py +768 -0
- oscura/reporting/formatting/measurements.py +55 -14
- oscura/reporting/templates/enhanced/protocol_re.html +504 -503
- oscura/sessions/legacy.py +49 -1
- oscura/side_channel/__init__.py +38 -57
- oscura/utils/builders/signal_builder.py +5 -5
- oscura/utils/comparison/compare.py +7 -9
- oscura/utils/comparison/golden.py +1 -1
- oscura/utils/filtering/convenience.py +2 -2
- oscura/utils/math/arithmetic.py +38 -62
- oscura/utils/math/interpolation.py +20 -20
- oscura/utils/pipeline/__init__.py +4 -17
- oscura/utils/progressive.py +1 -4
- oscura/utils/triggering/edge.py +1 -1
- oscura/utils/triggering/pattern.py +2 -2
- oscura/utils/triggering/pulse.py +2 -2
- oscura/utils/triggering/window.py +3 -3
- oscura/validation/hil_testing.py +11 -11
- oscura/visualization/__init__.py +46 -284
- oscura/visualization/batch.py +72 -433
- oscura/visualization/plot.py +542 -53
- oscura/visualization/styles.py +184 -318
- oscura/workflows/batch/advanced.py +1 -1
- oscura/workflows/batch/aggregate.py +12 -9
- oscura/workflows/complete_re.py +251 -23
- oscura/workflows/digital.py +27 -4
- oscura/workflows/multi_trace.py +136 -17
- oscura/workflows/waveform.py +11 -6
- oscura-0.11.0.dist-info/METADATA +460 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/RECORD +120 -145
- oscura/side_channel/dpa.py +0 -1025
- oscura/utils/optimization/__init__.py +0 -19
- oscura/utils/optimization/parallel.py +0 -443
- oscura/utils/optimization/search.py +0 -532
- oscura/utils/pipeline/base.py +0 -338
- oscura/utils/pipeline/composition.py +0 -248
- oscura/utils/pipeline/parallel.py +0 -449
- oscura/utils/pipeline/pipeline.py +0 -375
- oscura/utils/search/__init__.py +0 -16
- oscura/utils/search/anomaly.py +0 -424
- oscura/utils/search/context.py +0 -294
- oscura/utils/search/pattern.py +0 -288
- oscura/utils/storage/__init__.py +0 -61
- oscura/utils/storage/database.py +0 -1166
- oscura/visualization/accessibility.py +0 -526
- oscura/visualization/annotations.py +0 -371
- oscura/visualization/axis_scaling.py +0 -305
- oscura/visualization/colors.py +0 -451
- oscura/visualization/digital.py +0 -436
- oscura/visualization/eye.py +0 -571
- oscura/visualization/histogram.py +0 -281
- oscura/visualization/interactive.py +0 -1035
- oscura/visualization/jitter.py +0 -1042
- oscura/visualization/keyboard.py +0 -394
- oscura/visualization/layout.py +0 -400
- oscura/visualization/optimization.py +0 -1079
- oscura/visualization/palettes.py +0 -446
- oscura/visualization/power.py +0 -508
- oscura/visualization/power_extended.py +0 -955
- oscura/visualization/presets.py +0 -469
- oscura/visualization/protocols.py +0 -1246
- oscura/visualization/render.py +0 -223
- oscura/visualization/rendering.py +0 -444
- oscura/visualization/reverse_engineering.py +0 -838
- oscura/visualization/signal_integrity.py +0 -989
- oscura/visualization/specialized.py +0 -643
- oscura/visualization/spectral.py +0 -1226
- oscura/visualization/thumbnails.py +0 -340
- oscura/visualization/time_axis.py +0 -351
- oscura/visualization/waveform.py +0 -454
- oscura-0.8.0.dist-info/METADATA +0 -661
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/WHEEL +0 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,643 +0,0 @@
|
|
|
1
|
-
"""Specialized plot types for protocol analysis and state visualization.
|
|
2
|
-
|
|
3
|
-
This module provides specialized visualizations including protocol timing
|
|
4
|
-
diagrams, state machine views, and bus transaction timelines.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Example:
|
|
8
|
-
>>> from oscura.visualization.specialized import plot_protocol_timing
|
|
9
|
-
>>> fig = plot_protocol_timing(decoded_packets, sample_rate=1e6)
|
|
10
|
-
|
|
11
|
-
References:
|
|
12
|
-
- Wavedrom-style digital waveform rendering
|
|
13
|
-
- State machine diagram standards
|
|
14
|
-
- Bus protocol visualization best practices
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
from __future__ import annotations
|
|
18
|
-
|
|
19
|
-
from dataclasses import dataclass
|
|
20
|
-
from typing import TYPE_CHECKING, Any, Literal
|
|
21
|
-
|
|
22
|
-
import numpy as np
|
|
23
|
-
|
|
24
|
-
if TYPE_CHECKING:
|
|
25
|
-
from matplotlib.axes import Axes
|
|
26
|
-
from matplotlib.figure import Figure
|
|
27
|
-
from numpy.typing import NDArray
|
|
28
|
-
|
|
29
|
-
try:
|
|
30
|
-
import matplotlib.pyplot as plt
|
|
31
|
-
from matplotlib import patches
|
|
32
|
-
|
|
33
|
-
HAS_MATPLOTLIB = True
|
|
34
|
-
except ImportError:
|
|
35
|
-
HAS_MATPLOTLIB = False
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
@dataclass
|
|
39
|
-
class ProtocolSignal:
|
|
40
|
-
"""Protocol signal for timing diagram.
|
|
41
|
-
|
|
42
|
-
Attributes:
|
|
43
|
-
name: Signal name
|
|
44
|
-
data: Signal data (0/1 for digital, values for analog)
|
|
45
|
-
type: Signal type ("digital", "clock", "bus", "analog")
|
|
46
|
-
transitions: List of transition times
|
|
47
|
-
annotations: Dict of time -> annotation text
|
|
48
|
-
"""
|
|
49
|
-
|
|
50
|
-
name: str
|
|
51
|
-
data: NDArray[np.float64]
|
|
52
|
-
type: Literal["digital", "clock", "bus", "analog"] = "digital"
|
|
53
|
-
transitions: list[float] | None = None
|
|
54
|
-
annotations: dict[float, str] | None = None
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
@dataclass
|
|
58
|
-
class StateTransition:
|
|
59
|
-
"""State machine transition.
|
|
60
|
-
|
|
61
|
-
Attributes:
|
|
62
|
-
from_state: Source state name
|
|
63
|
-
to_state: Target state name
|
|
64
|
-
condition: Transition condition/label
|
|
65
|
-
style: Line style ("solid", "dashed", "dotted")
|
|
66
|
-
"""
|
|
67
|
-
|
|
68
|
-
from_state: str
|
|
69
|
-
to_state: str
|
|
70
|
-
condition: str = ""
|
|
71
|
-
style: Literal["solid", "dashed", "dotted"] = "solid"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def plot_protocol_timing(
|
|
75
|
-
signals: list[ProtocolSignal],
|
|
76
|
-
sample_rate: float,
|
|
77
|
-
*,
|
|
78
|
-
time_range: tuple[float, float] | None = None,
|
|
79
|
-
time_unit: str = "auto",
|
|
80
|
-
style: Literal["wavedrom", "classic"] = "wavedrom",
|
|
81
|
-
figsize: tuple[float, float] | None = None,
|
|
82
|
-
title: str | None = None,
|
|
83
|
-
) -> Figure:
|
|
84
|
-
"""Plot protocol timing diagram in wavedrom style.
|
|
85
|
-
|
|
86
|
-
Creates a timing diagram showing digital signals, clock edges, and
|
|
87
|
-
bus transactions with annotations for protocol events.
|
|
88
|
-
|
|
89
|
-
Args:
|
|
90
|
-
signals: List of ProtocolSignal objects to plot.
|
|
91
|
-
sample_rate: Sample rate in Hz.
|
|
92
|
-
time_range: Time range to plot (t_min, t_max) in seconds. None = full range.
|
|
93
|
-
time_unit: Time unit for x-axis ("s", "ms", "us", "ns", "auto").
|
|
94
|
-
style: Diagram style ("wavedrom" = clean digital, "classic" = traditional).
|
|
95
|
-
figsize: Figure size (width, height). Auto-calculated if None.
|
|
96
|
-
title: Plot title.
|
|
97
|
-
|
|
98
|
-
Returns:
|
|
99
|
-
Matplotlib Figure object.
|
|
100
|
-
|
|
101
|
-
Raises:
|
|
102
|
-
ImportError: If matplotlib is not available.
|
|
103
|
-
ValueError: If signals list is empty.
|
|
104
|
-
|
|
105
|
-
Example:
|
|
106
|
-
>>> sda = ProtocolSignal("SDA", sda_data, type="digital")
|
|
107
|
-
>>> scl = ProtocolSignal("SCL", scl_data, type="clock")
|
|
108
|
-
>>> fig = plot_protocol_timing(
|
|
109
|
-
... [scl, sda],
|
|
110
|
-
... sample_rate=1e6,
|
|
111
|
-
... style="wavedrom",
|
|
112
|
-
... title="I2C Transaction"
|
|
113
|
-
... )
|
|
114
|
-
|
|
115
|
-
References:
|
|
116
|
-
VIS-021: Specialized - Protocol Timing Diagram
|
|
117
|
-
Wavedrom digital waveform rendering
|
|
118
|
-
"""
|
|
119
|
-
if not HAS_MATPLOTLIB:
|
|
120
|
-
raise ImportError("matplotlib is required for visualization")
|
|
121
|
-
|
|
122
|
-
if len(signals) == 0:
|
|
123
|
-
raise ValueError("signals list cannot be empty")
|
|
124
|
-
|
|
125
|
-
# Setup figure and axes
|
|
126
|
-
fig, axes = _setup_timing_figure(signals, figsize)
|
|
127
|
-
|
|
128
|
-
# Determine time parameters
|
|
129
|
-
t_min, t_max = _determine_time_range(signals, sample_rate, time_range)
|
|
130
|
-
time_unit_final, time_mult = _select_timing_unit(time_unit, t_min, t_max)
|
|
131
|
-
|
|
132
|
-
# Plot each signal
|
|
133
|
-
_plot_all_signals(axes, signals, sample_rate, t_min, t_max, time_mult, style)
|
|
134
|
-
|
|
135
|
-
# Finalize plot
|
|
136
|
-
_finalize_timing_plot(fig, axes, time_unit_final, title)
|
|
137
|
-
|
|
138
|
-
return fig
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def _setup_timing_figure(
|
|
142
|
-
signals: list[ProtocolSignal], figsize: tuple[float, float] | None
|
|
143
|
-
) -> tuple[Figure, list[Any]]:
|
|
144
|
-
"""Setup figure and axes for timing diagram."""
|
|
145
|
-
n_signals = len(signals)
|
|
146
|
-
|
|
147
|
-
if figsize is None:
|
|
148
|
-
width = 12
|
|
149
|
-
height = max(4, n_signals * 0.8 + 1)
|
|
150
|
-
figsize = (width, height)
|
|
151
|
-
|
|
152
|
-
fig, axes = plt.subplots(
|
|
153
|
-
n_signals,
|
|
154
|
-
1,
|
|
155
|
-
figsize=figsize,
|
|
156
|
-
sharex=True,
|
|
157
|
-
gridspec_kw={"hspace": 0.1},
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
if n_signals == 1:
|
|
161
|
-
axes = [axes]
|
|
162
|
-
|
|
163
|
-
return fig, axes
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def _determine_time_range(
|
|
167
|
-
signals: list[ProtocolSignal],
|
|
168
|
-
sample_rate: float,
|
|
169
|
-
time_range: tuple[float, float] | None,
|
|
170
|
-
) -> tuple[float, float]:
|
|
171
|
-
"""Determine time range for plotting."""
|
|
172
|
-
if time_range is not None:
|
|
173
|
-
return time_range
|
|
174
|
-
|
|
175
|
-
max_len = max(len(sig.data) for sig in signals)
|
|
176
|
-
return 0.0, max_len / sample_rate
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def _select_timing_unit(time_unit: str, t_min: float, t_max: float) -> tuple[str, float]:
|
|
180
|
-
"""Select appropriate time unit and multiplier."""
|
|
181
|
-
if time_unit == "auto":
|
|
182
|
-
time_range_val = t_max - t_min
|
|
183
|
-
if time_range_val < 1e-6:
|
|
184
|
-
return "ns", 1e9
|
|
185
|
-
elif time_range_val < 1e-3:
|
|
186
|
-
return "us", 1e6
|
|
187
|
-
elif time_range_val < 1:
|
|
188
|
-
return "ms", 1e3
|
|
189
|
-
else:
|
|
190
|
-
return "s", 1.0
|
|
191
|
-
else:
|
|
192
|
-
time_mult = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}.get(time_unit, 1.0)
|
|
193
|
-
return time_unit, time_mult
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def _plot_all_signals(
|
|
197
|
-
axes: list[Any],
|
|
198
|
-
signals: list[ProtocolSignal],
|
|
199
|
-
sample_rate: float,
|
|
200
|
-
t_min: float,
|
|
201
|
-
t_max: float,
|
|
202
|
-
time_mult: float,
|
|
203
|
-
style: str,
|
|
204
|
-
) -> None:
|
|
205
|
-
"""Plot all signals on their respective axes."""
|
|
206
|
-
for _idx, (signal, ax) in enumerate(zip(signals, axes, strict=False)):
|
|
207
|
-
time = np.arange(len(signal.data)) / sample_rate * time_mult
|
|
208
|
-
|
|
209
|
-
# Filter to time range
|
|
210
|
-
mask = (time >= t_min * time_mult) & (time <= t_max * time_mult)
|
|
211
|
-
time_filtered = time[mask]
|
|
212
|
-
data_filtered = signal.data[mask]
|
|
213
|
-
|
|
214
|
-
# Plot signal
|
|
215
|
-
if style == "wavedrom":
|
|
216
|
-
_plot_wavedrom_signal(ax, time_filtered, data_filtered, signal)
|
|
217
|
-
else:
|
|
218
|
-
_plot_classic_signal(ax, time_filtered, data_filtered, signal)
|
|
219
|
-
|
|
220
|
-
# Format axes
|
|
221
|
-
_format_signal_axes(ax, signal.name)
|
|
222
|
-
|
|
223
|
-
# Add annotations
|
|
224
|
-
_add_signal_annotations(ax, signal, t_min, t_max, time_mult)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
def _format_signal_axes(ax: Axes, signal_name: str) -> None:
|
|
228
|
-
"""Format axes for a single signal."""
|
|
229
|
-
ax.set_ylabel(signal_name, rotation=0, ha="right", va="center", fontsize=10)
|
|
230
|
-
ax.set_ylim(-0.2, 1.3)
|
|
231
|
-
ax.set_yticks([])
|
|
232
|
-
ax.grid(True, axis="x", alpha=0.3, linestyle=":")
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
def _add_signal_annotations(
|
|
236
|
-
ax: Axes,
|
|
237
|
-
signal: ProtocolSignal,
|
|
238
|
-
t_min: float,
|
|
239
|
-
t_max: float,
|
|
240
|
-
time_mult: float,
|
|
241
|
-
) -> None:
|
|
242
|
-
"""Add annotations to signal plot."""
|
|
243
|
-
if signal.annotations:
|
|
244
|
-
for t, text in signal.annotations.items():
|
|
245
|
-
if t_min <= t <= t_max:
|
|
246
|
-
ax.annotate(
|
|
247
|
-
text,
|
|
248
|
-
xy=(t * time_mult, 1.2),
|
|
249
|
-
fontsize=8,
|
|
250
|
-
ha="center",
|
|
251
|
-
bbox={"boxstyle": "round,pad=0.3", "facecolor": "yellow", "alpha": 0.7},
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
def _finalize_timing_plot(fig: Figure, axes: list[Any], time_unit: str, title: str | None) -> None:
|
|
256
|
-
"""Finalize timing plot with labels and title."""
|
|
257
|
-
axes[-1].set_xlabel(f"Time ({time_unit})", fontsize=11)
|
|
258
|
-
|
|
259
|
-
if title:
|
|
260
|
-
fig.suptitle(title, fontsize=14, y=0.98)
|
|
261
|
-
|
|
262
|
-
fig.tight_layout()
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
def _plot_wavedrom_signal(
|
|
266
|
-
ax: Axes,
|
|
267
|
-
time: NDArray[np.float64],
|
|
268
|
-
data: NDArray[np.float64],
|
|
269
|
-
signal: ProtocolSignal,
|
|
270
|
-
) -> None:
|
|
271
|
-
"""Plot signal in wavedrom style (clean digital waveform)."""
|
|
272
|
-
if signal.type == "clock":
|
|
273
|
-
# Clock signal: square wave
|
|
274
|
-
for i in range(len(time) - 1):
|
|
275
|
-
level = 1 if data[i] > 0.5 else 0
|
|
276
|
-
ax.plot(
|
|
277
|
-
[time[i], time[i + 1]],
|
|
278
|
-
[level, level],
|
|
279
|
-
"b-",
|
|
280
|
-
linewidth=1.5,
|
|
281
|
-
)
|
|
282
|
-
# Vertical transition
|
|
283
|
-
if i < len(time) - 1:
|
|
284
|
-
next_level = 1 if data[i + 1] > 0.5 else 0
|
|
285
|
-
if level != next_level:
|
|
286
|
-
ax.plot(
|
|
287
|
-
[time[i + 1], time[i + 1]],
|
|
288
|
-
[level, next_level],
|
|
289
|
-
"b-",
|
|
290
|
-
linewidth=1.5,
|
|
291
|
-
)
|
|
292
|
-
|
|
293
|
-
elif signal.type == "digital":
|
|
294
|
-
# Digital signal: step function with transitions
|
|
295
|
-
for i in range(len(time) - 1):
|
|
296
|
-
level = 1 if data[i] > 0.5 else 0
|
|
297
|
-
ax.plot(
|
|
298
|
-
[time[i], time[i + 1]],
|
|
299
|
-
[level, level],
|
|
300
|
-
"k-",
|
|
301
|
-
linewidth=1.5,
|
|
302
|
-
)
|
|
303
|
-
# Vertical transition with slight slant for visual clarity
|
|
304
|
-
if i < len(time) - 1:
|
|
305
|
-
next_level = 1 if data[i + 1] > 0.5 else 0
|
|
306
|
-
if level != next_level:
|
|
307
|
-
transition_width = (time[i + 1] - time[i]) * 0.1
|
|
308
|
-
ax.plot(
|
|
309
|
-
[time[i + 1] - transition_width, time[i + 1]],
|
|
310
|
-
[level, next_level],
|
|
311
|
-
"k-",
|
|
312
|
-
linewidth=1.5,
|
|
313
|
-
)
|
|
314
|
-
|
|
315
|
-
elif signal.type == "bus":
|
|
316
|
-
# Bus signal: show as high-impedance or data values
|
|
317
|
-
ax.fill_between(time, 0.3, 0.7, alpha=0.3, color="gray")
|
|
318
|
-
ax.plot(time, np.full_like(time, 0.5), "k-", linewidth=0.5)
|
|
319
|
-
|
|
320
|
-
else:
|
|
321
|
-
# Analog signal
|
|
322
|
-
ax.plot(time, data, "r-", linewidth=1.2)
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
def _plot_classic_signal(
|
|
326
|
-
ax: Axes,
|
|
327
|
-
time: NDArray[np.float64],
|
|
328
|
-
data: NDArray[np.float64],
|
|
329
|
-
signal: ProtocolSignal,
|
|
330
|
-
) -> None:
|
|
331
|
-
"""Plot signal in classic style (traditional oscilloscope-like)."""
|
|
332
|
-
ax.plot(time, data, "b-", linewidth=1.2)
|
|
333
|
-
ax.axhline(0.5, color="gray", linestyle="--", linewidth=0.5, alpha=0.5)
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
def plot_state_machine(
|
|
337
|
-
states: list[str],
|
|
338
|
-
transitions: list[StateTransition],
|
|
339
|
-
*,
|
|
340
|
-
initial_state: str | None = None,
|
|
341
|
-
final_states: list[str] | None = None,
|
|
342
|
-
layout: Literal["circular", "hierarchical", "force"] = "circular",
|
|
343
|
-
figsize: tuple[float, float] = (10, 8),
|
|
344
|
-
title: str | None = None,
|
|
345
|
-
) -> Figure:
|
|
346
|
-
"""Plot state machine diagram.
|
|
347
|
-
|
|
348
|
-
Creates a state diagram showing states as nodes and transitions as
|
|
349
|
-
directed edges with condition labels.
|
|
350
|
-
|
|
351
|
-
Args:
|
|
352
|
-
states: List of state names.
|
|
353
|
-
transitions: List of StateTransition objects.
|
|
354
|
-
initial_state: Initial state (marked with double circle).
|
|
355
|
-
final_states: List of final states (marked with double circle).
|
|
356
|
-
layout: Layout algorithm for state positioning.
|
|
357
|
-
figsize: Figure size (width, height).
|
|
358
|
-
title: Plot title.
|
|
359
|
-
|
|
360
|
-
Returns:
|
|
361
|
-
Matplotlib Figure object.
|
|
362
|
-
|
|
363
|
-
Raises:
|
|
364
|
-
ImportError: If matplotlib is not available.
|
|
365
|
-
|
|
366
|
-
Example:
|
|
367
|
-
>>> states = ["IDLE", "ACTIVE", "WAIT", "DONE"]
|
|
368
|
-
>>> transitions = [
|
|
369
|
-
... StateTransition("IDLE", "ACTIVE", "START"),
|
|
370
|
-
... StateTransition("ACTIVE", "WAIT", "BUSY"),
|
|
371
|
-
... StateTransition("WAIT", "ACTIVE", "RETRY"),
|
|
372
|
-
... StateTransition("ACTIVE", "DONE", "COMPLETE"),
|
|
373
|
-
... ]
|
|
374
|
-
>>> fig = plot_state_machine(
|
|
375
|
-
... states, transitions, initial_state="IDLE", final_states=["DONE"]
|
|
376
|
-
... )
|
|
377
|
-
|
|
378
|
-
References:
|
|
379
|
-
VIS-022: Specialized - State Machine View
|
|
380
|
-
"""
|
|
381
|
-
if not HAS_MATPLOTLIB:
|
|
382
|
-
raise ImportError("matplotlib is required for visualization")
|
|
383
|
-
|
|
384
|
-
# Setup figure
|
|
385
|
-
fig, ax = plt.subplots(figsize=figsize)
|
|
386
|
-
positions = _calculate_state_positions(states, layout)
|
|
387
|
-
state_radius = 0.15
|
|
388
|
-
|
|
389
|
-
# Draw all states
|
|
390
|
-
_draw_all_states(ax, positions, state_radius, initial_state, final_states)
|
|
391
|
-
|
|
392
|
-
# Draw all transitions
|
|
393
|
-
_draw_all_transitions(ax, transitions, positions, state_radius)
|
|
394
|
-
|
|
395
|
-
# Finalize axes
|
|
396
|
-
_finalize_state_machine_plot(ax, title)
|
|
397
|
-
|
|
398
|
-
fig.tight_layout()
|
|
399
|
-
return fig
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
def _draw_all_states(
|
|
403
|
-
ax: Axes,
|
|
404
|
-
positions: dict[str, tuple[float, float]],
|
|
405
|
-
state_radius: float,
|
|
406
|
-
initial_state: str | None,
|
|
407
|
-
final_states: list[str] | None,
|
|
408
|
-
) -> None:
|
|
409
|
-
"""Draw all state nodes with appropriate markers."""
|
|
410
|
-
for state, (x, y) in positions.items():
|
|
411
|
-
# Draw state circle
|
|
412
|
-
circle = patches.Circle(
|
|
413
|
-
(x, y),
|
|
414
|
-
state_radius,
|
|
415
|
-
fill=True,
|
|
416
|
-
facecolor="lightblue",
|
|
417
|
-
edgecolor="black",
|
|
418
|
-
linewidth=2.0,
|
|
419
|
-
)
|
|
420
|
-
ax.add_patch(circle)
|
|
421
|
-
|
|
422
|
-
# Mark initial state with double circle
|
|
423
|
-
if state == initial_state:
|
|
424
|
-
outer_circle = patches.Circle(
|
|
425
|
-
(x, y),
|
|
426
|
-
state_radius * 1.2,
|
|
427
|
-
fill=False,
|
|
428
|
-
edgecolor="black",
|
|
429
|
-
linewidth=2.0,
|
|
430
|
-
)
|
|
431
|
-
ax.add_patch(outer_circle)
|
|
432
|
-
|
|
433
|
-
# Mark final states with double circle
|
|
434
|
-
if final_states and state in final_states:
|
|
435
|
-
inner_circle = patches.Circle(
|
|
436
|
-
(x, y),
|
|
437
|
-
state_radius * 0.8,
|
|
438
|
-
fill=False,
|
|
439
|
-
edgecolor="black",
|
|
440
|
-
linewidth=2.0,
|
|
441
|
-
)
|
|
442
|
-
ax.add_patch(inner_circle)
|
|
443
|
-
|
|
444
|
-
# Add state label
|
|
445
|
-
ax.text(
|
|
446
|
-
x,
|
|
447
|
-
y,
|
|
448
|
-
state,
|
|
449
|
-
ha="center",
|
|
450
|
-
va="center",
|
|
451
|
-
fontsize=10,
|
|
452
|
-
fontweight="bold",
|
|
453
|
-
)
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
def _draw_all_transitions(
|
|
457
|
-
ax: Axes,
|
|
458
|
-
transitions: list[StateTransition],
|
|
459
|
-
positions: dict[str, tuple[float, float]],
|
|
460
|
-
state_radius: float,
|
|
461
|
-
) -> None:
|
|
462
|
-
"""Draw all transition arrows between states."""
|
|
463
|
-
for trans in transitions:
|
|
464
|
-
if trans.from_state not in positions or trans.to_state not in positions:
|
|
465
|
-
continue
|
|
466
|
-
|
|
467
|
-
x1, y1 = positions[trans.from_state]
|
|
468
|
-
x2, y2 = positions[trans.to_state]
|
|
469
|
-
|
|
470
|
-
# Check for self-loop
|
|
471
|
-
dx = x2 - x1
|
|
472
|
-
dy = y2 - y1
|
|
473
|
-
dist = np.sqrt(dx**2 + dy**2)
|
|
474
|
-
|
|
475
|
-
if dist < 1e-6:
|
|
476
|
-
_draw_self_loop(ax, x1, y1, state_radius, trans.condition)
|
|
477
|
-
continue
|
|
478
|
-
|
|
479
|
-
# Draw regular transition arrow
|
|
480
|
-
_draw_transition_arrow(ax, x1, y1, x2, y2, state_radius, trans)
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
def _draw_transition_arrow(
|
|
484
|
-
ax: Axes,
|
|
485
|
-
x1: float,
|
|
486
|
-
y1: float,
|
|
487
|
-
x2: float,
|
|
488
|
-
y2: float,
|
|
489
|
-
state_radius: float,
|
|
490
|
-
trans: StateTransition,
|
|
491
|
-
) -> None:
|
|
492
|
-
"""Draw single transition arrow with label."""
|
|
493
|
-
# Calculate arrow start/end on circle perimeter
|
|
494
|
-
dx = x2 - x1
|
|
495
|
-
dy = y2 - y1
|
|
496
|
-
dist = np.sqrt(dx**2 + dy**2)
|
|
497
|
-
|
|
498
|
-
# Normalize
|
|
499
|
-
dx_norm = dx / dist
|
|
500
|
-
dy_norm = dy / dist
|
|
501
|
-
|
|
502
|
-
# Arrow start/end on circle edges
|
|
503
|
-
arrow_start_x = x1 + dx_norm * state_radius
|
|
504
|
-
arrow_start_y = y1 + dy_norm * state_radius
|
|
505
|
-
arrow_end_x = x2 - dx_norm * state_radius
|
|
506
|
-
arrow_end_y = y2 - dy_norm * state_radius
|
|
507
|
-
|
|
508
|
-
# Line style
|
|
509
|
-
linestyle = {
|
|
510
|
-
"solid": "-",
|
|
511
|
-
"dashed": "--",
|
|
512
|
-
"dotted": ":",
|
|
513
|
-
}.get(trans.style, "-")
|
|
514
|
-
|
|
515
|
-
# Draw arrow
|
|
516
|
-
ax.annotate(
|
|
517
|
-
"",
|
|
518
|
-
xy=(arrow_end_x, arrow_end_y),
|
|
519
|
-
xytext=(arrow_start_x, arrow_start_y),
|
|
520
|
-
arrowprops={
|
|
521
|
-
"arrowstyle": "->",
|
|
522
|
-
"lw": 1.5,
|
|
523
|
-
"linestyle": linestyle,
|
|
524
|
-
"color": "black",
|
|
525
|
-
},
|
|
526
|
-
)
|
|
527
|
-
|
|
528
|
-
# Add transition label
|
|
529
|
-
if trans.condition:
|
|
530
|
-
mid_x = (x1 + x2) / 2
|
|
531
|
-
mid_y = (y1 + y2) / 2
|
|
532
|
-
ax.text(
|
|
533
|
-
mid_x,
|
|
534
|
-
mid_y,
|
|
535
|
-
trans.condition,
|
|
536
|
-
fontsize=8,
|
|
537
|
-
ha="center",
|
|
538
|
-
bbox={
|
|
539
|
-
"boxstyle": "round,pad=0.3",
|
|
540
|
-
"facecolor": "white",
|
|
541
|
-
"edgecolor": "gray",
|
|
542
|
-
"alpha": 0.9,
|
|
543
|
-
},
|
|
544
|
-
)
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
def _finalize_state_machine_plot(ax: Axes, title: str | None) -> None:
|
|
548
|
-
"""Set axis properties and title for state machine plot."""
|
|
549
|
-
ax.set_aspect("equal")
|
|
550
|
-
ax.axis("off")
|
|
551
|
-
ax.set_xlim(-0.2, 1.2)
|
|
552
|
-
ax.set_ylim(-0.2, 1.2)
|
|
553
|
-
|
|
554
|
-
if title:
|
|
555
|
-
ax.set_title(title, fontsize=14, pad=20)
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
def _calculate_state_positions(
|
|
559
|
-
states: list[str],
|
|
560
|
-
layout: str,
|
|
561
|
-
) -> dict[str, tuple[float, float]]:
|
|
562
|
-
"""Calculate state positions using layout algorithm."""
|
|
563
|
-
n_states = len(states)
|
|
564
|
-
positions = {}
|
|
565
|
-
|
|
566
|
-
if layout == "circular":
|
|
567
|
-
# Arrange states in a circle
|
|
568
|
-
angle_step = 2 * np.pi / n_states
|
|
569
|
-
for i, state in enumerate(states):
|
|
570
|
-
angle = i * angle_step
|
|
571
|
-
x = 0.5 + 0.4 * np.cos(angle)
|
|
572
|
-
y = 0.5 + 0.4 * np.sin(angle)
|
|
573
|
-
positions[state] = (x, y)
|
|
574
|
-
|
|
575
|
-
elif layout == "hierarchical":
|
|
576
|
-
# Arrange in rows (simplified hierarchical)
|
|
577
|
-
states_per_row = int(np.ceil(np.sqrt(n_states)))
|
|
578
|
-
for i, state in enumerate(states):
|
|
579
|
-
row = i // states_per_row
|
|
580
|
-
col = i % states_per_row
|
|
581
|
-
x = (col + 0.5) / states_per_row
|
|
582
|
-
y = 1.0 - (row + 0.5) / np.ceil(n_states / states_per_row)
|
|
583
|
-
positions[state] = (x, y)
|
|
584
|
-
|
|
585
|
-
else: # force-directed (simplified)
|
|
586
|
-
# Use random positions as a placeholder for true force-directed layout
|
|
587
|
-
np.random.seed(42)
|
|
588
|
-
for i, state in enumerate(states):
|
|
589
|
-
x = 0.2 + 0.6 * np.random.rand()
|
|
590
|
-
y = 0.2 + 0.6 * np.random.rand()
|
|
591
|
-
positions[state] = (x, y)
|
|
592
|
-
|
|
593
|
-
return positions
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
def _draw_self_loop(
|
|
597
|
-
ax: Axes,
|
|
598
|
-
x: float,
|
|
599
|
-
y: float,
|
|
600
|
-
radius: float,
|
|
601
|
-
label: str,
|
|
602
|
-
) -> None:
|
|
603
|
-
"""Draw self-loop transition on state."""
|
|
604
|
-
# Draw arc above state
|
|
605
|
-
arc = patches.Arc(
|
|
606
|
-
(x, y + radius),
|
|
607
|
-
width=radius * 1.5,
|
|
608
|
-
height=radius * 1.5,
|
|
609
|
-
angle=0,
|
|
610
|
-
theta1=0,
|
|
611
|
-
theta2=180,
|
|
612
|
-
linewidth=1.5,
|
|
613
|
-
edgecolor="black",
|
|
614
|
-
fill=False,
|
|
615
|
-
)
|
|
616
|
-
ax.add_patch(arc)
|
|
617
|
-
|
|
618
|
-
# Add arrow head
|
|
619
|
-
ax.annotate(
|
|
620
|
-
"",
|
|
621
|
-
xy=(x - radius * 0.7, y + radius * 0.3),
|
|
622
|
-
xytext=(x - radius * 0.5, y + radius * 0.5),
|
|
623
|
-
arrowprops={"arrowstyle": "->", "lw": 1.5, "color": "black"},
|
|
624
|
-
)
|
|
625
|
-
|
|
626
|
-
# Add label
|
|
627
|
-
if label:
|
|
628
|
-
ax.text(
|
|
629
|
-
x,
|
|
630
|
-
y + radius * 2.2,
|
|
631
|
-
label,
|
|
632
|
-
fontsize=8,
|
|
633
|
-
ha="center",
|
|
634
|
-
bbox={"boxstyle": "round,pad=0.2", "facecolor": "white", "alpha": 0.9},
|
|
635
|
-
)
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
__all__ = [
|
|
639
|
-
"ProtocolSignal",
|
|
640
|
-
"StateTransition",
|
|
641
|
-
"plot_protocol_timing",
|
|
642
|
-
"plot_state_machine",
|
|
643
|
-
]
|