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,451 +0,0 @@
1
- """Color palette selection and accessibility utilities.
2
-
3
- This module provides intelligent color palette selection based on data
4
- characteristics and accessibility requirements with WCAG contrast checking.
5
-
6
-
7
- Example:
8
- >>> from oscura.visualization.colors import select_optimal_palette
9
- >>> colors = select_optimal_palette(n_channels=3, palette_type="qualitative")
10
-
11
- References:
12
- WCAG 2.1 contrast guidelines
13
- Colorblind-safe palette design (Brettel 1997)
14
- ColorBrewer schemes
15
- """
16
-
17
- from typing import Literal
18
-
19
- import numpy as np
20
-
21
- # Predefined colorblind-safe palettes
22
- COLORBLIND_SAFE_QUALITATIVE = [
23
- "#0173B2", # Blue
24
- "#DE8F05", # Orange
25
- "#029E73", # Green
26
- "#CC78BC", # Purple
27
- "#CA9161", # Brown
28
- "#949494", # Gray
29
- "#ECE133", # Yellow
30
- "#56B4E9", # Light blue
31
- ]
32
-
33
- SEQUENTIAL_VIRIDIS = [
34
- "#440154",
35
- "#481567",
36
- "#482677",
37
- "#453781",
38
- "#404788",
39
- "#39568C",
40
- "#33638D",
41
- "#2D708E",
42
- "#287D8E",
43
- "#238A8D",
44
- "#1F968B",
45
- "#20A387",
46
- "#29AF7F",
47
- "#3CBB75",
48
- "#55C667",
49
- "#73D055",
50
- "#95D840",
51
- "#B8DE29",
52
- "#DCE319",
53
- "#FDE724",
54
- ]
55
-
56
- DIVERGING_COOLWARM = [
57
- "#3B4CC0",
58
- "#5977E3",
59
- "#7D9EF2",
60
- "#A2C0F9",
61
- "#C7DDFA",
62
- "#E8F0FC",
63
- "#F9EBE5",
64
- "#F6CFBB",
65
- "#F0AD8E",
66
- "#E68462",
67
- "#D8583E",
68
- "#C52A1E",
69
- "#B40426",
70
- ]
71
-
72
-
73
- def select_optimal_palette(
74
- n_colors: int,
75
- *,
76
- palette_type: Literal["sequential", "diverging", "qualitative"] | None = None,
77
- data_range: tuple[float, float] | None = None,
78
- colorblind_safe: bool = True,
79
- background_color: str = "#FFFFFF",
80
- min_contrast_ratio: float = 4.5,
81
- ) -> list[str]:
82
- """Select optimal color palette based on data characteristics.
83
-
84
- : Automatically select optimal color palettes based on
85
- data characteristics, plot type, and accessibility requirements.
86
-
87
- Args:
88
- n_colors: Number of colors needed
89
- palette_type: Type of palette ("sequential", "diverging", "qualitative")
90
- If None, auto-select based on n_colors and data_range
91
- data_range: Data range (min, max) for auto-detecting bipolar signals
92
- colorblind_safe: Ensure colorblind-safe palette (default: True)
93
- background_color: Background color for contrast checking (default: white)
94
- min_contrast_ratio: Minimum WCAG contrast ratio (default: 4.5 for AA)
95
-
96
- Returns:
97
- List of color hex codes
98
-
99
- Raises:
100
- ValueError: If n_colors is invalid or palette cannot meet requirements
101
-
102
- Example:
103
- >>> # Auto-select for 3 channels
104
- >>> colors = select_optimal_palette(3)
105
- >>> # Diverging palette for bipolar data
106
- >>> colors = select_optimal_palette(10, palette_type="diverging")
107
-
108
- References:
109
- VIS-023: Data-Driven Color Palette
110
- WCAG 2.1 contrast ratio guidelines (AA: 4.5:1, AAA: 7:1)
111
- ColorBrewer sequential/diverging schemes
112
- """
113
- if n_colors < 1:
114
- raise ValueError("n_colors must be >= 1")
115
- if min_contrast_ratio < 1.0:
116
- raise ValueError("min_contrast_ratio must be >= 1.0")
117
-
118
- # Auto-select palette type if not specified
119
- if palette_type is None:
120
- palette_type = _auto_select_palette_type(n_colors, data_range)
121
-
122
- # Select base palette
123
- if palette_type == "qualitative":
124
- base_colors = (
125
- COLORBLIND_SAFE_QUALITATIVE if colorblind_safe else _generate_qualitative(n_colors)
126
- )
127
- elif palette_type == "sequential":
128
- base_colors = SEQUENTIAL_VIRIDIS
129
- elif palette_type == "diverging":
130
- base_colors = DIVERGING_COOLWARM
131
- else:
132
- raise ValueError(f"Unknown palette_type: {palette_type}")
133
-
134
- # Sample colors if we need fewer than available
135
- if n_colors <= len(base_colors):
136
- # Evenly sample from palette
137
- indices = np.linspace(0, len(base_colors) - 1, n_colors).astype(int)
138
- colors = [base_colors[i] for i in indices]
139
- else:
140
- # Interpolate if we need more colors
141
- colors = _interpolate_colors(base_colors, n_colors)
142
-
143
- # Check contrast ratios
144
- colors_with_contrast = []
145
- bg_luminance = _relative_luminance(background_color)
146
-
147
- for color in colors:
148
- color_luminance = _relative_luminance(color)
149
- contrast = _contrast_ratio(color_luminance, bg_luminance)
150
-
151
- if contrast >= min_contrast_ratio:
152
- colors_with_contrast.append(color)
153
- else:
154
- # Adjust lightness to meet contrast requirement
155
- adjusted = _adjust_for_contrast(color, background_color, min_contrast_ratio)
156
- colors_with_contrast.append(adjusted)
157
-
158
- return colors_with_contrast
159
-
160
-
161
- def _auto_select_palette_type(
162
- n_colors: int,
163
- data_range: tuple[float, float] | None,
164
- ) -> Literal["sequential", "diverging", "qualitative"]:
165
- """Auto-select palette type based on data characteristics.
166
-
167
- Args:
168
- n_colors: Number of colors needed
169
- data_range: Data range (min, max)
170
-
171
- Returns:
172
- Palette type
173
- """
174
- # Check for bipolar data (zero-crossing)
175
- if data_range is not None:
176
- min_val, max_val = data_range
177
- if min_val < 0 and max_val > 0:
178
- # Bipolar signal - use diverging
179
- return "diverging"
180
-
181
- # Multi-channel (distinct categories)
182
- if n_colors <= 8:
183
- return "qualitative"
184
-
185
- # Many colors or continuous data
186
- return "sequential"
187
-
188
-
189
- def _relative_luminance(color: str) -> float:
190
- """Calculate relative luminance per WCAG 2.1.
191
-
192
- Args:
193
- color: Hex color code
194
-
195
- Returns:
196
- Relative luminance (0-1)
197
- """
198
- # Parse hex color
199
- color = color.removeprefix("#")
200
-
201
- r = int(color[0:2], 16) / 255.0
202
- g = int(color[2:4], 16) / 255.0
203
- b = int(color[4:6], 16) / 255.0
204
-
205
- # Convert to linear RGB
206
- def to_linear(c: float) -> float:
207
- if c <= 0.03928:
208
- return c / 12.92
209
- else:
210
- return ((c + 0.055) / 1.055) ** 2.4 # type: ignore[no-any-return]
211
-
212
- r_linear = to_linear(r)
213
- g_linear = to_linear(g)
214
- b_linear = to_linear(b)
215
-
216
- # Calculate luminance
217
- return 0.2126 * r_linear + 0.7152 * g_linear + 0.0722 * b_linear
218
-
219
-
220
- def _contrast_ratio(lum1: float, lum2: float) -> float:
221
- """Calculate WCAG contrast ratio between two luminances.
222
-
223
- Args:
224
- lum1: First luminance (0-1)
225
- lum2: Second luminance (0-1)
226
-
227
- Returns:
228
- Contrast ratio (1-21)
229
- """
230
- lighter = max(lum1, lum2)
231
- darker = min(lum1, lum2)
232
-
233
- return (lighter + 0.05) / (darker + 0.05)
234
-
235
-
236
- def _adjust_for_contrast(
237
- color: str,
238
- background: str,
239
- target_ratio: float,
240
- ) -> str:
241
- """Adjust color lightness to meet contrast requirement.
242
-
243
- Args:
244
- color: Color to adjust
245
- background: Background color
246
- target_ratio: Target contrast ratio
247
-
248
- Returns:
249
- Adjusted color hex code
250
- """
251
- # Parse color
252
- color_val = color.removeprefix("#")
253
-
254
- r = int(color_val[0:2], 16)
255
- g = int(color_val[2:4], 16)
256
- b = int(color_val[4:6], 16)
257
-
258
- # Convert to HSL for easier lightness adjustment
259
- h, s, l = _rgb_to_hsl(r, g, b)
260
-
261
- bg_lum = _relative_luminance(background)
262
-
263
- # Binary search for appropriate lightness
264
- l_min, l_max = 0.0, 1.0
265
- iterations = 0
266
- max_iterations = 20
267
-
268
- while iterations < max_iterations:
269
- # Try current lightness
270
- test_r, test_g, test_b = _hsl_to_rgb(h, s, l)
271
- test_color = f"#{test_r:02x}{test_g:02x}{test_b:02x}"
272
- test_lum = _relative_luminance(test_color)
273
- ratio = _contrast_ratio(test_lum, bg_lum)
274
-
275
- if abs(ratio - target_ratio) < 0.1:
276
- break
277
-
278
- if ratio < target_ratio:
279
- # Need more contrast - adjust lightness
280
- if bg_lum > 0.5:
281
- # Dark background - make lighter
282
- l_min = l
283
- l = (l + l_max) / 2
284
- else:
285
- # Light background - make darker
286
- l_max = l
287
- l = (l_min + l) / 2
288
- # Too much contrast - move back
289
- elif bg_lum > 0.5:
290
- l_max = l
291
- l = (l_min + l) / 2
292
- else:
293
- l_min = l
294
- l = (l + l_max) / 2
295
-
296
- iterations += 1
297
-
298
- final_r, final_g, final_b = _hsl_to_rgb(h, s, l)
299
- return f"#{final_r:02x}{final_g:02x}{final_b:02x}"
300
-
301
-
302
- def _rgb_to_hsl(r: int, g: int, b: int) -> tuple[float, float, float]:
303
- """Convert RGB to HSL color space.
304
-
305
- Args:
306
- r: Red value (0-255).
307
- g: Green value (0-255).
308
- b: Blue value (0-255).
309
-
310
- Returns:
311
- (h, s, l) tuple where h in [0, 360), s and l in [0, 1]
312
- """
313
- r_norm = r / 255.0
314
- g_norm = g / 255.0
315
- b_norm = b / 255.0
316
-
317
- max_c = max(r_norm, g_norm, b_norm)
318
- min_c = min(r_norm, g_norm, b_norm)
319
- delta = max_c - min_c
320
-
321
- # Lightness
322
- l = (max_c + min_c) / 2.0
323
-
324
- if delta == 0:
325
- # Achromatic
326
- return (0.0, 0.0, l)
327
-
328
- # Saturation
329
- s = delta / (max_c + min_c) if l < 0.5 else delta / (2.0 - max_c - min_c)
330
-
331
- # Hue
332
- if max_c == r_norm:
333
- h = ((g_norm - b_norm) / delta) % 6
334
- elif max_c == g_norm:
335
- h = ((b_norm - r_norm) / delta) + 2
336
- else:
337
- h = ((r_norm - g_norm) / delta) + 4
338
-
339
- h = h * 60.0
340
-
341
- return (h, s, l)
342
-
343
-
344
- def _hsl_to_rgb(h: float, s: float, l: float) -> tuple[int, int, int]:
345
- """Convert HSL to RGB color space.
346
-
347
- Args:
348
- h: Hue in [0, 360)
349
- s: Saturation in [0, 1]
350
- l: Lightness in [0, 1]
351
-
352
- Returns:
353
- (r, g, b) tuple with values in [0, 255]
354
- """
355
- if s == 0:
356
- # Achromatic
357
- gray = int(l * 255)
358
- return (gray, gray, gray)
359
-
360
- def hue_to_rgb(p: float, q: float, t: float) -> float:
361
- if t < 0:
362
- t += 1
363
- if t > 1:
364
- t -= 1
365
- if t < 1 / 6:
366
- return p + (q - p) * 6 * t
367
- if t < 1 / 2:
368
- return q
369
- if t < 2 / 3:
370
- return p + (q - p) * (2 / 3 - t) * 6
371
- return p
372
-
373
- q = l * (1 + s) if l < 0.5 else l + s - l * s
374
-
375
- p = 2 * l - q
376
-
377
- h_norm = h / 360.0
378
-
379
- r = hue_to_rgb(p, q, h_norm + 1 / 3)
380
- g = hue_to_rgb(p, q, h_norm)
381
- b = hue_to_rgb(p, q, h_norm - 1 / 3)
382
-
383
- return (int(r * 255), int(g * 255), int(b * 255))
384
-
385
-
386
- def _generate_qualitative(n_colors: int) -> list[str]:
387
- """Generate qualitative color palette.
388
-
389
- Args:
390
- n_colors: Number of colors
391
-
392
- Returns:
393
- List of hex color codes
394
- """
395
- # Generate evenly spaced hues
396
- colors = []
397
- for i in range(n_colors):
398
- hue = (i * 360.0 / n_colors) % 360
399
- r, g, b = _hsl_to_rgb(hue, 0.7, 0.5)
400
- colors.append(f"#{r:02x}{g:02x}{b:02x}")
401
-
402
- return colors
403
-
404
-
405
- def _interpolate_colors(base_colors: list[str], n_colors: int) -> list[str]:
406
- """Interpolate between base colors to generate more colors.
407
-
408
- Args:
409
- base_colors: Base color palette
410
- n_colors: Target number of colors
411
-
412
- Returns:
413
- List of interpolated hex color codes
414
- """
415
- if n_colors <= len(base_colors):
416
- return base_colors[:n_colors]
417
-
418
- # Convert to RGB arrays
419
- rgb_array = np.zeros((len(base_colors), 3))
420
- for i, color in enumerate(base_colors):
421
- color = color.removeprefix("#")
422
- rgb_array[i, 0] = int(color[0:2], 16)
423
- rgb_array[i, 1] = int(color[2:4], 16)
424
- rgb_array[i, 2] = int(color[4:6], 16)
425
-
426
- # Interpolate
427
- indices = np.linspace(0, len(base_colors) - 1, n_colors)
428
- interp_rgb = np.zeros((n_colors, 3))
429
-
430
- for channel in range(3):
431
- interp_rgb[:, channel] = np.interp(
432
- indices, np.arange(len(base_colors)), rgb_array[:, channel]
433
- )
434
-
435
- # Convert back to hex
436
- colors = []
437
- for i in range(n_colors):
438
- r = int(interp_rgb[i, 0])
439
- g = int(interp_rgb[i, 1])
440
- b = int(interp_rgb[i, 2])
441
- colors.append(f"#{r:02x}{g:02x}{b:02x}")
442
-
443
- return colors
444
-
445
-
446
- __all__ = [
447
- "COLORBLIND_SAFE_QUALITATIVE",
448
- "DIVERGING_COOLWARM",
449
- "SEQUENTIAL_VIRIDIS",
450
- "select_optimal_palette",
451
- ]