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.
Files changed (161) hide show
  1. oscura/__init__.py +19 -19
  2. oscura/__main__.py +4 -0
  3. oscura/analyzers/__init__.py +2 -0
  4. oscura/analyzers/digital/extraction.py +2 -3
  5. oscura/analyzers/digital/quality.py +1 -1
  6. oscura/analyzers/digital/timing.py +1 -1
  7. oscura/analyzers/ml/signal_classifier.py +6 -0
  8. oscura/analyzers/patterns/__init__.py +66 -0
  9. oscura/analyzers/power/basic.py +3 -3
  10. oscura/analyzers/power/soa.py +1 -1
  11. oscura/analyzers/power/switching.py +3 -3
  12. oscura/analyzers/signal_classification.py +529 -0
  13. oscura/analyzers/signal_integrity/sparams.py +3 -3
  14. oscura/analyzers/statistics/basic.py +10 -7
  15. oscura/analyzers/validation.py +1 -1
  16. oscura/analyzers/waveform/measurements.py +200 -156
  17. oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
  18. oscura/analyzers/waveform/spectral.py +182 -84
  19. oscura/api/dsl/commands.py +15 -6
  20. oscura/api/server/templates/base.html +137 -146
  21. oscura/api/server/templates/export.html +84 -110
  22. oscura/api/server/templates/home.html +248 -267
  23. oscura/api/server/templates/protocols.html +44 -48
  24. oscura/api/server/templates/reports.html +27 -35
  25. oscura/api/server/templates/session_detail.html +68 -78
  26. oscura/api/server/templates/sessions.html +62 -72
  27. oscura/api/server/templates/waveforms.html +54 -64
  28. oscura/automotive/__init__.py +1 -1
  29. oscura/automotive/can/session.py +1 -1
  30. oscura/automotive/dbc/generator.py +638 -23
  31. oscura/automotive/dtc/data.json +17 -102
  32. oscura/automotive/flexray/fibex.py +9 -1
  33. oscura/automotive/uds/decoder.py +99 -6
  34. oscura/cli/analyze.py +8 -2
  35. oscura/cli/batch.py +36 -5
  36. oscura/cli/characterize.py +18 -4
  37. oscura/cli/export.py +47 -5
  38. oscura/cli/main.py +2 -0
  39. oscura/cli/onboarding/wizard.py +10 -6
  40. oscura/cli/pipeline.py +585 -0
  41. oscura/cli/visualize.py +6 -4
  42. oscura/convenience.py +400 -32
  43. oscura/core/measurement_result.py +286 -0
  44. oscura/core/progress.py +1 -1
  45. oscura/core/schemas/device_mapping.json +2 -8
  46. oscura/core/schemas/packet_format.json +4 -24
  47. oscura/core/schemas/protocol_definition.json +2 -12
  48. oscura/core/types.py +232 -239
  49. oscura/correlation/multi_protocol.py +1 -1
  50. oscura/export/legacy/__init__.py +11 -0
  51. oscura/export/legacy/wav.py +75 -0
  52. oscura/exporters/__init__.py +19 -0
  53. oscura/exporters/wireshark.py +809 -0
  54. oscura/hardware/acquisition/file.py +5 -19
  55. oscura/hardware/acquisition/saleae.py +10 -10
  56. oscura/hardware/acquisition/socketcan.py +4 -6
  57. oscura/hardware/acquisition/synthetic.py +1 -5
  58. oscura/hardware/acquisition/visa.py +6 -6
  59. oscura/hardware/security/side_channel_detector.py +5 -508
  60. oscura/inference/message_format.py +686 -1
  61. oscura/jupyter/display.py +2 -2
  62. oscura/jupyter/magic.py +3 -3
  63. oscura/loaders/__init__.py +17 -12
  64. oscura/loaders/binary.py +1 -1
  65. oscura/loaders/chipwhisperer.py +1 -2
  66. oscura/loaders/configurable.py +1 -1
  67. oscura/loaders/csv_loader.py +2 -2
  68. oscura/loaders/hdf5_loader.py +1 -1
  69. oscura/loaders/lazy.py +6 -1
  70. oscura/loaders/mmap_loader.py +0 -1
  71. oscura/loaders/numpy_loader.py +8 -7
  72. oscura/loaders/preprocessing.py +3 -5
  73. oscura/loaders/rigol.py +21 -7
  74. oscura/loaders/sigrok.py +2 -5
  75. oscura/loaders/tdms.py +3 -2
  76. oscura/loaders/tektronix.py +38 -32
  77. oscura/loaders/tss.py +20 -27
  78. oscura/loaders/validation.py +17 -10
  79. oscura/loaders/vcd.py +13 -8
  80. oscura/loaders/wav.py +1 -6
  81. oscura/pipeline/__init__.py +76 -0
  82. oscura/pipeline/handlers/__init__.py +165 -0
  83. oscura/pipeline/handlers/analyzers.py +1045 -0
  84. oscura/pipeline/handlers/decoders.py +899 -0
  85. oscura/pipeline/handlers/exporters.py +1103 -0
  86. oscura/pipeline/handlers/filters.py +891 -0
  87. oscura/pipeline/handlers/loaders.py +640 -0
  88. oscura/pipeline/handlers/transforms.py +768 -0
  89. oscura/reporting/formatting/measurements.py +55 -14
  90. oscura/reporting/templates/enhanced/protocol_re.html +504 -503
  91. oscura/sessions/legacy.py +49 -1
  92. oscura/side_channel/__init__.py +38 -57
  93. oscura/utils/builders/signal_builder.py +5 -5
  94. oscura/utils/comparison/compare.py +7 -9
  95. oscura/utils/comparison/golden.py +1 -1
  96. oscura/utils/filtering/convenience.py +2 -2
  97. oscura/utils/math/arithmetic.py +38 -62
  98. oscura/utils/math/interpolation.py +20 -20
  99. oscura/utils/pipeline/__init__.py +4 -17
  100. oscura/utils/progressive.py +1 -4
  101. oscura/utils/triggering/edge.py +1 -1
  102. oscura/utils/triggering/pattern.py +2 -2
  103. oscura/utils/triggering/pulse.py +2 -2
  104. oscura/utils/triggering/window.py +3 -3
  105. oscura/validation/hil_testing.py +11 -11
  106. oscura/visualization/__init__.py +46 -284
  107. oscura/visualization/batch.py +72 -433
  108. oscura/visualization/plot.py +542 -53
  109. oscura/visualization/styles.py +184 -318
  110. oscura/workflows/batch/advanced.py +1 -1
  111. oscura/workflows/batch/aggregate.py +12 -9
  112. oscura/workflows/complete_re.py +251 -23
  113. oscura/workflows/digital.py +27 -4
  114. oscura/workflows/multi_trace.py +136 -17
  115. oscura/workflows/waveform.py +11 -6
  116. oscura-0.11.0.dist-info/METADATA +460 -0
  117. {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/RECORD +120 -145
  118. oscura/side_channel/dpa.py +0 -1025
  119. oscura/utils/optimization/__init__.py +0 -19
  120. oscura/utils/optimization/parallel.py +0 -443
  121. oscura/utils/optimization/search.py +0 -532
  122. oscura/utils/pipeline/base.py +0 -338
  123. oscura/utils/pipeline/composition.py +0 -248
  124. oscura/utils/pipeline/parallel.py +0 -449
  125. oscura/utils/pipeline/pipeline.py +0 -375
  126. oscura/utils/search/__init__.py +0 -16
  127. oscura/utils/search/anomaly.py +0 -424
  128. oscura/utils/search/context.py +0 -294
  129. oscura/utils/search/pattern.py +0 -288
  130. oscura/utils/storage/__init__.py +0 -61
  131. oscura/utils/storage/database.py +0 -1166
  132. oscura/visualization/accessibility.py +0 -526
  133. oscura/visualization/annotations.py +0 -371
  134. oscura/visualization/axis_scaling.py +0 -305
  135. oscura/visualization/colors.py +0 -451
  136. oscura/visualization/digital.py +0 -436
  137. oscura/visualization/eye.py +0 -571
  138. oscura/visualization/histogram.py +0 -281
  139. oscura/visualization/interactive.py +0 -1035
  140. oscura/visualization/jitter.py +0 -1042
  141. oscura/visualization/keyboard.py +0 -394
  142. oscura/visualization/layout.py +0 -400
  143. oscura/visualization/optimization.py +0 -1079
  144. oscura/visualization/palettes.py +0 -446
  145. oscura/visualization/power.py +0 -508
  146. oscura/visualization/power_extended.py +0 -955
  147. oscura/visualization/presets.py +0 -469
  148. oscura/visualization/protocols.py +0 -1246
  149. oscura/visualization/render.py +0 -223
  150. oscura/visualization/rendering.py +0 -444
  151. oscura/visualization/reverse_engineering.py +0 -838
  152. oscura/visualization/signal_integrity.py +0 -989
  153. oscura/visualization/specialized.py +0 -643
  154. oscura/visualization/spectral.py +0 -1226
  155. oscura/visualization/thumbnails.py +0 -340
  156. oscura/visualization/time_axis.py +0 -351
  157. oscura/visualization/waveform.py +0 -454
  158. oscura-0.8.0.dist-info/METADATA +0 -661
  159. {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/WHEEL +0 -0
  160. {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/entry_points.txt +0 -0
  161. {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,381 +1,247 @@
1
- """Plot style presets for different output contexts.
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 apply_style_preset
9
- >>> with apply_style_preset("publication"):
10
- ... plot_waveform(signal)
11
-
12
- References:
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 contextlib import contextmanager
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
- import matplotlib.pyplot as plt
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
- @dataclass
35
- class StylePreset:
36
- """Style preset configuration for plots.
37
-
38
- Attributes:
39
- name: Preset name
40
- dpi: Target DPI (dots per inch)
41
- font_family: Font family (serif, sans-serif, monospace)
42
- font_size: Base font size in points
43
- line_width: Default line width in points
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
- name: str
57
- dpi: int = 96
58
- font_family: str = "sans-serif"
59
- font_size: int = 10
60
- line_width: float = 1.0
61
- marker_size: float = 6.0
62
- figure_facecolor: str = "white"
63
- axes_facecolor: str = "white"
64
- axes_edgecolor: str = "black"
65
- grid_color: str = "#B0B0B0"
66
- grid_alpha: float = 0.3
67
- grid_linestyle: str = "-"
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
- @contextmanager
188
- def apply_style_preset(
189
- preset: str | StylePreset,
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
- Args:
199
- preset: Preset name or StylePreset object
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
- Yields:
203
- None (use as context manager)
61
+ Configures fonts, line widths, and grid for publication-quality plots.
204
62
 
205
- Raises:
206
- ValueError: If preset name is unknown
207
- ImportError: If matplotlib is not available
63
+ Args:
64
+ ax: Matplotlib axes to style.
208
65
 
209
66
  Example:
210
- >>> with apply_style_preset("publication"):
211
- ... fig, ax = plt.subplots()
212
- ... ax.plot(x, y)
213
- ... plt.savefig("figure.pdf")
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 is required for style presets")
73
+ raise ImportError("matplotlib required for styling")
225
74
 
226
- # Get preset object
227
- if isinstance(preset, str):
228
- if preset not in PRESETS:
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
- # Build rcParams dictionary
235
- rc_dict = _preset_to_rcparams(preset_obj)
79
+ ax.grid(True, alpha=0.3, linestyle=":")
236
80
 
237
- # Apply overrides
238
- if overrides:
239
- rc_dict.update(overrides)
81
+ # Update tick parameters
82
+ ax.tick_params(width=0.8, labelsize=9)
240
83
 
241
- # Apply as context
242
- with plt.rc_context(rc_dict):
243
- yield
84
+ # Set font properties
85
+ ax.xaxis.label.set_fontsize(10)
86
+ ax.yaxis.label.set_fontsize(10)
244
87
 
245
88
 
246
- def _preset_to_rcparams(preset: StylePreset) -> dict[str, Any]:
247
- """Convert StylePreset to matplotlib rcParams dictionary.
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
- preset: StylePreset object
95
+ n_colors: Number of colors to return (default: all 7).
251
96
 
252
97
  Returns:
253
- Dictionary of rcParams
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
- rc = {
256
- "figure.dpi": preset.dpi,
257
- "font.family": preset.font_family,
258
- "font.size": preset.font_size,
259
- "lines.linewidth": preset.line_width,
260
- "lines.markersize": preset.marker_size,
261
- "figure.facecolor": preset.figure_facecolor,
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: Name for custom preset
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
- Custom StylePreset object
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
- Raises:
298
- ValueError: If base_preset is unknown
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
- >>> custom = create_custom_preset(
302
- ... "my_style",
303
- ... base_preset="publication",
304
- ... font_size=12,
305
- ... line_width=1.5
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
- References:
311
- VIS-024: Plot Style Presets with inheritance
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 base_preset not in PRESETS:
314
- raise ValueError(f"Unknown base_preset: {base_preset}")
315
-
316
- # Get base preset
317
- base = PRESETS[base_preset]
318
-
319
- # Create copy with overrides
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
- preset: StylePreset to register
171
+ ax: Matplotlib axes to style.
346
172
 
347
173
  Example:
348
- >>> custom = create_custom_preset("my_style", base_preset="publication")
349
- >>> register_preset(custom)
350
- >>> with apply_style_preset("my_style"):
351
- ... plot_data()
174
+ >>> fig, ax = plt.subplots()
175
+ >>> ax.plot(x, y)
176
+ >>> apply_presentation_style(ax)
352
177
  """
353
- PRESETS[preset.name] = preset
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
- def list_presets() -> list[str]:
357
- """Get list of available preset names.
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 preset names
206
+ List of style dictionaries with 'color', 'linestyle', and 'linewidth' keys.
361
207
 
362
208
  Example:
363
- >>> presets = list_presets()
364
- >>> print(presets)
365
- ['publication', 'presentation', 'screen', 'print']
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
- return list(PRESETS.keys())
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
- "PRESENTATION_PRESET",
372
- "PRESETS",
373
- "PRINT_PRESET",
374
- "PUBLICATION_PRESET",
375
- "SCREEN_PRESET",
376
- "StylePreset",
377
- "apply_style_preset",
378
- "create_custom_preset",
379
- "list_presets",
380
- "register_preset",
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
  ]
@@ -22,7 +22,7 @@ try:
22
22
 
23
23
  _HAS_PANDAS = True
24
24
  except ImportError:
25
- pd = None # type: ignore[assignment]
25
+ pd = None
26
26
  _HAS_PANDAS = False
27
27
 
28
28
  if TYPE_CHECKING:
@@ -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
- for metric in metrics:
301
- if metric not in aggregated:
302
- continue
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
- except ImportError:
308
- pass # Skip if matplotlib unavailable
306
+ _create_metric_plot(results, aggregated, metric, output_file)
307
+ plt.close()
309
308
 
310
309
 
311
310
  def _create_metric_plot(
@@ -340,7 +339,11 @@ def _create_metric_plot(
340
339
  plot_file.parent.mkdir(parents=True, exist_ok=True)
341
340
  plt.savefig(plot_file)
342
341
  else:
343
- plt.show()
342
+ # Try to show, but gracefully handle non-interactive backends
343
+ try:
344
+ plt.show()
345
+ except Exception:
346
+ pass # Silently skip if backend doesn't support interactive display
344
347
 
345
348
 
346
349
  def _plot_histogram(