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
@@ -39,7 +39,7 @@ logger = logging.getLogger(__name__)
39
39
 
40
40
  # Try to import tm_data_types for full Tektronix support
41
41
  try:
42
- import tm_data_types # type: ignore[import-untyped, import-not-found]
42
+ import tm_data_types # type: ignore[import-untyped] # Optional third-party library
43
43
 
44
44
  TM_DATA_TYPES_AVAILABLE = True
45
45
  except ImportError:
@@ -150,105 +150,8 @@ def _load_with_tm_data_types(
150
150
  if hasattr(wfm, "digital_waveforms"):
151
151
  logger.debug("Digital waveforms found: %d", len(wfm.digital_waveforms))
152
152
 
153
- # Extract waveform data - handle different file formats
154
- # Path 1: Multi-channel container format (wrapped analog)
155
- if hasattr(wfm, "analog_waveforms") and len(wfm.analog_waveforms) > channel:
156
- logger.debug("Loading from analog_waveforms[%d]", channel)
157
- waveform = wfm.analog_waveforms[channel]
158
- data = np.array(waveform.y_data, dtype=np.float64)
159
- sample_rate = 1.0 / waveform.x_increment if waveform.x_increment > 0 else 1e6
160
- vertical_scale = getattr(waveform, "y_scale", None)
161
- vertical_offset = getattr(waveform, "y_offset", None)
162
- channel_name = getattr(waveform, "name", f"CH{channel + 1}")
163
-
164
- return _build_waveform_trace(
165
- data=data,
166
- sample_rate=sample_rate,
167
- vertical_scale=vertical_scale,
168
- vertical_offset=vertical_offset,
169
- channel_name=channel_name,
170
- path=path,
171
- wfm=wfm,
172
- )
173
-
174
- # Path 2: Direct AnalogWaveform format (tm_data_types 0.3.0+)
175
- elif hasattr(wfm, "y_axis_values") and wfm_type == "AnalogWaveform":
176
- logger.debug("Loading direct AnalogWaveform with y_axis_values")
177
- # Extract raw integer values
178
- y_raw = np.array(wfm.y_axis_values, dtype=np.float64)
179
- # Reconstruct voltage values using offset and spacing
180
- y_spacing = float(wfm.y_axis_spacing) if wfm.y_axis_spacing else 1.0
181
- y_offset = float(wfm.y_axis_offset) if wfm.y_axis_offset else 0.0
182
- data = y_raw * y_spacing + y_offset
183
-
184
- x_spacing = float(wfm.x_axis_spacing) if wfm.x_axis_spacing else 1e-6
185
- sample_rate = 1.0 / x_spacing if x_spacing > 0 else 1e6
186
- vertical_offset = y_offset
187
- channel_name = (
188
- wfm.source_name
189
- if hasattr(wfm, "source_name") and wfm.source_name
190
- else f"CH{channel + 1}"
191
- )
192
-
193
- return _build_waveform_trace(
194
- data=data,
195
- sample_rate=sample_rate,
196
- vertical_scale=None,
197
- vertical_offset=vertical_offset,
198
- channel_name=channel_name,
199
- path=path,
200
- wfm=wfm,
201
- )
202
-
203
- # Path 3: DigitalWaveform format
204
- elif wfm_type == "DigitalWaveform" or hasattr(wfm, "y_axis_byte_values"):
205
- logger.debug("Loading DigitalWaveform with y_axis_byte_values")
206
- return _load_digital_waveform(wfm, path, channel)
207
-
208
- # Path 4: Legacy single channel format with y_data
209
- elif hasattr(wfm, "y_data"):
210
- logger.debug("Loading legacy format with y_data")
211
- data = np.array(wfm.y_data, dtype=np.float64)
212
- x_increment = getattr(wfm, "x_increment", 1e-6)
213
- sample_rate = 1.0 / x_increment if x_increment > 0 else 1e6
214
- vertical_scale = getattr(wfm, "y_scale", None)
215
- vertical_offset = getattr(wfm, "y_offset", None)
216
- channel_name = getattr(wfm, "name", "CH1")
217
-
218
- return _build_waveform_trace(
219
- data=data,
220
- sample_rate=sample_rate,
221
- vertical_scale=vertical_scale,
222
- vertical_offset=vertical_offset,
223
- channel_name=channel_name,
224
- path=path,
225
- wfm=wfm,
226
- )
227
-
228
- # Path 5: Check for wrapped digital waveforms
229
- elif hasattr(wfm, "digital_waveforms") and len(wfm.digital_waveforms) > channel:
230
- logger.debug("Loading from digital_waveforms[%d]", channel)
231
- digital_wfm = wfm.digital_waveforms[channel]
232
- return _load_digital_waveform(digital_wfm, path, channel)
233
-
234
- # Path 6: IQWaveform format (I/Q data)
235
- elif wfm_type == "IQWaveform" or (
236
- hasattr(wfm, "i_axis_values") and hasattr(wfm, "q_axis_values")
237
- ):
238
- logger.debug("Loading IQWaveform with i_axis_values and q_axis_values")
239
- return _load_iq_waveform(wfm, path)
240
-
241
- # No recognized format - provide detailed error
242
- raise FormatError(
243
- f"No waveform data found. Object type: {wfm_type}. "
244
- f"Available attributes: {', '.join(available_attrs[:15])}",
245
- file_path=str(path),
246
- expected="Tektronix analog or digital waveform data",
247
- fix_hint=(
248
- "This file may use an unsupported Tektronix format variant. "
249
- "Check that tm_data_types is up to date: pip install -U tm_data_types"
250
- ),
251
- )
153
+ # Dispatch to appropriate loader based on waveform format
154
+ return _dispatch_waveform_loader(wfm, wfm_type, available_attrs, path, channel)
252
155
 
253
156
  except Exception as e:
254
157
  if isinstance(e, LoaderError | FormatError):
@@ -261,6 +164,176 @@ def _load_with_tm_data_types(
261
164
  ) from e
262
165
 
263
166
 
167
+ def _dispatch_waveform_loader(
168
+ wfm: Any,
169
+ wfm_type: str,
170
+ available_attrs: list[str],
171
+ path: Path,
172
+ channel: int,
173
+ ) -> TektronixTrace:
174
+ """Dispatch to appropriate waveform loader based on format.
175
+
176
+ Args:
177
+ wfm: Waveform object from tm_data_types.
178
+ wfm_type: Type name of waveform object.
179
+ available_attrs: List of available attributes on waveform object.
180
+ path: Path to WFM file.
181
+ channel: Channel index.
182
+
183
+ Returns:
184
+ Loaded trace (WaveformTrace, DigitalTrace, or IQTrace).
185
+
186
+ Raises:
187
+ FormatError: If no recognized waveform format found.
188
+ """
189
+ # Path 1: Multi-channel container format (wrapped analog)
190
+ if hasattr(wfm, "analog_waveforms") and len(wfm.analog_waveforms) > channel:
191
+ logger.debug("Loading from analog_waveforms[%d]", channel)
192
+ return _load_analog_waveforms_container(wfm.analog_waveforms[channel], path, channel)
193
+
194
+ # Path 2: Direct AnalogWaveform format (tm_data_types 0.3.0+)
195
+ if hasattr(wfm, "y_axis_values") and wfm_type == "AnalogWaveform":
196
+ logger.debug("Loading direct AnalogWaveform with y_axis_values")
197
+ return _load_analog_waveform_direct(wfm, path, channel)
198
+
199
+ # Path 3: DigitalWaveform format
200
+ if wfm_type == "DigitalWaveform" or hasattr(wfm, "y_axis_byte_values"):
201
+ logger.debug("Loading DigitalWaveform with y_axis_byte_values")
202
+ return _load_digital_waveform(wfm, path, channel)
203
+
204
+ # Path 4: Legacy single channel format with y_data
205
+ if hasattr(wfm, "y_data"):
206
+ logger.debug("Loading legacy format with y_data")
207
+ return _load_legacy_y_data(wfm, path)
208
+
209
+ # Path 5: Check for wrapped digital waveforms
210
+ if hasattr(wfm, "digital_waveforms") and len(wfm.digital_waveforms) > channel:
211
+ logger.debug("Loading from digital_waveforms[%d]", channel)
212
+ return _load_digital_waveform(wfm.digital_waveforms[channel], path, channel)
213
+
214
+ # Path 6: IQWaveform format (I/Q data)
215
+ if wfm_type == "IQWaveform" or (
216
+ hasattr(wfm, "i_axis_values") and hasattr(wfm, "q_axis_values")
217
+ ):
218
+ logger.debug("Loading IQWaveform with i_axis_values and q_axis_values")
219
+ return _load_iq_waveform(wfm, path)
220
+
221
+ # No recognized format - provide detailed error
222
+ raise FormatError(
223
+ f"No waveform data found. Object type: {wfm_type}. "
224
+ f"Available attributes: {', '.join(available_attrs[:15])}",
225
+ file_path=str(path),
226
+ expected="Tektronix analog or digital waveform data",
227
+ fix_hint=(
228
+ "This file may use an unsupported Tektronix format variant. "
229
+ "Check that tm_data_types is up to date: pip install -U tm_data_types"
230
+ ),
231
+ )
232
+
233
+
234
+ def _load_analog_waveforms_container(
235
+ waveform: Any,
236
+ path: Path,
237
+ channel: int,
238
+ ) -> WaveformTrace:
239
+ """Load analog waveform from multi-channel container format.
240
+
241
+ Args:
242
+ waveform: Analog waveform object from container.
243
+ path: Path to WFM file.
244
+ channel: Channel index.
245
+
246
+ Returns:
247
+ WaveformTrace with extracted data.
248
+ """
249
+ data = np.array(waveform.y_data, dtype=np.float64)
250
+ sample_rate = 1.0 / waveform.x_increment if waveform.x_increment > 0 else 1e6
251
+ vertical_scale = getattr(waveform, "y_scale", None)
252
+ vertical_offset = getattr(waveform, "y_offset", None)
253
+ channel_name = getattr(waveform, "name", f"CH{channel + 1}")
254
+
255
+ # Use original wfm for trigger info (need to get it from parent)
256
+ return _build_waveform_trace(
257
+ data=data,
258
+ sample_rate=sample_rate,
259
+ vertical_scale=vertical_scale,
260
+ vertical_offset=vertical_offset,
261
+ channel_name=channel_name,
262
+ path=path,
263
+ wfm=waveform,
264
+ )
265
+
266
+
267
+ def _load_analog_waveform_direct(
268
+ wfm: Any,
269
+ path: Path,
270
+ channel: int,
271
+ ) -> WaveformTrace:
272
+ """Load direct AnalogWaveform format (tm_data_types 0.3.0+).
273
+
274
+ Args:
275
+ wfm: AnalogWaveform object.
276
+ path: Path to WFM file.
277
+ channel: Channel index.
278
+
279
+ Returns:
280
+ WaveformTrace with extracted data.
281
+ """
282
+ # Extract raw integer values and reconstruct voltage values
283
+ y_raw = np.array(wfm.y_axis_values, dtype=np.float64)
284
+ y_spacing = float(wfm.y_axis_spacing) if wfm.y_axis_spacing else 1.0
285
+ y_offset = float(wfm.y_axis_offset) if wfm.y_axis_offset else 0.0
286
+ data = y_raw * y_spacing + y_offset
287
+
288
+ x_spacing = float(wfm.x_axis_spacing) if wfm.x_axis_spacing else 1e-6
289
+ sample_rate = 1.0 / x_spacing if x_spacing > 0 else 1e6
290
+ vertical_offset = y_offset
291
+ channel_name = (
292
+ wfm.source_name if hasattr(wfm, "source_name") and wfm.source_name else f"CH{channel + 1}"
293
+ )
294
+
295
+ return _build_waveform_trace(
296
+ data=data,
297
+ sample_rate=sample_rate,
298
+ vertical_scale=None,
299
+ vertical_offset=vertical_offset,
300
+ channel_name=channel_name,
301
+ path=path,
302
+ wfm=wfm,
303
+ )
304
+
305
+
306
+ def _load_legacy_y_data(
307
+ wfm: Any,
308
+ path: Path,
309
+ ) -> WaveformTrace:
310
+ """Load legacy single channel format with y_data.
311
+
312
+ Args:
313
+ wfm: Legacy waveform object with y_data.
314
+ path: Path to WFM file.
315
+
316
+ Returns:
317
+ WaveformTrace with extracted data.
318
+ """
319
+ data = np.array(wfm.y_data, dtype=np.float64)
320
+ x_increment = getattr(wfm, "x_increment", 1e-6)
321
+ sample_rate = 1.0 / x_increment if x_increment > 0 else 1e6
322
+ vertical_scale = getattr(wfm, "y_scale", None)
323
+ vertical_offset = getattr(wfm, "y_offset", None)
324
+ channel_name = getattr(wfm, "name", "CH1")
325
+
326
+ return _build_waveform_trace(
327
+ data=data,
328
+ sample_rate=sample_rate,
329
+ vertical_scale=vertical_scale,
330
+ vertical_offset=vertical_offset,
331
+ channel_name=channel_name,
332
+ path=path,
333
+ wfm=wfm,
334
+ )
335
+
336
+
264
337
  def _build_waveform_trace(
265
338
  data: NDArray[np.float64],
266
339
  sample_rate: float,
@@ -328,64 +401,95 @@ def _load_digital_waveform(
328
401
  logger.debug("Extracting digital waveform data")
329
402
 
330
403
  # Extract digital sample data
404
+ data = _extract_digital_samples(wfm, path)
405
+
406
+ # Extract timing information
407
+ sample_rate = _extract_sample_rate(wfm)
408
+
409
+ # Extract channel name
410
+ channel_name = _extract_channel_name(wfm, channel)
411
+
412
+ # Build metadata
413
+ metadata = TraceMetadata(
414
+ sample_rate=sample_rate,
415
+ source_file=str(path),
416
+ channel_name=channel_name,
417
+ )
418
+
419
+ # Extract edge information if available
420
+ edges = _extract_edges(wfm)
421
+
422
+ return DigitalTrace(data=data, metadata=metadata, edges=edges)
423
+
424
+
425
+ def _extract_digital_samples(wfm: Any, path: Path) -> NDArray[np.bool_]:
426
+ """Extract digital sample data from waveform object."""
427
+ # Try y_axis_byte_values (most common)
331
428
  if hasattr(wfm, "y_axis_byte_values"):
332
- # y_axis_byte_values contains byte-level digital data
333
429
  raw_bytes = wfm.y_axis_byte_values
334
- # Convert bytes to numpy array and interpret as boolean
335
- # Each byte typically represents a logic state (0 = low, non-zero = high)
336
430
  byte_array = np.frombuffer(bytes(raw_bytes), dtype=np.uint8)
337
431
  data = byte_array.astype(np.bool_)
338
432
  logger.debug("Loaded %d digital samples from y_axis_byte_values", len(data))
339
- elif hasattr(wfm, "samples"):
340
- # Alternative attribute name
433
+ return data
434
+
435
+ # Try samples attribute
436
+ if hasattr(wfm, "samples"):
341
437
  data = np.array(wfm.samples, dtype=np.bool_)
342
438
  logger.debug("Loaded %d digital samples from samples", len(data))
343
- else:
344
- # Try to find any data attribute
345
- for attr in ["data", "digital_data", "logic_data"]:
346
- if hasattr(wfm, attr):
347
- data = np.array(getattr(wfm, attr), dtype=np.bool_)
348
- logger.debug("Loaded %d digital samples from %s", len(data), attr)
349
- break
350
- else:
351
- raise FormatError(
352
- "DigitalWaveform has no recognized data attribute",
353
- file_path=str(path),
354
- expected="y_axis_byte_values, samples, or data attribute",
355
- )
439
+ return data
440
+
441
+ # Try alternative data attributes
442
+ for attr in ["data", "digital_data", "logic_data"]:
443
+ if hasattr(wfm, attr):
444
+ data = np.array(getattr(wfm, attr), dtype=np.bool_)
445
+ logger.debug("Loaded %d digital samples from %s", len(data), attr)
446
+ return data
447
+
448
+ # No recognized attribute found
449
+ raise FormatError(
450
+ "DigitalWaveform has no recognized data attribute",
451
+ file_path=str(path),
452
+ expected="y_axis_byte_values, samples, or data attribute",
453
+ )
356
454
 
357
- # Extract timing information
455
+
456
+ def _extract_sample_rate(wfm: Any) -> float:
457
+ """Extract sample rate from waveform timing attributes."""
358
458
  x_spacing = 1e-6 # Default 1 microsecond per sample
459
+
359
460
  if hasattr(wfm, "x_axis_spacing") and wfm.x_axis_spacing:
360
461
  x_spacing = float(wfm.x_axis_spacing)
361
462
  elif hasattr(wfm, "horizontal_spacing") and wfm.horizontal_spacing:
362
463
  x_spacing = float(wfm.horizontal_spacing)
363
464
 
364
- sample_rate = 1.0 / x_spacing if x_spacing > 0 else 1e6
465
+ return 1.0 / x_spacing if x_spacing > 0 else 1e6
365
466
 
366
- # Extract channel name
367
- channel_name = f"D{channel + 1}" # Digital channels typically labeled D1, D2, etc.
467
+
468
+ def _extract_channel_name(wfm: Any, channel: int) -> str:
469
+ """Extract channel name from waveform object."""
470
+ # Try source_name first
368
471
  if hasattr(wfm, "source_name") and wfm.source_name:
369
- channel_name = wfm.source_name
370
- elif hasattr(wfm, "name") and wfm.name:
371
- channel_name = wfm.name
472
+ name: str = str(wfm.source_name)
473
+ return name
372
474
 
373
- # Build metadata
374
- metadata = TraceMetadata(
375
- sample_rate=sample_rate,
376
- source_file=str(path),
377
- channel_name=channel_name,
378
- )
475
+ # Try name attribute
476
+ if hasattr(wfm, "name") and wfm.name:
477
+ name_str: str = str(wfm.name)
478
+ return name_str
379
479
 
380
- # Extract edge information if available
381
- edges = None
382
- if hasattr(wfm, "edges"):
383
- try:
384
- edges = [(float(ts), bool(is_rising)) for ts, is_rising in wfm.edges]
385
- except (TypeError, ValueError):
386
- pass
480
+ # Default: digital channels labeled D1, D2, etc.
481
+ return f"D{channel + 1}"
387
482
 
388
- return DigitalTrace(data=data, metadata=metadata, edges=edges)
483
+
484
+ def _extract_edges(wfm: Any) -> list[tuple[float, bool]] | None:
485
+ """Extract edge timing information if available."""
486
+ if not hasattr(wfm, "edges"):
487
+ return None
488
+
489
+ try:
490
+ return [(float(ts), bool(is_rising)) for ts, is_rising in wfm.edges]
491
+ except (TypeError, ValueError):
492
+ return None
389
493
 
390
494
 
391
495
  def _load_iq_waveform(
@@ -528,9 +632,30 @@ def _parse_wfm003(
528
632
  Raises:
529
633
  FormatError: If the file signature is invalid or no waveform data found.
530
634
  """
531
- import struct
532
635
 
533
- # Validate signature
636
+ _validate_wfm003_signature(file_data, path)
637
+ header_size = 838
638
+ waveform_bytes = _extract_waveform_data(file_data, header_size, path)
639
+ data = np.frombuffer(waveform_bytes, dtype=np.int16).astype(np.float64)
640
+
641
+ # Extract metadata from header
642
+ sample_rate = _extract_sample_interval(file_data, header_size)
643
+ vertical_scale, vertical_offset = _extract_vertical_params(file_data, header_size)
644
+ channel_name = f"CH{channel + 1}"
645
+
646
+ metadata = TraceMetadata(
647
+ sample_rate=sample_rate,
648
+ vertical_scale=vertical_scale,
649
+ vertical_offset=vertical_offset,
650
+ source_file=str(path),
651
+ channel_name=channel_name,
652
+ )
653
+
654
+ return WaveformTrace(data=data, metadata=metadata)
655
+
656
+
657
+ def _validate_wfm003_signature(file_data: bytes, path: Path) -> None:
658
+ """Validate WFM#003 file signature."""
534
659
  signature = file_data[2:10]
535
660
  if signature != b":WFM#003":
536
661
  raise FormatError(
@@ -540,20 +665,14 @@ def _parse_wfm003(
540
665
  got=signature.decode("latin-1", errors="replace"),
541
666
  )
542
667
 
543
- # WFM#003 files have a fixed header size of 838 bytes
544
- # This is consistent across all WFM#003 files
545
- header_size = 838
546
668
 
547
- # Find metadata footer (tekmeta!) if present
548
- # This helps us determine where waveform data ends
669
+ def _extract_waveform_data(file_data: bytes, header_size: int, path: Path) -> bytes:
670
+ """Extract waveform data region from file."""
549
671
  footer_start = len(file_data)
550
672
  if b"tekmeta!" in file_data:
551
673
  footer_start = file_data.find(b"tekmeta!")
552
674
 
553
- # Extract waveform data region
554
- data_start = header_size
555
- data_end = footer_start
556
- waveform_bytes = file_data[data_start:data_end]
675
+ waveform_bytes = file_data[header_size:footer_start]
557
676
 
558
677
  if len(waveform_bytes) < 2:
559
678
  raise FormatError(
@@ -561,23 +680,20 @@ def _parse_wfm003(
561
680
  file_path=str(path),
562
681
  )
563
682
 
564
- # WFM#003 data is stored as int16 (16-bit signed integers)
565
- # Ensure we have an even number of bytes
683
+ # Ensure even number of bytes for int16
566
684
  if len(waveform_bytes) % 2 != 0:
567
685
  waveform_bytes = waveform_bytes[:-1]
568
686
 
569
- # Parse as int16 little-endian
570
- data = np.frombuffer(waveform_bytes, dtype=np.int16).astype(np.float64)
687
+ return waveform_bytes
571
688
 
572
- # Try to extract metadata from header
573
- sample_rate = 1e6 # Default 1 MSa/s
574
- vertical_scale = None
575
- vertical_offset = None
576
- channel_name = f"CH{channel + 1}"
577
689
 
578
- # Try to find sample interval in header
579
- # The header contains doubles at various offsets
580
- # Sample interval is typically found in the horizontal dimension info
690
+ def _extract_sample_interval(file_data: bytes, header_size: int) -> float:
691
+ """Extract sample rate from header doubles."""
692
+ import struct
693
+
694
+ # Default 1 MSa/s
695
+ sample_rate = 1e6
696
+
581
697
  try:
582
698
  # Search for reasonable sample interval values (doubles in header)
583
699
  for offset in range(16, min(header_size - 8, 200), 8):
@@ -589,8 +705,18 @@ def _parse_wfm003(
589
705
  except (struct.error, ZeroDivisionError):
590
706
  pass
591
707
 
592
- # Try to extract vertical scale/offset
593
- # These are also doubles in the header
708
+ return sample_rate
709
+
710
+
711
+ def _extract_vertical_params(
712
+ file_data: bytes, header_size: int
713
+ ) -> tuple[float | None, float | None]:
714
+ """Extract vertical scale and offset from header."""
715
+ import struct
716
+
717
+ vertical_scale = None
718
+ vertical_offset = None
719
+
594
720
  try:
595
721
  # Vertical scale is often in a specific range
596
722
  for offset in range(16, min(header_size - 8, 400), 8):
@@ -606,16 +732,7 @@ def _parse_wfm003(
606
732
  except struct.error:
607
733
  pass
608
734
 
609
- # Build metadata
610
- metadata = TraceMetadata(
611
- sample_rate=sample_rate,
612
- vertical_scale=vertical_scale,
613
- vertical_offset=vertical_offset,
614
- source_file=str(path),
615
- channel_name=channel_name,
616
- )
617
-
618
- return WaveformTrace(data=data, metadata=metadata)
735
+ return vertical_scale, vertical_offset
619
736
 
620
737
 
621
738
  def _parse_wfm_legacy(