oscura 0.8.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/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 +164 -73
- 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/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/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/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/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 +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 +11 -6
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/RECORD +111 -136
- 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 → oscura-0.10.0.dist-info}/WHEEL +0 -0
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/licenses/LICENSE +0 -0
oscura/visualization/styles.py
CHANGED
|
@@ -1,381 +1,247 @@
|
|
|
1
|
-
"""Plot
|
|
2
|
-
|
|
3
|
-
This module provides comprehensive style presets for publication-quality,
|
|
4
|
-
presentation, screen viewing, and print output.
|
|
1
|
+
"""Plot styling presets and colorblind-safe palettes.
|
|
5
2
|
|
|
3
|
+
Provides IEEE styling and accessibility-focused color schemes.
|
|
6
4
|
|
|
7
5
|
Example:
|
|
8
|
-
>>> from oscura.visualization.styles import
|
|
9
|
-
>>>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
matplotlib rcParams customization
|
|
14
|
-
Publication and presentation best practices
|
|
6
|
+
>>> from oscura.visualization.styles import apply_ieee_style
|
|
7
|
+
>>> import matplotlib.pyplot as plt
|
|
8
|
+
>>> fig, ax = plt.subplots()
|
|
9
|
+
>>> ax.plot([1, 2, 3])
|
|
10
|
+
>>> apply_ieee_style(ax)
|
|
15
11
|
"""
|
|
16
12
|
|
|
17
13
|
from __future__ import annotations
|
|
18
14
|
|
|
19
|
-
from
|
|
20
|
-
from dataclasses import dataclass, field
|
|
21
|
-
from typing import TYPE_CHECKING, Any
|
|
22
|
-
|
|
23
|
-
if TYPE_CHECKING:
|
|
24
|
-
from collections.abc import Iterator
|
|
15
|
+
from typing import Literal
|
|
25
16
|
|
|
26
17
|
try:
|
|
27
|
-
|
|
18
|
+
from matplotlib.axes import Axes
|
|
28
19
|
|
|
29
20
|
HAS_MATPLOTLIB = True
|
|
30
21
|
except ImportError:
|
|
31
22
|
HAS_MATPLOTLIB = False
|
|
32
23
|
|
|
33
24
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
""
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
marker_size: Default marker size
|
|
45
|
-
figure_facecolor: Figure background color
|
|
46
|
-
axes_facecolor: Axes background color
|
|
47
|
-
axes_edgecolor: Axes edge color
|
|
48
|
-
grid_color: Grid line color
|
|
49
|
-
grid_alpha: Grid line transparency
|
|
50
|
-
grid_linestyle: Grid line style
|
|
51
|
-
use_latex: Use LaTeX for text rendering
|
|
52
|
-
tight_layout: Use tight layout
|
|
53
|
-
rcparams: Additional matplotlib rcParams
|
|
54
|
-
"""
|
|
25
|
+
# Colorblind-safe palettes (Tol Bright)
|
|
26
|
+
COLORBLIND_PALETTE: list[str] = [
|
|
27
|
+
"#4477AA", # Blue
|
|
28
|
+
"#EE6677", # Red
|
|
29
|
+
"#228833", # Green
|
|
30
|
+
"#CCBB44", # Yellow
|
|
31
|
+
"#66CCEE", # Cyan
|
|
32
|
+
"#AA3377", # Purple
|
|
33
|
+
"#BBBBBB", # Gray
|
|
34
|
+
]
|
|
55
35
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
use_latex: bool = False
|
|
69
|
-
tight_layout: bool = True
|
|
70
|
-
rcparams: dict[str, Any] = field(default_factory=dict)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
# Predefined style presets
|
|
74
|
-
|
|
75
|
-
PUBLICATION_PRESET = StylePreset(
|
|
76
|
-
name="publication",
|
|
77
|
-
dpi=600,
|
|
78
|
-
font_family="serif",
|
|
79
|
-
font_size=10,
|
|
80
|
-
line_width=0.8,
|
|
81
|
-
marker_size=4.0,
|
|
82
|
-
figure_facecolor="white",
|
|
83
|
-
axes_facecolor="white",
|
|
84
|
-
axes_edgecolor="black",
|
|
85
|
-
grid_color="#808080",
|
|
86
|
-
grid_alpha=0.3,
|
|
87
|
-
grid_linestyle=":",
|
|
88
|
-
use_latex=False, # LaTeX optional - requires system install
|
|
89
|
-
tight_layout=True,
|
|
90
|
-
rcparams={
|
|
91
|
-
"axes.linewidth": 0.8,
|
|
92
|
-
"xtick.major.width": 0.8,
|
|
93
|
-
"ytick.major.width": 0.8,
|
|
94
|
-
"xtick.minor.width": 0.6,
|
|
95
|
-
"ytick.minor.width": 0.6,
|
|
96
|
-
"lines.antialiased": True,
|
|
97
|
-
"patch.antialiased": True,
|
|
98
|
-
"savefig.dpi": 600,
|
|
99
|
-
"savefig.format": "pdf",
|
|
100
|
-
"savefig.bbox": "tight",
|
|
101
|
-
},
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
PRESENTATION_PRESET = StylePreset(
|
|
105
|
-
name="presentation",
|
|
106
|
-
dpi=96,
|
|
107
|
-
font_family="sans-serif",
|
|
108
|
-
font_size=18,
|
|
109
|
-
line_width=2.5,
|
|
110
|
-
marker_size=10.0,
|
|
111
|
-
figure_facecolor="white",
|
|
112
|
-
axes_facecolor="white",
|
|
113
|
-
axes_edgecolor="black",
|
|
114
|
-
grid_color="#CCCCCC",
|
|
115
|
-
grid_alpha=0.5,
|
|
116
|
-
grid_linestyle="-",
|
|
117
|
-
use_latex=False,
|
|
118
|
-
tight_layout=True,
|
|
119
|
-
rcparams={
|
|
120
|
-
"axes.linewidth": 2.0,
|
|
121
|
-
"xtick.major.width": 2.0,
|
|
122
|
-
"ytick.major.width": 2.0,
|
|
123
|
-
"xtick.major.size": 8,
|
|
124
|
-
"ytick.major.size": 8,
|
|
125
|
-
"lines.antialiased": True,
|
|
126
|
-
"savefig.dpi": 150,
|
|
127
|
-
},
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
SCREEN_PRESET = StylePreset(
|
|
131
|
-
name="screen",
|
|
132
|
-
dpi=96,
|
|
133
|
-
font_family="sans-serif",
|
|
134
|
-
font_size=10,
|
|
135
|
-
line_width=1.2,
|
|
136
|
-
marker_size=6.0,
|
|
137
|
-
figure_facecolor="white",
|
|
138
|
-
axes_facecolor="white",
|
|
139
|
-
axes_edgecolor="#333333",
|
|
140
|
-
grid_color="#B0B0B0",
|
|
141
|
-
grid_alpha=0.3,
|
|
142
|
-
grid_linestyle="-",
|
|
143
|
-
use_latex=False,
|
|
144
|
-
tight_layout=True,
|
|
145
|
-
rcparams={
|
|
146
|
-
"axes.linewidth": 1.0,
|
|
147
|
-
"lines.antialiased": True,
|
|
148
|
-
"patch.antialiased": True,
|
|
149
|
-
},
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
PRINT_PRESET = StylePreset(
|
|
153
|
-
name="print",
|
|
154
|
-
dpi=300,
|
|
155
|
-
font_family="serif",
|
|
156
|
-
font_size=11,
|
|
157
|
-
line_width=1.2,
|
|
158
|
-
marker_size=5.0,
|
|
159
|
-
figure_facecolor="white",
|
|
160
|
-
axes_facecolor="white",
|
|
161
|
-
axes_edgecolor="black",
|
|
162
|
-
grid_color="#707070",
|
|
163
|
-
grid_alpha=0.3,
|
|
164
|
-
grid_linestyle=":",
|
|
165
|
-
use_latex=False,
|
|
166
|
-
tight_layout=True,
|
|
167
|
-
rcparams={
|
|
168
|
-
"axes.linewidth": 1.0,
|
|
169
|
-
"xtick.major.width": 1.0,
|
|
170
|
-
"ytick.major.width": 1.0,
|
|
171
|
-
"lines.antialiased": False, # Sharper lines for print
|
|
172
|
-
"patch.antialiased": False,
|
|
173
|
-
"savefig.dpi": 300,
|
|
174
|
-
"savefig.format": "pdf",
|
|
175
|
-
},
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
# Registry of available presets
|
|
179
|
-
PRESETS: dict[str, StylePreset] = {
|
|
180
|
-
"publication": PUBLICATION_PRESET,
|
|
181
|
-
"presentation": PRESENTATION_PRESET,
|
|
182
|
-
"screen": SCREEN_PRESET,
|
|
183
|
-
"print": PRINT_PRESET,
|
|
36
|
+
# IEEE publication styling defaults
|
|
37
|
+
IEEE_STYLE: dict[str, str | float] = {
|
|
38
|
+
"font.family": "serif",
|
|
39
|
+
"font.size": 10,
|
|
40
|
+
"axes.linewidth": 0.8,
|
|
41
|
+
"grid.alpha": 0.3,
|
|
42
|
+
"grid.linestyle": ":",
|
|
43
|
+
"lines.linewidth": 1.0,
|
|
44
|
+
"lines.markersize": 4.0,
|
|
45
|
+
"figure.dpi": 600,
|
|
46
|
+
"savefig.dpi": 600,
|
|
47
|
+
"savefig.bbox": "tight",
|
|
184
48
|
}
|
|
185
49
|
|
|
50
|
+
# Line styles for accessibility (when color alone isn't sufficient)
|
|
51
|
+
LINE_STYLES: list[str] = ["solid", "dashed", "dotted", "dashdot"]
|
|
186
52
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
*,
|
|
191
|
-
overrides: dict[str, Any] | None = None,
|
|
192
|
-
) -> Iterator[None]:
|
|
193
|
-
"""Apply style preset as context manager.
|
|
53
|
+
# Pass/fail symbols for reports
|
|
54
|
+
PASS_SYMBOL = "✓"
|
|
55
|
+
FAIL_SYMBOL = "✗"
|
|
194
56
|
|
|
195
|
-
: Provide comprehensive style presets for common use cases
|
|
196
|
-
with support for custom overrides.
|
|
197
57
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
overrides: Dictionary of rcParams to override
|
|
58
|
+
def apply_ieee_style(ax: Axes) -> None:
|
|
59
|
+
"""Apply IEEE publication styling to axes.
|
|
201
60
|
|
|
202
|
-
|
|
203
|
-
None (use as context manager)
|
|
61
|
+
Configures fonts, line widths, and grid for publication-quality plots.
|
|
204
62
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
ImportError: If matplotlib is not available
|
|
63
|
+
Args:
|
|
64
|
+
ax: Matplotlib axes to style.
|
|
208
65
|
|
|
209
66
|
Example:
|
|
210
|
-
>>>
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
>>> # With overrides
|
|
216
|
-
>>> with apply_style_preset("screen", overrides={"font.size": 14}):
|
|
217
|
-
... plot_waveform(signal)
|
|
218
|
-
|
|
219
|
-
References:
|
|
220
|
-
VIS-024: Plot Style Presets
|
|
221
|
-
matplotlib style sheets and rcParams
|
|
67
|
+
>>> fig, ax = plt.subplots()
|
|
68
|
+
>>> ax.plot(x, y)
|
|
69
|
+
>>> apply_ieee_style(ax)
|
|
70
|
+
>>> fig.savefig("publication.pdf")
|
|
222
71
|
"""
|
|
223
72
|
if not HAS_MATPLOTLIB:
|
|
224
|
-
raise ImportError("matplotlib
|
|
73
|
+
raise ImportError("matplotlib required for styling")
|
|
225
74
|
|
|
226
|
-
#
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
raise ValueError(f"Unknown preset: {preset}. Available: {list(PRESETS.keys())}")
|
|
230
|
-
preset_obj = PRESETS[preset]
|
|
231
|
-
else:
|
|
232
|
-
preset_obj = preset
|
|
75
|
+
# Apply IEEE styling
|
|
76
|
+
for spine in ax.spines.values():
|
|
77
|
+
spine.set_linewidth(0.8)
|
|
233
78
|
|
|
234
|
-
|
|
235
|
-
rc_dict = _preset_to_rcparams(preset_obj)
|
|
79
|
+
ax.grid(True, alpha=0.3, linestyle=":")
|
|
236
80
|
|
|
237
|
-
#
|
|
238
|
-
|
|
239
|
-
rc_dict.update(overrides)
|
|
81
|
+
# Update tick parameters
|
|
82
|
+
ax.tick_params(width=0.8, labelsize=9)
|
|
240
83
|
|
|
241
|
-
#
|
|
242
|
-
|
|
243
|
-
|
|
84
|
+
# Set font properties
|
|
85
|
+
ax.xaxis.label.set_fontsize(10)
|
|
86
|
+
ax.yaxis.label.set_fontsize(10)
|
|
244
87
|
|
|
245
88
|
|
|
246
|
-
def
|
|
247
|
-
"""
|
|
89
|
+
def get_colorblind_palette(n_colors: int | None = None) -> list[str]:
|
|
90
|
+
"""Get colorblind-safe color palette.
|
|
91
|
+
|
|
92
|
+
Returns perceptually distinct colors that work for colorblind viewers.
|
|
248
93
|
|
|
249
94
|
Args:
|
|
250
|
-
|
|
95
|
+
n_colors: Number of colors to return (default: all 7).
|
|
251
96
|
|
|
252
97
|
Returns:
|
|
253
|
-
|
|
98
|
+
List of hex color codes.
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
>>> colors = get_colorblind_palette(3)
|
|
102
|
+
>>> for i, color in enumerate(colors):
|
|
103
|
+
... ax.plot(x, y[i], color=color)
|
|
254
104
|
"""
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
"axes.facecolor": preset.axes_facecolor,
|
|
263
|
-
"axes.edgecolor": preset.axes_edgecolor,
|
|
264
|
-
"grid.color": preset.grid_color,
|
|
265
|
-
"grid.alpha": preset.grid_alpha,
|
|
266
|
-
"grid.linestyle": preset.grid_linestyle,
|
|
267
|
-
"figure.autolayout": preset.tight_layout,
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
# LaTeX rendering
|
|
271
|
-
if preset.use_latex:
|
|
272
|
-
rc["text.usetex"] = True
|
|
273
|
-
|
|
274
|
-
# Merge with additional rcparams
|
|
275
|
-
rc.update(preset.rcparams)
|
|
276
|
-
|
|
277
|
-
return rc
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
def create_custom_preset(
|
|
281
|
-
name: str,
|
|
282
|
-
base_preset: str = "screen",
|
|
283
|
-
**kwargs: Any,
|
|
284
|
-
) -> StylePreset:
|
|
285
|
-
"""Create custom preset by inheriting from base preset.
|
|
286
|
-
|
|
287
|
-
: Support custom presets with inheritance and override.
|
|
105
|
+
if n_colors is None:
|
|
106
|
+
return COLORBLIND_PALETTE.copy()
|
|
107
|
+
return COLORBLIND_PALETTE[:n_colors]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def get_colorblind_cmap(name: Literal["viridis", "cividis", "plasma"] = "viridis") -> str:
|
|
111
|
+
"""Get colorblind-safe colormap name.
|
|
288
112
|
|
|
289
113
|
Args:
|
|
290
|
-
name:
|
|
291
|
-
base_preset: Base preset to inherit from
|
|
292
|
-
**kwargs: Attributes to override
|
|
114
|
+
name: Colormap name (viridis, cividis, plasma).
|
|
293
115
|
|
|
294
116
|
Returns:
|
|
295
|
-
|
|
117
|
+
Matplotlib colormap name.
|
|
118
|
+
|
|
119
|
+
Example:
|
|
120
|
+
>>> cmap = get_colorblind_cmap("viridis")
|
|
121
|
+
>>> ax.imshow(data, cmap=cmap)
|
|
122
|
+
"""
|
|
123
|
+
valid = {"viridis", "cividis", "plasma"}
|
|
124
|
+
if name not in valid:
|
|
125
|
+
raise ValueError(f"Unknown colormap: {name}. Valid: {valid}")
|
|
126
|
+
return name
|
|
127
|
+
|
|
296
128
|
|
|
297
|
-
|
|
298
|
-
|
|
129
|
+
def get_line_styles(n_lines: int) -> list[str]:
|
|
130
|
+
"""Get distinct line styles for multi-line plots.
|
|
131
|
+
|
|
132
|
+
Useful when color alone isn't sufficient (grayscale printing, accessibility).
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
n_lines: Number of line styles needed.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
List of line style strings.
|
|
299
139
|
|
|
300
140
|
Example:
|
|
301
|
-
>>>
|
|
302
|
-
|
|
303
|
-
...
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
>>> with apply_style_preset(custom):
|
|
308
|
-
... plot_data()
|
|
141
|
+
>>> styles = get_line_styles(4)
|
|
142
|
+
>>> for i, style in enumerate(styles):
|
|
143
|
+
... ax.plot(x, y[i], linestyle=style)
|
|
144
|
+
"""
|
|
145
|
+
# Cycle through available styles if needed
|
|
146
|
+
return [LINE_STYLES[i % len(LINE_STYLES)] for i in range(n_lines)]
|
|
309
147
|
|
|
310
|
-
|
|
311
|
-
|
|
148
|
+
|
|
149
|
+
def format_pass_fail(passed: bool) -> str:
|
|
150
|
+
"""Format pass/fail status with symbol.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
passed: Test result.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Formatted string with symbol.
|
|
157
|
+
|
|
158
|
+
Example:
|
|
159
|
+
>>> status = format_pass_fail(True)
|
|
160
|
+
>>> print(status) # "✓ PASS"
|
|
312
161
|
"""
|
|
313
|
-
if
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
preset_dict = {
|
|
321
|
-
"name": name,
|
|
322
|
-
"dpi": kwargs.get("dpi", base.dpi),
|
|
323
|
-
"font_family": kwargs.get("font_family", base.font_family),
|
|
324
|
-
"font_size": kwargs.get("font_size", base.font_size),
|
|
325
|
-
"line_width": kwargs.get("line_width", base.line_width),
|
|
326
|
-
"marker_size": kwargs.get("marker_size", base.marker_size),
|
|
327
|
-
"figure_facecolor": kwargs.get("figure_facecolor", base.figure_facecolor),
|
|
328
|
-
"axes_facecolor": kwargs.get("axes_facecolor", base.axes_facecolor),
|
|
329
|
-
"axes_edgecolor": kwargs.get("axes_edgecolor", base.axes_edgecolor),
|
|
330
|
-
"grid_color": kwargs.get("grid_color", base.grid_color),
|
|
331
|
-
"grid_alpha": kwargs.get("grid_alpha", base.grid_alpha),
|
|
332
|
-
"grid_linestyle": kwargs.get("grid_linestyle", base.grid_linestyle),
|
|
333
|
-
"use_latex": kwargs.get("use_latex", base.use_latex),
|
|
334
|
-
"tight_layout": kwargs.get("tight_layout", base.tight_layout),
|
|
335
|
-
"rcparams": kwargs.get("rcparams", base.rcparams.copy()),
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
return StylePreset(**preset_dict)
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
def register_preset(preset: StylePreset) -> None:
|
|
342
|
-
"""Register custom preset in global registry.
|
|
162
|
+
return f"{PASS_SYMBOL} PASS" if passed else f"{FAIL_SYMBOL} FAIL"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def apply_presentation_style(ax: Axes) -> None:
|
|
166
|
+
"""Apply presentation styling (larger fonts, thicker lines).
|
|
167
|
+
|
|
168
|
+
Optimized for slides and projection.
|
|
343
169
|
|
|
344
170
|
Args:
|
|
345
|
-
|
|
171
|
+
ax: Matplotlib axes to style.
|
|
346
172
|
|
|
347
173
|
Example:
|
|
348
|
-
>>>
|
|
349
|
-
>>>
|
|
350
|
-
>>>
|
|
351
|
-
... plot_data()
|
|
174
|
+
>>> fig, ax = plt.subplots()
|
|
175
|
+
>>> ax.plot(x, y)
|
|
176
|
+
>>> apply_presentation_style(ax)
|
|
352
177
|
"""
|
|
353
|
-
|
|
178
|
+
if not HAS_MATPLOTLIB:
|
|
179
|
+
raise ImportError("matplotlib required for styling")
|
|
354
180
|
|
|
181
|
+
# Thicker lines and larger fonts
|
|
182
|
+
for line in ax.get_lines():
|
|
183
|
+
line.set_linewidth(2.5)
|
|
355
184
|
|
|
356
|
-
|
|
357
|
-
|
|
185
|
+
for spine in ax.spines.values():
|
|
186
|
+
spine.set_linewidth(2.0)
|
|
187
|
+
|
|
188
|
+
ax.grid(True, alpha=0.5, linestyle="-", linewidth=1.0)
|
|
189
|
+
ax.tick_params(width=2.0, labelsize=14)
|
|
190
|
+
ax.xaxis.label.set_fontsize(16)
|
|
191
|
+
ax.yaxis.label.set_fontsize(16)
|
|
192
|
+
if ax.get_title():
|
|
193
|
+
ax.title.set_fontsize(18)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def get_multi_line_styles(num_lines: int) -> list[dict[str, str | float]]:
|
|
197
|
+
"""Get distinct line styles for multiple traces on one plot.
|
|
198
|
+
|
|
199
|
+
Generates unique combinations of colors and line styles to ensure
|
|
200
|
+
visual differentiation, even for colorblind users.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
num_lines: Number of distinct line styles needed.
|
|
358
204
|
|
|
359
205
|
Returns:
|
|
360
|
-
List of
|
|
206
|
+
List of style dictionaries with 'color', 'linestyle', and 'linewidth' keys.
|
|
361
207
|
|
|
362
208
|
Example:
|
|
363
|
-
>>>
|
|
364
|
-
>>>
|
|
365
|
-
|
|
209
|
+
>>> styles = get_multi_line_styles(3)
|
|
210
|
+
>>> for i, style in enumerate(styles):
|
|
211
|
+
... ax.plot(x, data[i], **style, label=f"Trace {i}")
|
|
212
|
+
|
|
213
|
+
References:
|
|
214
|
+
Tol, P. (2021). Color Schemes. Technical Note SRON/EPS/TN/09-002.
|
|
366
215
|
"""
|
|
367
|
-
|
|
216
|
+
if not HAS_MATPLOTLIB:
|
|
217
|
+
raise ImportError("matplotlib required for line styles")
|
|
218
|
+
|
|
219
|
+
styles: list[dict[str, str | float]] = []
|
|
220
|
+
for i in range(num_lines):
|
|
221
|
+
color_idx = i % len(COLORBLIND_PALETTE)
|
|
222
|
+
linestyle_idx = (i // len(COLORBLIND_PALETTE)) % len(LINE_STYLES)
|
|
223
|
+
|
|
224
|
+
style_dict: dict[str, str | float] = {
|
|
225
|
+
"color": COLORBLIND_PALETTE[color_idx],
|
|
226
|
+
"linestyle": LINE_STYLES[linestyle_idx],
|
|
227
|
+
"linewidth": 1.5,
|
|
228
|
+
}
|
|
229
|
+
styles.append(style_dict)
|
|
230
|
+
|
|
231
|
+
return styles
|
|
368
232
|
|
|
369
233
|
|
|
370
234
|
__all__ = [
|
|
371
|
-
"
|
|
372
|
-
"
|
|
373
|
-
"
|
|
374
|
-
"
|
|
375
|
-
"
|
|
376
|
-
"
|
|
377
|
-
"
|
|
378
|
-
"
|
|
379
|
-
"
|
|
380
|
-
"
|
|
235
|
+
"COLORBLIND_PALETTE",
|
|
236
|
+
"FAIL_SYMBOL",
|
|
237
|
+
"IEEE_STYLE",
|
|
238
|
+
"LINE_STYLES",
|
|
239
|
+
"PASS_SYMBOL",
|
|
240
|
+
"apply_ieee_style",
|
|
241
|
+
"apply_presentation_style",
|
|
242
|
+
"format_pass_fail",
|
|
243
|
+
"get_colorblind_cmap",
|
|
244
|
+
"get_colorblind_palette",
|
|
245
|
+
"get_line_styles",
|
|
246
|
+
"get_multi_line_styles",
|
|
381
247
|
]
|
|
@@ -296,16 +296,15 @@ def _generate_metric_plots(
|
|
|
296
296
|
"""
|
|
297
297
|
try:
|
|
298
298
|
import matplotlib.pyplot as plt
|
|
299
|
+
except ImportError:
|
|
300
|
+
return # Skip if matplotlib unavailable
|
|
299
301
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
_create_metric_plot(results, aggregated, metric, output_file)
|
|
305
|
-
plt.close()
|
|
302
|
+
for metric in metrics:
|
|
303
|
+
if metric not in aggregated:
|
|
304
|
+
continue
|
|
306
305
|
|
|
307
|
-
|
|
308
|
-
|
|
306
|
+
_create_metric_plot(results, aggregated, metric, output_file)
|
|
307
|
+
plt.close()
|
|
309
308
|
|
|
310
309
|
|
|
311
310
|
def _create_metric_plot(
|