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,449 +0,0 @@
1
- """Parallel pipeline execution with automatic dependency analysis.
2
-
3
- This module provides a parallel-capable Pipeline that analyzes stage dependencies
4
- and executes independent stages concurrently using thread or process pools.
5
-
6
- The ParallelPipeline maintains full API compatibility with the standard Pipeline
7
- while providing linear speedup for independent transformations.
8
-
9
- Example:
10
- >>> from oscura.utils.pipeline import ParallelPipeline
11
- >>> # Create pipeline with independent branches
12
- >>> pipeline = ParallelPipeline([
13
- ... ('filter1', LowPassFilter(cutoff=1e6)),
14
- ... ('filter2', HighPassFilter(cutoff=1e5)),
15
- ... ('merge', MergeTransformer())
16
- ... ])
17
- >>> # Independent filters run in parallel
18
- >>> result = pipeline.transform(trace)
19
-
20
- Performance:
21
- For N independent stages, ParallelPipeline provides up to Nx speedup
22
- compared to sequential execution. Overhead is ~10ms for thread pool,
23
- ~50ms for process pool.
24
-
25
- References:
26
- IEEE 1057-2017: Parallel processing for digitizer characterization
27
- sklearn.pipeline.Pipeline: Sequential pipeline pattern
28
- """
29
-
30
- from __future__ import annotations
31
-
32
- import os
33
- from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
34
- from typing import TYPE_CHECKING, Literal
35
-
36
- from .pipeline import Pipeline
37
-
38
- if TYPE_CHECKING:
39
- from collections.abc import Sequence
40
-
41
- from oscura.core.types import WaveformTrace
42
-
43
- from .base import TraceTransformer
44
-
45
-
46
- class ParallelPipeline(Pipeline):
47
- """Pipeline with parallel execution of independent stages.
48
-
49
- Analyzes dependencies between pipeline stages and executes independent
50
- stages concurrently. Automatically determines dependency graph from
51
- stage inputs/outputs.
52
-
53
- Stages are independent if they:
54
- 1. Don't modify shared state
55
- 2. Only depend on the input trace (not other stage outputs)
56
- 3. Can be executed in any order
57
-
58
- Attributes:
59
- steps: List of (name, transformer) tuples defining the pipeline stages.
60
- named_steps: Dictionary mapping step names to transformers.
61
- executor_type: Type of executor ('thread' or 'process').
62
- max_workers: Maximum number of parallel workers (None = auto).
63
-
64
- Example - Independent Filters:
65
- >>> # These filters are independent - they both take the input trace
66
- >>> pipeline = ParallelPipeline([
67
- ... ('lowpass', LowPassFilter(cutoff=1e6)),
68
- ... ('highpass', HighPassFilter(cutoff=1e5)),
69
- ... ('bandpass', BandPassFilter(low=1e5, high=1e6)),
70
- ... ], executor_type='thread', max_workers=3)
71
- >>> result = pipeline.transform(trace) # All 3 run in parallel
72
-
73
- Example - Mixed Sequential and Parallel:
74
- >>> # First stage sequential, then parallel analysis
75
- >>> pipeline = ParallelPipeline([
76
- ... ('preprocess', Normalize()), # Sequential
77
- ... ('fft', FFT()), # Parallel (from preprocessed)
78
- ... ('wavelet', WaveletTransform()), # Parallel (from preprocessed)
79
- ... ('merge', CombineResults()) # Sequential (waits for fft+wavelet)
80
- ... ])
81
-
82
- Example - Process Pool for CPU-Intensive Tasks:
83
- >>> # Use process pool for heavy computation
84
- >>> pipeline = ParallelPipeline([
85
- ... ('fft1', FFT(nfft=65536)),
86
- ... ('fft2', FFT(nfft=32768)),
87
- ... ('fft3', FFT(nfft=16384)),
88
- ... ], executor_type='process', max_workers=None) # Auto worker count
89
-
90
- Performance Characteristics:
91
- Thread pool:
92
- - Best for I/O-bound tasks (file loading, network)
93
- - Low overhead (~10ms startup)
94
- - Shared memory (no serialization)
95
- - Limited by GIL for CPU-bound tasks
96
-
97
- Process pool:
98
- - Best for CPU-bound tasks (FFT, filtering, analysis)
99
- - Higher overhead (~50ms startup)
100
- - Requires picklable transformers
101
- - True parallelism (bypasses GIL)
102
-
103
- References:
104
- concurrent.futures.ThreadPoolExecutor
105
- concurrent.futures.ProcessPoolExecutor
106
- """
107
-
108
- def __init__(
109
- self,
110
- steps: Sequence[tuple[str, TraceTransformer]],
111
- executor_type: Literal["thread", "process"] = "thread",
112
- max_workers: int | None = None,
113
- ) -> None:
114
- """Initialize parallel pipeline with executor configuration.
115
-
116
- Args:
117
- steps: Sequence of (name, transformer) tuples. Each transformer
118
- must be a TraceTransformer instance.
119
- executor_type: Type of executor to use:
120
- - 'thread': ThreadPoolExecutor (default, low overhead)
121
- - 'process': ProcessPoolExecutor (true parallelism, higher overhead)
122
- max_workers: Maximum number of parallel workers. If None, uses
123
- automatic selection:
124
- - Thread pool: min(32, num_cpu + 4)
125
- - Process pool: num_cpu
126
-
127
- Raises:
128
- ValueError: If step names are not unique, empty, or executor_type invalid.
129
-
130
- Example:
131
- >>> # Auto worker count (recommended)
132
- >>> pipeline = ParallelPipeline([
133
- ... ('stage1', Transformer1()),
134
- ... ('stage2', Transformer2())
135
- ... ])
136
- >>> # Explicit worker count
137
- >>> pipeline = ParallelPipeline([
138
- ... ('stage1', Transformer1()),
139
- ... ('stage2', Transformer2())
140
- ... ], max_workers=4)
141
- """
142
- # Initialize parent Pipeline
143
- super().__init__(steps)
144
-
145
- # Validate executor type
146
- if executor_type not in ("thread", "process"):
147
- raise ValueError(f"executor_type must be 'thread' or 'process', got '{executor_type}'")
148
-
149
- self.executor_type = executor_type
150
- self.max_workers = max_workers
151
- self._dependency_graph: dict[str, list[str]] = {}
152
- self._execution_order: list[list[str]] = []
153
-
154
- # Analyze dependencies on initialization
155
- self._analyze_dependencies()
156
-
157
- def _analyze_dependencies(self) -> None:
158
- """Analyze dependencies between pipeline stages.
159
-
160
- Builds a dependency graph by examining which stages depend on outputs
161
- from other stages. Currently uses a conservative approach:
162
-
163
- - First stage has no dependencies
164
- - All other stages depend on the previous stage (sequential)
165
- - Future enhancement: analyze transformer inputs to detect true dependencies
166
-
167
- This conservative approach ensures correctness while still allowing
168
- parallel execution when stages are explicitly independent.
169
-
170
- The dependency analysis produces an execution order as a list of
171
- "generations" (lists of stage names). Stages in the same generation
172
- can be executed in parallel.
173
-
174
- Example:
175
- For pipeline: [filter1, filter2, merge]
176
- If all stages are sequential:
177
- execution_order = [['filter1'], ['filter2'], ['merge']]
178
- If filter1 and filter2 are independent:
179
- execution_order = [['filter1', 'filter2'], ['merge']]
180
-
181
- NOTE: Current implementation is conservative and assumes sequential
182
- dependencies. Future versions will support dependency hints via
183
- transformer metadata to enable automatic parallelization.
184
-
185
- References:
186
- FUTURE-002: Advanced dependency analysis
187
- """
188
- # Build dependency graph (conservative: each stage depends on previous)
189
- self._dependency_graph = {}
190
-
191
- for i, (name, _transformer) in enumerate(self.steps):
192
- if i == 0:
193
- # First stage has no dependencies
194
- self._dependency_graph[name] = []
195
- else:
196
- # Each stage depends on the previous stage
197
- prev_name = self.steps[i - 1][0]
198
- self._dependency_graph[name] = [prev_name]
199
-
200
- # Build execution order (topological sort by generations)
201
- self._execution_order = self._compute_execution_order()
202
-
203
- def _compute_execution_order(self) -> list[list[str]]:
204
- """Compute execution order as list of parallel generations.
205
-
206
- Uses topological sort to group stages into generations where each
207
- generation contains stages that can execute in parallel.
208
-
209
- Returns:
210
- List of generations, where each generation is a list of stage names
211
- that can execute in parallel.
212
-
213
- Raises:
214
- ValueError: If circular dependencies detected.
215
-
216
- Example:
217
- For graph: {A: [], B: [A], C: [A], D: [B, C]}
218
- Returns: [[A], [B, C], [D]]
219
- """
220
- # Copy dependency graph (we'll modify it)
221
- deps = {name: set(deps) for name, deps in self._dependency_graph.items()}
222
- generations: list[list[str]] = []
223
-
224
- # All stage names
225
- all_stages = set(self._dependency_graph.keys())
226
- completed: set[str] = set()
227
-
228
- # Build generations
229
- while completed != all_stages:
230
- # Find all stages with no remaining dependencies
231
- ready = [name for name in all_stages - completed if not deps[name]]
232
-
233
- if not ready:
234
- # Cycle detected (shouldn't happen with valid pipeline)
235
- raise ValueError("Circular dependency detected in pipeline")
236
-
237
- generations.append(ready)
238
- completed.update(ready)
239
-
240
- # Remove completed stages from dependencies
241
- for name in all_stages - completed:
242
- deps[name] -= set(ready)
243
-
244
- return generations
245
-
246
- def _get_max_workers(self) -> int:
247
- """Get the maximum number of workers to use.
248
-
249
- Returns:
250
- Number of workers, using automatic selection if max_workers is None.
251
-
252
- Example:
253
- >>> pipeline._get_max_workers()
254
- 8 # On 8-core machine with thread executor
255
- """
256
- if self.max_workers is not None:
257
- return self.max_workers
258
-
259
- # Automatic worker count selection
260
- cpu_count = os.cpu_count() or 4
261
-
262
- if self.executor_type == "thread":
263
- # Thread pool: min(32, cpu_count + 4) - default from ThreadPoolExecutor
264
- return min(32, cpu_count + 4)
265
- else:
266
- # Process pool: cpu_count - default from ProcessPoolExecutor
267
- return cpu_count
268
-
269
- def transform(self, trace: WaveformTrace) -> WaveformTrace:
270
- """Transform trace through pipeline with parallel execution.
271
-
272
- Executes stages in parallel when possible, according to the dependency
273
- graph. Stages in the same generation run concurrently.
274
-
275
- Args:
276
- trace: Input WaveformTrace to transform.
277
-
278
- Returns:
279
- Transformed WaveformTrace after passing through all stages.
280
-
281
- Example:
282
- >>> result = pipeline.transform(trace)
283
-
284
- Performance:
285
- For N independent stages with T execution time each:
286
- - Sequential: N * T
287
- - Parallel: T + overhead (~10-50ms)
288
- - Speedup: ~Nx (minus overhead)
289
- """
290
- current = trace
291
- self._intermediate_results.clear()
292
-
293
- # Choose executor based on configuration
294
- executor_class = (
295
- ThreadPoolExecutor if self.executor_type == "thread" else ProcessPoolExecutor
296
- )
297
-
298
- with executor_class(max_workers=self._get_max_workers()) as executor:
299
- # Execute each generation in parallel
300
- for generation in self._execution_order:
301
- if len(generation) == 1:
302
- # Single stage - execute directly (no overhead)
303
- name = generation[0]
304
- transformer = self.named_steps[name]
305
- current = transformer.transform(current)
306
- self._intermediate_results[name] = current
307
- else:
308
- # Multiple stages - execute in parallel
309
- futures = {}
310
- for name in generation:
311
- transformer = self.named_steps[name]
312
- future = executor.submit(transformer.transform, current)
313
- futures[name] = future
314
-
315
- # Collect results
316
- results = {}
317
- for name, future in futures.items():
318
- result = future.result()
319
- results[name] = result
320
- self._intermediate_results[name] = result
321
-
322
- # For conservative sequential execution, current is the last result
323
- # For true parallel execution with merge, this would be handled differently
324
- current = results[generation[-1]]
325
-
326
- return current
327
-
328
- def fit(self, trace: WaveformTrace) -> ParallelPipeline:
329
- """Fit all transformers in the pipeline.
330
-
331
- Fits each transformer sequentially on the output of the previous stage.
332
- Fitting is always sequential because it may modify transformer state.
333
-
334
- Args:
335
- trace: Reference WaveformTrace to fit to.
336
-
337
- Returns:
338
- Self for method chaining.
339
-
340
- Example:
341
- >>> pipeline = ParallelPipeline([
342
- ... ('normalize', AdaptiveNormalizer()),
343
- ... ('filter', AdaptiveFilter())
344
- ... ])
345
- >>> pipeline.fit(reference_trace)
346
-
347
- NOTE: fit() is always sequential because transformers may have
348
- interdependent learned parameters. Use transform() for parallel execution.
349
- """
350
- # Fitting is always sequential - reuse parent implementation
351
- super().fit(trace)
352
- return self
353
-
354
- def clone(self) -> ParallelPipeline:
355
- """Create a copy of this parallel pipeline.
356
-
357
- Returns:
358
- New ParallelPipeline instance with cloned transformers and same configuration.
359
-
360
- Example:
361
- >>> pipeline_copy = pipeline.clone()
362
- """
363
- cloned_steps = [(name, transformer.clone()) for name, transformer in self.steps]
364
- return ParallelPipeline(
365
- cloned_steps, executor_type=self.executor_type, max_workers=self.max_workers
366
- )
367
-
368
- def get_dependency_graph(self) -> dict[str, list[str]]:
369
- """Get the dependency graph for pipeline stages.
370
-
371
- Returns:
372
- Dictionary mapping stage names to list of dependency stage names.
373
-
374
- Example:
375
- >>> graph = pipeline.get_dependency_graph()
376
- >>> print(graph)
377
- {'filter1': [], 'filter2': ['filter1'], 'merge': ['filter2']}
378
-
379
- References:
380
- API-006: Pipeline Dependency Analysis
381
- """
382
- return self._dependency_graph.copy()
383
-
384
- def get_execution_order(self) -> list[list[str]]:
385
- """Get the execution order as parallel generations.
386
-
387
- Returns:
388
- List of generations, where each generation is a list of stage names
389
- that will execute in parallel.
390
-
391
- Example:
392
- >>> order = pipeline.get_execution_order()
393
- >>> print(order)
394
- [['filter1', 'filter2'], ['merge']]
395
- >>> # filter1 and filter2 run in parallel, then merge
396
-
397
- References:
398
- API-006: Pipeline Dependency Analysis
399
- """
400
- return [gen.copy() for gen in self._execution_order]
401
-
402
- def set_parallel_config(
403
- self,
404
- executor_type: Literal["thread", "process"] | None = None,
405
- max_workers: int | None = None,
406
- ) -> ParallelPipeline:
407
- """Update parallel execution configuration.
408
-
409
- Args:
410
- executor_type: New executor type ('thread' or 'process'). If None, keeps current.
411
- max_workers: New max worker count. If None, keeps current (may be auto).
412
-
413
- Returns:
414
- Self for method chaining.
415
-
416
- Raises:
417
- ValueError: If executor_type is invalid.
418
-
419
- Example:
420
- >>> # Switch to process pool with 4 workers
421
- >>> pipeline.set_parallel_config(executor_type='process', max_workers=4)
422
- >>> # Switch to auto worker count
423
- >>> pipeline.set_parallel_config(max_workers=None)
424
-
425
- References:
426
- API-006: Pipeline Parallel Configuration
427
- """
428
- if executor_type is not None:
429
- if executor_type not in ("thread", "process"):
430
- raise ValueError(
431
- f"executor_type must be 'thread' or 'process', got '{executor_type}'"
432
- )
433
- self.executor_type = executor_type
434
-
435
- if max_workers is not None:
436
- self.max_workers = max_workers
437
-
438
- return self
439
-
440
- def __repr__(self) -> str:
441
- """String representation of the parallel pipeline."""
442
- step_strs = [
443
- f"('{name}', {transformer.__class__.__name__})" for name, transformer in self.steps
444
- ]
445
- config_str = f"executor={self.executor_type}, workers={self.max_workers or 'auto'}"
446
- return "ParallelPipeline([\n " + ",\n ".join(step_strs) + f"\n], {config_str})"
447
-
448
-
449
- __all__ = ["ParallelPipeline"]