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
oscura/loaders/sigrok.py CHANGED
@@ -60,90 +60,23 @@ def load_sigrok(
60
60
  sigrok session file format specification
61
61
  """
62
62
  path = Path(path)
63
-
64
- if not path.exists():
65
- raise LoaderError(
66
- "File not found",
67
- file_path=str(path),
68
- )
69
-
70
- if not zipfile.is_zipfile(path):
71
- raise FormatError(
72
- "File is not a valid sigrok session (not a ZIP archive)",
73
- file_path=str(path),
74
- expected="ZIP archive",
75
- )
63
+ _validate_sigrok_file(path)
76
64
 
77
65
  try:
78
66
  with zipfile.ZipFile(path, "r") as zf:
79
- # Parse metadata
67
+ # Parse metadata and extract channel info
80
68
  metadata_dict = _parse_metadata(zf, path)
69
+ sample_rate, channels, total_channels = _extract_channel_info(metadata_dict)
81
70
 
82
- # Get sample rate from metadata
83
- sample_rate = metadata_dict.get("samplerate", 1_000_000)
84
-
85
- # Get channel information
86
- channels = metadata_dict.get("channels", [])
87
- total_channels = metadata_dict.get("total probes", len(channels))
88
-
89
- # Find and read logic data files
90
- logic_files = [name for name in zf.namelist() if name.startswith("logic-1")]
91
-
92
- if not logic_files:
93
- raise FormatError(
94
- "No logic data found in sigrok session",
95
- file_path=str(path),
96
- expected="logic-1-* data files",
97
- )
98
-
99
- # Read and combine logic data
100
- data = _read_logic_data(zf, logic_files, total_channels)
101
-
102
- # Select specific channel if requested
103
- if channel is not None:
104
- if isinstance(channel, int):
105
- if channel < 0 or channel >= data.shape[0]:
106
- raise LoaderError(
107
- f"Channel index {channel} out of range",
108
- file_path=str(path),
109
- details=f"Available channels: 0-{data.shape[0] - 1}",
110
- )
111
- channel_data = data[channel]
112
- channel_name = channels[channel] if channel < len(channels) else f"D{channel}"
113
- elif isinstance(channel, str):
114
- if channel in channels:
115
- idx = channels.index(channel)
116
- channel_data = data[idx]
117
- channel_name = channel
118
- else:
119
- raise LoaderError(
120
- f"Channel '{channel}' not found",
121
- file_path=str(path),
122
- details=f"Available channels: {channels}",
123
- )
124
- else:
125
- channel_data = data[0] # type: ignore[unreachable]
126
- channel_name = channels[0] if channels else "D0"
127
- else:
128
- # Default to first channel
129
- channel_data = data[0] if data.ndim > 1 else data
130
- channel_name = channels[0] if channels else "D0"
131
-
132
- # Compute edges
133
- edges = _compute_edges(channel_data, sample_rate)
134
-
135
- # Build metadata
136
- trace_metadata = TraceMetadata(
137
- sample_rate=float(sample_rate),
138
- source_file=str(path),
139
- channel_name=channel_name,
140
- trigger_info=metadata_dict.get("trigger", None),
141
- )
71
+ # Read logic data
72
+ data = _load_logic_data(zf, path, total_channels)
142
73
 
143
- return DigitalTrace(
144
- data=channel_data,
145
- metadata=trace_metadata,
146
- edges=edges,
74
+ # Select channel and build trace
75
+ channel_data, channel_name = _select_channel_data(data, channel, channels, path)
76
+
77
+ # Compute edges and build trace
78
+ return _build_digital_trace(
79
+ channel_data, channel_name, sample_rate, metadata_dict, path
147
80
  )
148
81
 
149
82
  except zipfile.BadZipFile as e:
@@ -163,6 +96,196 @@ def load_sigrok(
163
96
  ) from e
164
97
 
165
98
 
99
+ def _validate_sigrok_file(path: Path) -> None:
100
+ """Validate that file exists and is a zip archive.
101
+
102
+ Args:
103
+ path: Path to sigrok file.
104
+
105
+ Raises:
106
+ LoaderError: If file not found.
107
+ FormatError: If not a valid ZIP file.
108
+ """
109
+ if not path.exists():
110
+ raise LoaderError("File not found", file_path=str(path))
111
+
112
+ if not zipfile.is_zipfile(path):
113
+ raise FormatError(
114
+ "File is not a valid sigrok session (not a ZIP archive)",
115
+ file_path=str(path),
116
+ expected="ZIP archive",
117
+ )
118
+
119
+
120
+ def _extract_channel_info(metadata_dict: dict[str, Any]) -> tuple[float, list[str], int]:
121
+ """Extract channel information from metadata.
122
+
123
+ Args:
124
+ metadata_dict: Parsed metadata dictionary.
125
+
126
+ Returns:
127
+ Tuple of (sample_rate, channels, total_channels).
128
+ """
129
+ sample_rate = metadata_dict.get("samplerate", 1_000_000)
130
+ channels = metadata_dict.get("channels", [])
131
+ total_channels = metadata_dict.get("total probes", len(channels))
132
+ return float(sample_rate), channels, total_channels
133
+
134
+
135
+ def _load_logic_data(zf: zipfile.ZipFile, path: Path, total_channels: int) -> NDArray[np.bool_]:
136
+ """Load logic data from sigrok session.
137
+
138
+ Args:
139
+ zf: Open ZipFile object.
140
+ path: Path to session file (for error messages).
141
+ total_channels: Total number of channels.
142
+
143
+ Returns:
144
+ Boolean array of shape (channels, samples).
145
+
146
+ Raises:
147
+ FormatError: If no logic data found.
148
+ """
149
+ logic_files = [name for name in zf.namelist() if name.startswith("logic-1")]
150
+
151
+ if not logic_files:
152
+ raise FormatError(
153
+ "No logic data found in sigrok session",
154
+ file_path=str(path),
155
+ expected="logic-1-* data files",
156
+ )
157
+
158
+ return _read_logic_data(zf, logic_files, total_channels)
159
+
160
+
161
+ def _select_channel_data(
162
+ data: NDArray[np.bool_],
163
+ channel: str | int | None,
164
+ channels: list[str],
165
+ path: Path,
166
+ ) -> tuple[NDArray[np.bool_], str]:
167
+ """Select specific channel data or default to first channel.
168
+
169
+ Args:
170
+ data: Multi-channel data array.
171
+ channel: Channel selector (name, index, or None).
172
+ channels: List of channel names.
173
+ path: Path to file (for error messages).
174
+
175
+ Returns:
176
+ Tuple of (channel_data, channel_name).
177
+
178
+ Raises:
179
+ LoaderError: If channel not found or out of range.
180
+ """
181
+ if channel is None:
182
+ channel_data = data[0] if data.ndim > 1 else data
183
+ channel_name = channels[0] if channels else "D0"
184
+ return channel_data, channel_name
185
+
186
+ if isinstance(channel, int):
187
+ return _select_channel_by_index(data, channel, channels, path)
188
+ # isinstance(channel, str) must be true here
189
+ return _select_channel_by_name(data, channel, channels, path)
190
+
191
+
192
+ def _select_channel_by_index(
193
+ data: NDArray[np.bool_],
194
+ channel: int,
195
+ channels: list[str],
196
+ path: Path,
197
+ ) -> tuple[NDArray[np.bool_], str]:
198
+ """Select channel by numeric index.
199
+
200
+ Args:
201
+ data: Multi-channel data array.
202
+ channel: Channel index.
203
+ channels: List of channel names.
204
+ path: Path to file (for error messages).
205
+
206
+ Returns:
207
+ Tuple of (channel_data, channel_name).
208
+
209
+ Raises:
210
+ LoaderError: If index out of range.
211
+ """
212
+ if channel < 0 or channel >= data.shape[0]:
213
+ raise LoaderError(
214
+ f"Channel index {channel} out of range",
215
+ file_path=str(path),
216
+ details=f"Available channels: 0-{data.shape[0] - 1}",
217
+ )
218
+ channel_data = data[channel]
219
+ channel_name = channels[channel] if channel < len(channels) else f"D{channel}"
220
+ return channel_data, channel_name
221
+
222
+
223
+ def _select_channel_by_name(
224
+ data: NDArray[np.bool_],
225
+ channel: str,
226
+ channels: list[str],
227
+ path: Path,
228
+ ) -> tuple[NDArray[np.bool_], str]:
229
+ """Select channel by name.
230
+
231
+ Args:
232
+ data: Multi-channel data array.
233
+ channel: Channel name.
234
+ channels: List of channel names.
235
+ path: Path to file (for error messages).
236
+
237
+ Returns:
238
+ Tuple of (channel_data, channel_name).
239
+
240
+ Raises:
241
+ LoaderError: If channel name not found.
242
+ """
243
+ if channel in channels:
244
+ idx = channels.index(channel)
245
+ return data[idx], channel
246
+ else:
247
+ raise LoaderError(
248
+ f"Channel '{channel}' not found",
249
+ file_path=str(path),
250
+ details=f"Available channels: {channels}",
251
+ )
252
+
253
+
254
+ def _build_digital_trace(
255
+ channel_data: NDArray[np.bool_],
256
+ channel_name: str,
257
+ sample_rate: float,
258
+ metadata_dict: dict[str, Any],
259
+ path: Path,
260
+ ) -> DigitalTrace:
261
+ """Build DigitalTrace object from channel data.
262
+
263
+ Args:
264
+ channel_data: Boolean array for selected channel.
265
+ channel_name: Name of the channel.
266
+ sample_rate: Sample rate in Hz.
267
+ metadata_dict: Metadata dictionary.
268
+ path: Path to source file.
269
+
270
+ Returns:
271
+ DigitalTrace object.
272
+ """
273
+ edges = _compute_edges(channel_data, sample_rate)
274
+
275
+ trace_metadata = TraceMetadata(
276
+ sample_rate=sample_rate,
277
+ source_file=str(path),
278
+ channel_name=channel_name,
279
+ trigger_info=metadata_dict.get("trigger"),
280
+ )
281
+
282
+ return DigitalTrace(
283
+ data=channel_data,
284
+ metadata=trace_metadata,
285
+ edges=edges,
286
+ )
287
+
288
+
166
289
  def _parse_metadata(zf: zipfile.ZipFile, path: Path) -> dict[str, Any]:
167
290
  """Parse sigrok session metadata.
168
291
 
oscura/loaders/tdms.py CHANGED
@@ -16,6 +16,7 @@ from pathlib import Path
16
16
  from typing import TYPE_CHECKING, Any
17
17
 
18
18
  import numpy as np
19
+ from numpy.typing import NDArray
19
20
 
20
21
  from oscura.core.exceptions import FormatError, LoaderError
21
22
  from oscura.core.types import TraceMetadata, WaveformTrace
@@ -112,6 +113,17 @@ def _load_with_nptdms(
112
113
  FormatError: If file is not valid TDMS format or has no data.
113
114
  LoaderError: If channel or group not found.
114
115
  """
116
+ tdms_file = _parse_tdms_file(path)
117
+ target_group = _select_tdms_group(tdms_file, group, path)
118
+ target_channel = _select_tdms_channel(target_group, channel, path)
119
+ data = _extract_channel_data(target_channel, path)
120
+ metadata = _build_tdms_metadata(target_channel, target_group, tdms_file, path)
121
+
122
+ return WaveformTrace(data=data, metadata=metadata)
123
+
124
+
125
+ def _parse_tdms_file(path: Path) -> Any:
126
+ """Parse TDMS file and validate structure."""
115
127
  try:
116
128
  tdms_file = TdmsFile.read(str(path))
117
129
  except Exception as e:
@@ -121,33 +133,37 @@ def _load_with_nptdms(
121
133
  expected="Valid NI TDMS format",
122
134
  ) from e
123
135
 
124
- # Get available groups
125
136
  groups = list(tdms_file.groups())
126
-
127
137
  if not groups:
128
138
  raise FormatError(
129
139
  "No groups found in TDMS file",
130
140
  file_path=str(path),
131
141
  )
132
142
 
133
- # Select group
134
- if group is not None:
135
- target_group = None
136
- for g in groups:
137
- if g.name == group:
138
- target_group = g
139
- break
140
- if target_group is None:
141
- available_groups = [g.name for g in groups]
142
- raise LoaderError(
143
- f"Group '{group}' not found",
144
- file_path=str(path),
145
- details=f"Available groups: {available_groups}",
146
- )
147
- else:
148
- target_group = groups[0]
143
+ return tdms_file
144
+
145
+
146
+ def _select_tdms_group(tdms_file: Any, group: str | None, path: Path) -> Any:
147
+ """Select target group from TDMS file."""
148
+ groups = list(tdms_file.groups())
149
+
150
+ if group is None:
151
+ return groups[0]
152
+
153
+ for g in groups:
154
+ if g.name == group:
155
+ return g
156
+
157
+ available_groups = [g.name for g in groups]
158
+ raise LoaderError(
159
+ f"Group '{group}' not found",
160
+ file_path=str(path),
161
+ details=f"Available groups: {available_groups}",
162
+ )
163
+
149
164
 
150
- # Get channels in group
165
+ def _select_tdms_channel(target_group: Any, channel: str | int | None, path: Path) -> Any:
166
+ """Select target channel from TDMS group."""
151
167
  channels = list(target_group.channels())
152
168
 
153
169
  if not channels:
@@ -156,35 +172,44 @@ def _load_with_nptdms(
156
172
  file_path=str(path),
157
173
  )
158
174
 
159
- # Select channel
160
- if channel is not None:
161
- if isinstance(channel, int):
162
- if channel < 0 or channel >= len(channels):
163
- raise LoaderError(
164
- f"Channel index {channel} out of range",
165
- file_path=str(path),
166
- details=f"Available channels: 0-{len(channels) - 1}",
167
- )
168
- target_channel = channels[channel]
169
- elif isinstance(channel, str):
170
- target_channel = None
171
- for ch in channels:
172
- if ch.name == channel:
173
- target_channel = ch
174
- break
175
- if target_channel is None:
176
- available_channels = [ch.name for ch in channels]
177
- raise LoaderError(
178
- f"Channel '{channel}' not found",
179
- file_path=str(path),
180
- details=f"Available channels: {available_channels}",
181
- )
182
- else:
183
- target_channel = channels[0] # type: ignore[unreachable]
175
+ if channel is None:
176
+ return channels[0]
177
+
178
+ if isinstance(channel, int):
179
+ return _select_channel_by_index(channels, channel, path)
180
+ elif isinstance(channel, str):
181
+ return _select_channel_by_name(channels, channel, path)
184
182
  else:
185
- target_channel = channels[0]
183
+ return channels[0] # type: ignore[unreachable]
184
+
186
185
 
187
- # Get channel data
186
+ def _select_channel_by_index(channels: list[Any], channel: int, path: Path) -> Any:
187
+ """Select channel by index."""
188
+ if channel < 0 or channel >= len(channels):
189
+ raise LoaderError(
190
+ f"Channel index {channel} out of range",
191
+ file_path=str(path),
192
+ details=f"Available channels: 0-{len(channels) - 1}",
193
+ )
194
+ return channels[channel]
195
+
196
+
197
+ def _select_channel_by_name(channels: list[Any], channel: str, path: Path) -> Any:
198
+ """Select channel by name."""
199
+ for ch in channels:
200
+ if ch.name == channel:
201
+ return ch
202
+
203
+ available_channels = [ch.name for ch in channels]
204
+ raise LoaderError(
205
+ f"Channel '{channel}' not found",
206
+ file_path=str(path),
207
+ details=f"Available channels: {available_channels}",
208
+ )
209
+
210
+
211
+ def _extract_channel_data(target_channel: Any, path: Path) -> NDArray[np.float64]:
212
+ """Extract and validate channel data."""
188
213
  data = target_channel.data
189
214
  if data is None or len(data) == 0:
190
215
  raise FormatError(
@@ -192,21 +217,21 @@ def _load_with_nptdms(
192
217
  file_path=str(path),
193
218
  )
194
219
 
195
- # Convert to float64
196
- data = np.asarray(data, dtype=np.float64)
220
+ return np.asarray(data, dtype=np.float64)
197
221
 
198
- # Extract sample rate from properties
199
- sample_rate = _get_sample_rate(target_channel, target_group, tdms_file)
200
222
 
201
- # Extract other metadata
223
+ def _build_tdms_metadata(
224
+ target_channel: Any,
225
+ target_group: Any,
226
+ tdms_file: Any,
227
+ path: Path,
228
+ ) -> TraceMetadata:
229
+ """Build metadata from TDMS channel properties."""
230
+ sample_rate = _get_sample_rate(target_channel, target_group, tdms_file)
202
231
  vertical_scale = target_channel.properties.get("NI_Scale[0]_Linear_Slope")
203
232
  vertical_offset = target_channel.properties.get("NI_Scale[0]_Linear_Y_Intercept")
204
233
 
205
- # Get units if available
206
- target_channel.properties.get("unit_string", None)
207
-
208
- # Build metadata
209
- metadata = TraceMetadata(
234
+ return TraceMetadata(
210
235
  sample_rate=sample_rate,
211
236
  vertical_scale=float(vertical_scale) if vertical_scale is not None else None,
212
237
  vertical_offset=float(vertical_offset) if vertical_offset is not None else None,
@@ -215,8 +240,6 @@ def _load_with_nptdms(
215
240
  trigger_info=_extract_tdms_properties(target_channel),
216
241
  )
217
242
 
218
- return WaveformTrace(data=data, metadata=metadata)
219
-
220
243
 
221
244
  def _get_sample_rate(
222
245
  channel: Any,