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.
Files changed (151) hide show
  1. oscura/__init__.py +19 -19
  2. oscura/analyzers/__init__.py +2 -0
  3. oscura/analyzers/digital/extraction.py +2 -3
  4. oscura/analyzers/digital/quality.py +1 -1
  5. oscura/analyzers/digital/timing.py +1 -1
  6. oscura/analyzers/patterns/__init__.py +66 -0
  7. oscura/analyzers/power/basic.py +3 -3
  8. oscura/analyzers/power/soa.py +1 -1
  9. oscura/analyzers/power/switching.py +3 -3
  10. oscura/analyzers/signal_classification.py +529 -0
  11. oscura/analyzers/signal_integrity/sparams.py +3 -3
  12. oscura/analyzers/statistics/basic.py +10 -7
  13. oscura/analyzers/validation.py +1 -1
  14. oscura/analyzers/waveform/measurements.py +200 -156
  15. oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
  16. oscura/analyzers/waveform/spectral.py +164 -73
  17. oscura/api/dsl/commands.py +15 -6
  18. oscura/api/server/templates/base.html +137 -146
  19. oscura/api/server/templates/export.html +84 -110
  20. oscura/api/server/templates/home.html +248 -267
  21. oscura/api/server/templates/protocols.html +44 -48
  22. oscura/api/server/templates/reports.html +27 -35
  23. oscura/api/server/templates/session_detail.html +68 -78
  24. oscura/api/server/templates/sessions.html +62 -72
  25. oscura/api/server/templates/waveforms.html +54 -64
  26. oscura/automotive/__init__.py +1 -1
  27. oscura/automotive/can/session.py +1 -1
  28. oscura/automotive/dbc/generator.py +638 -23
  29. oscura/automotive/uds/decoder.py +99 -6
  30. oscura/cli/analyze.py +8 -2
  31. oscura/cli/batch.py +36 -5
  32. oscura/cli/characterize.py +18 -4
  33. oscura/cli/export.py +47 -5
  34. oscura/cli/main.py +2 -0
  35. oscura/cli/onboarding/wizard.py +10 -6
  36. oscura/cli/pipeline.py +585 -0
  37. oscura/cli/visualize.py +6 -4
  38. oscura/convenience.py +400 -32
  39. oscura/core/measurement_result.py +286 -0
  40. oscura/core/progress.py +1 -1
  41. oscura/core/types.py +232 -239
  42. oscura/correlation/multi_protocol.py +1 -1
  43. oscura/export/legacy/__init__.py +11 -0
  44. oscura/export/legacy/wav.py +75 -0
  45. oscura/exporters/__init__.py +19 -0
  46. oscura/exporters/wireshark.py +809 -0
  47. oscura/hardware/acquisition/file.py +5 -19
  48. oscura/hardware/acquisition/saleae.py +10 -10
  49. oscura/hardware/acquisition/socketcan.py +4 -6
  50. oscura/hardware/acquisition/synthetic.py +1 -5
  51. oscura/hardware/acquisition/visa.py +6 -6
  52. oscura/hardware/security/side_channel_detector.py +5 -508
  53. oscura/inference/message_format.py +686 -1
  54. oscura/jupyter/display.py +2 -2
  55. oscura/jupyter/magic.py +3 -3
  56. oscura/loaders/__init__.py +17 -12
  57. oscura/loaders/binary.py +1 -1
  58. oscura/loaders/chipwhisperer.py +1 -2
  59. oscura/loaders/configurable.py +1 -1
  60. oscura/loaders/csv_loader.py +2 -2
  61. oscura/loaders/hdf5_loader.py +1 -1
  62. oscura/loaders/lazy.py +6 -1
  63. oscura/loaders/mmap_loader.py +0 -1
  64. oscura/loaders/numpy_loader.py +8 -7
  65. oscura/loaders/preprocessing.py +3 -5
  66. oscura/loaders/rigol.py +21 -7
  67. oscura/loaders/sigrok.py +2 -5
  68. oscura/loaders/tdms.py +3 -2
  69. oscura/loaders/tektronix.py +38 -32
  70. oscura/loaders/tss.py +20 -27
  71. oscura/loaders/vcd.py +13 -8
  72. oscura/loaders/wav.py +1 -6
  73. oscura/pipeline/__init__.py +76 -0
  74. oscura/pipeline/handlers/__init__.py +165 -0
  75. oscura/pipeline/handlers/analyzers.py +1045 -0
  76. oscura/pipeline/handlers/decoders.py +899 -0
  77. oscura/pipeline/handlers/exporters.py +1103 -0
  78. oscura/pipeline/handlers/filters.py +891 -0
  79. oscura/pipeline/handlers/loaders.py +640 -0
  80. oscura/pipeline/handlers/transforms.py +768 -0
  81. oscura/reporting/formatting/measurements.py +55 -14
  82. oscura/reporting/templates/enhanced/protocol_re.html +504 -503
  83. oscura/side_channel/__init__.py +38 -57
  84. oscura/utils/builders/signal_builder.py +5 -5
  85. oscura/utils/comparison/compare.py +7 -9
  86. oscura/utils/comparison/golden.py +1 -1
  87. oscura/utils/filtering/convenience.py +2 -2
  88. oscura/utils/math/arithmetic.py +38 -62
  89. oscura/utils/math/interpolation.py +20 -20
  90. oscura/utils/pipeline/__init__.py +4 -17
  91. oscura/utils/progressive.py +1 -4
  92. oscura/utils/triggering/edge.py +1 -1
  93. oscura/utils/triggering/pattern.py +2 -2
  94. oscura/utils/triggering/pulse.py +2 -2
  95. oscura/utils/triggering/window.py +3 -3
  96. oscura/validation/hil_testing.py +11 -11
  97. oscura/visualization/__init__.py +46 -284
  98. oscura/visualization/batch.py +72 -433
  99. oscura/visualization/plot.py +542 -53
  100. oscura/visualization/styles.py +184 -318
  101. oscura/workflows/batch/advanced.py +1 -1
  102. oscura/workflows/batch/aggregate.py +7 -8
  103. oscura/workflows/complete_re.py +251 -23
  104. oscura/workflows/digital.py +27 -4
  105. oscura/workflows/multi_trace.py +136 -17
  106. oscura/workflows/waveform.py +11 -6
  107. {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
  108. {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/RECORD +111 -136
  109. oscura/side_channel/dpa.py +0 -1025
  110. oscura/utils/optimization/__init__.py +0 -19
  111. oscura/utils/optimization/parallel.py +0 -443
  112. oscura/utils/optimization/search.py +0 -532
  113. oscura/utils/pipeline/base.py +0 -338
  114. oscura/utils/pipeline/composition.py +0 -248
  115. oscura/utils/pipeline/parallel.py +0 -449
  116. oscura/utils/pipeline/pipeline.py +0 -375
  117. oscura/utils/search/__init__.py +0 -16
  118. oscura/utils/search/anomaly.py +0 -424
  119. oscura/utils/search/context.py +0 -294
  120. oscura/utils/search/pattern.py +0 -288
  121. oscura/utils/storage/__init__.py +0 -61
  122. oscura/utils/storage/database.py +0 -1166
  123. oscura/visualization/accessibility.py +0 -526
  124. oscura/visualization/annotations.py +0 -371
  125. oscura/visualization/axis_scaling.py +0 -305
  126. oscura/visualization/colors.py +0 -451
  127. oscura/visualization/digital.py +0 -436
  128. oscura/visualization/eye.py +0 -571
  129. oscura/visualization/histogram.py +0 -281
  130. oscura/visualization/interactive.py +0 -1035
  131. oscura/visualization/jitter.py +0 -1042
  132. oscura/visualization/keyboard.py +0 -394
  133. oscura/visualization/layout.py +0 -400
  134. oscura/visualization/optimization.py +0 -1079
  135. oscura/visualization/palettes.py +0 -446
  136. oscura/visualization/power.py +0 -508
  137. oscura/visualization/power_extended.py +0 -955
  138. oscura/visualization/presets.py +0 -469
  139. oscura/visualization/protocols.py +0 -1246
  140. oscura/visualization/render.py +0 -223
  141. oscura/visualization/rendering.py +0 -444
  142. oscura/visualization/reverse_engineering.py +0 -838
  143. oscura/visualization/signal_integrity.py +0 -989
  144. oscura/visualization/specialized.py +0 -643
  145. oscura/visualization/spectral.py +0 -1226
  146. oscura/visualization/thumbnails.py +0 -340
  147. oscura/visualization/time_axis.py +0 -351
  148. oscura/visualization/waveform.py +0 -454
  149. {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
  150. {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
  151. {oscura-0.8.0.dist-info → oscura-0.10.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(