oscura 0.7.0__py3-none-any.whl → 0.10.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/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/eye/__init__.py +5 -1
- oscura/analyzers/eye/generation.py +501 -0
- oscura/analyzers/jitter/__init__.py +6 -6
- oscura/analyzers/jitter/timing.py +419 -0
- oscura/analyzers/patterns/__init__.py +94 -0
- oscura/analyzers/patterns/reverse_engineering.py +991 -0
- oscura/analyzers/power/__init__.py +35 -12
- 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/__init__.py +4 -0
- oscura/analyzers/statistics/basic.py +152 -0
- oscura/analyzers/statistics/correlation.py +47 -6
- oscura/analyzers/validation.py +1 -1
- oscura/analyzers/waveform/__init__.py +2 -0
- oscura/analyzers/waveform/measurements.py +329 -163
- oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
- oscura/analyzers/waveform/spectral.py +498 -54
- 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 +102 -17
- 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/config/loader.py +0 -1
- oscura/core/measurement_result.py +286 -0
- oscura/core/progress.py +1 -1
- oscura/core/schemas/device_mapping.json +8 -2
- oscura/core/schemas/packet_format.json +24 -4
- oscura/core/schemas/protocol_definition.json +12 -2
- oscura/core/types.py +300 -199
- 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/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/__init__.py +88 -1
- oscura/reporting/automation.py +348 -0
- oscura/reporting/citations.py +374 -0
- oscura/reporting/core.py +54 -0
- oscura/reporting/formatting/__init__.py +11 -0
- oscura/reporting/formatting/measurements.py +320 -0
- oscura/reporting/html.py +57 -0
- oscura/reporting/interpretation.py +431 -0
- oscura/reporting/summary.py +329 -0
- oscura/reporting/templates/enhanced/protocol_re.html +504 -503
- oscura/reporting/visualization.py +542 -0
- 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 +47 -284
- oscura/visualization/batch.py +160 -0
- oscura/visualization/plot.py +542 -53
- oscura/visualization/styles.py +184 -318
- oscura/workflows/__init__.py +2 -0
- oscura/workflows/batch/advanced.py +1 -1
- oscura/workflows/batch/aggregate.py +7 -8
- oscura/workflows/complete_re.py +251 -23
- oscura/workflows/digital.py +27 -4
- oscura/workflows/multi_trace.py +136 -17
- oscura/workflows/waveform.py +788 -0
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/RECORD +135 -149
- 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.7.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,838 +0,0 @@
|
|
|
1
|
-
"""Reverse Engineering Pipeline Visualization Module.
|
|
2
|
-
|
|
3
|
-
This module provides comprehensive visualization functions for reverse engineering
|
|
4
|
-
pipeline results, including message type distributions, field layouts, confidence
|
|
5
|
-
heatmaps, protocol detection scores, and pipeline performance metrics.
|
|
6
|
-
|
|
7
|
-
Functions:
|
|
8
|
-
plot_re_summary: Multi-panel dashboard of RE pipeline results
|
|
9
|
-
plot_message_type_distribution: Pie/bar chart of discovered message types
|
|
10
|
-
plot_message_field_layout: Visual field layout with byte positions
|
|
11
|
-
plot_field_confidence_heatmap: Heatmap of field inference confidence scores
|
|
12
|
-
plot_protocol_candidates: Bar chart of protocol detection scores
|
|
13
|
-
plot_crc_parameters: Visualization of detected CRC parameters
|
|
14
|
-
plot_pipeline_timing: Performance metrics for each pipeline stage
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
from __future__ import annotations
|
|
18
|
-
|
|
19
|
-
from typing import TYPE_CHECKING, Any
|
|
20
|
-
|
|
21
|
-
import matplotlib.pyplot as plt
|
|
22
|
-
import numpy as np
|
|
23
|
-
from matplotlib.patches import Rectangle
|
|
24
|
-
|
|
25
|
-
if TYPE_CHECKING:
|
|
26
|
-
from matplotlib.figure import Figure
|
|
27
|
-
|
|
28
|
-
from oscura.inference.crc_reverse import CRCParameters
|
|
29
|
-
from oscura.inference.message_format import InferredField, MessageSchema
|
|
30
|
-
from oscura.utils.pipeline.reverse_engineering import (
|
|
31
|
-
MessageTypeInfo,
|
|
32
|
-
ProtocolCandidate,
|
|
33
|
-
REAnalysisResult,
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
__all__ = [
|
|
37
|
-
"plot_crc_parameters",
|
|
38
|
-
"plot_field_confidence_heatmap",
|
|
39
|
-
"plot_message_field_layout",
|
|
40
|
-
"plot_message_type_distribution",
|
|
41
|
-
"plot_pipeline_timing",
|
|
42
|
-
"plot_protocol_candidates",
|
|
43
|
-
"plot_re_summary",
|
|
44
|
-
]
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def plot_re_summary(
|
|
48
|
-
result: REAnalysisResult,
|
|
49
|
-
*,
|
|
50
|
-
figsize: tuple[float, float] = (16, 10),
|
|
51
|
-
title: str | None = None,
|
|
52
|
-
) -> Figure:
|
|
53
|
-
"""Create multi-panel dashboard showing RE pipeline results overview.
|
|
54
|
-
|
|
55
|
-
Displays a comprehensive summary of the reverse engineering analysis including:
|
|
56
|
-
- Message type distribution
|
|
57
|
-
- Protocol candidates
|
|
58
|
-
- Pipeline timing
|
|
59
|
-
- Key statistics
|
|
60
|
-
|
|
61
|
-
Args:
|
|
62
|
-
result: REAnalysisResult from pipeline analysis.
|
|
63
|
-
figsize: Figure size (width, height) in inches.
|
|
64
|
-
title: Optional custom title for the dashboard.
|
|
65
|
-
|
|
66
|
-
Returns:
|
|
67
|
-
Matplotlib figure object.
|
|
68
|
-
|
|
69
|
-
Example:
|
|
70
|
-
>>> from oscura.utils.pipeline.reverse_engineering import REPipeline
|
|
71
|
-
>>> pipeline = REPipeline()
|
|
72
|
-
>>> result = pipeline.analyze(data)
|
|
73
|
-
>>> fig = plot_re_summary(result)
|
|
74
|
-
>>> fig.savefig("re_summary.png", dpi=300)
|
|
75
|
-
"""
|
|
76
|
-
fig = plt.figure(figsize=figsize)
|
|
77
|
-
|
|
78
|
-
# Create grid layout
|
|
79
|
-
gs = fig.add_gridspec(2, 3, hspace=0.3, wspace=0.3)
|
|
80
|
-
|
|
81
|
-
# Panel 1: Summary statistics (top-left)
|
|
82
|
-
ax1 = fig.add_subplot(gs[0, 0])
|
|
83
|
-
_plot_summary_stats(ax1, result)
|
|
84
|
-
|
|
85
|
-
# Panel 2: Message type distribution (top-center)
|
|
86
|
-
ax2 = fig.add_subplot(gs[0, 1])
|
|
87
|
-
_plot_message_types_panel(ax2, result.message_types)
|
|
88
|
-
|
|
89
|
-
# Panel 3: Protocol candidates (top-right)
|
|
90
|
-
ax3 = fig.add_subplot(gs[0, 2])
|
|
91
|
-
_plot_protocol_panel(ax3, result.protocol_candidates)
|
|
92
|
-
|
|
93
|
-
# Panel 4: Pipeline timing (bottom-left and center)
|
|
94
|
-
ax4 = fig.add_subplot(gs[1, :2])
|
|
95
|
-
_plot_timing_panel(ax4, result.statistics)
|
|
96
|
-
|
|
97
|
-
# Panel 5: Warnings/info (bottom-right)
|
|
98
|
-
ax5 = fig.add_subplot(gs[1, 2])
|
|
99
|
-
_plot_warnings_panel(ax5, result)
|
|
100
|
-
|
|
101
|
-
# Set main title
|
|
102
|
-
if title:
|
|
103
|
-
fig.suptitle(title, fontsize=14, fontweight="bold")
|
|
104
|
-
else:
|
|
105
|
-
fig.suptitle("Reverse Engineering Analysis Summary", fontsize=14, fontweight="bold")
|
|
106
|
-
|
|
107
|
-
return fig
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def _get_field_type_colors() -> dict[str, str]:
|
|
111
|
-
"""Get color map for field types."""
|
|
112
|
-
return {
|
|
113
|
-
"constant": "#4CAF50", # Green
|
|
114
|
-
"counter": "#2196F3", # Blue
|
|
115
|
-
"timestamp": "#9C27B0", # Purple
|
|
116
|
-
"length": "#FF9800", # Orange
|
|
117
|
-
"checksum": "#F44336", # Red
|
|
118
|
-
"data": "#607D8B", # Gray
|
|
119
|
-
"unknown": "#9E9E9E", # Light gray
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def _draw_field_rectangles(
|
|
124
|
-
ax: Any,
|
|
125
|
-
fields: list[InferredField],
|
|
126
|
-
total_bytes: int,
|
|
127
|
-
bar_height: float,
|
|
128
|
-
y_center: float,
|
|
129
|
-
type_colors: dict[str, str],
|
|
130
|
-
) -> None:
|
|
131
|
-
"""Draw field rectangles with labels."""
|
|
132
|
-
for field in fields:
|
|
133
|
-
color = type_colors.get(field.field_type, "#9E9E9E")
|
|
134
|
-
width = field.size / total_bytes
|
|
135
|
-
|
|
136
|
-
rect = Rectangle(
|
|
137
|
-
(field.offset / total_bytes, y_center - bar_height / 2),
|
|
138
|
-
width,
|
|
139
|
-
bar_height,
|
|
140
|
-
facecolor=color,
|
|
141
|
-
edgecolor="black",
|
|
142
|
-
linewidth=1.5,
|
|
143
|
-
)
|
|
144
|
-
ax.add_patch(rect)
|
|
145
|
-
|
|
146
|
-
x_center = (field.offset + field.size / 2) / total_bytes
|
|
147
|
-
label = f"{field.name}\n({field.field_type})" if field.size > 2 else field.field_type[:3]
|
|
148
|
-
|
|
149
|
-
ax.text(
|
|
150
|
-
x_center,
|
|
151
|
-
y_center,
|
|
152
|
-
label,
|
|
153
|
-
ha="center",
|
|
154
|
-
va="center",
|
|
155
|
-
fontsize=9 if field.size > 2 else 7,
|
|
156
|
-
fontweight="bold",
|
|
157
|
-
color="white",
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
def _add_field_offsets(
|
|
162
|
-
ax: Any,
|
|
163
|
-
fields: list[InferredField],
|
|
164
|
-
total_bytes: int,
|
|
165
|
-
bar_height: float,
|
|
166
|
-
y_center: float,
|
|
167
|
-
) -> None:
|
|
168
|
-
"""Add byte offset labels."""
|
|
169
|
-
for field in fields:
|
|
170
|
-
ax.text(
|
|
171
|
-
field.offset / total_bytes,
|
|
172
|
-
y_center - bar_height / 2 - 0.08,
|
|
173
|
-
f"{field.offset}",
|
|
174
|
-
ha="center",
|
|
175
|
-
va="top",
|
|
176
|
-
fontsize=8,
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
ax.text(
|
|
180
|
-
1.0,
|
|
181
|
-
y_center - bar_height / 2 - 0.08,
|
|
182
|
-
f"{total_bytes}",
|
|
183
|
-
ha="center",
|
|
184
|
-
va="top",
|
|
185
|
-
fontsize=8,
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def _add_field_legend(ax: Any, fields: list[InferredField], type_colors: dict[str, str]) -> None:
|
|
190
|
-
"""Add legend for field types."""
|
|
191
|
-
legend_elements = [
|
|
192
|
-
Rectangle((0, 0), 1, 1, facecolor=color, label=ftype)
|
|
193
|
-
for ftype, color in type_colors.items()
|
|
194
|
-
if any(f.field_type == ftype for f in fields)
|
|
195
|
-
]
|
|
196
|
-
ax.legend(handles=legend_elements, loc="upper right", fontsize=8)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
def _format_layout_axes(ax: Any, total_bytes: int, title: str | None) -> None:
|
|
200
|
-
"""Format axes for layout plot."""
|
|
201
|
-
ax.set_xlim(-0.02, 1.02)
|
|
202
|
-
ax.set_ylim(0, 1)
|
|
203
|
-
ax.set_aspect("equal")
|
|
204
|
-
ax.axis("off")
|
|
205
|
-
|
|
206
|
-
if title:
|
|
207
|
-
ax.set_title(title, fontsize=12, fontweight="bold", pad=20)
|
|
208
|
-
else:
|
|
209
|
-
ax.set_title(
|
|
210
|
-
f"Message Field Layout ({total_bytes} bytes)", fontsize=12, fontweight="bold", pad=20
|
|
211
|
-
)
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
def plot_message_type_distribution(
|
|
215
|
-
message_types: list[MessageTypeInfo],
|
|
216
|
-
*,
|
|
217
|
-
figsize: tuple[float, float] = (12, 5),
|
|
218
|
-
chart_type: str = "both",
|
|
219
|
-
title: str | None = None,
|
|
220
|
-
) -> Figure:
|
|
221
|
-
"""Plot pie/bar chart of discovered message types.
|
|
222
|
-
|
|
223
|
-
Args:
|
|
224
|
-
message_types: List of MessageTypeInfo from RE analysis.
|
|
225
|
-
figsize: Figure size (width, height) in inches.
|
|
226
|
-
chart_type: Type of chart - "pie", "bar", or "both".
|
|
227
|
-
title: Optional custom title.
|
|
228
|
-
|
|
229
|
-
Returns:
|
|
230
|
-
Matplotlib figure object.
|
|
231
|
-
|
|
232
|
-
Example:
|
|
233
|
-
>>> fig = plot_message_type_distribution(result.message_types)
|
|
234
|
-
"""
|
|
235
|
-
if not message_types:
|
|
236
|
-
fig, ax = plt.subplots(figsize=figsize)
|
|
237
|
-
ax.text(0.5, 0.5, "No message types detected", ha="center", va="center", fontsize=14)
|
|
238
|
-
ax.set_axis_off()
|
|
239
|
-
return fig
|
|
240
|
-
|
|
241
|
-
if chart_type == "both":
|
|
242
|
-
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
|
|
243
|
-
_plot_type_pie(ax1, message_types)
|
|
244
|
-
_plot_type_bar(ax2, message_types)
|
|
245
|
-
elif chart_type == "pie":
|
|
246
|
-
fig, ax = plt.subplots(figsize=figsize)
|
|
247
|
-
_plot_type_pie(ax, message_types)
|
|
248
|
-
else: # bar
|
|
249
|
-
fig, ax = plt.subplots(figsize=figsize)
|
|
250
|
-
_plot_type_bar(ax, message_types)
|
|
251
|
-
|
|
252
|
-
if title:
|
|
253
|
-
fig.suptitle(title, fontsize=12, fontweight="bold")
|
|
254
|
-
else:
|
|
255
|
-
fig.suptitle("Message Type Distribution", fontsize=12, fontweight="bold")
|
|
256
|
-
|
|
257
|
-
plt.tight_layout()
|
|
258
|
-
return fig
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
def plot_message_field_layout(
|
|
262
|
-
schema: MessageSchema,
|
|
263
|
-
*,
|
|
264
|
-
figsize: tuple[float, float] = (14, 6),
|
|
265
|
-
title: str | None = None,
|
|
266
|
-
show_values: bool = True,
|
|
267
|
-
) -> Figure:
|
|
268
|
-
"""Create visual field layout diagram showing byte positions.
|
|
269
|
-
|
|
270
|
-
Displays a horizontal layout of message fields with:
|
|
271
|
-
- Field boundaries marked
|
|
272
|
-
- Field types color-coded
|
|
273
|
-
- Byte offsets labeled
|
|
274
|
-
- Optional sample values
|
|
275
|
-
|
|
276
|
-
Args:
|
|
277
|
-
schema: MessageSchema with inferred field structure.
|
|
278
|
-
figsize: Figure size (width, height) in inches.
|
|
279
|
-
title: Optional custom title.
|
|
280
|
-
show_values: Whether to show sample field values.
|
|
281
|
-
|
|
282
|
-
Returns:
|
|
283
|
-
Matplotlib figure object.
|
|
284
|
-
|
|
285
|
-
Example:
|
|
286
|
-
>>> from oscura.inference.message_format import infer_format
|
|
287
|
-
>>> schema = infer_format(messages)
|
|
288
|
-
>>> fig = plot_message_field_layout(schema)
|
|
289
|
-
"""
|
|
290
|
-
fig, ax = plt.subplots(figsize=figsize)
|
|
291
|
-
|
|
292
|
-
if not schema.fields:
|
|
293
|
-
ax.text(0.5, 0.5, "No fields detected", ha="center", va="center", fontsize=14)
|
|
294
|
-
ax.set_axis_off()
|
|
295
|
-
return fig
|
|
296
|
-
|
|
297
|
-
type_colors = _get_field_type_colors()
|
|
298
|
-
total_bytes = schema.total_size
|
|
299
|
-
bar_height = 0.6
|
|
300
|
-
y_center = 0.5
|
|
301
|
-
|
|
302
|
-
_draw_field_rectangles(ax, schema.fields, total_bytes, bar_height, y_center, type_colors)
|
|
303
|
-
_add_field_offsets(ax, schema.fields, total_bytes, bar_height, y_center)
|
|
304
|
-
_add_field_legend(ax, schema.fields, type_colors)
|
|
305
|
-
_format_layout_axes(ax, schema.total_size, title)
|
|
306
|
-
|
|
307
|
-
return fig
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
def plot_field_confidence_heatmap(
|
|
311
|
-
fields: list[InferredField],
|
|
312
|
-
*,
|
|
313
|
-
figsize: tuple[float, float] = (12, 6),
|
|
314
|
-
title: str | None = None,
|
|
315
|
-
) -> Figure:
|
|
316
|
-
"""Create heatmap showing confidence scores for field inferences.
|
|
317
|
-
|
|
318
|
-
Displays a visual representation of how confident the inference
|
|
319
|
-
algorithm is about each field's type and boundaries.
|
|
320
|
-
|
|
321
|
-
Args:
|
|
322
|
-
fields: List of InferredField objects with confidence scores.
|
|
323
|
-
figsize: Figure size (width, height) in inches.
|
|
324
|
-
title: Optional custom title.
|
|
325
|
-
|
|
326
|
-
Returns:
|
|
327
|
-
Matplotlib figure object.
|
|
328
|
-
|
|
329
|
-
Example:
|
|
330
|
-
>>> fig = plot_field_confidence_heatmap(schema.fields)
|
|
331
|
-
"""
|
|
332
|
-
fig, ax = plt.subplots(figsize=figsize)
|
|
333
|
-
|
|
334
|
-
if not fields:
|
|
335
|
-
ax.text(0.5, 0.5, "No fields to display", ha="center", va="center", fontsize=14)
|
|
336
|
-
ax.set_axis_off()
|
|
337
|
-
return fig
|
|
338
|
-
|
|
339
|
-
# Create data matrix
|
|
340
|
-
n_fields = len(fields)
|
|
341
|
-
metrics = ["Confidence", "Entropy", "Variance"]
|
|
342
|
-
data = np.zeros((len(metrics), n_fields))
|
|
343
|
-
|
|
344
|
-
for i, field in enumerate(fields):
|
|
345
|
-
data[0, i] = field.confidence
|
|
346
|
-
# Normalize entropy (0-8 bits) to 0-1
|
|
347
|
-
data[1, i] = min(field.entropy / 8.0, 1.0)
|
|
348
|
-
# Normalize variance using log scale
|
|
349
|
-
data[2, i] = min(np.log1p(field.variance) / 10.0, 1.0)
|
|
350
|
-
|
|
351
|
-
# Create heatmap
|
|
352
|
-
im = ax.imshow(data, cmap="RdYlGn", aspect="auto", vmin=0, vmax=1)
|
|
353
|
-
|
|
354
|
-
# Add colorbar
|
|
355
|
-
cbar = plt.colorbar(im, ax=ax)
|
|
356
|
-
cbar.set_label("Score (normalized)", fontsize=10)
|
|
357
|
-
|
|
358
|
-
# Set labels
|
|
359
|
-
field_names = [f.name for f in fields]
|
|
360
|
-
ax.set_xticks(range(n_fields))
|
|
361
|
-
ax.set_xticklabels(field_names, rotation=45, ha="right", fontsize=9)
|
|
362
|
-
ax.set_yticks(range(len(metrics)))
|
|
363
|
-
ax.set_yticklabels(metrics, fontsize=10)
|
|
364
|
-
|
|
365
|
-
# Add text annotations
|
|
366
|
-
for i in range(len(metrics)):
|
|
367
|
-
for j in range(n_fields):
|
|
368
|
-
value = data[i, j]
|
|
369
|
-
color = "white" if value < 0.5 else "black"
|
|
370
|
-
ax.text(j, i, f"{value:.2f}", ha="center", va="center", color=color, fontsize=8)
|
|
371
|
-
|
|
372
|
-
if title:
|
|
373
|
-
ax.set_title(title, fontsize=12, fontweight="bold", pad=10)
|
|
374
|
-
else:
|
|
375
|
-
ax.set_title("Field Inference Confidence Heatmap", fontsize=12, fontweight="bold", pad=10)
|
|
376
|
-
|
|
377
|
-
plt.tight_layout()
|
|
378
|
-
return fig
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
def plot_protocol_candidates(
|
|
382
|
-
candidates: list[ProtocolCandidate],
|
|
383
|
-
*,
|
|
384
|
-
figsize: tuple[float, float] = (10, 6),
|
|
385
|
-
title: str | None = None,
|
|
386
|
-
top_n: int = 10,
|
|
387
|
-
) -> Figure:
|
|
388
|
-
"""Create bar chart of protocol detection scores.
|
|
389
|
-
|
|
390
|
-
Shows the confidence scores for each detected protocol candidate,
|
|
391
|
-
with visual indicators for the evidence sources.
|
|
392
|
-
|
|
393
|
-
Args:
|
|
394
|
-
candidates: List of ProtocolCandidate objects.
|
|
395
|
-
figsize: Figure size (width, height) in inches.
|
|
396
|
-
title: Optional custom title.
|
|
397
|
-
top_n: Maximum number of candidates to display.
|
|
398
|
-
|
|
399
|
-
Returns:
|
|
400
|
-
Matplotlib figure object.
|
|
401
|
-
|
|
402
|
-
Example:
|
|
403
|
-
>>> fig = plot_protocol_candidates(result.protocol_candidates)
|
|
404
|
-
"""
|
|
405
|
-
fig, ax = plt.subplots(figsize=figsize)
|
|
406
|
-
|
|
407
|
-
if not candidates:
|
|
408
|
-
ax.text(0.5, 0.5, "No protocol candidates detected", ha="center", va="center", fontsize=14)
|
|
409
|
-
ax.set_axis_off()
|
|
410
|
-
return fig
|
|
411
|
-
|
|
412
|
-
# Sort by confidence and take top N
|
|
413
|
-
sorted_candidates = sorted(candidates, key=lambda c: c.confidence, reverse=True)[:top_n]
|
|
414
|
-
|
|
415
|
-
names = [c.name for c in sorted_candidates]
|
|
416
|
-
confidences = [c.confidence for c in sorted_candidates]
|
|
417
|
-
|
|
418
|
-
# Color based on confidence level
|
|
419
|
-
colors = []
|
|
420
|
-
for conf in confidences:
|
|
421
|
-
if conf >= 0.8:
|
|
422
|
-
colors.append("#4CAF50") # Green - high confidence
|
|
423
|
-
elif conf >= 0.5:
|
|
424
|
-
colors.append("#FF9800") # Orange - medium
|
|
425
|
-
else:
|
|
426
|
-
colors.append("#F44336") # Red - low
|
|
427
|
-
|
|
428
|
-
y_pos = np.arange(len(names))
|
|
429
|
-
ax.barh(y_pos, confidences, color=colors, edgecolor="black")
|
|
430
|
-
|
|
431
|
-
# Add evidence indicators
|
|
432
|
-
for i, cand in enumerate(sorted_candidates):
|
|
433
|
-
indicators = []
|
|
434
|
-
if cand.port_hint:
|
|
435
|
-
indicators.append("P") # Port hint
|
|
436
|
-
if cand.header_match:
|
|
437
|
-
indicators.append("H") # Header match
|
|
438
|
-
if cand.matched_patterns:
|
|
439
|
-
indicators.append(f"M{len(cand.matched_patterns)}") # Pattern matches
|
|
440
|
-
|
|
441
|
-
if indicators:
|
|
442
|
-
ax.text(
|
|
443
|
-
confidences[i] + 0.02,
|
|
444
|
-
i,
|
|
445
|
-
" ".join(indicators),
|
|
446
|
-
va="center",
|
|
447
|
-
fontsize=8,
|
|
448
|
-
color="gray",
|
|
449
|
-
)
|
|
450
|
-
|
|
451
|
-
ax.set_yticks(y_pos)
|
|
452
|
-
ax.set_yticklabels(names)
|
|
453
|
-
ax.set_xlim(0, 1.15)
|
|
454
|
-
ax.set_xlabel("Confidence Score")
|
|
455
|
-
ax.axvline(x=0.5, color="gray", linestyle="--", alpha=0.5, label="Threshold")
|
|
456
|
-
|
|
457
|
-
# Add legend for evidence indicators
|
|
458
|
-
ax.text(
|
|
459
|
-
0.95,
|
|
460
|
-
-0.12,
|
|
461
|
-
"P=Port hint, H=Header match, M#=Pattern matches",
|
|
462
|
-
transform=ax.transAxes,
|
|
463
|
-
fontsize=8,
|
|
464
|
-
ha="right",
|
|
465
|
-
style="italic",
|
|
466
|
-
color="gray",
|
|
467
|
-
)
|
|
468
|
-
|
|
469
|
-
if title:
|
|
470
|
-
ax.set_title(title, fontsize=12, fontweight="bold")
|
|
471
|
-
else:
|
|
472
|
-
ax.set_title("Protocol Detection Candidates", fontsize=12, fontweight="bold")
|
|
473
|
-
|
|
474
|
-
plt.tight_layout()
|
|
475
|
-
return fig
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
def plot_crc_parameters(
|
|
479
|
-
params: CRCParameters,
|
|
480
|
-
*,
|
|
481
|
-
figsize: tuple[float, float] = (10, 6),
|
|
482
|
-
title: str | None = None,
|
|
483
|
-
) -> Figure:
|
|
484
|
-
"""Create visualization of detected CRC parameters.
|
|
485
|
-
|
|
486
|
-
Shows the recovered CRC parameters in a visually informative format,
|
|
487
|
-
including polynomial representation, configuration flags, and
|
|
488
|
-
confidence metrics.
|
|
489
|
-
|
|
490
|
-
Args:
|
|
491
|
-
params: CRCParameters object with recovered CRC settings.
|
|
492
|
-
figsize: Figure size (width, height) in inches.
|
|
493
|
-
title: Optional custom title.
|
|
494
|
-
|
|
495
|
-
Returns:
|
|
496
|
-
Matplotlib figure object.
|
|
497
|
-
|
|
498
|
-
Example:
|
|
499
|
-
>>> from oscura.inference.crc_reverse import CRCReverser
|
|
500
|
-
>>> reverser = CRCReverser()
|
|
501
|
-
>>> params = reverser.reverse(messages)
|
|
502
|
-
>>> fig = plot_crc_parameters(params)
|
|
503
|
-
"""
|
|
504
|
-
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
|
|
505
|
-
_plot_crc_parameter_table(ax1, params)
|
|
506
|
-
_plot_crc_confidence_gauge(ax2, params)
|
|
507
|
-
_finalize_crc_plot(fig, title)
|
|
508
|
-
return fig
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
def _plot_crc_parameter_table(ax: Any, params: CRCParameters) -> None:
|
|
512
|
-
"""Plot CRC parameter table.
|
|
513
|
-
|
|
514
|
-
Args:
|
|
515
|
-
ax: Axes object.
|
|
516
|
-
params: CRC parameters.
|
|
517
|
-
"""
|
|
518
|
-
ax.axis("off")
|
|
519
|
-
param_lines = [
|
|
520
|
-
f"Width: {params.width} bits",
|
|
521
|
-
f"Polynomial: 0x{params.polynomial:0{params.width // 4}X}",
|
|
522
|
-
f"Init Value: 0x{params.init:0{params.width // 4}X}",
|
|
523
|
-
f"XOR Out: 0x{params.xor_out:0{params.width // 4}X}",
|
|
524
|
-
f"Reflect In: {'Yes' if params.reflect_in else 'No'}",
|
|
525
|
-
f"Reflect Out: {'Yes' if params.reflect_out else 'No'}",
|
|
526
|
-
]
|
|
527
|
-
|
|
528
|
-
if params.algorithm_name:
|
|
529
|
-
param_lines.insert(0, f"Algorithm: {params.algorithm_name}")
|
|
530
|
-
|
|
531
|
-
ax.text(0.5, 0.98, "CRC Parameters", ha="center", fontsize=14, fontweight="bold")
|
|
532
|
-
|
|
533
|
-
y_start = 0.9
|
|
534
|
-
y_step = 0.12
|
|
535
|
-
for i, line in enumerate(param_lines):
|
|
536
|
-
y = y_start - i * y_step
|
|
537
|
-
ax.text(0.1, y, line, fontsize=11, fontfamily="monospace", va="top")
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
def _plot_crc_confidence_gauge(ax: Any, params: CRCParameters) -> None:
|
|
541
|
-
"""Plot confidence gauge for CRC parameters.
|
|
542
|
-
|
|
543
|
-
Args:
|
|
544
|
-
ax: Axes object.
|
|
545
|
-
params: CRC parameters.
|
|
546
|
-
"""
|
|
547
|
-
ax.set_aspect("equal")
|
|
548
|
-
|
|
549
|
-
# Background arc
|
|
550
|
-
theta = np.linspace(0, np.pi, 100)
|
|
551
|
-
r = 0.8
|
|
552
|
-
x = r * np.cos(theta)
|
|
553
|
-
y = r * np.sin(theta)
|
|
554
|
-
ax.plot(x, y, color="#E0E0E0", linewidth=20, solid_capstyle="round")
|
|
555
|
-
|
|
556
|
-
# Confidence arc (colored)
|
|
557
|
-
conf_angle = np.pi * params.confidence
|
|
558
|
-
theta_conf = np.linspace(0, conf_angle, int(100 * params.confidence) + 1)
|
|
559
|
-
x_conf = r * np.cos(theta_conf)
|
|
560
|
-
y_conf = r * np.sin(theta_conf)
|
|
561
|
-
|
|
562
|
-
color = _get_confidence_color(params.confidence)
|
|
563
|
-
ax.plot(x_conf, y_conf, color=color, linewidth=20, solid_capstyle="round")
|
|
564
|
-
|
|
565
|
-
# Add text annotations
|
|
566
|
-
ax.text(
|
|
567
|
-
0, 0.2, f"{params.confidence:.0%}", ha="center", va="center", fontsize=24, fontweight="bold"
|
|
568
|
-
)
|
|
569
|
-
ax.text(0, -0.1, "Confidence", ha="center", va="center", fontsize=12)
|
|
570
|
-
ax.text(
|
|
571
|
-
0,
|
|
572
|
-
-0.4,
|
|
573
|
-
f"Test Pass Rate: {params.test_pass_rate:.0%}",
|
|
574
|
-
ha="center",
|
|
575
|
-
va="center",
|
|
576
|
-
fontsize=10,
|
|
577
|
-
)
|
|
578
|
-
|
|
579
|
-
ax.set_xlim(-1.2, 1.2)
|
|
580
|
-
ax.set_ylim(-0.6, 1.2)
|
|
581
|
-
ax.axis("off")
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
def _get_confidence_color(confidence: float) -> str:
|
|
585
|
-
"""Get color based on confidence level.
|
|
586
|
-
|
|
587
|
-
Args:
|
|
588
|
-
confidence: Confidence value (0-1).
|
|
589
|
-
|
|
590
|
-
Returns:
|
|
591
|
-
Hex color code.
|
|
592
|
-
"""
|
|
593
|
-
if confidence >= 0.8:
|
|
594
|
-
return "#4CAF50" # Green
|
|
595
|
-
if confidence >= 0.5:
|
|
596
|
-
return "#FF9800" # Orange
|
|
597
|
-
return "#F44336" # Red
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
def _finalize_crc_plot(fig: Figure, title: str | None) -> None:
|
|
601
|
-
"""Finalize CRC plot with title.
|
|
602
|
-
|
|
603
|
-
Args:
|
|
604
|
-
fig: Figure object.
|
|
605
|
-
title: Optional title.
|
|
606
|
-
"""
|
|
607
|
-
if title:
|
|
608
|
-
fig.suptitle(title, fontsize=14, fontweight="bold")
|
|
609
|
-
else:
|
|
610
|
-
fig.suptitle("CRC Parameter Recovery", fontsize=14, fontweight="bold")
|
|
611
|
-
plt.tight_layout()
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
def plot_pipeline_timing(
|
|
615
|
-
statistics: dict[str, Any],
|
|
616
|
-
*,
|
|
617
|
-
figsize: tuple[float, float] = (12, 6),
|
|
618
|
-
title: str | None = None,
|
|
619
|
-
) -> Figure:
|
|
620
|
-
"""Create performance metrics visualization for pipeline stages.
|
|
621
|
-
|
|
622
|
-
Shows timing breakdown for each stage of the RE pipeline,
|
|
623
|
-
identifying bottlenecks and processing efficiency.
|
|
624
|
-
|
|
625
|
-
Args:
|
|
626
|
-
statistics: Statistics dictionary from REAnalysisResult.
|
|
627
|
-
figsize: Figure size (width, height) in inches.
|
|
628
|
-
title: Optional custom title.
|
|
629
|
-
|
|
630
|
-
Returns:
|
|
631
|
-
Matplotlib figure object.
|
|
632
|
-
|
|
633
|
-
Example:
|
|
634
|
-
>>> fig = plot_pipeline_timing(result.statistics)
|
|
635
|
-
"""
|
|
636
|
-
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
|
|
637
|
-
|
|
638
|
-
stage_timing = statistics.get("stage_timing", {})
|
|
639
|
-
|
|
640
|
-
if not stage_timing:
|
|
641
|
-
ax1.text(0.5, 0.5, "No timing data available", ha="center", va="center", fontsize=14)
|
|
642
|
-
ax1.set_axis_off()
|
|
643
|
-
ax2.set_axis_off()
|
|
644
|
-
return fig
|
|
645
|
-
|
|
646
|
-
stages = list(stage_timing.keys())
|
|
647
|
-
times = list(stage_timing.values())
|
|
648
|
-
total_time = sum(times)
|
|
649
|
-
|
|
650
|
-
# Left panel: Bar chart of stage times
|
|
651
|
-
colors = plt.cm.viridis(np.linspace(0.2, 0.8, len(stages)))
|
|
652
|
-
y_pos = np.arange(len(stages))
|
|
653
|
-
|
|
654
|
-
bars = ax1.barh(y_pos, times, color=colors)
|
|
655
|
-
ax1.set_yticks(y_pos)
|
|
656
|
-
ax1.set_yticklabels([s.replace("_", " ").title() for s in stages])
|
|
657
|
-
ax1.set_xlabel("Time (seconds)")
|
|
658
|
-
|
|
659
|
-
# Add time labels on bars
|
|
660
|
-
for i, (_bar, t) in enumerate(zip(bars, times, strict=False)):
|
|
661
|
-
pct = t / total_time * 100 if total_time > 0 else 0
|
|
662
|
-
ax1.text(t + 0.01, i, f"{t:.3f}s ({pct:.1f}%)", va="center", fontsize=9)
|
|
663
|
-
|
|
664
|
-
ax1.set_title("Stage Execution Time", fontsize=11, fontweight="bold")
|
|
665
|
-
|
|
666
|
-
# Right panel: Pie chart of time distribution
|
|
667
|
-
# Filter out very small stages for cleaner pie
|
|
668
|
-
significant = [(s, t) for s, t in zip(stages, times, strict=False) if t / total_time > 0.02]
|
|
669
|
-
if significant:
|
|
670
|
-
pie_stages_tuple, pie_times_tuple = zip(*significant, strict=False)
|
|
671
|
-
other_time = total_time - sum(pie_times_tuple)
|
|
672
|
-
pie_stages: list[str] = list(pie_stages_tuple)
|
|
673
|
-
pie_times: list[float] = list(pie_times_tuple)
|
|
674
|
-
if other_time > 0:
|
|
675
|
-
pie_stages = pie_stages + ["Other"]
|
|
676
|
-
pie_times = pie_times + [other_time]
|
|
677
|
-
|
|
678
|
-
wedges, texts, autotexts = ax2.pie(
|
|
679
|
-
pie_times,
|
|
680
|
-
labels=[s.replace("_", " ").title() for s in pie_stages],
|
|
681
|
-
autopct="%1.1f%%",
|
|
682
|
-
colors=plt.cm.viridis(np.linspace(0.2, 0.8, len(pie_stages))),
|
|
683
|
-
)
|
|
684
|
-
ax2.set_title("Time Distribution", fontsize=11, fontweight="bold")
|
|
685
|
-
else:
|
|
686
|
-
ax2.text(0.5, 0.5, "Insufficient data", ha="center", va="center")
|
|
687
|
-
ax2.set_axis_off()
|
|
688
|
-
|
|
689
|
-
# Add total time annotation
|
|
690
|
-
fig.text(
|
|
691
|
-
0.5,
|
|
692
|
-
0.02,
|
|
693
|
-
f"Total Pipeline Time: {total_time:.3f} seconds",
|
|
694
|
-
ha="center",
|
|
695
|
-
fontsize=10,
|
|
696
|
-
style="italic",
|
|
697
|
-
)
|
|
698
|
-
|
|
699
|
-
if title:
|
|
700
|
-
fig.suptitle(title, fontsize=12, fontweight="bold")
|
|
701
|
-
else:
|
|
702
|
-
fig.suptitle("Pipeline Performance Metrics", fontsize=12, fontweight="bold")
|
|
703
|
-
|
|
704
|
-
plt.tight_layout(rect=(0, 0.05, 1, 0.95))
|
|
705
|
-
return fig
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
# ============================================================================
|
|
709
|
-
# Helper functions for panel plotting
|
|
710
|
-
# ============================================================================
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
def _plot_summary_stats(ax: Any, result: REAnalysisResult) -> None:
|
|
714
|
-
"""Plot summary statistics panel."""
|
|
715
|
-
ax.axis("off")
|
|
716
|
-
|
|
717
|
-
stats = [
|
|
718
|
-
("Flows Analyzed", str(result.flow_count)),
|
|
719
|
-
("Messages", str(result.message_count)),
|
|
720
|
-
("Message Types", str(len(result.message_types))),
|
|
721
|
-
("Protocol Candidates", str(len(result.protocol_candidates))),
|
|
722
|
-
("Field Schemas", str(len(result.field_schemas))),
|
|
723
|
-
("Duration", f"{result.duration_seconds:.2f}s"),
|
|
724
|
-
("Warnings", str(len(result.warnings))),
|
|
725
|
-
]
|
|
726
|
-
|
|
727
|
-
ax.text(0.5, 0.95, "Analysis Summary", ha="center", fontsize=11, fontweight="bold")
|
|
728
|
-
|
|
729
|
-
y_pos = 0.8
|
|
730
|
-
for label, value in stats:
|
|
731
|
-
ax.text(0.1, y_pos, f"{label}:", fontsize=9, fontweight="bold")
|
|
732
|
-
ax.text(0.7, y_pos, value, fontsize=9, ha="right")
|
|
733
|
-
y_pos -= 0.11
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
def _plot_message_types_panel(ax: Any, message_types: list[MessageTypeInfo]) -> None:
|
|
737
|
-
"""Plot message types panel."""
|
|
738
|
-
if not message_types:
|
|
739
|
-
ax.text(0.5, 0.5, "No types", ha="center", va="center")
|
|
740
|
-
ax.set_title("Message Types", fontsize=10)
|
|
741
|
-
return
|
|
742
|
-
|
|
743
|
-
names = [mt.name[:15] for mt in message_types[:8]]
|
|
744
|
-
counts = [mt.sample_count for mt in message_types[:8]]
|
|
745
|
-
|
|
746
|
-
colors = plt.cm.Set3(np.linspace(0, 1, len(names)))
|
|
747
|
-
ax.pie(counts, labels=names, colors=colors, autopct="%1.0f%%", textprops={"fontsize": 8})
|
|
748
|
-
ax.set_title("Message Types", fontsize=10)
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
def _plot_protocol_panel(ax: Any, candidates: list[ProtocolCandidate]) -> None:
|
|
752
|
-
"""Plot protocol candidates panel."""
|
|
753
|
-
if not candidates:
|
|
754
|
-
ax.text(0.5, 0.5, "No candidates", ha="center", va="center")
|
|
755
|
-
ax.set_title("Protocol Candidates", fontsize=10)
|
|
756
|
-
return
|
|
757
|
-
|
|
758
|
-
sorted_cand = sorted(candidates, key=lambda c: c.confidence, reverse=True)[:5]
|
|
759
|
-
names = [c.name for c in sorted_cand]
|
|
760
|
-
confs = [c.confidence for c in sorted_cand]
|
|
761
|
-
|
|
762
|
-
y_pos = np.arange(len(names))
|
|
763
|
-
colors = ["#4CAF50" if c >= 0.7 else "#FF9800" if c >= 0.4 else "#F44336" for c in confs]
|
|
764
|
-
|
|
765
|
-
ax.barh(y_pos, confs, color=colors)
|
|
766
|
-
ax.set_yticks(y_pos)
|
|
767
|
-
ax.set_yticklabels(names, fontsize=8)
|
|
768
|
-
ax.set_xlim(0, 1)
|
|
769
|
-
ax.set_title("Protocol Candidates", fontsize=10)
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
def _plot_timing_panel(ax: Any, statistics: dict[str, Any]) -> None:
|
|
773
|
-
"""Plot timing panel."""
|
|
774
|
-
stage_timing = statistics.get("stage_timing", {})
|
|
775
|
-
|
|
776
|
-
if not stage_timing:
|
|
777
|
-
ax.text(0.5, 0.5, "No timing data", ha="center", va="center")
|
|
778
|
-
ax.set_title("Stage Timing", fontsize=10)
|
|
779
|
-
return
|
|
780
|
-
|
|
781
|
-
stages = list(stage_timing.keys())
|
|
782
|
-
times = list(stage_timing.values())
|
|
783
|
-
|
|
784
|
-
colors = plt.cm.viridis(np.linspace(0.2, 0.8, len(stages)))
|
|
785
|
-
bars = ax.bar(range(len(stages)), times, color=colors)
|
|
786
|
-
|
|
787
|
-
ax.set_xticks(range(len(stages)))
|
|
788
|
-
ax.set_xticklabels([s.replace("_", "\n") for s in stages], fontsize=8)
|
|
789
|
-
ax.set_ylabel("Time (s)")
|
|
790
|
-
ax.set_title("Pipeline Stage Timing", fontsize=10)
|
|
791
|
-
|
|
792
|
-
for bar, t in zip(bars, times, strict=False):
|
|
793
|
-
ax.text(
|
|
794
|
-
bar.get_x() + bar.get_width() / 2,
|
|
795
|
-
bar.get_height(),
|
|
796
|
-
f"{t:.2f}s",
|
|
797
|
-
ha="center",
|
|
798
|
-
va="bottom",
|
|
799
|
-
fontsize=7,
|
|
800
|
-
)
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
def _plot_warnings_panel(ax: Any, result: REAnalysisResult) -> None:
|
|
804
|
-
"""Plot warnings/info panel."""
|
|
805
|
-
ax.axis("off")
|
|
806
|
-
|
|
807
|
-
ax.text(0.5, 0.95, "Status & Warnings", ha="center", fontsize=10, fontweight="bold")
|
|
808
|
-
|
|
809
|
-
if result.warnings:
|
|
810
|
-
y_pos = 0.8
|
|
811
|
-
for warning in result.warnings[:5]:
|
|
812
|
-
ax.text(0.05, y_pos, f"! {warning[:40]}...", fontsize=8, color="orange")
|
|
813
|
-
y_pos -= 0.15
|
|
814
|
-
else:
|
|
815
|
-
ax.text(0.5, 0.5, "No warnings", ha="center", va="center", fontsize=10, color="green")
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
def _plot_type_pie(ax: Any, message_types: list[MessageTypeInfo]) -> None:
|
|
819
|
-
"""Plot message type pie chart."""
|
|
820
|
-
names = [mt.name for mt in message_types]
|
|
821
|
-
counts = [mt.sample_count for mt in message_types]
|
|
822
|
-
|
|
823
|
-
colors = plt.cm.Set3(np.linspace(0, 1, len(names)))
|
|
824
|
-
ax.pie(counts, labels=names, colors=colors, autopct="%1.1f%%")
|
|
825
|
-
ax.set_title("Distribution by Sample Count")
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
def _plot_type_bar(ax: Any, message_types: list[MessageTypeInfo]) -> None:
|
|
829
|
-
"""Plot message type bar chart."""
|
|
830
|
-
names = [mt.name for mt in message_types]
|
|
831
|
-
counts = [mt.sample_count for mt in message_types]
|
|
832
|
-
|
|
833
|
-
colors = plt.cm.viridis(np.linspace(0.2, 0.8, len(names)))
|
|
834
|
-
ax.bar(range(len(names)), counts, color=colors)
|
|
835
|
-
ax.set_xticks(range(len(names)))
|
|
836
|
-
ax.set_xticklabels(names, rotation=45, ha="right")
|
|
837
|
-
ax.set_ylabel("Sample Count")
|
|
838
|
-
ax.set_title("Message Count per Type")
|