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.
Files changed (175) 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/eye/__init__.py +5 -1
  7. oscura/analyzers/eye/generation.py +501 -0
  8. oscura/analyzers/jitter/__init__.py +6 -6
  9. oscura/analyzers/jitter/timing.py +419 -0
  10. oscura/analyzers/patterns/__init__.py +94 -0
  11. oscura/analyzers/patterns/reverse_engineering.py +991 -0
  12. oscura/analyzers/power/__init__.py +35 -12
  13. oscura/analyzers/power/basic.py +3 -3
  14. oscura/analyzers/power/soa.py +1 -1
  15. oscura/analyzers/power/switching.py +3 -3
  16. oscura/analyzers/signal_classification.py +529 -0
  17. oscura/analyzers/signal_integrity/sparams.py +3 -3
  18. oscura/analyzers/statistics/__init__.py +4 -0
  19. oscura/analyzers/statistics/basic.py +152 -0
  20. oscura/analyzers/statistics/correlation.py +47 -6
  21. oscura/analyzers/validation.py +1 -1
  22. oscura/analyzers/waveform/__init__.py +2 -0
  23. oscura/analyzers/waveform/measurements.py +329 -163
  24. oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
  25. oscura/analyzers/waveform/spectral.py +498 -54
  26. oscura/api/dsl/commands.py +15 -6
  27. oscura/api/server/templates/base.html +137 -146
  28. oscura/api/server/templates/export.html +84 -110
  29. oscura/api/server/templates/home.html +248 -267
  30. oscura/api/server/templates/protocols.html +44 -48
  31. oscura/api/server/templates/reports.html +27 -35
  32. oscura/api/server/templates/session_detail.html +68 -78
  33. oscura/api/server/templates/sessions.html +62 -72
  34. oscura/api/server/templates/waveforms.html +54 -64
  35. oscura/automotive/__init__.py +1 -1
  36. oscura/automotive/can/session.py +1 -1
  37. oscura/automotive/dbc/generator.py +638 -23
  38. oscura/automotive/dtc/data.json +102 -17
  39. oscura/automotive/uds/decoder.py +99 -6
  40. oscura/cli/analyze.py +8 -2
  41. oscura/cli/batch.py +36 -5
  42. oscura/cli/characterize.py +18 -4
  43. oscura/cli/export.py +47 -5
  44. oscura/cli/main.py +2 -0
  45. oscura/cli/onboarding/wizard.py +10 -6
  46. oscura/cli/pipeline.py +585 -0
  47. oscura/cli/visualize.py +6 -4
  48. oscura/convenience.py +400 -32
  49. oscura/core/config/loader.py +0 -1
  50. oscura/core/measurement_result.py +286 -0
  51. oscura/core/progress.py +1 -1
  52. oscura/core/schemas/device_mapping.json +8 -2
  53. oscura/core/schemas/packet_format.json +24 -4
  54. oscura/core/schemas/protocol_definition.json +12 -2
  55. oscura/core/types.py +300 -199
  56. oscura/correlation/multi_protocol.py +1 -1
  57. oscura/export/legacy/__init__.py +11 -0
  58. oscura/export/legacy/wav.py +75 -0
  59. oscura/exporters/__init__.py +19 -0
  60. oscura/exporters/wireshark.py +809 -0
  61. oscura/hardware/acquisition/file.py +5 -19
  62. oscura/hardware/acquisition/saleae.py +10 -10
  63. oscura/hardware/acquisition/socketcan.py +4 -6
  64. oscura/hardware/acquisition/synthetic.py +1 -5
  65. oscura/hardware/acquisition/visa.py +6 -6
  66. oscura/hardware/security/side_channel_detector.py +5 -508
  67. oscura/inference/message_format.py +686 -1
  68. oscura/jupyter/display.py +2 -2
  69. oscura/jupyter/magic.py +3 -3
  70. oscura/loaders/__init__.py +17 -12
  71. oscura/loaders/binary.py +1 -1
  72. oscura/loaders/chipwhisperer.py +1 -2
  73. oscura/loaders/configurable.py +1 -1
  74. oscura/loaders/csv_loader.py +2 -2
  75. oscura/loaders/hdf5_loader.py +1 -1
  76. oscura/loaders/lazy.py +6 -1
  77. oscura/loaders/mmap_loader.py +0 -1
  78. oscura/loaders/numpy_loader.py +8 -7
  79. oscura/loaders/preprocessing.py +3 -5
  80. oscura/loaders/rigol.py +21 -7
  81. oscura/loaders/sigrok.py +2 -5
  82. oscura/loaders/tdms.py +3 -2
  83. oscura/loaders/tektronix.py +38 -32
  84. oscura/loaders/tss.py +20 -27
  85. oscura/loaders/vcd.py +13 -8
  86. oscura/loaders/wav.py +1 -6
  87. oscura/pipeline/__init__.py +76 -0
  88. oscura/pipeline/handlers/__init__.py +165 -0
  89. oscura/pipeline/handlers/analyzers.py +1045 -0
  90. oscura/pipeline/handlers/decoders.py +899 -0
  91. oscura/pipeline/handlers/exporters.py +1103 -0
  92. oscura/pipeline/handlers/filters.py +891 -0
  93. oscura/pipeline/handlers/loaders.py +640 -0
  94. oscura/pipeline/handlers/transforms.py +768 -0
  95. oscura/reporting/__init__.py +88 -1
  96. oscura/reporting/automation.py +348 -0
  97. oscura/reporting/citations.py +374 -0
  98. oscura/reporting/core.py +54 -0
  99. oscura/reporting/formatting/__init__.py +11 -0
  100. oscura/reporting/formatting/measurements.py +320 -0
  101. oscura/reporting/html.py +57 -0
  102. oscura/reporting/interpretation.py +431 -0
  103. oscura/reporting/summary.py +329 -0
  104. oscura/reporting/templates/enhanced/protocol_re.html +504 -503
  105. oscura/reporting/visualization.py +542 -0
  106. oscura/side_channel/__init__.py +38 -57
  107. oscura/utils/builders/signal_builder.py +5 -5
  108. oscura/utils/comparison/compare.py +7 -9
  109. oscura/utils/comparison/golden.py +1 -1
  110. oscura/utils/filtering/convenience.py +2 -2
  111. oscura/utils/math/arithmetic.py +38 -62
  112. oscura/utils/math/interpolation.py +20 -20
  113. oscura/utils/pipeline/__init__.py +4 -17
  114. oscura/utils/progressive.py +1 -4
  115. oscura/utils/triggering/edge.py +1 -1
  116. oscura/utils/triggering/pattern.py +2 -2
  117. oscura/utils/triggering/pulse.py +2 -2
  118. oscura/utils/triggering/window.py +3 -3
  119. oscura/validation/hil_testing.py +11 -11
  120. oscura/visualization/__init__.py +47 -284
  121. oscura/visualization/batch.py +160 -0
  122. oscura/visualization/plot.py +542 -53
  123. oscura/visualization/styles.py +184 -318
  124. oscura/workflows/__init__.py +2 -0
  125. oscura/workflows/batch/advanced.py +1 -1
  126. oscura/workflows/batch/aggregate.py +7 -8
  127. oscura/workflows/complete_re.py +251 -23
  128. oscura/workflows/digital.py +27 -4
  129. oscura/workflows/multi_trace.py +136 -17
  130. oscura/workflows/waveform.py +788 -0
  131. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
  132. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/RECORD +135 -149
  133. oscura/side_channel/dpa.py +0 -1025
  134. oscura/utils/optimization/__init__.py +0 -19
  135. oscura/utils/optimization/parallel.py +0 -443
  136. oscura/utils/optimization/search.py +0 -532
  137. oscura/utils/pipeline/base.py +0 -338
  138. oscura/utils/pipeline/composition.py +0 -248
  139. oscura/utils/pipeline/parallel.py +0 -449
  140. oscura/utils/pipeline/pipeline.py +0 -375
  141. oscura/utils/search/__init__.py +0 -16
  142. oscura/utils/search/anomaly.py +0 -424
  143. oscura/utils/search/context.py +0 -294
  144. oscura/utils/search/pattern.py +0 -288
  145. oscura/utils/storage/__init__.py +0 -61
  146. oscura/utils/storage/database.py +0 -1166
  147. oscura/visualization/accessibility.py +0 -526
  148. oscura/visualization/annotations.py +0 -371
  149. oscura/visualization/axis_scaling.py +0 -305
  150. oscura/visualization/colors.py +0 -451
  151. oscura/visualization/digital.py +0 -436
  152. oscura/visualization/eye.py +0 -571
  153. oscura/visualization/histogram.py +0 -281
  154. oscura/visualization/interactive.py +0 -1035
  155. oscura/visualization/jitter.py +0 -1042
  156. oscura/visualization/keyboard.py +0 -394
  157. oscura/visualization/layout.py +0 -400
  158. oscura/visualization/optimization.py +0 -1079
  159. oscura/visualization/palettes.py +0 -446
  160. oscura/visualization/power.py +0 -508
  161. oscura/visualization/power_extended.py +0 -955
  162. oscura/visualization/presets.py +0 -469
  163. oscura/visualization/protocols.py +0 -1246
  164. oscura/visualization/render.py +0 -223
  165. oscura/visualization/rendering.py +0 -444
  166. oscura/visualization/reverse_engineering.py +0 -838
  167. oscura/visualization/signal_integrity.py +0 -989
  168. oscura/visualization/specialized.py +0 -643
  169. oscura/visualization/spectral.py +0 -1226
  170. oscura/visualization/thumbnails.py +0 -340
  171. oscura/visualization/time_axis.py +0 -351
  172. oscura/visualization/waveform.py +0 -454
  173. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
  174. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
  175. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,446 +0,0 @@
1
- """Colorblind-safe palettes for Oscura visualizations.
2
-
3
- This module provides colorblind-safe color palettes and utilities for
4
- creating accessible visualizations.
5
-
6
-
7
- Example:
8
- >>> from oscura.visualization.palettes import get_palette, show_palette
9
- >>> colors = get_palette("colorblind_safe")
10
- >>> show_palette("viridis")
11
-
12
- References:
13
- - Wong, B. (2011). Color blindness. Nature Methods, 8(6), 441.
14
- - Colorbrewer 2.0 (colorbrewer2.org)
15
- - WCAG 2.1 color contrast guidelines
16
- """
17
-
18
- from __future__ import annotations
19
-
20
- from typing import TYPE_CHECKING, Literal
21
-
22
- import matplotlib.pyplot as plt
23
- import numpy as np
24
- from matplotlib.patches import Rectangle
25
-
26
- if TYPE_CHECKING:
27
- from matplotlib.colors import Colormap
28
-
29
- # Define colorblind-safe palettes
30
- # Based on Wong 2011 and Paul Tol's schemes
31
- PALETTES = {
32
- "colorblind_safe": [
33
- "#0173B2", # Blue
34
- "#DE8F05", # Orange
35
- "#029E73", # Green
36
- "#CC78BC", # Purple
37
- "#CA9161", # Brown
38
- "#FBAFE4", # Pink
39
- "#949494", # Gray
40
- "#ECE133", # Yellow
41
- ],
42
- "colorblind8": [ # Paul Tol's bright scheme
43
- "#4477AA", # Blue
44
- "#EE6677", # Red
45
- "#228833", # Green
46
- "#CCBB44", # Yellow
47
- "#66CCEE", # Cyan
48
- "#AA3377", # Purple
49
- "#BBBBBB", # Gray
50
- "#EE8866", # Orange
51
- ],
52
- "high_contrast": [
53
- "#000000", # Black
54
- "#E69F00", # Orange
55
- "#56B4E9", # Sky Blue
56
- "#009E73", # Bluish Green
57
- "#F0E442", # Yellow
58
- "#0072B2", # Blue
59
- "#D55E00", # Vermillion
60
- "#CC79A7", # Reddish Purple
61
- ],
62
- "grayscale": [
63
- "#000000", # Black
64
- "#404040", # Dark Gray
65
- "#808080", # Medium Gray
66
- "#BFBFBF", # Light Gray
67
- "#E0E0E0", # Very Light Gray
68
- ],
69
- }
70
-
71
- # Line styles for multi-line plots
72
- LINE_STYLES = ["solid", "dashed", "dotted", "dashdot"]
73
-
74
- # Marker styles for scatter plots
75
- MARKER_STYLES = ["o", "s", "^", "D", "v", "p", "*", "h"]
76
-
77
-
78
- def get_palette(
79
- name: Literal[
80
- "colorblind_safe", "colorblind8", "high_contrast", "grayscale"
81
- ] = "colorblind_safe",
82
- ) -> list[str]:
83
- """Get a colorblind-safe color palette.
84
-
85
- : Default palette is colorblind-safe.
86
- Returns list of hex color codes that are distinguishable for colorblind users.
87
-
88
- Args:
89
- name: Palette name. Options:
90
- - "colorblind_safe": Default palette (Wong 2011) with 8 colors
91
- - "colorblind8": Paul Tol's bright scheme with 8 colors
92
- - "high_contrast": High contrast palette suitable for presentations
93
- - "grayscale": Grayscale palette for printing
94
-
95
- Returns:
96
- List of hex color codes
97
-
98
- Raises:
99
- ValueError: If palette name is not recognized
100
-
101
- Example:
102
- >>> from oscura.visualization.palettes import get_palette
103
- >>> colors = get_palette("colorblind_safe")
104
- >>> print(colors[0]) # First color (blue)
105
- #0173B2
106
-
107
- References:
108
- ACC-001: Colorblind-Safe Visualization Palette
109
- """
110
- if name not in PALETTES:
111
- valid = ", ".join(PALETTES.keys())
112
- raise ValueError(f"Unknown palette: {name}. Valid options: {valid}")
113
- return PALETTES[name].copy()
114
-
115
-
116
- def get_colormap(
117
- name: Literal["viridis", "cividis", "plasma", "inferno", "magma"] = "viridis",
118
- ) -> Colormap:
119
- """Get a colorblind-safe matplotlib colormap.
120
-
121
- : Default palette is colorblind-safe (e.g., viridis, cividis).
122
- Returns perceptually uniform colormaps suitable for continuous data.
123
-
124
- Args:
125
- name: Colormap name. All options are colorblind-safe:
126
- - "viridis": Default, blue-green-yellow (recommended)
127
- - "cividis": Blue-yellow, optimized for CVD
128
- - "plasma": Purple-red-yellow
129
- - "inferno": Black-purple-yellow
130
- - "magma": Black-purple-white
131
-
132
- Returns:
133
- Matplotlib Colormap object
134
-
135
- Raises:
136
- ValueError: If colormap name is not recognized
137
-
138
- Example:
139
- >>> from oscura.visualization.palettes import get_colormap
140
- >>> import matplotlib.pyplot as plt
141
- >>> cmap = get_colormap("viridis")
142
- >>> plt.imshow(data, cmap=cmap)
143
-
144
- References:
145
- ACC-001: Colorblind-Safe Visualization Palette
146
- """
147
- valid = ["viridis", "cividis", "plasma", "inferno", "magma"]
148
- if name not in valid:
149
- raise ValueError(f"Unknown colormap: {name}. Valid options: {', '.join(valid)}")
150
- return plt.get_cmap(name)
151
-
152
-
153
- def get_line_styles(
154
- n_lines: int,
155
- *,
156
- palette: str = "colorblind_safe",
157
- cycle_styles: bool = True,
158
- ) -> list[dict]: # type: ignore[type-arg]
159
- """Get line styles for multi-line plots.
160
-
161
- : Multi-line plots use distinct line styles in addition to colors.
162
- Combines colors with line styles for maximum distinguishability.
163
-
164
- Args:
165
- n_lines: Number of lines to style
166
- palette: Palette name for colors
167
- cycle_styles: Cycle through line styles if more lines than styles
168
-
169
- Returns:
170
- List of style dictionaries with 'color' and 'linestyle' keys
171
-
172
- Example:
173
- >>> from oscura.visualization.palettes import get_line_styles
174
- >>> styles = get_line_styles(4)
175
- >>> for i, style in enumerate(styles):
176
- ... plt.plot(x, y[i], **style, label=f"Line {i}")
177
-
178
- References:
179
- ACC-001: Multi-line plots use distinct line styles in addition to colors
180
- """
181
- colors = get_palette(palette) # type: ignore[arg-type]
182
- styles = []
183
-
184
- for i in range(n_lines):
185
- color = colors[i % len(colors)]
186
- linestyle = LINE_STYLES[i % len(LINE_STYLES)] if cycle_styles else "solid"
187
-
188
- styles.append({"color": color, "linestyle": linestyle})
189
-
190
- return styles
191
-
192
-
193
- def get_pass_fail_symbols() -> dict[str, str]:
194
- """Get pass/fail symbols for accessible reporting.
195
-
196
- : Pass/fail uses symbols (✓/✗) not just red/green.
197
- Returns symbols that work in text and don't rely on color alone.
198
-
199
- Returns:
200
- Dictionary with 'pass' and 'fail' symbol keys
201
-
202
- Example:
203
- >>> from oscura.visualization.palettes import get_pass_fail_symbols
204
- >>> symbols = get_pass_fail_symbols()
205
- >>> print(f"{symbols['pass']} Test passed")
206
- ✓ Test passed
207
-
208
- References:
209
- ACC-001: Pass/fail uses symbols (✓/✗) not just red/green
210
- """
211
- return {"pass": "✓", "fail": "✗"}
212
-
213
-
214
- def get_pass_fail_colors(
215
- *,
216
- colorblind_safe: bool = True,
217
- ) -> dict[str, str]:
218
- """Get pass/fail colors.
219
-
220
- : Pass/fail colors are colorblind-safe when combined with symbols.
221
- Returns green/red or blue/orange based on colorblind_safe flag.
222
-
223
- Args:
224
- colorblind_safe: Use colorblind-safe blue/orange instead of green/red
225
-
226
- Returns:
227
- Dictionary with 'pass' and 'fail' color keys (hex codes)
228
-
229
- Example:
230
- >>> from oscura.visualization.palettes import get_pass_fail_colors
231
- >>> colors = get_pass_fail_colors(colorblind_safe=True)
232
- >>> print(colors['pass']) # Blue for pass
233
- #0173B2
234
-
235
- References:
236
- ACC-001: Colorblind-Safe Visualization Palette
237
- """
238
- if colorblind_safe:
239
- return {
240
- "pass": "#0173B2", # Blue
241
- "fail": "#DE8F05", # Orange
242
- }
243
- else:
244
- return {
245
- "pass": "#2CA02C", # Green
246
- "fail": "#D62728", # Red
247
- }
248
-
249
-
250
- def show_palette(
251
- name: str = "colorblind_safe",
252
- *,
253
- save_path: str | None = None,
254
- ) -> None:
255
- """Display a color palette preview.
256
-
257
- : Palette preview: show_palette(name).
258
- Shows palette colors in a matplotlib figure for visual inspection.
259
-
260
- Args:
261
- name: Palette name or colormap name
262
- save_path: Optional path to save the figure
263
-
264
- Raises:
265
- ValueError: If unknown palette or colormap name.
266
-
267
- Example:
268
- >>> from oscura.visualization.palettes import show_palette
269
- >>> show_palette("colorblind_safe")
270
- >>> show_palette("viridis", save_path="palette.png")
271
-
272
- References:
273
- ACC-001: Colorblind-Safe Visualization Palette
274
- """
275
- _fig, ax = plt.subplots(figsize=(8, 2))
276
-
277
- # Check if it's a discrete palette or continuous colormap
278
- if name in PALETTES:
279
- # Discrete palette
280
- colors = PALETTES[name]
281
- n_colors = len(colors)
282
- x = np.arange(n_colors)
283
-
284
- # Create color swatches
285
- for i, color in enumerate(colors):
286
- ax.add_patch(Rectangle((i, 0), 1, 1, facecolor=color, edgecolor="black"))
287
-
288
- ax.set_xlim(0, n_colors)
289
- ax.set_ylim(0, 1)
290
- ax.set_yticks([])
291
- ax.set_xticks(x + 0.5)
292
- ax.set_xticklabels([f"C{i}" for i in range(n_colors)])
293
- ax.set_title(f"Palette: {name}")
294
-
295
- else:
296
- # Continuous colormap
297
- try:
298
- cmap = get_colormap(name) # type: ignore[arg-type]
299
- gradient = np.linspace(0, 1, 256).reshape(1, -1)
300
- ax.imshow(gradient, aspect="auto", cmap=cmap)
301
- ax.set_yticks([])
302
- ax.set_xticks([0, 64, 128, 192, 255])
303
- ax.set_xticklabels(["0", "0.25", "0.5", "0.75", "1.0"])
304
- ax.set_title(f"Colormap: {name}")
305
- except ValueError:
306
- raise ValueError(f"Unknown palette or colormap: {name}")
307
-
308
- plt.tight_layout()
309
-
310
- if save_path:
311
- plt.savefig(save_path, dpi=150, bbox_inches="tight")
312
- else:
313
- plt.show()
314
-
315
-
316
- def create_custom_palette(
317
- colors: list[str],
318
- *,
319
- name: str = "custom",
320
- ) -> list[str]:
321
- """Create a custom color palette.
322
-
323
- : Custom palette creation support.
324
- Validates and registers a custom color palette.
325
-
326
- Args:
327
- colors: List of hex color codes
328
- name: Name for the custom palette
329
-
330
- Returns:
331
- List of validated hex color codes
332
-
333
- Raises:
334
- ValueError: If color codes are invalid
335
-
336
- Example:
337
- >>> from oscura.visualization.palettes import create_custom_palette
338
- >>> custom = create_custom_palette(
339
- ... ["#FF0000", "#00FF00", "#0000FF"],
340
- ... name="rgb"
341
- ... )
342
- >>> print(custom)
343
- ['#FF0000', '#00FF00', '#0000FF']
344
-
345
- References:
346
- ACC-001: Custom palette creation support
347
- """
348
- # Validate hex codes
349
- import re
350
-
351
- hex_pattern = re.compile(r"^#[0-9A-Fa-f]{6}$")
352
- validated = []
353
-
354
- for color in colors:
355
- if not hex_pattern.match(color):
356
- raise ValueError(f"Invalid hex color code: {color}")
357
- validated.append(color.upper())
358
-
359
- # Optionally register the palette
360
- PALETTES[name] = validated
361
-
362
- return validated
363
-
364
-
365
- def simulate_colorblindness(
366
- color: str,
367
- *,
368
- deficiency: Literal["protanopia", "deuteranopia", "tritanopia"] = "deuteranopia",
369
- ) -> str:
370
- """Simulate how a color appears with color vision deficiency.
371
-
372
- : Test with color blindness simulators.
373
- Converts a color to approximate how it appears with CVD.
374
-
375
- Args:
376
- color: Hex color code
377
- deficiency: Type of color vision deficiency:
378
- - "protanopia": Red-blind (1% of males)
379
- - "deuteranopia": Green-blind (1% of males)
380
- - "tritanopia": Blue-blind (rare)
381
-
382
- Returns:
383
- Simulated hex color code
384
-
385
- Raises:
386
- ValueError: If unknown deficiency type.
387
-
388
- Example:
389
- >>> from oscura.visualization.palettes import simulate_colorblindness
390
- >>> red = "#FF0000"
391
- >>> simulated = simulate_colorblindness(red, deficiency="deuteranopia")
392
- >>> print(simulated) # Appears brownish
393
- #9C7A00
394
-
395
- References:
396
- ACC-001: Test with color blindness simulators
397
- Brettel, H., Viénot, F., & Mollon, J. D. (1997). Computerized simulation
398
- of color appearance for dichromats. JOSA A, 14(10), 2647-2655.
399
- """
400
- # Convert hex to RGB
401
- hex_color = color.lstrip("#")
402
- r, g, b = int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)
403
-
404
- # Normalize to 0-1
405
- r, g, b = r / 255.0, g / 255.0, b / 255.0 # type: ignore[assignment]
406
-
407
- # Apply transformation matrices (simplified Brettel algorithm)
408
- if deficiency == "protanopia":
409
- # Red-blind: confuse red and green
410
- r_sim = 0.56667 * r + 0.43333 * g
411
- g_sim = 0.55833 * r + 0.44167 * g
412
- b_sim = b
413
- elif deficiency == "deuteranopia":
414
- # Green-blind: confuse red and green
415
- r_sim = 0.625 * r + 0.375 * g
416
- g_sim = 0.7 * r + 0.3 * g
417
- b_sim = b
418
- elif deficiency == "tritanopia":
419
- # Blue-blind: confuse blue and yellow
420
- r_sim = r
421
- g_sim = 0.95 * g + 0.05 * b
422
- b_sim = 0.433 * g + 0.567 * b # type: ignore[assignment]
423
- else:
424
- raise ValueError(f"Unknown deficiency: {deficiency}")
425
-
426
- # Convert back to 0-255 and hex
427
- r_int = int(np.clip(r_sim * 255, 0, 255))
428
- g_int = int(np.clip(g_sim * 255, 0, 255))
429
- b_int = int(np.clip(b_sim * 255, 0, 255))
430
-
431
- return f"#{r_int:02X}{g_int:02X}{b_int:02X}"
432
-
433
-
434
- __all__ = [
435
- "LINE_STYLES",
436
- "MARKER_STYLES",
437
- "PALETTES",
438
- "create_custom_palette",
439
- "get_colormap",
440
- "get_line_styles",
441
- "get_palette",
442
- "get_pass_fail_colors",
443
- "get_pass_fail_symbols",
444
- "show_palette",
445
- "simulate_colorblindness",
446
- ]