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
@@ -0,0 +1,640 @@
1
+ """Input loader handlers for pipeline system.
2
+
3
+ This module provides handlers for loading data from various file formats.
4
+ All handlers follow the standard signature: (inputs, params, step_name) -> outputs.
5
+
6
+ Available Handlers:
7
+ - input.file: Load from file with auto-format detection
8
+ - input.vcd: Load VCD (Value Change Dump) files
9
+ - input.wav: Load WAV audio files
10
+ - input.csv: Load CSV data files
11
+ - input.pcap: Load PCAP network capture files
12
+ - input.binary: Load raw binary data
13
+ - input.hdf5: Load HDF5 files
14
+ - input.numpy: Load NumPy .npz files
15
+ - input.tektronix: Load Tektronix oscilloscope files
16
+ - input.rigol: Load Rigol oscilloscope files
17
+ - input.sigrok: Load Sigrok/PulseView captures
18
+ - input.tdms: Load NI TDMS files
19
+ - input.touchstone: Load Touchstone S-parameter files
20
+ - input.chipwhisperer: Load ChipWhisperer captures
21
+ - input.multi_channel: Load all channels from multi-channel file
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from pathlib import Path
27
+ from typing import Any
28
+
29
+ from oscura.core.config.pipeline import PipelineExecutionError
30
+ from oscura.pipeline.handlers import register_handler
31
+
32
+ # Lazy imports to avoid circular dependencies
33
+ _loaders = None
34
+
35
+
36
+ def _get_loaders() -> Any:
37
+ """Lazy import loaders module."""
38
+ global _loaders
39
+ if _loaders is None:
40
+ import oscura.loaders as _loaders_module
41
+
42
+ _loaders = _loaders_module
43
+ return _loaders
44
+
45
+
46
+ @register_handler("input.file")
47
+ def handle_input_file(
48
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
49
+ ) -> dict[str, Any]:
50
+ """Load trace from file with auto-format detection.
51
+
52
+ Parameters:
53
+ path (str): File path to load
54
+ format (str, optional): Format override (tektronix, rigol, csv, etc.)
55
+ channel (str|int, optional): Channel name or index for multi-channel files
56
+ lazy (bool, optional): Enable lazy loading for large files
57
+
58
+ Outputs:
59
+ trace: Loaded WaveformTrace, DigitalTrace, or IQTrace
60
+ path: Absolute file path
61
+ format: File extension
62
+ metadata: Trace metadata dict
63
+ """
64
+ loaders = _get_loaders()
65
+
66
+ path = params.get("path")
67
+ if not path:
68
+ raise PipelineExecutionError(
69
+ "Missing required parameter 'path'. Suggestion: Add 'path: filename.wfm' to params",
70
+ step_name=step_name,
71
+ )
72
+
73
+ format_override = params.get("format")
74
+ channel = params.get("channel")
75
+ lazy = params.get("lazy", False)
76
+
77
+ try:
78
+ trace = loaders.load(path, format=format_override, channel=channel, lazy=lazy)
79
+ except FileNotFoundError as e:
80
+ raise PipelineExecutionError(f"File not found: {path}", step_name=step_name) from e
81
+ except Exception as e:
82
+ raise PipelineExecutionError(f"Failed to load file: {e}", step_name=step_name) from e
83
+
84
+ return {
85
+ "trace": trace,
86
+ "path": str(Path(path).resolve()),
87
+ "format": Path(path).suffix,
88
+ "metadata": trace.metadata.__dict__ if hasattr(trace, "metadata") else {},
89
+ }
90
+
91
+
92
+ @register_handler("input.vcd")
93
+ def handle_input_vcd(
94
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
95
+ ) -> dict[str, Any]:
96
+ """Load VCD (Value Change Dump) file.
97
+
98
+ Parameters:
99
+ path (str): Path to VCD file
100
+ signal_name (str, optional): Specific signal to extract
101
+
102
+ Outputs:
103
+ trace: Loaded DigitalTrace
104
+ path: Absolute file path
105
+ signals: List of available signal names
106
+ """
107
+ loaders = _get_loaders()
108
+
109
+ path = params.get("path")
110
+ if not path:
111
+ raise PipelineExecutionError("Missing required parameter 'path'", step_name=step_name)
112
+
113
+ signal_name = params.get("signal_name")
114
+
115
+ try:
116
+ trace = loaders.load(path, format="vcd", channel=signal_name)
117
+ # Get all signals if needed
118
+ from oscura.loaders.vcd import load_vcd
119
+
120
+ vcd_data = load_vcd(path)
121
+ signals = list(vcd_data.keys()) if isinstance(vcd_data, dict) else []
122
+ except Exception as e:
123
+ raise PipelineExecutionError(f"Failed to load VCD: {e}", step_name=step_name) from e
124
+
125
+ return {
126
+ "trace": trace,
127
+ "path": str(Path(path).resolve()),
128
+ "signals": signals,
129
+ }
130
+
131
+
132
+ @register_handler("input.wav")
133
+ def handle_input_wav(
134
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
135
+ ) -> dict[str, Any]:
136
+ """Load WAV audio file.
137
+
138
+ Parameters:
139
+ path (str): Path to WAV file
140
+ channel (int, optional): Channel index (0 for mono/left, 1 for right)
141
+
142
+ Outputs:
143
+ trace: Loaded WaveformTrace
144
+ path: Absolute file path
145
+ sample_rate: Audio sample rate in Hz
146
+ num_channels: Number of audio channels
147
+ """
148
+ loaders = _get_loaders()
149
+
150
+ path = params.get("path")
151
+ if not path:
152
+ raise PipelineExecutionError("Missing required parameter 'path'", step_name=step_name)
153
+
154
+ channel = params.get("channel")
155
+
156
+ try:
157
+ trace = loaders.load(path, format="wav", channel=channel)
158
+ sample_rate = trace.metadata.sample_rate
159
+ # WAV files have num_channels in metadata
160
+ num_channels = getattr(trace.metadata, "num_channels", 1)
161
+ except Exception as e:
162
+ raise PipelineExecutionError(f"Failed to load WAV: {e}", step_name=step_name) from e
163
+
164
+ return {
165
+ "trace": trace,
166
+ "path": str(Path(path).resolve()),
167
+ "sample_rate": sample_rate,
168
+ "num_channels": num_channels,
169
+ }
170
+
171
+
172
+ @register_handler("input.csv")
173
+ def handle_input_csv(
174
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
175
+ ) -> dict[str, Any]:
176
+ """Load CSV data file.
177
+
178
+ Parameters:
179
+ path (str): Path to CSV file
180
+ time_column (str, optional): Name of time column
181
+ data_column (str, optional): Name of data column
182
+ delimiter (str, optional): CSV delimiter (default: ',')
183
+ skip_rows (int, optional): Number of header rows to skip
184
+
185
+ Outputs:
186
+ trace: Loaded WaveformTrace
187
+ path: Absolute file path
188
+ columns: List of available column names
189
+ """
190
+ loaders = _get_loaders()
191
+
192
+ path = params.get("path")
193
+ if not path:
194
+ raise PipelineExecutionError("Missing required parameter 'path'", step_name=step_name)
195
+
196
+ time_col = params.get("time_column")
197
+ data_col = params.get("data_column")
198
+ delimiter = params.get("delimiter", ",")
199
+ skip_rows = params.get("skip_rows", 0)
200
+
201
+ try:
202
+ trace = loaders.load(
203
+ path,
204
+ format="csv",
205
+ time_column=time_col,
206
+ data_column=data_col,
207
+ delimiter=delimiter,
208
+ skip_rows=skip_rows,
209
+ )
210
+ # Try to get column names
211
+ import pandas as pd
212
+
213
+ df = pd.read_csv(path, delimiter=delimiter, skiprows=skip_rows, nrows=0)
214
+ columns = list(df.columns)
215
+ except Exception as e:
216
+ raise PipelineExecutionError(f"Failed to load CSV: {e}", step_name=step_name) from e
217
+
218
+ return {
219
+ "trace": trace,
220
+ "path": str(Path(path).resolve()),
221
+ "columns": columns,
222
+ }
223
+
224
+
225
+ @register_handler("input.pcap")
226
+ def handle_input_pcap(
227
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
228
+ ) -> dict[str, Any]:
229
+ """Load PCAP network capture file.
230
+
231
+ Parameters:
232
+ path (str): Path to PCAP file
233
+ filter (str, optional): BPF filter expression
234
+
235
+ Outputs:
236
+ trace: Loaded trace with packet data
237
+ path: Absolute file path
238
+ packet_count: Number of packets
239
+ """
240
+ loaders = _get_loaders()
241
+
242
+ path = params.get("path")
243
+ if not path:
244
+ raise PipelineExecutionError("Missing required parameter 'path'", step_name=step_name)
245
+
246
+ bpf_filter = params.get("filter")
247
+
248
+ try:
249
+ trace = loaders.load(path, format="pcap", filter=bpf_filter)
250
+ # Count packets (trace data should be packet array)
251
+ packet_count = len(trace.data) if hasattr(trace, "data") else 0
252
+ except Exception as e:
253
+ raise PipelineExecutionError(f"Failed to load PCAP: {e}", step_name=step_name) from e
254
+
255
+ return {
256
+ "trace": trace,
257
+ "path": str(Path(path).resolve()),
258
+ "packet_count": packet_count,
259
+ }
260
+
261
+
262
+ @register_handler("input.binary")
263
+ def handle_input_binary(
264
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
265
+ ) -> dict[str, Any]:
266
+ """Load raw binary data file.
267
+
268
+ Parameters:
269
+ path (str): Path to binary file
270
+ sample_rate (float): Sample rate in Hz
271
+ dtype (str, optional): NumPy dtype (default: 'float32')
272
+ offset (int, optional): Byte offset to start reading
273
+ count (int, optional): Number of samples to read
274
+
275
+ Outputs:
276
+ trace: Loaded WaveformTrace
277
+ path: Absolute file path
278
+ num_samples: Number of samples loaded
279
+ """
280
+ loaders = _get_loaders()
281
+
282
+ path = params.get("path")
283
+ sample_rate = params.get("sample_rate")
284
+
285
+ if not path:
286
+ raise PipelineExecutionError("Missing required parameter 'path'", step_name=step_name)
287
+ if not sample_rate:
288
+ raise PipelineExecutionError(
289
+ "Missing required parameter 'sample_rate'", step_name=step_name
290
+ )
291
+
292
+ dtype = params.get("dtype", "float32")
293
+ offset = params.get("offset", 0)
294
+ count = params.get("count", -1)
295
+
296
+ try:
297
+ trace = loaders.load_binary(
298
+ path, sample_rate=sample_rate, dtype=dtype, offset=offset, count=count
299
+ )
300
+ except Exception as e:
301
+ raise PipelineExecutionError(f"Failed to load binary: {e}", step_name=step_name) from e
302
+
303
+ return {
304
+ "trace": trace,
305
+ "path": str(Path(path).resolve()),
306
+ "num_samples": len(trace.data),
307
+ }
308
+
309
+
310
+ @register_handler("input.hdf5")
311
+ def handle_input_hdf5(
312
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
313
+ ) -> dict[str, Any]:
314
+ """Load HDF5 file.
315
+
316
+ Parameters:
317
+ path (str): Path to HDF5 file
318
+ dataset (str, optional): Dataset path within HDF5 file
319
+
320
+ Outputs:
321
+ trace: Loaded WaveformTrace
322
+ path: Absolute file path
323
+ datasets: List of available dataset paths
324
+ """
325
+ loaders = _get_loaders()
326
+
327
+ path = params.get("path")
328
+ if not path:
329
+ raise PipelineExecutionError("Missing required parameter 'path'", step_name=step_name)
330
+
331
+ dataset = params.get("dataset")
332
+
333
+ try:
334
+ trace = loaders.load(path, format="hdf5", dataset=dataset)
335
+ # Get available datasets
336
+ import h5py
337
+
338
+ with h5py.File(path, "r") as f:
339
+
340
+ def list_datasets(group: Any, prefix: str = "") -> list[str]:
341
+ datasets = []
342
+ for key in group:
343
+ item = group[key]
344
+ if isinstance(item, h5py.Dataset):
345
+ datasets.append(f"{prefix}/{key}")
346
+ elif isinstance(item, h5py.Group):
347
+ datasets.extend(list_datasets(item, f"{prefix}/{key}"))
348
+ return datasets
349
+
350
+ datasets = list_datasets(f)
351
+ except Exception as e:
352
+ raise PipelineExecutionError(f"Failed to load HDF5: {e}", step_name=step_name) from e
353
+
354
+ return {
355
+ "trace": trace,
356
+ "path": str(Path(path).resolve()),
357
+ "datasets": datasets,
358
+ }
359
+
360
+
361
+ @register_handler("input.numpy")
362
+ def handle_input_numpy(
363
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
364
+ ) -> dict[str, Any]:
365
+ """Load NumPy .npz file.
366
+
367
+ Parameters:
368
+ path (str): Path to .npz file
369
+ array_name (str, optional): Name of array to load (default: first array)
370
+
371
+ Outputs:
372
+ trace: Loaded WaveformTrace
373
+ path: Absolute file path
374
+ arrays: List of available array names
375
+ """
376
+ loaders = _get_loaders()
377
+
378
+ path = params.get("path")
379
+ if not path:
380
+ raise PipelineExecutionError("Missing required parameter 'path'", step_name=step_name)
381
+
382
+ array_name = params.get("array_name")
383
+
384
+ try:
385
+ trace = loaders.load(path, format="numpy", array_name=array_name)
386
+ # Get available arrays
387
+ import numpy as np
388
+
389
+ with np.load(path) as npz:
390
+ arrays = list(npz.keys())
391
+ except Exception as e:
392
+ raise PipelineExecutionError(f"Failed to load NumPy: {e}", step_name=step_name) from e
393
+
394
+ return {
395
+ "trace": trace,
396
+ "path": str(Path(path).resolve()),
397
+ "arrays": arrays,
398
+ }
399
+
400
+
401
+ @register_handler("input.tektronix")
402
+ def handle_input_tektronix(
403
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
404
+ ) -> dict[str, Any]:
405
+ """Load Tektronix oscilloscope file (.wfm, .isf).
406
+
407
+ Parameters:
408
+ path (str): Path to Tektronix file
409
+ channel (str|int, optional): Channel to load
410
+
411
+ Outputs:
412
+ trace: Loaded WaveformTrace
413
+ path: Absolute file path
414
+ """
415
+ loaders = _get_loaders()
416
+
417
+ path = params.get("path")
418
+ if not path:
419
+ raise PipelineExecutionError("Missing required parameter 'path'", step_name=step_name)
420
+
421
+ channel = params.get("channel")
422
+
423
+ try:
424
+ trace = loaders.load(path, format="tektronix", channel=channel)
425
+ except Exception as e:
426
+ raise PipelineExecutionError(
427
+ f"Failed to load Tektronix file: {e}", step_name=step_name
428
+ ) from e
429
+
430
+ return {
431
+ "trace": trace,
432
+ "path": str(Path(path).resolve()),
433
+ }
434
+
435
+
436
+ @register_handler("input.rigol")
437
+ def handle_input_rigol(
438
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
439
+ ) -> dict[str, Any]:
440
+ """Load Rigol oscilloscope file.
441
+
442
+ Parameters:
443
+ path (str): Path to Rigol file
444
+ channel (str|int, optional): Channel to load
445
+
446
+ Outputs:
447
+ trace: Loaded WaveformTrace
448
+ path: Absolute file path
449
+ """
450
+ loaders = _get_loaders()
451
+
452
+ path = params.get("path")
453
+ if not path:
454
+ raise PipelineExecutionError("Missing required parameter 'path'", step_name=step_name)
455
+
456
+ channel = params.get("channel")
457
+
458
+ try:
459
+ trace = loaders.load(path, format="rigol", channel=channel)
460
+ except Exception as e:
461
+ raise PipelineExecutionError(f"Failed to load Rigol file: {e}", step_name=step_name) from e
462
+
463
+ return {
464
+ "trace": trace,
465
+ "path": str(Path(path).resolve()),
466
+ }
467
+
468
+
469
+ @register_handler("input.sigrok")
470
+ def handle_input_sigrok(
471
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
472
+ ) -> dict[str, Any]:
473
+ """Load Sigrok/PulseView capture.
474
+
475
+ Parameters:
476
+ path (str): Path to Sigrok file
477
+ channel (str|int, optional): Channel to load
478
+
479
+ Outputs:
480
+ trace: Loaded DigitalTrace
481
+ path: Absolute file path
482
+ """
483
+ loaders = _get_loaders()
484
+
485
+ path = params.get("path")
486
+ if not path:
487
+ raise PipelineExecutionError("Missing required parameter 'path'", step_name=step_name)
488
+
489
+ channel = params.get("channel")
490
+
491
+ try:
492
+ trace = loaders.load(path, format="sigrok", channel=channel)
493
+ except Exception as e:
494
+ raise PipelineExecutionError(f"Failed to load Sigrok file: {e}", step_name=step_name) from e
495
+
496
+ return {
497
+ "trace": trace,
498
+ "path": str(Path(path).resolve()),
499
+ }
500
+
501
+
502
+ @register_handler("input.tdms")
503
+ def handle_input_tdms(
504
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
505
+ ) -> dict[str, Any]:
506
+ """Load National Instruments TDMS file.
507
+
508
+ Parameters:
509
+ path (str): Path to TDMS file
510
+ group (str, optional): Group name
511
+ channel (str, optional): Channel name
512
+
513
+ Outputs:
514
+ trace: Loaded WaveformTrace
515
+ path: Absolute file path
516
+ """
517
+ loaders = _get_loaders()
518
+
519
+ path = params.get("path")
520
+ if not path:
521
+ raise PipelineExecutionError("Missing required parameter 'path'", step_name=step_name)
522
+
523
+ group = params.get("group")
524
+ channel = params.get("channel")
525
+
526
+ try:
527
+ trace = loaders.load(path, format="tdms", group=group, channel=channel)
528
+ except Exception as e:
529
+ raise PipelineExecutionError(f"Failed to load TDMS file: {e}", step_name=step_name) from e
530
+
531
+ return {
532
+ "trace": trace,
533
+ "path": str(Path(path).resolve()),
534
+ }
535
+
536
+
537
+ @register_handler("input.touchstone")
538
+ def handle_input_touchstone(
539
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
540
+ ) -> dict[str, Any]:
541
+ """Load Touchstone S-parameter file (.s1p, .s2p, etc.).
542
+
543
+ Parameters:
544
+ path (str): Path to Touchstone file
545
+
546
+ Outputs:
547
+ sparams: S-parameter data object
548
+ path: Absolute file path
549
+ num_ports: Number of ports
550
+ """
551
+ loaders = _get_loaders()
552
+
553
+ path = params.get("path")
554
+ if not path:
555
+ raise PipelineExecutionError("Missing required parameter 'path'", step_name=step_name)
556
+
557
+ try:
558
+ sparams = loaders.load_touchstone(path)
559
+ num_ports = getattr(sparams, "num_ports", 0)
560
+ except Exception as e:
561
+ raise PipelineExecutionError(
562
+ f"Failed to load Touchstone file: {e}", step_name=step_name
563
+ ) from e
564
+
565
+ return {
566
+ "sparams": sparams,
567
+ "path": str(Path(path).resolve()),
568
+ "num_ports": num_ports,
569
+ }
570
+
571
+
572
+ @register_handler("input.chipwhisperer")
573
+ def handle_input_chipwhisperer(
574
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
575
+ ) -> dict[str, Any]:
576
+ """Load ChipWhisperer capture.
577
+
578
+ Parameters:
579
+ path (str): Path to ChipWhisperer file
580
+
581
+ Outputs:
582
+ trace: Loaded WaveformTrace
583
+ path: Absolute file path
584
+ """
585
+ loaders = _get_loaders()
586
+
587
+ path = params.get("path")
588
+ if not path:
589
+ raise PipelineExecutionError("Missing required parameter 'path'", step_name=step_name)
590
+
591
+ try:
592
+ trace = loaders.load(path, format="chipwhisperer")
593
+ except Exception as e:
594
+ raise PipelineExecutionError(
595
+ f"Failed to load ChipWhisperer file: {e}", step_name=step_name
596
+ ) from e
597
+
598
+ return {
599
+ "trace": trace,
600
+ "path": str(Path(path).resolve()),
601
+ }
602
+
603
+
604
+ @register_handler("input.multi_channel")
605
+ def handle_input_multi_channel(
606
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
607
+ ) -> dict[str, Any]:
608
+ """Load all channels from a multi-channel file.
609
+
610
+ Parameters:
611
+ path (str): Path to multi-channel file
612
+ format (str, optional): Format override
613
+
614
+ Outputs:
615
+ channels: Dict mapping channel names to traces
616
+ path: Absolute file path
617
+ channel_names: List of channel names
618
+ """
619
+ loaders = _get_loaders()
620
+
621
+ path = params.get("path")
622
+ if not path:
623
+ raise PipelineExecutionError("Missing required parameter 'path'", step_name=step_name)
624
+
625
+ format_override = params.get("format")
626
+
627
+ try:
628
+ # Load all channels
629
+ channels = loaders.load_all_channels(path, format=format_override)
630
+ channel_names = list(channels.keys())
631
+ except Exception as e:
632
+ raise PipelineExecutionError(
633
+ f"Failed to load multi-channel file: {e}", step_name=step_name
634
+ ) from e
635
+
636
+ return {
637
+ "channels": channels,
638
+ "path": str(Path(path).resolve()),
639
+ "channel_names": channel_names,
640
+ }