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