oscura 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (513) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/__init__.py +0 -48
  5. oscura/analyzers/digital/edges.py +325 -65
  6. oscura/analyzers/digital/extraction.py +0 -195
  7. oscura/analyzers/digital/quality.py +293 -166
  8. oscura/analyzers/digital/timing.py +260 -115
  9. oscura/analyzers/digital/timing_numba.py +334 -0
  10. oscura/analyzers/entropy.py +605 -0
  11. oscura/analyzers/eye/diagram.py +176 -109
  12. oscura/analyzers/eye/metrics.py +5 -5
  13. oscura/analyzers/jitter/__init__.py +6 -4
  14. oscura/analyzers/jitter/ber.py +52 -52
  15. oscura/analyzers/jitter/classification.py +156 -0
  16. oscura/analyzers/jitter/decomposition.py +163 -113
  17. oscura/analyzers/jitter/spectrum.py +80 -64
  18. oscura/analyzers/ml/__init__.py +39 -0
  19. oscura/analyzers/ml/features.py +600 -0
  20. oscura/analyzers/ml/signal_classifier.py +604 -0
  21. oscura/analyzers/packet/daq.py +246 -158
  22. oscura/analyzers/packet/parser.py +12 -1
  23. oscura/analyzers/packet/payload.py +50 -2110
  24. oscura/analyzers/packet/payload_analysis.py +361 -181
  25. oscura/analyzers/packet/payload_patterns.py +133 -70
  26. oscura/analyzers/packet/stream.py +84 -23
  27. oscura/analyzers/patterns/__init__.py +26 -5
  28. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  29. oscura/analyzers/patterns/clustering.py +169 -108
  30. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  31. oscura/analyzers/patterns/discovery.py +1 -1
  32. oscura/analyzers/patterns/matching.py +581 -197
  33. oscura/analyzers/patterns/pattern_mining.py +778 -0
  34. oscura/analyzers/patterns/periodic.py +121 -38
  35. oscura/analyzers/patterns/sequences.py +175 -78
  36. oscura/analyzers/power/conduction.py +1 -1
  37. oscura/analyzers/power/soa.py +6 -6
  38. oscura/analyzers/power/switching.py +250 -110
  39. oscura/analyzers/protocol/__init__.py +17 -1
  40. oscura/analyzers/protocols/__init__.py +1 -22
  41. oscura/analyzers/protocols/base.py +6 -6
  42. oscura/analyzers/protocols/ble/__init__.py +38 -0
  43. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  44. oscura/analyzers/protocols/ble/uuids.py +288 -0
  45. oscura/analyzers/protocols/can.py +257 -127
  46. oscura/analyzers/protocols/can_fd.py +107 -80
  47. oscura/analyzers/protocols/flexray.py +139 -80
  48. oscura/analyzers/protocols/hdlc.py +93 -58
  49. oscura/analyzers/protocols/i2c.py +247 -106
  50. oscura/analyzers/protocols/i2s.py +138 -86
  51. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  52. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  53. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  54. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  55. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  56. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  57. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  58. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  59. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  60. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  61. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  62. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  63. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  64. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  65. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  66. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  67. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  68. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  69. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  70. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  71. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  72. oscura/analyzers/protocols/jtag.py +180 -98
  73. oscura/analyzers/protocols/lin.py +219 -114
  74. oscura/analyzers/protocols/manchester.py +4 -4
  75. oscura/analyzers/protocols/onewire.py +253 -149
  76. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  77. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  78. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  79. oscura/analyzers/protocols/spi.py +192 -95
  80. oscura/analyzers/protocols/swd.py +321 -167
  81. oscura/analyzers/protocols/uart.py +267 -125
  82. oscura/analyzers/protocols/usb.py +235 -131
  83. oscura/analyzers/side_channel/power.py +17 -12
  84. oscura/analyzers/signal/__init__.py +15 -0
  85. oscura/analyzers/signal/timing_analysis.py +1086 -0
  86. oscura/analyzers/signal_integrity/__init__.py +4 -1
  87. oscura/analyzers/signal_integrity/sparams.py +2 -19
  88. oscura/analyzers/spectral/chunked.py +129 -60
  89. oscura/analyzers/spectral/chunked_fft.py +300 -94
  90. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  91. oscura/analyzers/statistical/checksum.py +376 -217
  92. oscura/analyzers/statistical/classification.py +229 -107
  93. oscura/analyzers/statistical/entropy.py +78 -53
  94. oscura/analyzers/statistics/correlation.py +407 -211
  95. oscura/analyzers/statistics/outliers.py +2 -2
  96. oscura/analyzers/statistics/streaming.py +30 -5
  97. oscura/analyzers/validation.py +216 -101
  98. oscura/analyzers/waveform/measurements.py +9 -0
  99. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  100. oscura/analyzers/waveform/spectral.py +500 -228
  101. oscura/api/__init__.py +31 -5
  102. oscura/api/dsl/__init__.py +582 -0
  103. oscura/{dsl → api/dsl}/commands.py +43 -76
  104. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  105. oscura/{dsl → api/dsl}/parser.py +107 -77
  106. oscura/{dsl → api/dsl}/repl.py +2 -2
  107. oscura/api/dsl.py +1 -1
  108. oscura/{integrations → api/integrations}/__init__.py +1 -1
  109. oscura/{integrations → api/integrations}/llm.py +201 -102
  110. oscura/api/operators.py +3 -3
  111. oscura/api/optimization.py +144 -30
  112. oscura/api/rest_server.py +921 -0
  113. oscura/api/server/__init__.py +17 -0
  114. oscura/api/server/dashboard.py +850 -0
  115. oscura/api/server/static/README.md +34 -0
  116. oscura/api/server/templates/base.html +181 -0
  117. oscura/api/server/templates/export.html +120 -0
  118. oscura/api/server/templates/home.html +284 -0
  119. oscura/api/server/templates/protocols.html +58 -0
  120. oscura/api/server/templates/reports.html +43 -0
  121. oscura/api/server/templates/session_detail.html +89 -0
  122. oscura/api/server/templates/sessions.html +83 -0
  123. oscura/api/server/templates/waveforms.html +73 -0
  124. oscura/automotive/__init__.py +8 -1
  125. oscura/automotive/can/__init__.py +10 -0
  126. oscura/automotive/can/checksum.py +3 -1
  127. oscura/automotive/can/dbc_generator.py +590 -0
  128. oscura/automotive/can/message_wrapper.py +121 -74
  129. oscura/automotive/can/patterns.py +98 -21
  130. oscura/automotive/can/session.py +292 -56
  131. oscura/automotive/can/state_machine.py +6 -3
  132. oscura/automotive/can/stimulus_response.py +97 -75
  133. oscura/automotive/dbc/__init__.py +10 -2
  134. oscura/automotive/dbc/generator.py +84 -56
  135. oscura/automotive/dbc/parser.py +6 -6
  136. oscura/automotive/dtc/data.json +2763 -0
  137. oscura/automotive/dtc/database.py +2 -2
  138. oscura/automotive/flexray/__init__.py +31 -0
  139. oscura/automotive/flexray/analyzer.py +504 -0
  140. oscura/automotive/flexray/crc.py +185 -0
  141. oscura/automotive/flexray/fibex.py +449 -0
  142. oscura/automotive/j1939/__init__.py +45 -8
  143. oscura/automotive/j1939/analyzer.py +605 -0
  144. oscura/automotive/j1939/spns.py +326 -0
  145. oscura/automotive/j1939/transport.py +306 -0
  146. oscura/automotive/lin/__init__.py +47 -0
  147. oscura/automotive/lin/analyzer.py +612 -0
  148. oscura/automotive/loaders/blf.py +13 -2
  149. oscura/automotive/loaders/csv_can.py +143 -72
  150. oscura/automotive/loaders/dispatcher.py +50 -2
  151. oscura/automotive/loaders/mdf.py +86 -45
  152. oscura/automotive/loaders/pcap.py +111 -61
  153. oscura/automotive/uds/__init__.py +4 -0
  154. oscura/automotive/uds/analyzer.py +725 -0
  155. oscura/automotive/uds/decoder.py +140 -58
  156. oscura/automotive/uds/models.py +7 -1
  157. oscura/automotive/visualization.py +1 -1
  158. oscura/cli/analyze.py +348 -0
  159. oscura/cli/batch.py +142 -122
  160. oscura/cli/benchmark.py +275 -0
  161. oscura/cli/characterize.py +137 -82
  162. oscura/cli/compare.py +224 -131
  163. oscura/cli/completion.py +250 -0
  164. oscura/cli/config_cmd.py +361 -0
  165. oscura/cli/decode.py +164 -87
  166. oscura/cli/export.py +286 -0
  167. oscura/cli/main.py +115 -31
  168. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  169. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  170. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  171. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  172. oscura/cli/progress.py +147 -0
  173. oscura/cli/shell.py +157 -135
  174. oscura/cli/validate_cmd.py +204 -0
  175. oscura/cli/visualize.py +158 -0
  176. oscura/convenience.py +125 -79
  177. oscura/core/__init__.py +4 -2
  178. oscura/core/backend_selector.py +3 -3
  179. oscura/core/cache.py +126 -15
  180. oscura/core/cancellation.py +1 -1
  181. oscura/{config → core/config}/__init__.py +20 -11
  182. oscura/{config → core/config}/defaults.py +1 -1
  183. oscura/{config → core/config}/loader.py +7 -5
  184. oscura/{config → core/config}/memory.py +5 -5
  185. oscura/{config → core/config}/migration.py +1 -1
  186. oscura/{config → core/config}/pipeline.py +99 -23
  187. oscura/{config → core/config}/preferences.py +1 -1
  188. oscura/{config → core/config}/protocol.py +3 -3
  189. oscura/{config → core/config}/schema.py +426 -272
  190. oscura/{config → core/config}/settings.py +1 -1
  191. oscura/{config → core/config}/thresholds.py +195 -153
  192. oscura/core/correlation.py +5 -6
  193. oscura/core/cross_domain.py +0 -2
  194. oscura/core/debug.py +9 -5
  195. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  196. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  197. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  198. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  199. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  200. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  201. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  202. oscura/core/gpu_backend.py +11 -7
  203. oscura/core/log_query.py +101 -11
  204. oscura/core/logging.py +126 -54
  205. oscura/core/logging_advanced.py +5 -5
  206. oscura/core/memory_limits.py +108 -70
  207. oscura/core/memory_monitor.py +2 -2
  208. oscura/core/memory_progress.py +7 -7
  209. oscura/core/memory_warnings.py +1 -1
  210. oscura/core/numba_backend.py +13 -13
  211. oscura/{plugins → core/plugins}/__init__.py +9 -9
  212. oscura/{plugins → core/plugins}/base.py +7 -7
  213. oscura/{plugins → core/plugins}/cli.py +3 -3
  214. oscura/{plugins → core/plugins}/discovery.py +186 -106
  215. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  216. oscura/{plugins → core/plugins}/manager.py +7 -7
  217. oscura/{plugins → core/plugins}/registry.py +3 -3
  218. oscura/{plugins → core/plugins}/versioning.py +1 -1
  219. oscura/core/progress.py +16 -1
  220. oscura/core/provenance.py +8 -2
  221. oscura/{schemas → core/schemas}/__init__.py +2 -2
  222. oscura/core/schemas/bus_configuration.json +322 -0
  223. oscura/core/schemas/device_mapping.json +182 -0
  224. oscura/core/schemas/packet_format.json +418 -0
  225. oscura/core/schemas/protocol_definition.json +363 -0
  226. oscura/core/types.py +4 -0
  227. oscura/core/uncertainty.py +3 -3
  228. oscura/correlation/__init__.py +52 -0
  229. oscura/correlation/multi_protocol.py +811 -0
  230. oscura/discovery/auto_decoder.py +117 -35
  231. oscura/discovery/comparison.py +191 -86
  232. oscura/discovery/quality_validator.py +155 -68
  233. oscura/discovery/signal_detector.py +196 -79
  234. oscura/export/__init__.py +18 -20
  235. oscura/export/kaitai_struct.py +513 -0
  236. oscura/export/scapy_layer.py +801 -0
  237. oscura/export/wireshark/README.md +15 -15
  238. oscura/export/wireshark/generator.py +1 -1
  239. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  240. oscura/export/wireshark_dissector.py +746 -0
  241. oscura/guidance/wizard.py +207 -111
  242. oscura/hardware/__init__.py +19 -0
  243. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  244. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  245. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  246. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  247. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  248. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  249. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  250. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  251. oscura/hardware/firmware/__init__.py +29 -0
  252. oscura/hardware/firmware/pattern_recognition.py +874 -0
  253. oscura/hardware/hal_detector.py +736 -0
  254. oscura/hardware/security/__init__.py +37 -0
  255. oscura/hardware/security/side_channel_detector.py +1126 -0
  256. oscura/inference/__init__.py +4 -0
  257. oscura/inference/active_learning/README.md +7 -7
  258. oscura/inference/active_learning/observation_table.py +4 -1
  259. oscura/inference/alignment.py +216 -123
  260. oscura/inference/bayesian.py +113 -33
  261. oscura/inference/crc_reverse.py +101 -55
  262. oscura/inference/logic.py +6 -2
  263. oscura/inference/message_format.py +342 -183
  264. oscura/inference/protocol.py +95 -44
  265. oscura/inference/protocol_dsl.py +180 -82
  266. oscura/inference/signal_intelligence.py +1439 -706
  267. oscura/inference/spectral.py +99 -57
  268. oscura/inference/state_machine.py +810 -158
  269. oscura/inference/stream.py +270 -110
  270. oscura/iot/__init__.py +34 -0
  271. oscura/iot/coap/__init__.py +32 -0
  272. oscura/iot/coap/analyzer.py +668 -0
  273. oscura/iot/coap/options.py +212 -0
  274. oscura/iot/lorawan/__init__.py +21 -0
  275. oscura/iot/lorawan/crypto.py +206 -0
  276. oscura/iot/lorawan/decoder.py +801 -0
  277. oscura/iot/lorawan/mac_commands.py +341 -0
  278. oscura/iot/mqtt/__init__.py +27 -0
  279. oscura/iot/mqtt/analyzer.py +999 -0
  280. oscura/iot/mqtt/properties.py +315 -0
  281. oscura/iot/zigbee/__init__.py +31 -0
  282. oscura/iot/zigbee/analyzer.py +615 -0
  283. oscura/iot/zigbee/security.py +153 -0
  284. oscura/iot/zigbee/zcl.py +349 -0
  285. oscura/jupyter/display.py +125 -45
  286. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  287. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  288. oscura/jupyter/exploratory/fuzzy.py +746 -0
  289. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  290. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  291. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  292. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  293. oscura/jupyter/exploratory/sync.py +612 -0
  294. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  295. oscura/jupyter/magic.py +4 -4
  296. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  297. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  298. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  299. oscura/loaders/__init__.py +171 -63
  300. oscura/loaders/binary.py +88 -1
  301. oscura/loaders/chipwhisperer.py +153 -137
  302. oscura/loaders/configurable.py +208 -86
  303. oscura/loaders/csv_loader.py +458 -215
  304. oscura/loaders/hdf5_loader.py +278 -119
  305. oscura/loaders/lazy.py +87 -54
  306. oscura/loaders/mmap_loader.py +1 -1
  307. oscura/loaders/numpy_loader.py +253 -116
  308. oscura/loaders/pcap.py +226 -151
  309. oscura/loaders/rigol.py +110 -49
  310. oscura/loaders/sigrok.py +201 -78
  311. oscura/loaders/tdms.py +81 -58
  312. oscura/loaders/tektronix.py +291 -174
  313. oscura/loaders/touchstone.py +182 -87
  314. oscura/loaders/vcd.py +215 -117
  315. oscura/loaders/wav.py +155 -68
  316. oscura/reporting/__init__.py +9 -7
  317. oscura/reporting/analyze.py +352 -146
  318. oscura/reporting/argument_preparer.py +69 -14
  319. oscura/reporting/auto_report.py +97 -61
  320. oscura/reporting/batch.py +131 -58
  321. oscura/reporting/chart_selection.py +57 -45
  322. oscura/reporting/comparison.py +63 -17
  323. oscura/reporting/content/executive.py +76 -24
  324. oscura/reporting/core_formats/multi_format.py +11 -8
  325. oscura/reporting/engine.py +312 -158
  326. oscura/reporting/enhanced_reports.py +949 -0
  327. oscura/reporting/export.py +86 -43
  328. oscura/reporting/formatting/numbers.py +69 -42
  329. oscura/reporting/html.py +139 -58
  330. oscura/reporting/index.py +137 -65
  331. oscura/reporting/output.py +158 -67
  332. oscura/reporting/pdf.py +67 -102
  333. oscura/reporting/plots.py +191 -112
  334. oscura/reporting/sections.py +88 -47
  335. oscura/reporting/standards.py +104 -61
  336. oscura/reporting/summary_generator.py +75 -55
  337. oscura/reporting/tables.py +138 -54
  338. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  339. oscura/reporting/templates/index.md +13 -13
  340. oscura/sessions/__init__.py +14 -23
  341. oscura/sessions/base.py +3 -3
  342. oscura/sessions/blackbox.py +106 -10
  343. oscura/sessions/generic.py +2 -2
  344. oscura/sessions/legacy.py +783 -0
  345. oscura/side_channel/__init__.py +63 -0
  346. oscura/side_channel/dpa.py +1025 -0
  347. oscura/utils/__init__.py +15 -1
  348. oscura/utils/autodetect.py +1 -5
  349. oscura/utils/bitwise.py +118 -0
  350. oscura/{builders → utils/builders}/__init__.py +1 -1
  351. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  352. oscura/{comparison → utils/comparison}/compare.py +202 -101
  353. oscura/{comparison → utils/comparison}/golden.py +83 -63
  354. oscura/{comparison → utils/comparison}/limits.py +313 -89
  355. oscura/{comparison → utils/comparison}/mask.py +151 -45
  356. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  357. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  358. oscura/{component → utils/component}/__init__.py +3 -3
  359. oscura/{component → utils/component}/impedance.py +122 -58
  360. oscura/{component → utils/component}/reactive.py +165 -168
  361. oscura/{component → utils/component}/transmission_line.py +3 -3
  362. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  363. oscura/{filtering → utils/filtering}/base.py +1 -1
  364. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  365. oscura/{filtering → utils/filtering}/design.py +169 -93
  366. oscura/{filtering → utils/filtering}/filters.py +2 -2
  367. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  368. oscura/utils/geometry.py +31 -0
  369. oscura/utils/imports.py +184 -0
  370. oscura/utils/lazy.py +1 -1
  371. oscura/{math → utils/math}/__init__.py +2 -2
  372. oscura/{math → utils/math}/arithmetic.py +114 -48
  373. oscura/{math → utils/math}/interpolation.py +139 -106
  374. oscura/utils/memory.py +129 -66
  375. oscura/utils/memory_advanced.py +92 -9
  376. oscura/utils/memory_extensions.py +10 -8
  377. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  378. oscura/{optimization → utils/optimization}/search.py +2 -2
  379. oscura/utils/performance/__init__.py +58 -0
  380. oscura/utils/performance/caching.py +889 -0
  381. oscura/utils/performance/lsh_clustering.py +333 -0
  382. oscura/utils/performance/memory_optimizer.py +699 -0
  383. oscura/utils/performance/optimizations.py +675 -0
  384. oscura/utils/performance/parallel.py +654 -0
  385. oscura/utils/performance/profiling.py +661 -0
  386. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  387. oscura/{pipeline → utils/pipeline}/composition.py +11 -3
  388. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  389. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  390. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  391. oscura/{search → utils/search}/__init__.py +3 -3
  392. oscura/{search → utils/search}/anomaly.py +188 -58
  393. oscura/utils/search/context.py +294 -0
  394. oscura/{search → utils/search}/pattern.py +138 -10
  395. oscura/utils/serial.py +51 -0
  396. oscura/utils/storage/__init__.py +61 -0
  397. oscura/utils/storage/database.py +1166 -0
  398. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  399. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  400. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  401. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  402. oscura/{triggering → utils/triggering}/base.py +6 -6
  403. oscura/{triggering → utils/triggering}/edge.py +2 -2
  404. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  405. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  406. oscura/{triggering → utils/triggering}/window.py +2 -2
  407. oscura/utils/validation.py +32 -0
  408. oscura/validation/__init__.py +121 -0
  409. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  410. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  411. oscura/{compliance → validation/compliance}/masks.py +1 -1
  412. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  413. oscura/{compliance → validation/compliance}/testing.py +114 -52
  414. oscura/validation/compliance_tests.py +915 -0
  415. oscura/validation/fuzzer.py +990 -0
  416. oscura/validation/grammar_tests.py +596 -0
  417. oscura/validation/grammar_validator.py +904 -0
  418. oscura/validation/hil_testing.py +977 -0
  419. oscura/{quality → validation/quality}/__init__.py +4 -4
  420. oscura/{quality → validation/quality}/ensemble.py +251 -171
  421. oscura/{quality → validation/quality}/explainer.py +3 -3
  422. oscura/{quality → validation/quality}/scoring.py +1 -1
  423. oscura/{quality → validation/quality}/warnings.py +4 -4
  424. oscura/validation/regression_suite.py +808 -0
  425. oscura/validation/replay.py +788 -0
  426. oscura/{testing → validation/testing}/__init__.py +2 -2
  427. oscura/{testing → validation/testing}/synthetic.py +5 -5
  428. oscura/visualization/__init__.py +9 -0
  429. oscura/visualization/accessibility.py +1 -1
  430. oscura/visualization/annotations.py +64 -67
  431. oscura/visualization/colors.py +7 -7
  432. oscura/visualization/digital.py +180 -81
  433. oscura/visualization/eye.py +236 -85
  434. oscura/visualization/interactive.py +320 -143
  435. oscura/visualization/jitter.py +587 -247
  436. oscura/visualization/layout.py +169 -134
  437. oscura/visualization/optimization.py +103 -52
  438. oscura/visualization/palettes.py +1 -1
  439. oscura/visualization/power.py +427 -211
  440. oscura/visualization/power_extended.py +626 -297
  441. oscura/visualization/presets.py +2 -0
  442. oscura/visualization/protocols.py +495 -181
  443. oscura/visualization/render.py +79 -63
  444. oscura/visualization/reverse_engineering.py +171 -124
  445. oscura/visualization/signal_integrity.py +460 -279
  446. oscura/visualization/specialized.py +190 -100
  447. oscura/visualization/spectral.py +670 -255
  448. oscura/visualization/thumbnails.py +166 -137
  449. oscura/visualization/waveform.py +150 -63
  450. oscura/workflows/__init__.py +3 -0
  451. oscura/{batch → workflows/batch}/__init__.py +5 -5
  452. oscura/{batch → workflows/batch}/advanced.py +150 -75
  453. oscura/workflows/batch/aggregate.py +531 -0
  454. oscura/workflows/batch/analyze.py +236 -0
  455. oscura/{batch → workflows/batch}/logging.py +2 -2
  456. oscura/{batch → workflows/batch}/metrics.py +1 -1
  457. oscura/workflows/complete_re.py +1144 -0
  458. oscura/workflows/compliance.py +44 -54
  459. oscura/workflows/digital.py +197 -51
  460. oscura/workflows/legacy/__init__.py +12 -0
  461. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  462. oscura/workflows/multi_trace.py +9 -9
  463. oscura/workflows/power.py +42 -62
  464. oscura/workflows/protocol.py +82 -49
  465. oscura/workflows/reverse_engineering.py +351 -150
  466. oscura/workflows/signal_integrity.py +157 -82
  467. oscura-0.6.0.dist-info/METADATA +643 -0
  468. oscura-0.6.0.dist-info/RECORD +590 -0
  469. oscura/analyzers/digital/ic_database.py +0 -498
  470. oscura/analyzers/digital/timing_paths.py +0 -339
  471. oscura/analyzers/digital/vintage.py +0 -377
  472. oscura/analyzers/digital/vintage_result.py +0 -148
  473. oscura/analyzers/protocols/parallel_bus.py +0 -449
  474. oscura/batch/aggregate.py +0 -300
  475. oscura/batch/analyze.py +0 -139
  476. oscura/dsl/__init__.py +0 -73
  477. oscura/exceptions.py +0 -59
  478. oscura/exploratory/fuzzy.py +0 -513
  479. oscura/exploratory/sync.py +0 -384
  480. oscura/export/wavedrom.py +0 -430
  481. oscura/exporters/__init__.py +0 -94
  482. oscura/exporters/csv.py +0 -303
  483. oscura/exporters/exporters.py +0 -44
  484. oscura/exporters/hdf5.py +0 -217
  485. oscura/exporters/html_export.py +0 -701
  486. oscura/exporters/json_export.py +0 -338
  487. oscura/exporters/markdown_export.py +0 -367
  488. oscura/exporters/matlab_export.py +0 -354
  489. oscura/exporters/npz_export.py +0 -219
  490. oscura/exporters/spice_export.py +0 -210
  491. oscura/exporters/vintage_logic_csv.py +0 -247
  492. oscura/reporting/vintage_logic_report.py +0 -523
  493. oscura/search/context.py +0 -149
  494. oscura/session/__init__.py +0 -34
  495. oscura/session/annotations.py +0 -289
  496. oscura/session/history.py +0 -313
  497. oscura/session/session.py +0 -520
  498. oscura/visualization/digital_advanced.py +0 -718
  499. oscura/visualization/figure_manager.py +0 -156
  500. oscura/workflow/__init__.py +0 -13
  501. oscura-0.5.0.dist-info/METADATA +0 -407
  502. oscura-0.5.0.dist-info/RECORD +0 -486
  503. /oscura/core/{config.py → config/legacy.py} +0 -0
  504. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  505. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  506. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  507. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  508. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  509. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  510. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  511. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
  512. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
  513. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -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(