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,1045 @@
1
+ """Analysis handlers for pipeline system.
2
+
3
+ This module provides handlers for signal analysis including waveform measurements,
4
+ spectral analysis, power analysis, timing measurements, jitter analysis, and statistics.
5
+ All handlers follow the standard signature: (inputs, params, step_name) -> outputs.
6
+
7
+ Available Handlers:
8
+ - analysis.waveform: Basic waveform measurements (rise/fall time, frequency, amplitude)
9
+ - analysis.spectral: Spectral analysis (FFT, PSD, THD, SNR, SINAD, ENOB, SFDR)
10
+ - analysis.power: Power analysis (voltage, current, active/reactive/apparent power)
11
+ - analysis.timing: IEEE 181 pulse timing measurements and clock recovery
12
+ - analysis.jitter: Jitter analysis (TIE, period jitter, cycle-to-cycle)
13
+ - analysis.statistics: Statistical measurements (mean, std, histogram, outliers)
14
+ - analysis.fft: FFT analysis returning frequency and magnitude arrays
15
+ - analysis.psd: Power spectral density calculation
16
+ - analysis.thd: Total harmonic distortion measurement
17
+ - analysis.eye_diagram: Eye diagram analysis for digital signals
18
+ - analysis.auto: Auto-detect signal type and perform appropriate analysis
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from typing import Any
24
+
25
+ from oscura.core.config.pipeline import PipelineExecutionError
26
+ from oscura.pipeline.handlers import register_handler
27
+
28
+ # Lazy imports to avoid circular dependencies
29
+ _waveform = None
30
+ _spectral = None
31
+ _power = None
32
+ _jitter = None
33
+ _statistics = None
34
+ _eye = None
35
+ _signal = None
36
+
37
+
38
+ def _get_waveform() -> Any:
39
+ """Lazy import waveform analyzer module."""
40
+ global _waveform
41
+ if _waveform is None:
42
+ from oscura.analyzers import waveform as _waveform_module
43
+
44
+ _waveform = _waveform_module
45
+ return _waveform
46
+
47
+
48
+ def _get_spectral() -> Any:
49
+ """Lazy import spectral analyzer module."""
50
+ global _spectral
51
+ if _spectral is None:
52
+ from oscura.analyzers import spectral as _spectral_module
53
+
54
+ _spectral = _spectral_module
55
+ return _spectral
56
+
57
+
58
+ def _get_power() -> Any:
59
+ """Lazy import power analyzer module."""
60
+ global _power
61
+ if _power is None:
62
+ from oscura.analyzers import power as _power_module
63
+
64
+ _power = _power_module
65
+ return _power
66
+
67
+
68
+ def _get_jitter() -> Any:
69
+ """Lazy import jitter analyzer module."""
70
+ global _jitter
71
+ if _jitter is None:
72
+ from oscura.analyzers import jitter as _jitter_module
73
+
74
+ _jitter = _jitter_module
75
+ return _jitter
76
+
77
+
78
+ def _get_statistics() -> Any:
79
+ """Lazy import statistics analyzer module."""
80
+ global _statistics
81
+ if _statistics is None:
82
+ from oscura.analyzers import statistics as _statistics_module
83
+
84
+ _statistics = _statistics_module
85
+ return _statistics
86
+
87
+
88
+ def _get_eye() -> Any:
89
+ """Lazy import eye diagram analyzer module."""
90
+ global _eye
91
+ if _eye is None:
92
+ from oscura.analyzers import eye as _eye_module
93
+
94
+ _eye = _eye_module
95
+ return _eye
96
+
97
+
98
+ def _get_signal() -> Any:
99
+ """Lazy import signal analyzer module."""
100
+ global _signal
101
+ if _signal is None:
102
+ from oscura.analyzers import signal as _signal_module
103
+
104
+ _signal = _signal_module
105
+ return _signal
106
+
107
+
108
+ @register_handler("analysis.waveform")
109
+ def handle_analysis_waveform(
110
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
111
+ ) -> dict[str, Any]:
112
+ """Perform basic waveform measurements.
113
+
114
+ Inputs:
115
+ trace: WaveformTrace to analyze
116
+
117
+ Parameters:
118
+ measurements (list[str], optional): Specific measurements to perform
119
+ Available: rise_time, fall_time, frequency, amplitude, rms, duty_cycle,
120
+ period, pulse_width, overshoot, undershoot, preshoot, mean
121
+ Default: all measurements
122
+ threshold_low (float, optional): Low threshold percentage (default: 10%)
123
+ threshold_mid (float, optional): Mid threshold percentage (default: 50%)
124
+ threshold_high (float, optional): High threshold percentage (default: 90%)
125
+
126
+ Outputs:
127
+ measurements: Dict of measurement name to value
128
+ rise_time: Rise time in seconds (if measured)
129
+ fall_time: Fall time in seconds (if measured)
130
+ frequency: Frequency in Hz (if measured)
131
+ amplitude: Peak-to-peak amplitude (if measured)
132
+ rms: RMS value (if measured)
133
+ duty_cycle: Duty cycle percentage (if measured)
134
+ period: Period in seconds (if measured)
135
+ pulse_width: Pulse width in seconds (if measured)
136
+ mean: Mean value (if measured)
137
+ overshoot: Overshoot percentage (if measured)
138
+ undershoot: Undershoot percentage (if measured)
139
+ """
140
+ waveform = _get_waveform()
141
+
142
+ trace = inputs.get("trace")
143
+ if trace is None:
144
+ raise PipelineExecutionError(
145
+ "Missing required input 'trace'. Suggestion: Provide a WaveformTrace to analyze",
146
+ step_name=step_name,
147
+ )
148
+
149
+ requested_measurements = params.get("measurements")
150
+ threshold_low = params.get("threshold_low", 0.1)
151
+ threshold_mid = params.get("threshold_mid", 0.5)
152
+ threshold_high = params.get("threshold_high", 0.9)
153
+
154
+ # Define all available measurements
155
+ all_measurements = [
156
+ "rise_time",
157
+ "fall_time",
158
+ "frequency",
159
+ "amplitude",
160
+ "rms",
161
+ "duty_cycle",
162
+ "period",
163
+ "pulse_width",
164
+ "overshoot",
165
+ "undershoot",
166
+ "preshoot",
167
+ "mean",
168
+ ]
169
+
170
+ # Use requested or all measurements
171
+ measurements_to_run = requested_measurements or all_measurements
172
+
173
+ try:
174
+ results = {}
175
+ measurements_dict = {}
176
+
177
+ for measurement in measurements_to_run:
178
+ try:
179
+ if measurement == "rise_time":
180
+ value = waveform.rise_time(trace, low=threshold_low, high=threshold_high)
181
+ results["rise_time"] = value
182
+ measurements_dict["rise_time"] = value
183
+ elif measurement == "fall_time":
184
+ value = waveform.fall_time(trace, low=threshold_low, high=threshold_high)
185
+ results["fall_time"] = value
186
+ measurements_dict["fall_time"] = value
187
+ elif measurement == "frequency":
188
+ value = waveform.frequency(trace)
189
+ results["frequency"] = value
190
+ measurements_dict["frequency"] = value
191
+ elif measurement == "amplitude":
192
+ value = waveform.amplitude(trace)
193
+ results["amplitude"] = value
194
+ measurements_dict["amplitude"] = value
195
+ elif measurement == "rms":
196
+ value = waveform.rms(trace)
197
+ results["rms"] = value
198
+ measurements_dict["rms"] = value
199
+ elif measurement == "duty_cycle":
200
+ value = waveform.duty_cycle(trace, threshold=threshold_mid)
201
+ results["duty_cycle"] = value
202
+ measurements_dict["duty_cycle"] = value
203
+ elif measurement == "period":
204
+ value = waveform.period(trace)
205
+ results["period"] = value
206
+ measurements_dict["period"] = value
207
+ elif measurement == "pulse_width":
208
+ value = waveform.pulse_width(trace, threshold=threshold_mid)
209
+ results["pulse_width"] = value
210
+ measurements_dict["pulse_width"] = value
211
+ elif measurement == "overshoot":
212
+ value = waveform.overshoot(trace)
213
+ results["overshoot"] = value
214
+ measurements_dict["overshoot"] = value
215
+ elif measurement == "undershoot":
216
+ value = waveform.undershoot(trace)
217
+ results["undershoot"] = value
218
+ measurements_dict["undershoot"] = value
219
+ elif measurement == "preshoot":
220
+ value = waveform.preshoot(trace)
221
+ results["preshoot"] = value
222
+ measurements_dict["preshoot"] = value
223
+ elif measurement == "mean":
224
+ value = waveform.mean(trace)
225
+ results["mean"] = value
226
+ measurements_dict["mean"] = value
227
+ except Exception as e:
228
+ # Log measurement failure but continue with others
229
+ measurements_dict[f"{measurement}_error"] = str(e)
230
+
231
+ results["measurements"] = measurements_dict
232
+
233
+ except Exception as e:
234
+ raise PipelineExecutionError(f"Waveform analysis failed: {e}", step_name=step_name) from e
235
+
236
+ return results
237
+
238
+
239
+ @register_handler("analysis.spectral")
240
+ def handle_analysis_spectral(
241
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
242
+ ) -> dict[str, Any]:
243
+ """Perform spectral analysis with quality metrics.
244
+
245
+ Inputs:
246
+ trace: WaveformTrace to analyze
247
+
248
+ Parameters:
249
+ window (str, optional): FFT window function (default: 'hann')
250
+ nfft (int, optional): FFT length (default: trace length)
251
+ fundamental_freq (float, optional): Fundamental frequency for THD/SNR
252
+ num_harmonics (int, optional): Number of harmonics for THD (default: 5)
253
+
254
+ Outputs:
255
+ frequencies: FFT frequency array
256
+ magnitudes: FFT magnitude array (linear)
257
+ thd_db: Total harmonic distortion in dB
258
+ snr_db: Signal-to-noise ratio in dB
259
+ sinad_db: Signal-to-noise and distortion ratio in dB
260
+ enob: Effective number of bits
261
+ sfdr_db: Spurious-free dynamic range in dB
262
+ fundamental_power: Power at fundamental frequency
263
+ fundamental_idx: Index of fundamental frequency peak
264
+ """
265
+ spectral = _get_spectral()
266
+
267
+ trace = inputs.get("trace")
268
+ if trace is None:
269
+ raise PipelineExecutionError(
270
+ "Missing required input 'trace'. Suggestion: Provide a WaveformTrace to analyze",
271
+ step_name=step_name,
272
+ )
273
+
274
+ window = params.get("window", "hann")
275
+ nfft = params.get("nfft")
276
+ fundamental_freq = params.get("fundamental_freq")
277
+ num_harmonics = params.get("num_harmonics", 5)
278
+
279
+ try:
280
+ # Perform FFT
281
+ frequencies, magnitudes = spectral.fft(trace, window=window, nfft=nfft)
282
+
283
+ results = {
284
+ "frequencies": frequencies,
285
+ "magnitudes": magnitudes,
286
+ }
287
+
288
+ # Calculate quality metrics if fundamental frequency provided
289
+ if fundamental_freq:
290
+ thd_value = spectral.thd(
291
+ trace, fundamental_freq=fundamental_freq, harmonics=num_harmonics
292
+ )
293
+ results["thd_db"] = thd_value
294
+
295
+ snr_value = spectral.snr(trace, fundamental_freq=fundamental_freq)
296
+ results["snr_db"] = snr_value
297
+
298
+ sinad_value = spectral.sinad(trace, fundamental_freq=fundamental_freq)
299
+ results["sinad_db"] = sinad_value
300
+
301
+ enob_value = spectral.enob(trace, fundamental_freq=fundamental_freq)
302
+ results["enob"] = enob_value
303
+
304
+ sfdr_value = spectral.sfdr(trace, fundamental_freq=fundamental_freq)
305
+ results["sfdr_db"] = sfdr_value
306
+
307
+ # Find fundamental frequency peak
308
+ freq_resolution = frequencies[1] - frequencies[0]
309
+ fund_idx = int(fundamental_freq / freq_resolution)
310
+ results["fundamental_idx"] = fund_idx
311
+ results["fundamental_power"] = float(magnitudes[fund_idx] ** 2)
312
+
313
+ except Exception as e:
314
+ raise PipelineExecutionError(f"Spectral analysis failed: {e}", step_name=step_name) from e
315
+
316
+ return results
317
+
318
+
319
+ @register_handler("analysis.power")
320
+ def handle_analysis_power(
321
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
322
+ ) -> dict[str, Any]:
323
+ """Perform power analysis on voltage and current traces.
324
+
325
+ Inputs:
326
+ voltage: Voltage WaveformTrace
327
+ current: Current WaveformTrace
328
+
329
+ Parameters:
330
+ fundamental_freq (float, optional): Fundamental frequency for AC analysis
331
+
332
+ Outputs:
333
+ instantaneous_power: Power trace (voltage * current)
334
+ average_power: Average power in watts
335
+ peak_power: Peak instantaneous power
336
+ rms_power: RMS power
337
+ energy: Total energy in joules
338
+ active_power: Active (real) power in watts (if fundamental_freq provided)
339
+ reactive_power: Reactive power in VAR (if fundamental_freq provided)
340
+ apparent_power: Apparent power in VA (if fundamental_freq provided)
341
+ power_factor: Power factor (if fundamental_freq provided)
342
+ displacement_power_factor: Displacement power factor (if fundamental_freq provided)
343
+ distortion_power_factor: Distortion power factor (if fundamental_freq provided)
344
+ """
345
+ power = _get_power()
346
+
347
+ voltage = inputs.get("voltage")
348
+ current = inputs.get("current")
349
+
350
+ if voltage is None or current is None:
351
+ raise PipelineExecutionError(
352
+ "Missing required inputs 'voltage' and 'current'. Suggestion: Provide both voltage and current WaveformTraces",
353
+ step_name=step_name,
354
+ )
355
+
356
+ fundamental_freq = params.get("fundamental_freq")
357
+
358
+ try:
359
+ # Basic power measurements
360
+ power_trace = power.instantaneous_power(voltage, current)
361
+ avg_power = power.average_power(voltage, current)
362
+ peak = power.peak_power(voltage, current)
363
+ rms_pwr = power.rms_power(voltage, current)
364
+ energy_val = power.energy(voltage, current)
365
+
366
+ results = {
367
+ "instantaneous_power": power_trace,
368
+ "average_power": avg_power,
369
+ "peak_power": peak,
370
+ "rms_power": rms_pwr,
371
+ "energy": energy_val,
372
+ }
373
+
374
+ # AC power analysis if fundamental frequency provided
375
+ if fundamental_freq:
376
+ active_pwr = power.average_power(voltage, current) # Same as average
377
+ reactive_pwr = power.reactive_power(voltage, current, fundamental_freq)
378
+ apparent_pwr = power.apparent_power(voltage, current)
379
+ pf = power.power_factor(voltage, current, fundamental_freq)
380
+ dpf = power.displacement_power_factor(voltage, current, fundamental_freq)
381
+ distortion_pf = power.distortion_power_factor(voltage, current, fundamental_freq)
382
+
383
+ results.update(
384
+ {
385
+ "active_power": active_pwr,
386
+ "reactive_power": reactive_pwr,
387
+ "apparent_power": apparent_pwr,
388
+ "power_factor": pf,
389
+ "displacement_power_factor": dpf,
390
+ "distortion_power_factor": distortion_pf,
391
+ }
392
+ )
393
+
394
+ except Exception as e:
395
+ raise PipelineExecutionError(f"Power analysis failed: {e}", step_name=step_name) from e
396
+
397
+ return results
398
+
399
+
400
+ @register_handler("analysis.timing")
401
+ def handle_analysis_timing(
402
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
403
+ ) -> dict[str, Any]:
404
+ """Perform IEEE 181 pulse timing measurements and clock recovery.
405
+
406
+ Inputs:
407
+ trace: WaveformTrace or DigitalTrace to analyze
408
+
409
+ Parameters:
410
+ method (str, optional): Clock recovery method
411
+ ('zcd', 'histogram', 'autocorrelation', 'pll', 'fft')
412
+ Default: 'autocorrelation'
413
+ expected_freq (float, optional): Expected clock frequency for validation
414
+
415
+ Outputs:
416
+ detected_clock_rate: Recovered clock frequency in Hz
417
+ confidence: Clock recovery confidence (0-1)
418
+ jitter_rms: RMS jitter in seconds
419
+ drift_rate: Clock drift in ppm
420
+ snr_db: Signal-to-noise ratio in dB
421
+ method: Method used for clock recovery
422
+ statistics: Additional timing statistics
423
+ """
424
+ trace = inputs.get("trace")
425
+ if trace is None:
426
+ raise PipelineExecutionError(
427
+ "Missing required input 'trace'. Suggestion: Provide a WaveformTrace or DigitalTrace to analyze",
428
+ step_name=step_name,
429
+ )
430
+
431
+ method = params.get("method", "autocorrelation")
432
+ expected_freq = params.get("expected_freq")
433
+
434
+ try:
435
+ # Import TimingAnalyzer
436
+ from oscura.analyzers.signal.timing_analysis import TimingAnalyzer
437
+
438
+ analyzer = TimingAnalyzer(method=method)
439
+ result = analyzer.recover_clock(trace.data, trace.metadata.sample_rate)
440
+
441
+ outputs = {
442
+ "detected_clock_rate": result.detected_clock_rate,
443
+ "confidence": result.confidence,
444
+ "jitter_rms": result.jitter_rms,
445
+ "drift_rate": result.drift_rate,
446
+ "snr_db": result.snr_db,
447
+ "method": result.method,
448
+ "statistics": result.statistics,
449
+ }
450
+
451
+ # Validate against expected frequency if provided
452
+ if expected_freq:
453
+ freq_error_pct = abs(result.detected_clock_rate - expected_freq) / expected_freq * 100
454
+ outputs["frequency_error_percent"] = freq_error_pct
455
+ outputs["frequency_match"] = freq_error_pct < 1.0 # Within 1%
456
+
457
+ except Exception as e:
458
+ raise PipelineExecutionError(f"Timing analysis failed: {e}", step_name=step_name) from e
459
+
460
+ return outputs
461
+
462
+
463
+ @register_handler("analysis.jitter")
464
+ def handle_analysis_jitter(
465
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
466
+ ) -> dict[str, Any]:
467
+ """Perform jitter analysis on digital signals.
468
+
469
+ Inputs:
470
+ trace: DigitalTrace or WaveformTrace to analyze
471
+ edges (array, optional): Pre-computed edge times
472
+
473
+ Parameters:
474
+ unit_interval (float, optional): Unit interval in seconds (required for TIE)
475
+ decompose (bool, optional): Perform jitter decomposition into RJ/DJ (default: False)
476
+ bathtub_ber (float, optional): Target BER for bathtub curve (e.g., 1e-12)
477
+
478
+ Outputs:
479
+ tie: Time interval error array (if unit_interval provided)
480
+ period_jitter: Period jitter in seconds
481
+ cycle_jitter: Cycle-to-cycle jitter in seconds
482
+ dcd: Duty cycle distortion in seconds
483
+ rj_rms: Random jitter RMS (if decompose=True)
484
+ dj_pp: Deterministic jitter peak-to-peak (if decompose=True)
485
+ tj_at_ber: Total jitter at target BER (if bathtub_ber provided)
486
+ bathtub_positions: Bathtub curve positions (if bathtub_ber provided)
487
+ bathtub_ber_values: Bathtub curve BER values (if bathtub_ber provided)
488
+ """
489
+ jitter = _get_jitter()
490
+
491
+ trace = inputs.get("trace")
492
+ edges = inputs.get("edges")
493
+
494
+ if trace is None and edges is None:
495
+ raise PipelineExecutionError(
496
+ "Missing required input 'trace' or 'edges'. Suggestion: Provide either a trace or pre-computed edge times",
497
+ step_name=step_name,
498
+ )
499
+
500
+ unit_interval = params.get("unit_interval")
501
+ decompose = params.get("decompose", False)
502
+ bathtub_ber = params.get("bathtub_ber")
503
+
504
+ try:
505
+ results = {}
506
+
507
+ # Extract edges if not provided
508
+ if edges is None:
509
+ import numpy as np
510
+
511
+ from oscura.analyzers.digital.edges import detect_edges
512
+
513
+ # trace is guaranteed non-None here due to validation at line 494
514
+ assert trace is not None # Help mypy understand the guarantee
515
+ edge_list = detect_edges(trace.data, sample_rate=trace.metadata.sample_rate)
516
+ edges = np.array([edge.time for edge in edge_list])
517
+
518
+ # Period jitter
519
+ period_jitter_result = jitter.period_jitter(edges)
520
+ results["period_jitter"] = period_jitter_result.rms_jitter
521
+
522
+ # Cycle-to-cycle jitter
523
+ cycle_jitter_result = jitter.cycle_to_cycle_jitter(edges)
524
+ results["cycle_jitter"] = cycle_jitter_result.rms_jitter
525
+
526
+ # Duty cycle distortion
527
+ dcd_result = jitter.measure_dcd(edges)
528
+ results["dcd"] = dcd_result.dcd_mean
529
+
530
+ # TIE calculation if unit interval provided
531
+ if unit_interval:
532
+ tie = jitter.tie_from_edges(edges, unit_interval)
533
+ results["tie"] = tie
534
+
535
+ # Jitter decomposition
536
+ if decompose:
537
+ decomposition = jitter.decompose_jitter(tie)
538
+ results["rj_rms"] = decomposition.rj.rj_rms
539
+ results["dj_pp"] = decomposition.dj.dj_pp
540
+
541
+ # Total jitter at BER if requested
542
+ if bathtub_ber:
543
+ tj = jitter.tj_at_ber(
544
+ rj_rms=decomposition.rj.rj_rms,
545
+ dj_pp=decomposition.dj.dj_pp,
546
+ ber=bathtub_ber,
547
+ )
548
+ results["tj_at_ber"] = tj
549
+
550
+ # Bathtub curve
551
+ if bathtub_ber:
552
+ bathtub_result = jitter.bathtub_curve(tie, unit_interval)
553
+ results["bathtub_positions"] = bathtub_result.sample_positions
554
+ results["bathtub_ber_values"] = bathtub_result.ber_values
555
+
556
+ except Exception as e:
557
+ raise PipelineExecutionError(f"Jitter analysis failed: {e}", step_name=step_name) from e
558
+
559
+ return results
560
+
561
+
562
+ @register_handler("analysis.statistics")
563
+ def handle_analysis_statistics(
564
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
565
+ ) -> dict[str, Any]:
566
+ """Perform statistical measurements on signal data.
567
+
568
+ Inputs:
569
+ trace: WaveformTrace to analyze
570
+
571
+ Parameters:
572
+ outliers (bool, optional): Detect outliers (default: False)
573
+ outlier_method (str, optional): Outlier detection method
574
+ ('zscore', 'modified_zscore', 'iqr') (default: 'modified_zscore')
575
+ histogram_bins (int, optional): Number of histogram bins (default: 50)
576
+ percentiles (list[float], optional): Percentiles to calculate (default: [25, 50, 75])
577
+
578
+ Outputs:
579
+ mean: Mean value
580
+ std: Standard deviation
581
+ min: Minimum value
582
+ max: Maximum value
583
+ median: Median value
584
+ variance: Variance
585
+ skewness: Skewness
586
+ kurtosis: Kurtosis
587
+ percentiles: Dict of percentile values
588
+ histogram_counts: Histogram bin counts
589
+ histogram_edges: Histogram bin edges
590
+ outlier_indices: Indices of outliers (if outliers=True)
591
+ outlier_count: Number of outliers detected (if outliers=True)
592
+ """
593
+ statistics = _get_statistics()
594
+
595
+ trace = inputs.get("trace")
596
+ if trace is None:
597
+ raise PipelineExecutionError(
598
+ "Missing required input 'trace'. Suggestion: Provide a WaveformTrace to analyze",
599
+ step_name=step_name,
600
+ )
601
+
602
+ detect_outliers_flag = params.get("outliers", False)
603
+ outlier_method = params.get("outlier_method", "modified_zscore")
604
+ histogram_bins = params.get("histogram_bins", 50)
605
+ percentiles_list = params.get("percentiles", [25, 50, 75])
606
+
607
+ try:
608
+ # Basic statistics
609
+ stats = statistics.summary_stats(trace.data)
610
+
611
+ results = {
612
+ "mean": stats.mean,
613
+ "std": stats.std,
614
+ "min": stats.min,
615
+ "max": stats.max,
616
+ "median": stats.median,
617
+ "variance": stats.variance,
618
+ "skewness": stats.skewness,
619
+ "kurtosis": stats.kurtosis,
620
+ }
621
+
622
+ # Percentiles
623
+ percentile_values = statistics.percentiles(trace.data, percentiles_list)
624
+ results["percentiles"] = {
625
+ f"p{int(p)}": v for p, v in zip(percentiles_list, percentile_values, strict=True)
626
+ }
627
+
628
+ # Histogram
629
+ hist_result = statistics.histogram(trace.data, bins=histogram_bins)
630
+ results["histogram_counts"] = hist_result.counts
631
+ results["histogram_edges"] = hist_result.edges
632
+
633
+ # Outlier detection
634
+ if detect_outliers_flag:
635
+ outlier_result = statistics.detect_outliers(trace.data, method=outlier_method)
636
+ results["outlier_indices"] = outlier_result.indices
637
+ results["outlier_count"] = len(outlier_result.indices)
638
+
639
+ except Exception as e:
640
+ raise PipelineExecutionError(
641
+ f"Statistical analysis failed: {e}", step_name=step_name
642
+ ) from e
643
+
644
+ return results
645
+
646
+
647
+ @register_handler("analysis.fft")
648
+ def handle_analysis_fft(
649
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
650
+ ) -> dict[str, Any]:
651
+ """Perform FFT analysis returning frequency and magnitude arrays.
652
+
653
+ Inputs:
654
+ trace: WaveformTrace to analyze
655
+
656
+ Parameters:
657
+ window (str, optional): FFT window function (default: 'hann')
658
+ Options: 'hann', 'hamming', 'blackman', 'bartlett', 'kaiser', 'rectangular'
659
+ nfft (int, optional): FFT length (default: trace length)
660
+ return_phase (bool, optional): Also return phase information (default: False)
661
+
662
+ Outputs:
663
+ frequencies: Frequency array in Hz
664
+ magnitudes: Magnitude array (linear scale)
665
+ magnitudes_db: Magnitude array in dB (20*log10)
666
+ phases: Phase array in radians (if return_phase=True)
667
+ peak_freq: Frequency of peak magnitude
668
+ peak_magnitude: Peak magnitude value
669
+ """
670
+ spectral = _get_spectral()
671
+
672
+ trace = inputs.get("trace")
673
+ if trace is None:
674
+ raise PipelineExecutionError(
675
+ "Missing required input 'trace'. Suggestion: Provide a WaveformTrace to analyze",
676
+ step_name=step_name,
677
+ )
678
+
679
+ window = params.get("window", "hann")
680
+ nfft = params.get("nfft")
681
+ return_phase = params.get("return_phase", False)
682
+
683
+ try:
684
+ # Perform FFT
685
+ frequencies, magnitudes = spectral.fft(trace, window=window, nfft=nfft)
686
+
687
+ import numpy as np
688
+
689
+ # Convert to dB
690
+ magnitudes_db = 20 * np.log10(magnitudes + 1e-12) # Avoid log(0)
691
+
692
+ # Find peak
693
+ peak_idx = np.argmax(magnitudes)
694
+ peak_freq = float(frequencies[peak_idx])
695
+ peak_mag = float(magnitudes[peak_idx])
696
+
697
+ results = {
698
+ "frequencies": frequencies,
699
+ "magnitudes": magnitudes,
700
+ "magnitudes_db": magnitudes_db,
701
+ "peak_freq": peak_freq,
702
+ "peak_magnitude": peak_mag,
703
+ }
704
+
705
+ # Phase information if requested
706
+ if return_phase:
707
+ fft_result = np.fft.rfft(trace.data, n=nfft)
708
+ phases = np.angle(fft_result)
709
+ results["phases"] = phases
710
+
711
+ except Exception as e:
712
+ raise PipelineExecutionError(f"FFT analysis failed: {e}", step_name=step_name) from e
713
+
714
+ return results
715
+
716
+
717
+ @register_handler("analysis.psd")
718
+ def handle_analysis_psd(
719
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
720
+ ) -> dict[str, Any]:
721
+ """Calculate power spectral density.
722
+
723
+ Inputs:
724
+ trace: WaveformTrace to analyze
725
+
726
+ Parameters:
727
+ method (str, optional): PSD estimation method ('periodogram', 'welch', 'bartlett')
728
+ Default: 'periodogram'
729
+ nfft (int, optional): FFT length for PSD calculation
730
+ window (str, optional): Window function (default: 'hann')
731
+
732
+ Outputs:
733
+ frequencies: Frequency array in Hz
734
+ psd: Power spectral density array (V^2/Hz or A^2/Hz)
735
+ psd_db: PSD in dB scale (10*log10)
736
+ total_power: Total power integrated across frequency
737
+ """
738
+ spectral = _get_spectral()
739
+
740
+ trace = inputs.get("trace")
741
+ if trace is None:
742
+ raise PipelineExecutionError(
743
+ "Missing required input 'trace'. Suggestion: Provide a WaveformTrace to analyze",
744
+ step_name=step_name,
745
+ )
746
+
747
+ method = params.get("method", "periodogram")
748
+ nfft = params.get("nfft")
749
+ window = params.get("window", "hann")
750
+
751
+ try:
752
+ # Calculate PSD based on method
753
+ if method == "periodogram":
754
+ frequencies, psd_values = spectral.periodogram(trace, nfft=nfft, window=window)
755
+ elif method == "welch":
756
+ frequencies, psd_values = spectral.psd(trace, nfft=nfft, window=window)
757
+ elif method == "bartlett":
758
+ frequencies, psd_values = spectral.bartlett_psd(trace, nfft=nfft)
759
+ else:
760
+ raise ValueError(f"Unknown PSD method: {method}")
761
+
762
+ import numpy as np
763
+
764
+ # Convert to dB
765
+ psd_db = 10 * np.log10(psd_values + 1e-20) # Avoid log(0)
766
+
767
+ # Total power (integrate PSD)
768
+ freq_spacing = frequencies[1] - frequencies[0]
769
+ total_power = float(np.sum(psd_values) * freq_spacing)
770
+
771
+ results = {
772
+ "frequencies": frequencies,
773
+ "psd": psd_values,
774
+ "psd_db": psd_db,
775
+ "total_power": total_power,
776
+ }
777
+
778
+ except Exception as e:
779
+ raise PipelineExecutionError(f"PSD calculation failed: {e}", step_name=step_name) from e
780
+
781
+ return results
782
+
783
+
784
+ @register_handler("analysis.thd")
785
+ def handle_analysis_thd(
786
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
787
+ ) -> dict[str, Any]:
788
+ """Calculate total harmonic distortion.
789
+
790
+ Inputs:
791
+ trace: WaveformTrace to analyze
792
+
793
+ Parameters:
794
+ fundamental_freq (float): Fundamental frequency in Hz
795
+ num_harmonics (int, optional): Number of harmonics to analyze (default: 5)
796
+
797
+ Outputs:
798
+ thd_db: Total harmonic distortion in dB
799
+ thd_percent: THD as percentage
800
+ harmonic_powers: Array of harmonic power values
801
+ harmonic_frequencies: Array of harmonic frequencies
802
+ fundamental_power: Power at fundamental frequency
803
+ """
804
+ spectral = _get_spectral()
805
+
806
+ trace = inputs.get("trace")
807
+ if trace is None:
808
+ raise PipelineExecutionError(
809
+ "Missing required input 'trace'. Suggestion: Provide a WaveformTrace to analyze",
810
+ step_name=step_name,
811
+ )
812
+
813
+ fundamental_freq = params.get("fundamental_freq")
814
+ if fundamental_freq is None:
815
+ raise PipelineExecutionError(
816
+ "Missing required parameter 'fundamental_freq'. Suggestion: Specify the fundamental frequency for THD calculation",
817
+ step_name=step_name,
818
+ )
819
+
820
+ num_harmonics = params.get("num_harmonics", 5)
821
+
822
+ try:
823
+ # Calculate THD
824
+ thd_db = spectral.thd(trace, fundamental_freq=fundamental_freq, harmonics=num_harmonics)
825
+
826
+ # Convert to percentage
827
+ thd_percent = 100 * (10 ** (thd_db / 20))
828
+
829
+ # Get FFT to extract harmonic powers
830
+ frequencies, magnitudes = spectral.fft(trace)
831
+
832
+ freq_resolution = frequencies[1] - frequencies[0]
833
+ harmonic_powers = []
834
+ harmonic_frequencies = []
835
+
836
+ for h in range(1, num_harmonics + 2): # Include fundamental + harmonics
837
+ harmonic_freq = fundamental_freq * h
838
+ harmonic_idx = int(harmonic_freq / freq_resolution)
839
+ if harmonic_idx < len(magnitudes):
840
+ harmonic_powers.append(float(magnitudes[harmonic_idx] ** 2))
841
+ harmonic_frequencies.append(harmonic_freq)
842
+
843
+ fundamental_power = harmonic_powers[0] if harmonic_powers else 0.0
844
+
845
+ results = {
846
+ "thd_db": thd_db,
847
+ "thd_percent": thd_percent,
848
+ "harmonic_powers": harmonic_powers[1:], # Exclude fundamental
849
+ "harmonic_frequencies": harmonic_frequencies[1:],
850
+ "fundamental_power": fundamental_power,
851
+ }
852
+
853
+ except Exception as e:
854
+ raise PipelineExecutionError(f"THD calculation failed: {e}", step_name=step_name) from e
855
+
856
+ return results
857
+
858
+
859
+ @register_handler("analysis.eye_diagram")
860
+ def handle_analysis_eye_diagram(
861
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
862
+ ) -> dict[str, Any]:
863
+ """Generate and analyze eye diagram for digital signals.
864
+
865
+ Inputs:
866
+ trace: WaveformTrace or DigitalTrace to analyze
867
+
868
+ Parameters:
869
+ unit_interval (float): Unit interval (bit period) in seconds
870
+ samples_per_ui (int, optional): Samples per unit interval (default: 100)
871
+ threshold (float, optional): Decision threshold (default: auto)
872
+
873
+ Outputs:
874
+ eye_diagram: 2D array of eye diagram samples
875
+ eye_height: Eye height (voltage units)
876
+ eye_width: Eye width (time units)
877
+ eye_opening: Eye opening at center (voltage units)
878
+ q_factor: Q-factor (signal quality metric)
879
+ crossing_percentage: Percentage of ideal crossing point
880
+ jitter_rms: RMS jitter at threshold crossings
881
+ """
882
+ eye = _get_eye()
883
+
884
+ trace = inputs.get("trace")
885
+ if trace is None:
886
+ raise PipelineExecutionError(
887
+ "Missing required input 'trace'. Suggestion: Provide a WaveformTrace or DigitalTrace to analyze",
888
+ step_name=step_name,
889
+ )
890
+
891
+ unit_interval = params.get("unit_interval")
892
+ if unit_interval is None:
893
+ raise PipelineExecutionError(
894
+ "Missing required parameter 'unit_interval'. Suggestion: Specify the unit interval (bit period) in seconds",
895
+ step_name=step_name,
896
+ )
897
+
898
+ samples_per_ui = params.get("samples_per_ui", 100)
899
+ threshold = params.get("threshold")
900
+
901
+ try:
902
+ # Generate eye diagram
903
+ eye_result = eye.generate_eye(trace, unit_interval, samples_per_ui=samples_per_ui)
904
+
905
+ # Calculate eye metrics
906
+ eye_metrics = eye.measure_eye(eye_result, threshold=threshold)
907
+
908
+ results = {
909
+ "eye_diagram": eye_result.data,
910
+ "eye_height": eye_metrics.height,
911
+ "eye_width": eye_metrics.width,
912
+ "eye_opening": eye_metrics.opening,
913
+ "q_factor": eye_metrics.q_factor,
914
+ "crossing_percentage": eye_metrics.crossing_percentage,
915
+ "jitter_rms": eye_metrics.jitter_rms,
916
+ }
917
+
918
+ except Exception as e:
919
+ raise PipelineExecutionError(
920
+ f"Eye diagram analysis failed: {e}", step_name=step_name
921
+ ) from e
922
+
923
+ return results
924
+
925
+
926
+ @register_handler("analysis.auto")
927
+ def handle_analysis_auto(
928
+ inputs: dict[str, Any], params: dict[str, Any], step_name: str
929
+ ) -> dict[str, Any]:
930
+ """Auto-detect signal type and perform appropriate analysis.
931
+
932
+ Automatically determines signal characteristics and performs relevant measurements.
933
+
934
+ Inputs:
935
+ trace: WaveformTrace or DigitalTrace to analyze
936
+
937
+ Parameters:
938
+ detailed (bool, optional): Include all available measurements (default: False)
939
+
940
+ Outputs:
941
+ signal_type: Detected signal type ('analog', 'digital', 'mixed')
942
+ frequency: Dominant frequency if detected
943
+ measurements: Dict of relevant measurements
944
+ recommendations: List of suggested additional analyses
945
+ """
946
+ waveform = _get_waveform()
947
+ statistics = _get_statistics()
948
+
949
+ trace = inputs.get("trace")
950
+ if trace is None:
951
+ raise PipelineExecutionError(
952
+ "Missing required input 'trace'. Suggestion: Provide a trace to analyze",
953
+ step_name=step_name,
954
+ )
955
+
956
+ detailed = params.get("detailed", False)
957
+
958
+ try:
959
+ import numpy as np
960
+
961
+ # Detect signal type
962
+ data = trace.data
963
+ unique_values = len(np.unique(data))
964
+ data_range = np.ptp(data)
965
+
966
+ # Simple heuristic: digital if few unique values
967
+ if unique_values <= 10 and data_range > 0:
968
+ signal_type = "digital"
969
+ elif unique_values > 10:
970
+ signal_type = "analog"
971
+ else:
972
+ signal_type = "unknown"
973
+
974
+ measurements = {}
975
+ recommendations = []
976
+
977
+ # Frequency detection
978
+ try:
979
+ freq = waveform.frequency(trace)
980
+ measurements["frequency"] = freq
981
+ except Exception:
982
+ # Best-effort: frequency detection may fail on noisy/aperiodic signals
983
+ freq = None
984
+
985
+ # Basic statistics
986
+ stats = statistics.summary_stats(data)
987
+ measurements.update(
988
+ {
989
+ "mean": stats.mean,
990
+ "std": stats.std,
991
+ "min": stats.min,
992
+ "max": stats.max,
993
+ }
994
+ )
995
+
996
+ # Additional measurements based on signal type
997
+ if signal_type == "analog":
998
+ measurements["amplitude"] = waveform.amplitude(trace)
999
+ measurements["rms"] = waveform.rms(trace)
1000
+
1001
+ if detailed:
1002
+ try:
1003
+ measurements["rise_time"] = waveform.rise_time(trace)
1004
+ measurements["fall_time"] = waveform.fall_time(trace)
1005
+ except Exception:
1006
+ # Best-effort: rise/fall time may fail on slow/noisy edges
1007
+ pass
1008
+
1009
+ recommendations.extend(
1010
+ [
1011
+ "analysis.spectral - for frequency domain analysis",
1012
+ "analysis.waveform - for detailed timing measurements",
1013
+ ]
1014
+ )
1015
+
1016
+ elif signal_type == "digital":
1017
+ try:
1018
+ measurements["duty_cycle"] = waveform.duty_cycle(trace)
1019
+ except Exception:
1020
+ # Best-effort: duty cycle may fail on aperiodic signals
1021
+ pass
1022
+
1023
+ recommendations.extend(
1024
+ [
1025
+ "analysis.jitter - for jitter analysis",
1026
+ "analysis.eye_diagram - if data rate is known",
1027
+ "decoder.auto - to identify protocol",
1028
+ ]
1029
+ )
1030
+
1031
+ # Suggest power analysis if signal looks like voltage or current
1032
+ if data_range > 0.1: # Arbitrary threshold
1033
+ recommendations.append("analysis.power - if you have both voltage and current traces")
1034
+
1035
+ results = {
1036
+ "signal_type": signal_type,
1037
+ "frequency": freq,
1038
+ "measurements": measurements,
1039
+ "recommendations": recommendations,
1040
+ }
1041
+
1042
+ except Exception as e:
1043
+ raise PipelineExecutionError(f"Auto-analysis failed: {e}", step_name=step_name) from e
1044
+
1045
+ return results