oscura 0.5.0__py3-none-any.whl → 0.6.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 (513) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/__init__.py +0 -48
  5. oscura/analyzers/digital/edges.py +325 -65
  6. oscura/analyzers/digital/extraction.py +0 -195
  7. oscura/analyzers/digital/quality.py +293 -166
  8. oscura/analyzers/digital/timing.py +260 -115
  9. oscura/analyzers/digital/timing_numba.py +334 -0
  10. oscura/analyzers/entropy.py +605 -0
  11. oscura/analyzers/eye/diagram.py +176 -109
  12. oscura/analyzers/eye/metrics.py +5 -5
  13. oscura/analyzers/jitter/__init__.py +6 -4
  14. oscura/analyzers/jitter/ber.py +52 -52
  15. oscura/analyzers/jitter/classification.py +156 -0
  16. oscura/analyzers/jitter/decomposition.py +163 -113
  17. oscura/analyzers/jitter/spectrum.py +80 -64
  18. oscura/analyzers/ml/__init__.py +39 -0
  19. oscura/analyzers/ml/features.py +600 -0
  20. oscura/analyzers/ml/signal_classifier.py +604 -0
  21. oscura/analyzers/packet/daq.py +246 -158
  22. oscura/analyzers/packet/parser.py +12 -1
  23. oscura/analyzers/packet/payload.py +50 -2110
  24. oscura/analyzers/packet/payload_analysis.py +361 -181
  25. oscura/analyzers/packet/payload_patterns.py +133 -70
  26. oscura/analyzers/packet/stream.py +84 -23
  27. oscura/analyzers/patterns/__init__.py +26 -5
  28. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  29. oscura/analyzers/patterns/clustering.py +169 -108
  30. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  31. oscura/analyzers/patterns/discovery.py +1 -1
  32. oscura/analyzers/patterns/matching.py +581 -197
  33. oscura/analyzers/patterns/pattern_mining.py +778 -0
  34. oscura/analyzers/patterns/periodic.py +121 -38
  35. oscura/analyzers/patterns/sequences.py +175 -78
  36. oscura/analyzers/power/conduction.py +1 -1
  37. oscura/analyzers/power/soa.py +6 -6
  38. oscura/analyzers/power/switching.py +250 -110
  39. oscura/analyzers/protocol/__init__.py +17 -1
  40. oscura/analyzers/protocols/__init__.py +1 -22
  41. oscura/analyzers/protocols/base.py +6 -6
  42. oscura/analyzers/protocols/ble/__init__.py +38 -0
  43. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  44. oscura/analyzers/protocols/ble/uuids.py +288 -0
  45. oscura/analyzers/protocols/can.py +257 -127
  46. oscura/analyzers/protocols/can_fd.py +107 -80
  47. oscura/analyzers/protocols/flexray.py +139 -80
  48. oscura/analyzers/protocols/hdlc.py +93 -58
  49. oscura/analyzers/protocols/i2c.py +247 -106
  50. oscura/analyzers/protocols/i2s.py +138 -86
  51. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  52. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  53. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  54. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  55. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  56. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  57. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  58. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  59. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  60. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  61. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  62. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  63. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  64. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  65. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  66. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  67. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  68. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  69. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  70. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  71. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  72. oscura/analyzers/protocols/jtag.py +180 -98
  73. oscura/analyzers/protocols/lin.py +219 -114
  74. oscura/analyzers/protocols/manchester.py +4 -4
  75. oscura/analyzers/protocols/onewire.py +253 -149
  76. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  77. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  78. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  79. oscura/analyzers/protocols/spi.py +192 -95
  80. oscura/analyzers/protocols/swd.py +321 -167
  81. oscura/analyzers/protocols/uart.py +267 -125
  82. oscura/analyzers/protocols/usb.py +235 -131
  83. oscura/analyzers/side_channel/power.py +17 -12
  84. oscura/analyzers/signal/__init__.py +15 -0
  85. oscura/analyzers/signal/timing_analysis.py +1086 -0
  86. oscura/analyzers/signal_integrity/__init__.py +4 -1
  87. oscura/analyzers/signal_integrity/sparams.py +2 -19
  88. oscura/analyzers/spectral/chunked.py +129 -60
  89. oscura/analyzers/spectral/chunked_fft.py +300 -94
  90. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  91. oscura/analyzers/statistical/checksum.py +376 -217
  92. oscura/analyzers/statistical/classification.py +229 -107
  93. oscura/analyzers/statistical/entropy.py +78 -53
  94. oscura/analyzers/statistics/correlation.py +407 -211
  95. oscura/analyzers/statistics/outliers.py +2 -2
  96. oscura/analyzers/statistics/streaming.py +30 -5
  97. oscura/analyzers/validation.py +216 -101
  98. oscura/analyzers/waveform/measurements.py +9 -0
  99. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  100. oscura/analyzers/waveform/spectral.py +500 -228
  101. oscura/api/__init__.py +31 -5
  102. oscura/api/dsl/__init__.py +582 -0
  103. oscura/{dsl → api/dsl}/commands.py +43 -76
  104. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  105. oscura/{dsl → api/dsl}/parser.py +107 -77
  106. oscura/{dsl → api/dsl}/repl.py +2 -2
  107. oscura/api/dsl.py +1 -1
  108. oscura/{integrations → api/integrations}/__init__.py +1 -1
  109. oscura/{integrations → api/integrations}/llm.py +201 -102
  110. oscura/api/operators.py +3 -3
  111. oscura/api/optimization.py +144 -30
  112. oscura/api/rest_server.py +921 -0
  113. oscura/api/server/__init__.py +17 -0
  114. oscura/api/server/dashboard.py +850 -0
  115. oscura/api/server/static/README.md +34 -0
  116. oscura/api/server/templates/base.html +181 -0
  117. oscura/api/server/templates/export.html +120 -0
  118. oscura/api/server/templates/home.html +284 -0
  119. oscura/api/server/templates/protocols.html +58 -0
  120. oscura/api/server/templates/reports.html +43 -0
  121. oscura/api/server/templates/session_detail.html +89 -0
  122. oscura/api/server/templates/sessions.html +83 -0
  123. oscura/api/server/templates/waveforms.html +73 -0
  124. oscura/automotive/__init__.py +8 -1
  125. oscura/automotive/can/__init__.py +10 -0
  126. oscura/automotive/can/checksum.py +3 -1
  127. oscura/automotive/can/dbc_generator.py +590 -0
  128. oscura/automotive/can/message_wrapper.py +121 -74
  129. oscura/automotive/can/patterns.py +98 -21
  130. oscura/automotive/can/session.py +292 -56
  131. oscura/automotive/can/state_machine.py +6 -3
  132. oscura/automotive/can/stimulus_response.py +97 -75
  133. oscura/automotive/dbc/__init__.py +10 -2
  134. oscura/automotive/dbc/generator.py +84 -56
  135. oscura/automotive/dbc/parser.py +6 -6
  136. oscura/automotive/dtc/data.json +2763 -0
  137. oscura/automotive/dtc/database.py +2 -2
  138. oscura/automotive/flexray/__init__.py +31 -0
  139. oscura/automotive/flexray/analyzer.py +504 -0
  140. oscura/automotive/flexray/crc.py +185 -0
  141. oscura/automotive/flexray/fibex.py +449 -0
  142. oscura/automotive/j1939/__init__.py +45 -8
  143. oscura/automotive/j1939/analyzer.py +605 -0
  144. oscura/automotive/j1939/spns.py +326 -0
  145. oscura/automotive/j1939/transport.py +306 -0
  146. oscura/automotive/lin/__init__.py +47 -0
  147. oscura/automotive/lin/analyzer.py +612 -0
  148. oscura/automotive/loaders/blf.py +13 -2
  149. oscura/automotive/loaders/csv_can.py +143 -72
  150. oscura/automotive/loaders/dispatcher.py +50 -2
  151. oscura/automotive/loaders/mdf.py +86 -45
  152. oscura/automotive/loaders/pcap.py +111 -61
  153. oscura/automotive/uds/__init__.py +4 -0
  154. oscura/automotive/uds/analyzer.py +725 -0
  155. oscura/automotive/uds/decoder.py +140 -58
  156. oscura/automotive/uds/models.py +7 -1
  157. oscura/automotive/visualization.py +1 -1
  158. oscura/cli/analyze.py +348 -0
  159. oscura/cli/batch.py +142 -122
  160. oscura/cli/benchmark.py +275 -0
  161. oscura/cli/characterize.py +137 -82
  162. oscura/cli/compare.py +224 -131
  163. oscura/cli/completion.py +250 -0
  164. oscura/cli/config_cmd.py +361 -0
  165. oscura/cli/decode.py +164 -87
  166. oscura/cli/export.py +286 -0
  167. oscura/cli/main.py +115 -31
  168. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  169. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  170. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  171. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  172. oscura/cli/progress.py +147 -0
  173. oscura/cli/shell.py +157 -135
  174. oscura/cli/validate_cmd.py +204 -0
  175. oscura/cli/visualize.py +158 -0
  176. oscura/convenience.py +125 -79
  177. oscura/core/__init__.py +4 -2
  178. oscura/core/backend_selector.py +3 -3
  179. oscura/core/cache.py +126 -15
  180. oscura/core/cancellation.py +1 -1
  181. oscura/{config → core/config}/__init__.py +20 -11
  182. oscura/{config → core/config}/defaults.py +1 -1
  183. oscura/{config → core/config}/loader.py +7 -5
  184. oscura/{config → core/config}/memory.py +5 -5
  185. oscura/{config → core/config}/migration.py +1 -1
  186. oscura/{config → core/config}/pipeline.py +99 -23
  187. oscura/{config → core/config}/preferences.py +1 -1
  188. oscura/{config → core/config}/protocol.py +3 -3
  189. oscura/{config → core/config}/schema.py +426 -272
  190. oscura/{config → core/config}/settings.py +1 -1
  191. oscura/{config → core/config}/thresholds.py +195 -153
  192. oscura/core/correlation.py +5 -6
  193. oscura/core/cross_domain.py +0 -2
  194. oscura/core/debug.py +9 -5
  195. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  196. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  197. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  198. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  199. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  200. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  201. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  202. oscura/core/gpu_backend.py +11 -7
  203. oscura/core/log_query.py +101 -11
  204. oscura/core/logging.py +126 -54
  205. oscura/core/logging_advanced.py +5 -5
  206. oscura/core/memory_limits.py +108 -70
  207. oscura/core/memory_monitor.py +2 -2
  208. oscura/core/memory_progress.py +7 -7
  209. oscura/core/memory_warnings.py +1 -1
  210. oscura/core/numba_backend.py +13 -13
  211. oscura/{plugins → core/plugins}/__init__.py +9 -9
  212. oscura/{plugins → core/plugins}/base.py +7 -7
  213. oscura/{plugins → core/plugins}/cli.py +3 -3
  214. oscura/{plugins → core/plugins}/discovery.py +186 -106
  215. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  216. oscura/{plugins → core/plugins}/manager.py +7 -7
  217. oscura/{plugins → core/plugins}/registry.py +3 -3
  218. oscura/{plugins → core/plugins}/versioning.py +1 -1
  219. oscura/core/progress.py +16 -1
  220. oscura/core/provenance.py +8 -2
  221. oscura/{schemas → core/schemas}/__init__.py +2 -2
  222. oscura/core/schemas/bus_configuration.json +322 -0
  223. oscura/core/schemas/device_mapping.json +182 -0
  224. oscura/core/schemas/packet_format.json +418 -0
  225. oscura/core/schemas/protocol_definition.json +363 -0
  226. oscura/core/types.py +4 -0
  227. oscura/core/uncertainty.py +3 -3
  228. oscura/correlation/__init__.py +52 -0
  229. oscura/correlation/multi_protocol.py +811 -0
  230. oscura/discovery/auto_decoder.py +117 -35
  231. oscura/discovery/comparison.py +191 -86
  232. oscura/discovery/quality_validator.py +155 -68
  233. oscura/discovery/signal_detector.py +196 -79
  234. oscura/export/__init__.py +18 -20
  235. oscura/export/kaitai_struct.py +513 -0
  236. oscura/export/scapy_layer.py +801 -0
  237. oscura/export/wireshark/README.md +15 -15
  238. oscura/export/wireshark/generator.py +1 -1
  239. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  240. oscura/export/wireshark_dissector.py +746 -0
  241. oscura/guidance/wizard.py +207 -111
  242. oscura/hardware/__init__.py +19 -0
  243. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  244. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  245. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  246. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  247. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  248. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  249. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  250. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  251. oscura/hardware/firmware/__init__.py +29 -0
  252. oscura/hardware/firmware/pattern_recognition.py +874 -0
  253. oscura/hardware/hal_detector.py +736 -0
  254. oscura/hardware/security/__init__.py +37 -0
  255. oscura/hardware/security/side_channel_detector.py +1126 -0
  256. oscura/inference/__init__.py +4 -0
  257. oscura/inference/active_learning/README.md +7 -7
  258. oscura/inference/active_learning/observation_table.py +4 -1
  259. oscura/inference/alignment.py +216 -123
  260. oscura/inference/bayesian.py +113 -33
  261. oscura/inference/crc_reverse.py +101 -55
  262. oscura/inference/logic.py +6 -2
  263. oscura/inference/message_format.py +342 -183
  264. oscura/inference/protocol.py +95 -44
  265. oscura/inference/protocol_dsl.py +180 -82
  266. oscura/inference/signal_intelligence.py +1439 -706
  267. oscura/inference/spectral.py +99 -57
  268. oscura/inference/state_machine.py +810 -158
  269. oscura/inference/stream.py +270 -110
  270. oscura/iot/__init__.py +34 -0
  271. oscura/iot/coap/__init__.py +32 -0
  272. oscura/iot/coap/analyzer.py +668 -0
  273. oscura/iot/coap/options.py +212 -0
  274. oscura/iot/lorawan/__init__.py +21 -0
  275. oscura/iot/lorawan/crypto.py +206 -0
  276. oscura/iot/lorawan/decoder.py +801 -0
  277. oscura/iot/lorawan/mac_commands.py +341 -0
  278. oscura/iot/mqtt/__init__.py +27 -0
  279. oscura/iot/mqtt/analyzer.py +999 -0
  280. oscura/iot/mqtt/properties.py +315 -0
  281. oscura/iot/zigbee/__init__.py +31 -0
  282. oscura/iot/zigbee/analyzer.py +615 -0
  283. oscura/iot/zigbee/security.py +153 -0
  284. oscura/iot/zigbee/zcl.py +349 -0
  285. oscura/jupyter/display.py +125 -45
  286. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  287. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  288. oscura/jupyter/exploratory/fuzzy.py +746 -0
  289. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  290. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  291. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  292. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  293. oscura/jupyter/exploratory/sync.py +612 -0
  294. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  295. oscura/jupyter/magic.py +4 -4
  296. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  297. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  298. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  299. oscura/loaders/__init__.py +171 -63
  300. oscura/loaders/binary.py +88 -1
  301. oscura/loaders/chipwhisperer.py +153 -137
  302. oscura/loaders/configurable.py +208 -86
  303. oscura/loaders/csv_loader.py +458 -215
  304. oscura/loaders/hdf5_loader.py +278 -119
  305. oscura/loaders/lazy.py +87 -54
  306. oscura/loaders/mmap_loader.py +1 -1
  307. oscura/loaders/numpy_loader.py +253 -116
  308. oscura/loaders/pcap.py +226 -151
  309. oscura/loaders/rigol.py +110 -49
  310. oscura/loaders/sigrok.py +201 -78
  311. oscura/loaders/tdms.py +81 -58
  312. oscura/loaders/tektronix.py +291 -174
  313. oscura/loaders/touchstone.py +182 -87
  314. oscura/loaders/vcd.py +215 -117
  315. oscura/loaders/wav.py +155 -68
  316. oscura/reporting/__init__.py +9 -7
  317. oscura/reporting/analyze.py +352 -146
  318. oscura/reporting/argument_preparer.py +69 -14
  319. oscura/reporting/auto_report.py +97 -61
  320. oscura/reporting/batch.py +131 -58
  321. oscura/reporting/chart_selection.py +57 -45
  322. oscura/reporting/comparison.py +63 -17
  323. oscura/reporting/content/executive.py +76 -24
  324. oscura/reporting/core_formats/multi_format.py +11 -8
  325. oscura/reporting/engine.py +312 -158
  326. oscura/reporting/enhanced_reports.py +949 -0
  327. oscura/reporting/export.py +86 -43
  328. oscura/reporting/formatting/numbers.py +69 -42
  329. oscura/reporting/html.py +139 -58
  330. oscura/reporting/index.py +137 -65
  331. oscura/reporting/output.py +158 -67
  332. oscura/reporting/pdf.py +67 -102
  333. oscura/reporting/plots.py +191 -112
  334. oscura/reporting/sections.py +88 -47
  335. oscura/reporting/standards.py +104 -61
  336. oscura/reporting/summary_generator.py +75 -55
  337. oscura/reporting/tables.py +138 -54
  338. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  339. oscura/reporting/templates/index.md +13 -13
  340. oscura/sessions/__init__.py +14 -23
  341. oscura/sessions/base.py +3 -3
  342. oscura/sessions/blackbox.py +106 -10
  343. oscura/sessions/generic.py +2 -2
  344. oscura/sessions/legacy.py +783 -0
  345. oscura/side_channel/__init__.py +63 -0
  346. oscura/side_channel/dpa.py +1025 -0
  347. oscura/utils/__init__.py +15 -1
  348. oscura/utils/autodetect.py +1 -5
  349. oscura/utils/bitwise.py +118 -0
  350. oscura/{builders → utils/builders}/__init__.py +1 -1
  351. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  352. oscura/{comparison → utils/comparison}/compare.py +202 -101
  353. oscura/{comparison → utils/comparison}/golden.py +83 -63
  354. oscura/{comparison → utils/comparison}/limits.py +313 -89
  355. oscura/{comparison → utils/comparison}/mask.py +151 -45
  356. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  357. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  358. oscura/{component → utils/component}/__init__.py +3 -3
  359. oscura/{component → utils/component}/impedance.py +122 -58
  360. oscura/{component → utils/component}/reactive.py +165 -168
  361. oscura/{component → utils/component}/transmission_line.py +3 -3
  362. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  363. oscura/{filtering → utils/filtering}/base.py +1 -1
  364. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  365. oscura/{filtering → utils/filtering}/design.py +169 -93
  366. oscura/{filtering → utils/filtering}/filters.py +2 -2
  367. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  368. oscura/utils/geometry.py +31 -0
  369. oscura/utils/imports.py +184 -0
  370. oscura/utils/lazy.py +1 -1
  371. oscura/{math → utils/math}/__init__.py +2 -2
  372. oscura/{math → utils/math}/arithmetic.py +114 -48
  373. oscura/{math → utils/math}/interpolation.py +139 -106
  374. oscura/utils/memory.py +129 -66
  375. oscura/utils/memory_advanced.py +92 -9
  376. oscura/utils/memory_extensions.py +10 -8
  377. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  378. oscura/{optimization → utils/optimization}/search.py +2 -2
  379. oscura/utils/performance/__init__.py +58 -0
  380. oscura/utils/performance/caching.py +889 -0
  381. oscura/utils/performance/lsh_clustering.py +333 -0
  382. oscura/utils/performance/memory_optimizer.py +699 -0
  383. oscura/utils/performance/optimizations.py +675 -0
  384. oscura/utils/performance/parallel.py +654 -0
  385. oscura/utils/performance/profiling.py +661 -0
  386. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  387. oscura/{pipeline → utils/pipeline}/composition.py +11 -3
  388. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  389. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  390. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  391. oscura/{search → utils/search}/__init__.py +3 -3
  392. oscura/{search → utils/search}/anomaly.py +188 -58
  393. oscura/utils/search/context.py +294 -0
  394. oscura/{search → utils/search}/pattern.py +138 -10
  395. oscura/utils/serial.py +51 -0
  396. oscura/utils/storage/__init__.py +61 -0
  397. oscura/utils/storage/database.py +1166 -0
  398. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  399. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  400. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  401. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  402. oscura/{triggering → utils/triggering}/base.py +6 -6
  403. oscura/{triggering → utils/triggering}/edge.py +2 -2
  404. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  405. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  406. oscura/{triggering → utils/triggering}/window.py +2 -2
  407. oscura/utils/validation.py +32 -0
  408. oscura/validation/__init__.py +121 -0
  409. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  410. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  411. oscura/{compliance → validation/compliance}/masks.py +1 -1
  412. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  413. oscura/{compliance → validation/compliance}/testing.py +114 -52
  414. oscura/validation/compliance_tests.py +915 -0
  415. oscura/validation/fuzzer.py +990 -0
  416. oscura/validation/grammar_tests.py +596 -0
  417. oscura/validation/grammar_validator.py +904 -0
  418. oscura/validation/hil_testing.py +977 -0
  419. oscura/{quality → validation/quality}/__init__.py +4 -4
  420. oscura/{quality → validation/quality}/ensemble.py +251 -171
  421. oscura/{quality → validation/quality}/explainer.py +3 -3
  422. oscura/{quality → validation/quality}/scoring.py +1 -1
  423. oscura/{quality → validation/quality}/warnings.py +4 -4
  424. oscura/validation/regression_suite.py +808 -0
  425. oscura/validation/replay.py +788 -0
  426. oscura/{testing → validation/testing}/__init__.py +2 -2
  427. oscura/{testing → validation/testing}/synthetic.py +5 -5
  428. oscura/visualization/__init__.py +9 -0
  429. oscura/visualization/accessibility.py +1 -1
  430. oscura/visualization/annotations.py +64 -67
  431. oscura/visualization/colors.py +7 -7
  432. oscura/visualization/digital.py +180 -81
  433. oscura/visualization/eye.py +236 -85
  434. oscura/visualization/interactive.py +320 -143
  435. oscura/visualization/jitter.py +587 -247
  436. oscura/visualization/layout.py +169 -134
  437. oscura/visualization/optimization.py +103 -52
  438. oscura/visualization/palettes.py +1 -1
  439. oscura/visualization/power.py +427 -211
  440. oscura/visualization/power_extended.py +626 -297
  441. oscura/visualization/presets.py +2 -0
  442. oscura/visualization/protocols.py +495 -181
  443. oscura/visualization/render.py +79 -63
  444. oscura/visualization/reverse_engineering.py +171 -124
  445. oscura/visualization/signal_integrity.py +460 -279
  446. oscura/visualization/specialized.py +190 -100
  447. oscura/visualization/spectral.py +670 -255
  448. oscura/visualization/thumbnails.py +166 -137
  449. oscura/visualization/waveform.py +150 -63
  450. oscura/workflows/__init__.py +3 -0
  451. oscura/{batch → workflows/batch}/__init__.py +5 -5
  452. oscura/{batch → workflows/batch}/advanced.py +150 -75
  453. oscura/workflows/batch/aggregate.py +531 -0
  454. oscura/workflows/batch/analyze.py +236 -0
  455. oscura/{batch → workflows/batch}/logging.py +2 -2
  456. oscura/{batch → workflows/batch}/metrics.py +1 -1
  457. oscura/workflows/complete_re.py +1144 -0
  458. oscura/workflows/compliance.py +44 -54
  459. oscura/workflows/digital.py +197 -51
  460. oscura/workflows/legacy/__init__.py +12 -0
  461. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  462. oscura/workflows/multi_trace.py +9 -9
  463. oscura/workflows/power.py +42 -62
  464. oscura/workflows/protocol.py +82 -49
  465. oscura/workflows/reverse_engineering.py +351 -150
  466. oscura/workflows/signal_integrity.py +157 -82
  467. oscura-0.6.0.dist-info/METADATA +643 -0
  468. oscura-0.6.0.dist-info/RECORD +590 -0
  469. oscura/analyzers/digital/ic_database.py +0 -498
  470. oscura/analyzers/digital/timing_paths.py +0 -339
  471. oscura/analyzers/digital/vintage.py +0 -377
  472. oscura/analyzers/digital/vintage_result.py +0 -148
  473. oscura/analyzers/protocols/parallel_bus.py +0 -449
  474. oscura/batch/aggregate.py +0 -300
  475. oscura/batch/analyze.py +0 -139
  476. oscura/dsl/__init__.py +0 -73
  477. oscura/exceptions.py +0 -59
  478. oscura/exploratory/fuzzy.py +0 -513
  479. oscura/exploratory/sync.py +0 -384
  480. oscura/export/wavedrom.py +0 -430
  481. oscura/exporters/__init__.py +0 -94
  482. oscura/exporters/csv.py +0 -303
  483. oscura/exporters/exporters.py +0 -44
  484. oscura/exporters/hdf5.py +0 -217
  485. oscura/exporters/html_export.py +0 -701
  486. oscura/exporters/json_export.py +0 -338
  487. oscura/exporters/markdown_export.py +0 -367
  488. oscura/exporters/matlab_export.py +0 -354
  489. oscura/exporters/npz_export.py +0 -219
  490. oscura/exporters/spice_export.py +0 -210
  491. oscura/exporters/vintage_logic_csv.py +0 -247
  492. oscura/reporting/vintage_logic_report.py +0 -523
  493. oscura/search/context.py +0 -149
  494. oscura/session/__init__.py +0 -34
  495. oscura/session/annotations.py +0 -289
  496. oscura/session/history.py +0 -313
  497. oscura/session/session.py +0 -520
  498. oscura/visualization/digital_advanced.py +0 -718
  499. oscura/visualization/figure_manager.py +0 -156
  500. oscura/workflow/__init__.py +0 -13
  501. oscura-0.5.0.dist-info/METADATA +0 -407
  502. oscura-0.5.0.dist-info/RECORD +0 -486
  503. /oscura/core/{config.py → config/legacy.py} +0 -0
  504. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  505. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  506. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  507. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  508. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  509. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  510. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  511. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
  512. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
  513. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -5,7 +5,7 @@ results first with options to drill down into details on demand.
5
5
 
6
6
 
7
7
  Example:
8
- >>> from oscura.ui import ProgressiveDisplay
8
+ >>> from oscura.jupyter.ui import ProgressiveDisplay
9
9
  >>> display = ProgressiveDisplay()
10
10
  >>> output = display.render(result)
11
11
  >>> print(output.summary()) # Level 1: Summary
@@ -215,7 +215,27 @@ class ProgressiveDisplay:
215
215
  >>> output = display.render(characterization_result)
216
216
  >>> print(output.summary())
217
217
  """
218
- # Level 1: Build summary (3-5 key items)
218
+ level1_content = self._build_level1_summary(result)
219
+ level2_sections = self._build_level2_sections(result)
220
+ level3_data = self._build_level3_expert_data(result)
221
+ current_level = self._determine_current_level()
222
+
223
+ return ProgressiveOutput(
224
+ level1_content=level1_content,
225
+ level2_sections=level2_sections,
226
+ level3_data=level3_data,
227
+ current_level=current_level,
228
+ )
229
+
230
+ def _build_level1_summary(self, result: Any) -> str:
231
+ """Build Level 1 summary content.
232
+
233
+ Args:
234
+ result: Analysis result.
235
+
236
+ Returns:
237
+ Summary string with key items.
238
+ """
219
239
  level1_items = []
220
240
 
221
241
  if hasattr(result, "signal_type"):
@@ -231,85 +251,136 @@ class ProgressiveDisplay:
231
251
  if hasattr(result, "status"):
232
252
  level1_items.append(f"Status: {result.status}")
233
253
 
234
- # Limit to max_summary_items
235
254
  level1_items = level1_items[: self.max_summary_items]
236
255
 
237
- level1_content = "\n".join(level1_items)
238
-
239
256
  if not level1_items:
240
- level1_content = "Analysis complete. Expand for details."
257
+ return "Analysis complete. Expand for details."
258
+
259
+ return "\n".join(level1_items)
260
+
261
+ def _build_level2_sections(self, result: Any) -> list[Section]:
262
+ """Build Level 2 detailed sections.
263
+
264
+ Args:
265
+ result: Analysis result.
266
+
267
+ Returns:
268
+ List of Section objects.
269
+ """
270
+ sections = []
271
+
272
+ params_section = self._build_parameters_section(result)
273
+ if params_section:
274
+ sections.append(params_section)
241
275
 
242
- # Level 2: Build sections
243
- level2_sections = []
276
+ quality_section = self._build_quality_metrics_section(result)
277
+ if quality_section:
278
+ sections.append(quality_section)
244
279
 
245
- # Parameters section
246
- if hasattr(result, "parameters") and result.parameters:
247
- params = result.parameters
248
- summary = f"{len(params)} parameters detected"
280
+ findings_section = self._build_findings_section(result)
281
+ if findings_section:
282
+ sections.append(findings_section)
283
+
284
+ return sections
285
+
286
+ def _build_parameters_section(self, result: Any) -> Section | None:
287
+ """Build parameters section.
288
+
289
+ Args:
290
+ result: Analysis result.
249
291
 
250
- content = "Parameters:\n"
251
- for key, value in params.items():
292
+ Returns:
293
+ Section or None if no parameters.
294
+ """
295
+ if not (hasattr(result, "parameters") and result.parameters):
296
+ return None
297
+
298
+ params = result.parameters
299
+ summary = f"{len(params)} parameters detected"
300
+
301
+ content = "Parameters:\n"
302
+ for key, value in params.items():
303
+ content += f" {key}: {value}\n"
304
+
305
+ return Section(
306
+ title="Parameters",
307
+ summary=summary,
308
+ content=content,
309
+ is_collapsed=self.enable_collapsible_sections,
310
+ detail_level=2,
311
+ )
312
+
313
+ def _build_quality_metrics_section(self, result: Any) -> Section | None:
314
+ """Build quality metrics section.
315
+
316
+ Args:
317
+ result: Analysis result.
318
+
319
+ Returns:
320
+ Section or None if no quality data.
321
+ """
322
+ if not (hasattr(result, "quality") or hasattr(result, "metrics")):
323
+ return None
324
+
325
+ metrics = getattr(result, "metrics", {})
326
+ summary = "Quality assessment available"
327
+ content = "Quality Metrics:\n"
328
+
329
+ if hasattr(result, "quality"):
330
+ content += f" Overall: {result.quality}\n"
331
+
332
+ if metrics:
333
+ for key, value in metrics.items():
252
334
  content += f" {key}: {value}\n"
253
335
 
254
- level2_sections.append(
255
- Section(
256
- title="Parameters",
257
- summary=summary,
258
- content=content,
259
- is_collapsed=self.enable_collapsible_sections,
260
- detail_level=2,
261
- )
262
- )
263
-
264
- # Quality metrics section
265
- if hasattr(result, "quality") or hasattr(result, "metrics"):
266
- metrics = getattr(result, "metrics", {})
267
-
268
- summary = "Quality assessment available"
269
- content = "Quality Metrics:\n"
270
-
271
- if hasattr(result, "quality"):
272
- content += f" Overall: {result.quality}\n"
273
-
274
- if metrics:
275
- for key, value in metrics.items():
276
- content += f" {key}: {value}\n"
277
-
278
- level2_sections.append(
279
- Section(
280
- title="Quality Metrics",
281
- summary=summary,
282
- content=content,
283
- is_collapsed=self.enable_collapsible_sections,
284
- detail_level=2,
285
- )
286
- )
287
-
288
- # Findings section
289
- if hasattr(result, "findings") and result.findings:
290
- findings = result.findings
291
-
292
- summary = f"{len(findings)} findings"
293
- content = "Findings:\n"
294
-
295
- for i, finding in enumerate(findings, 1):
296
- if hasattr(finding, "title") and hasattr(finding, "description"):
297
- content += f"\n{i}. {finding.title}\n"
298
- content += f" {finding.description}\n"
299
- else:
300
- content += f"\n{i}. {finding}\n"
301
-
302
- level2_sections.append(
303
- Section(
304
- title="Findings",
305
- summary=summary,
306
- content=content,
307
- is_collapsed=self.enable_collapsible_sections,
308
- detail_level=2,
309
- )
310
- )
311
-
312
- # Level 3: Build expert data
336
+ return Section(
337
+ title="Quality Metrics",
338
+ summary=summary,
339
+ content=content,
340
+ is_collapsed=self.enable_collapsible_sections,
341
+ detail_level=2,
342
+ )
343
+
344
+ def _build_findings_section(self, result: Any) -> Section | None:
345
+ """Build findings section.
346
+
347
+ Args:
348
+ result: Analysis result.
349
+
350
+ Returns:
351
+ Section or None if no findings.
352
+ """
353
+ if not (hasattr(result, "findings") and result.findings):
354
+ return None
355
+
356
+ findings = result.findings
357
+ summary = f"{len(findings)} findings"
358
+ content = "Findings:\n"
359
+
360
+ for i, finding in enumerate(findings, 1):
361
+ if hasattr(finding, "title") and hasattr(finding, "description"):
362
+ content += f"\n{i}. {finding.title}\n"
363
+ content += f" {finding.description}\n"
364
+ else:
365
+ content += f"\n{i}. {finding}\n"
366
+
367
+ return Section(
368
+ title="Findings",
369
+ summary=summary,
370
+ content=content,
371
+ is_collapsed=self.enable_collapsible_sections,
372
+ detail_level=2,
373
+ )
374
+
375
+ def _build_level3_expert_data(self, result: Any) -> dict[str, Any]:
376
+ """Build Level 3 expert data.
377
+
378
+ Args:
379
+ result: Analysis result.
380
+
381
+ Returns:
382
+ Dict of expert-level data.
383
+ """
313
384
  level3_data = {}
314
385
 
315
386
  if hasattr(result, "raw_data"):
@@ -321,16 +392,16 @@ class ProgressiveDisplay:
321
392
  if hasattr(result, "debug_trace"):
322
393
  level3_data["debug_trace"] = result.debug_trace
323
394
 
324
- # Determine current level
325
- level_map = {"summary": 1, "intermediate": 2, "expert": 3}
326
- current_level = level_map.get(self.default_level, 1)
395
+ return level3_data
327
396
 
328
- return ProgressiveOutput(
329
- level1_content=level1_content,
330
- level2_sections=level2_sections,
331
- level3_data=level3_data,
332
- current_level=current_level,
333
- )
397
+ def _determine_current_level(self) -> int:
398
+ """Determine current detail level from default.
399
+
400
+ Returns:
401
+ Current level (1-3).
402
+ """
403
+ level_map = {"summary": 1, "intermediate": 2, "expert": 3}
404
+ return level_map.get(self.default_level, 1)
334
405
 
335
406
 
336
407
  __all__ = [
@@ -95,6 +95,7 @@ from oscura.loaders import (
95
95
  csv,
96
96
  hdf5,
97
97
  )
98
+ from oscura.loaders.binary import load_binary
98
99
 
99
100
  # Import configurable binary loading functionality
100
101
  from oscura.loaders.configurable import (
@@ -122,6 +123,44 @@ from oscura.loaders.preprocessing import (
122
123
  get_idle_statistics,
123
124
  trim_idle,
124
125
  )
126
+
127
+ # LAZY IMPORT: load_touchstone is loaded on first use to avoid 14.5s import penalty
128
+ # The touchstone loader imports signal_integrity which pulls in the entire analyzer chain
129
+ # See .claude/PERFORMANCE_PROFILE_2026-01-25.md for performance analysis
130
+ _load_touchstone_impl: Any = None
131
+
132
+
133
+ def load_touchstone(path: str | Path) -> Any:
134
+ """Load S-parameter data from Touchstone file (lazy import).
135
+
136
+ This function uses lazy imports to avoid a 14.5 second import penalty
137
+ from the touchstone -> signal_integrity -> analyzer dependency chain.
138
+
139
+ Supports .s1p through .s8p formats and both Touchstone 1.0
140
+ and 2.0 file formats.
141
+
142
+ Args:
143
+ path: Path to Touchstone file.
144
+
145
+ Returns:
146
+ SParameterData with loaded S-parameters.
147
+
148
+ Raises:
149
+ LoaderError: If file cannot be read.
150
+ FormatError: If file format is invalid.
151
+
152
+ Example:
153
+ >>> s_params = load_touchstone("cable.s2p")
154
+ >>> print(f"Loaded {s_params.n_ports}-port, {len(s_params.frequencies)} points")
155
+ """
156
+ global _load_touchstone_impl
157
+ if _load_touchstone_impl is None:
158
+ from oscura.loaders.touchstone import load_touchstone as _impl
159
+
160
+ _load_touchstone_impl = _impl
161
+ return _load_touchstone_impl(path)
162
+
163
+
125
164
  from oscura.loaders.validation import (
126
165
  PacketValidator,
127
166
  SequenceGap,
@@ -302,7 +341,7 @@ def load_all_channels(
302
341
  path: str | PathLike[str],
303
342
  *,
304
343
  format: str | None = None,
305
- ) -> dict[str, WaveformTrace | DigitalTrace]:
344
+ ) -> dict[str, WaveformTrace | DigitalTrace | IQTrace]:
306
345
  """Load all channels from a multi-channel waveform file.
307
346
 
308
347
  Reads the file once and extracts all available channels (both analog
@@ -360,12 +399,12 @@ def load_all_channels(
360
399
  # For other formats, try loading as single channel
361
400
  trace = load(path, format=format)
362
401
  channel_name = getattr(trace.metadata, "channel_name", None) or "ch1"
363
- return {channel_name: trace} # type: ignore[dict-item]
402
+ return {channel_name: trace}
364
403
 
365
404
 
366
405
  def _load_all_channels_tektronix(
367
406
  path: Path,
368
- ) -> dict[str, WaveformTrace | DigitalTrace]:
407
+ ) -> dict[str, WaveformTrace | DigitalTrace | IQTrace]:
369
408
  """Load all channels from a Tektronix WFM file.
370
409
 
371
410
  Args:
@@ -377,16 +416,52 @@ def _load_all_channels_tektronix(
377
416
  Raises:
378
417
  LoaderError: If the file cannot be read or parsed.
379
418
  """
419
+ wfm = _read_tektronix_file(path)
420
+ channels: dict[str, WaveformTrace | DigitalTrace | IQTrace] = {}
421
+
422
+ # Extract analog waveforms
423
+ _extract_analog_waveforms(wfm, path, channels)
424
+
425
+ # Extract digital waveforms
426
+ _extract_digital_waveforms(wfm, path, channels)
427
+
428
+ # Handle direct waveform formats (single file = single channel)
429
+ if not channels:
430
+ _extract_direct_waveform(wfm, path, channels)
431
+
432
+ if not channels:
433
+ raise LoaderError(
434
+ "No channels found in file",
435
+ file_path=str(path),
436
+ fix_hint="File may be empty or use an unsupported format variant.",
437
+ )
438
+
439
+ return channels
440
+
441
+
442
+ def _read_tektronix_file(path: Path) -> Any:
443
+ """Read Tektronix WFM file, falling back to single channel if tm_data_types unavailable.
444
+
445
+ Args:
446
+ path: Path to file.
447
+
448
+ Returns:
449
+ WFM file object.
450
+
451
+ Raises:
452
+ LoaderError: If file cannot be read.
453
+ """
380
454
  try:
381
- import tm_data_types # type: ignore[import-not-found, import-untyped]
455
+ import tm_data_types # type: ignore[import-untyped]
382
456
  except ImportError:
383
457
  # Fall back to single channel loading
384
458
  trace = load(path, format="tektronix")
385
459
  channel_name = getattr(trace.metadata, "channel_name", None) or "ch1"
386
- return {channel_name: trace} # type: ignore[dict-item]
460
+ # Return a dict-like object to maintain compatibility
461
+ return {"__fallback__": {channel_name: trace}}
387
462
 
388
463
  try:
389
- wfm = tm_data_types.read_file(str(path))
464
+ return tm_data_types.read_file(str(path))
390
465
  except Exception as e:
391
466
  raise LoaderError(
392
467
  "Failed to read Tektronix WFM file",
@@ -394,72 +469,103 @@ def _load_all_channels_tektronix(
394
469
  details=str(e),
395
470
  ) from e
396
471
 
397
- channels: dict[str, WaveformTrace | DigitalTrace] = {}
398
472
 
399
- # Extract analog waveforms
400
- if hasattr(wfm, "analog_waveforms") and wfm.analog_waveforms:
401
- import numpy as np
402
-
403
- from oscura.loaders.tektronix import _build_waveform_trace
404
-
405
- for i, awfm in enumerate(wfm.analog_waveforms):
406
- try:
407
- data = np.array(awfm.y_data, dtype=np.float64)
408
- x_increment = getattr(awfm, "x_increment", 1e-6)
409
- sample_rate = 1.0 / x_increment if x_increment > 0 else 1e6
410
- vertical_scale = getattr(awfm, "y_scale", None)
411
- vertical_offset = getattr(awfm, "y_offset", None)
412
- channel_name = getattr(awfm, "name", f"CH{i + 1}")
413
-
414
- trace = _build_waveform_trace(
415
- data=data,
416
- sample_rate=sample_rate,
417
- vertical_scale=vertical_scale,
418
- vertical_offset=vertical_offset,
419
- channel_name=channel_name,
420
- path=path,
421
- wfm=awfm,
422
- )
423
- channels[f"ch{i + 1}"] = trace
424
- except Exception as e:
425
- logger.warning("Failed to extract analog channel %d: %s", i + 1, e)
473
+ def _extract_analog_waveforms(
474
+ wfm: Any, path: Path, channels: dict[str, WaveformTrace | DigitalTrace | IQTrace]
475
+ ) -> None:
476
+ """Extract analog waveforms from WFM file.
426
477
 
427
- # Extract digital waveforms
428
- if hasattr(wfm, "digital_waveforms") and wfm.digital_waveforms:
429
- from oscura.loaders.tektronix import _load_digital_waveform
478
+ Args:
479
+ wfm: WFM file object.
480
+ path: Path to file.
481
+ channels: Dictionary to populate with channels.
482
+ """
483
+ if not hasattr(wfm, "analog_waveforms") or not wfm.analog_waveforms:
484
+ return
485
+
486
+ import numpy as np
487
+
488
+ from oscura.loaders.tektronix import _build_waveform_trace
489
+
490
+ for i, awfm in enumerate(wfm.analog_waveforms):
491
+ try:
492
+ data = np.array(awfm.y_data, dtype=np.float64)
493
+ x_increment = getattr(awfm, "x_increment", 1e-6)
494
+ sample_rate = 1.0 / x_increment if x_increment > 0 else 1e6
495
+ vertical_scale = getattr(awfm, "y_scale", None)
496
+ vertical_offset = getattr(awfm, "y_offset", None)
497
+ channel_name = getattr(awfm, "name", f"CH{i + 1}")
498
+
499
+ trace = _build_waveform_trace(
500
+ data=data,
501
+ sample_rate=sample_rate,
502
+ vertical_scale=vertical_scale,
503
+ vertical_offset=vertical_offset,
504
+ channel_name=channel_name,
505
+ path=path,
506
+ wfm=awfm,
507
+ )
508
+ channels[f"ch{i + 1}"] = trace
509
+ except Exception as e:
510
+ logger.warning("Failed to extract analog channel %d: %s", i + 1, e)
430
511
 
431
- for i, dwfm in enumerate(wfm.digital_waveforms):
432
- try:
433
- trace = _load_digital_waveform(dwfm, path, i)
434
- channels[f"d{i + 1}"] = trace
435
- except Exception as e:
436
- logger.warning("Failed to extract digital channel %d: %s", i + 1, e)
437
512
 
438
- # Handle direct waveform formats (single file = single channel)
439
- if not channels:
440
- wfm_type = type(wfm).__name__
513
+ def _extract_digital_waveforms(
514
+ wfm: Any, path: Path, channels: dict[str, WaveformTrace | DigitalTrace | IQTrace]
515
+ ) -> None:
516
+ """Extract digital waveforms from WFM file.
441
517
 
442
- if wfm_type == "DigitalWaveform" or hasattr(wfm, "y_axis_byte_values"):
443
- from oscura.loaders.tektronix import _load_digital_waveform
518
+ Args:
519
+ wfm: WFM file object.
520
+ path: Path to file.
521
+ channels: Dictionary to populate with channels.
522
+ """
523
+ if not hasattr(wfm, "digital_waveforms") or not wfm.digital_waveforms:
524
+ return
444
525
 
445
- trace = _load_digital_waveform(wfm, path, 0)
446
- channel_name = trace.metadata.channel_name or "d1"
447
- channels[channel_name.lower()] = trace
526
+ from oscura.loaders.tektronix import _load_digital_waveform
448
527
 
449
- elif hasattr(wfm, "y_axis_values") or hasattr(wfm, "y_data"):
450
- # Direct analog waveform
451
- trace = load(path, format="tektronix")
452
- channel_name = trace.metadata.channel_name or "ch1"
453
- channels[channel_name.lower()] = trace # type: ignore[assignment]
528
+ for i, dwfm in enumerate(wfm.digital_waveforms):
529
+ try:
530
+ trace = _load_digital_waveform(dwfm, path, i)
531
+ channels[f"d{i + 1}"] = trace
532
+ except Exception as e:
533
+ logger.warning("Failed to extract digital channel %d: %s", i + 1, e)
454
534
 
455
- if not channels:
456
- raise LoaderError(
457
- "No channels found in file",
458
- file_path=str(path),
459
- fix_hint="File may be empty or use an unsupported format variant.",
460
- )
461
535
 
462
- return channels
536
+ def _extract_direct_waveform(
537
+ wfm: Any, path: Path, channels: dict[str, WaveformTrace | DigitalTrace | IQTrace]
538
+ ) -> None:
539
+ """Extract single channel from direct waveform format.
540
+
541
+ Args:
542
+ wfm: WFM file object.
543
+ path: Path to file.
544
+ channels: Dictionary to populate with channels.
545
+ """
546
+ wfm_type = type(wfm).__name__
547
+
548
+ if wfm_type == "DigitalWaveform" or hasattr(wfm, "y_axis_byte_values"):
549
+ from oscura.loaders.tektronix import _load_digital_waveform
550
+
551
+ trace = _load_digital_waveform(wfm, path, 0)
552
+ channel_name = trace.metadata.channel_name or "d1"
553
+ channels[channel_name.lower()] = trace
554
+
555
+ elif wfm_type == "IQWaveform" or (
556
+ hasattr(wfm, "i_axis_values") and hasattr(wfm, "q_axis_values")
557
+ ):
558
+ # IQWaveform format (RF/SDR data)
559
+ loaded_trace = load(path, format="tektronix")
560
+ channel_name = loaded_trace.metadata.channel_name or "iq1"
561
+ channels[channel_name.lower()] = loaded_trace
562
+
563
+ elif hasattr(wfm, "y_axis_values") or hasattr(wfm, "y_data"):
564
+ # Direct analog waveform
565
+ loaded_trace = load(path, format="tektronix")
566
+ # Add both analog and digital traces to channels
567
+ channel_name = loaded_trace.metadata.channel_name or "ch1"
568
+ channels[channel_name.lower()] = loaded_trace
463
569
 
464
570
 
465
571
  def get_supported_formats() -> list[str]:
@@ -535,9 +641,11 @@ __all__ = [
535
641
  "hdf5",
536
642
  "load",
537
643
  "load_all_channels",
644
+ "load_binary",
538
645
  "load_binary_packets",
539
646
  "load_lazy",
540
647
  "load_packets_streaming",
648
+ "load_touchstone",
541
649
  "load_trace_lazy",
542
650
  "trim_idle",
543
651
  ]