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
@@ -212,104 +212,26 @@ def load_hdf5(
212
212
  >>> # Load as memory-mapped for large files
213
213
  >>> trace = load_hdf5("huge_data.h5", mmap=True)
214
214
  """
215
- if not H5PY_AVAILABLE:
216
- raise LoaderError(
217
- "HDF5 support not available",
218
- details="h5py package is required for HDF5 loading",
219
- fix_hint="Install h5py: pip install h5py",
220
- )
221
-
222
- path = Path(path)
223
-
224
- if not path.exists():
225
- raise LoaderError(
226
- "File not found",
227
- file_path=str(path),
228
- )
229
-
230
- # Use channel as dataset if dataset not specified
231
- if dataset is None and channel is not None:
232
- dataset = str(channel)
215
+ _validate_hdf5_availability()
216
+ file_path = _validate_file_path(path)
217
+ dataset_path = _resolve_dataset_name(dataset, channel)
233
218
 
234
219
  try:
235
- with h5py.File(path, "r") as f:
236
- # Find dataset
237
- if dataset is not None:
238
- if dataset in f:
239
- ds = f[dataset]
240
- else:
241
- # Try to find by name
242
- ds = _find_dataset_by_name(f, dataset)
243
- if ds is None:
244
- available = list_datasets(path)
245
- raise FormatError(
246
- f"Dataset not found: {dataset}",
247
- file_path=str(path),
248
- expected=dataset,
249
- got=f"Available: {', '.join(available)}",
250
- )
251
- else:
252
- # Auto-detect dataset
253
- ds = _find_waveform_dataset(f)
254
- if ds is None:
255
- available = list_datasets(path)
256
- raise FormatError(
257
- "No waveform data found in HDF5 file",
258
- file_path=str(path),
259
- expected=f"Dataset named: {', '.join(DATASET_NAMES)}",
260
- got=f"Datasets: {', '.join(available)}",
261
- )
262
-
263
- # Extract data
264
- if not isinstance(ds, h5py.Dataset):
265
- raise FormatError(
266
- "Selected path is not a dataset",
267
- file_path=str(path),
268
- got=type(ds).__name__,
269
- )
270
-
271
- data = np.asarray(ds, dtype=np.float64)
272
- if data.ndim > 1:
273
- data = data.ravel()
274
-
275
- # Extract metadata from attributes
276
- detected_sample_rate = sample_rate
277
- if detected_sample_rate is None:
278
- detected_sample_rate = _find_sample_rate(f, ds)
279
-
280
- if detected_sample_rate is None:
281
- detected_sample_rate = 1e6 # Default
282
-
283
- # Get other metadata
284
- vertical_scale = _get_attr(ds, ["vertical_scale", "v_scale", "scale"])
285
- vertical_offset = _get_attr(ds, ["vertical_offset", "v_offset", "offset"])
286
- channel_name = _get_attr(ds, ["channel_name", "name", "channel"])
287
-
288
- if channel_name is None:
289
- channel_name = ds.name.split("/")[-1] if ds.name else "CH1"
290
-
291
- metadata = TraceMetadata(
292
- sample_rate=float(detected_sample_rate),
293
- vertical_scale=float(vertical_scale) if vertical_scale else None,
294
- vertical_offset=float(vertical_offset) if vertical_offset else None,
295
- source_file=str(path),
296
- channel_name=str(channel_name),
297
- )
220
+ with h5py.File(file_path, "r") as f:
221
+ ds = _locate_dataset(f, dataset_path, file_path)
222
+ _validate_dataset(ds, file_path)
223
+ data = _extract_data_array(ds)
224
+ metadata = _build_metadata(f, ds, sample_rate, file_path)
298
225
 
299
- # Return memory-mapped trace if requested
300
226
  if mmap:
301
- return HDF5MmapTrace(
302
- file_path=path,
303
- dataset_path=ds.name,
304
- metadata=metadata,
305
- )
227
+ return _create_mmap_trace(file_path, ds.name, metadata)
306
228
 
307
229
  return WaveformTrace(data=data, metadata=metadata)
308
230
 
309
231
  except OSError as e:
310
232
  raise LoaderError(
311
233
  "Failed to read HDF5 file",
312
- file_path=str(path),
234
+ file_path=str(file_path),
313
235
  details=str(e),
314
236
  ) from e
315
237
  except Exception as e:
@@ -317,16 +239,239 @@ def load_hdf5(
317
239
  raise
318
240
  raise LoaderError(
319
241
  "Failed to load HDF5 file",
320
- file_path=str(path),
242
+ file_path=str(file_path),
321
243
  details=str(e),
322
244
  ) from e
323
245
 
324
246
 
247
+ def _validate_hdf5_availability() -> None:
248
+ """Validate that h5py package is available.
249
+
250
+ Raises:
251
+ LoaderError: If h5py is not installed.
252
+ """
253
+ if not H5PY_AVAILABLE:
254
+ raise LoaderError(
255
+ "HDF5 support not available",
256
+ details="h5py package is required for HDF5 loading",
257
+ fix_hint="Install h5py: pip install h5py",
258
+ )
259
+
260
+
261
+ def _validate_file_path(path: str | PathLike[str]) -> Path:
262
+ """Validate that the file path exists.
263
+
264
+ Args:
265
+ path: File path to validate.
266
+
267
+ Returns:
268
+ Validated Path object.
269
+
270
+ Raises:
271
+ LoaderError: If file does not exist.
272
+ """
273
+ file_path = Path(path)
274
+ if not file_path.exists():
275
+ raise LoaderError("File not found", file_path=str(file_path))
276
+ return file_path
277
+
278
+
279
+ def _resolve_dataset_name(dataset: str | None, channel: str | int | None) -> str | None:
280
+ """Resolve dataset name from dataset or channel parameter.
281
+
282
+ Args:
283
+ dataset: Explicit dataset path.
284
+ channel: Channel name/number (alternative to dataset).
285
+
286
+ Returns:
287
+ Resolved dataset name, or None for auto-detection.
288
+
289
+ Example:
290
+ >>> _resolve_dataset_name(None, 1)
291
+ '1'
292
+ >>> _resolve_dataset_name("/data", None)
293
+ '/data'
294
+ """
295
+ if dataset is None and channel is not None:
296
+ return str(channel)
297
+ return dataset
298
+
299
+
300
+ def _locate_dataset(f: h5py.File, dataset_path: str | None, file_path: Path) -> h5py.Dataset:
301
+ """Locate dataset in HDF5 file by path or auto-detection.
302
+
303
+ Args:
304
+ f: Open HDF5 file handle.
305
+ dataset_path: Specific dataset path, or None for auto-detect.
306
+ file_path: Path to HDF5 file (for error messages).
307
+
308
+ Returns:
309
+ Located HDF5 dataset.
310
+
311
+ Raises:
312
+ FormatError: If dataset not found.
313
+
314
+ Example:
315
+ >>> with h5py.File("data.h5") as f:
316
+ ... ds = _locate_dataset(f, "/waveform", Path("data.h5"))
317
+ """
318
+ if dataset_path is not None:
319
+ if dataset_path in f:
320
+ return f[dataset_path]
321
+
322
+ # Try fuzzy name matching
323
+ ds = _find_dataset_by_name(f, dataset_path)
324
+ if ds is None:
325
+ available = list_datasets(file_path)
326
+ raise FormatError(
327
+ f"Dataset not found: {dataset_path}",
328
+ file_path=str(file_path),
329
+ expected=dataset_path,
330
+ got=f"Available: {', '.join(available)}",
331
+ )
332
+ return ds
333
+
334
+ # Auto-detect waveform dataset
335
+ ds = _find_waveform_dataset(f)
336
+ if ds is None:
337
+ available = list_datasets(file_path)
338
+ raise FormatError(
339
+ "No waveform data found in HDF5 file",
340
+ file_path=str(file_path),
341
+ expected=f"Dataset named: {', '.join(DATASET_NAMES)}",
342
+ got=f"Datasets: {', '.join(available)}",
343
+ )
344
+ return ds
345
+
346
+
347
+ def _validate_dataset(ds: Any, file_path: Path) -> None:
348
+ """Validate that object is a valid HDF5 dataset.
349
+
350
+ Args:
351
+ ds: Object to validate.
352
+ file_path: Path to HDF5 file (for error messages).
353
+
354
+ Raises:
355
+ FormatError: If not a dataset.
356
+ """
357
+ if not isinstance(ds, h5py.Dataset):
358
+ raise FormatError(
359
+ "Selected path is not a dataset",
360
+ file_path=str(file_path),
361
+ got=type(ds).__name__,
362
+ )
363
+
364
+
365
+ def _extract_data_array(ds: h5py.Dataset) -> np.ndarray[Any, Any]:
366
+ """Extract and flatten data array from dataset.
367
+
368
+ Args:
369
+ ds: HDF5 dataset.
370
+
371
+ Returns:
372
+ 1D numpy array of float64 data.
373
+
374
+ Example:
375
+ >>> data = _extract_data_array(dataset)
376
+ >>> data.shape
377
+ (10000,)
378
+ """
379
+ data = np.asarray(ds, dtype=np.float64)
380
+ if data.ndim > 1:
381
+ data = data.ravel()
382
+ return data
383
+
384
+
385
+ def _build_metadata(
386
+ f: h5py.File, ds: h5py.Dataset, sample_rate: float | None, file_path: Path
387
+ ) -> TraceMetadata:
388
+ """Build trace metadata from HDF5 attributes.
389
+
390
+ Args:
391
+ f: Open HDF5 file handle.
392
+ ds: HDF5 dataset.
393
+ sample_rate: User-provided sample rate override.
394
+ file_path: Path to HDF5 file.
395
+
396
+ Returns:
397
+ TraceMetadata with extracted attributes.
398
+
399
+ Example:
400
+ >>> metadata = _build_metadata(f, ds, None, Path("data.h5"))
401
+ >>> metadata.sample_rate
402
+ 1000000.0
403
+ """
404
+ detected_sample_rate = sample_rate if sample_rate is not None else _find_sample_rate(f, ds)
405
+ if detected_sample_rate is None:
406
+ detected_sample_rate = 1e6 # Default 1 MHz
407
+
408
+ vertical_scale = _get_attr(ds, ["vertical_scale", "v_scale", "scale"])
409
+ vertical_offset = _get_attr(ds, ["vertical_offset", "v_offset", "offset"])
410
+ channel_name = _get_channel_name(ds)
411
+
412
+ return TraceMetadata(
413
+ sample_rate=float(detected_sample_rate),
414
+ vertical_scale=float(vertical_scale) if vertical_scale else None,
415
+ vertical_offset=float(vertical_offset) if vertical_offset else None,
416
+ source_file=str(file_path),
417
+ channel_name=str(channel_name),
418
+ )
419
+
420
+
421
+ def _get_channel_name(ds: h5py.Dataset) -> str:
422
+ """Get channel name from dataset attributes or path.
423
+
424
+ Args:
425
+ ds: HDF5 dataset.
426
+
427
+ Returns:
428
+ Channel name string.
429
+
430
+ Example:
431
+ >>> _get_channel_name(dataset)
432
+ 'CH1'
433
+ """
434
+ channel_name = _get_attr(ds, ["channel_name", "name", "channel"])
435
+ if channel_name is None:
436
+ channel_name = ds.name.split("/")[-1] if ds.name else "CH1"
437
+ return channel_name
438
+
439
+
440
+ def _create_mmap_trace(
441
+ file_path: Path, dataset_path: str, metadata: TraceMetadata
442
+ ) -> HDF5MmapTrace:
443
+ """Create memory-mapped HDF5 trace.
444
+
445
+ Args:
446
+ file_path: Path to HDF5 file.
447
+ dataset_path: Path to dataset within file.
448
+ metadata: Trace metadata.
449
+
450
+ Returns:
451
+ Memory-mapped trace object.
452
+
453
+ Example:
454
+ >>> trace = _create_mmap_trace(Path("data.h5"), "/waveform", metadata)
455
+ >>> trace[1000:2000] # Access subset without loading entire file
456
+ """
457
+ return HDF5MmapTrace(
458
+ file_path=file_path,
459
+ dataset_path=dataset_path,
460
+ metadata=metadata,
461
+ )
462
+
463
+
325
464
  def _find_waveform_dataset(f: h5py.File) -> h5py.Dataset | None:
326
465
  """Find a waveform dataset in the HDF5 file."""
327
466
  result: h5py.Dataset | None = None
328
467
 
329
468
  def visitor(name: str, obj: Any) -> None:
469
+ """Visit HDF5 items to find waveform dataset.
470
+
471
+ Args:
472
+ name: Dataset path within HDF5 file.
473
+ obj: HDF5 object (dataset or group).
474
+ """
330
475
  nonlocal result
331
476
  if result is not None:
332
477
  return
@@ -352,6 +497,12 @@ def _find_dataset_by_name(f: h5py.File, name: str) -> h5py.Dataset | None:
352
497
  result: h5py.Dataset | None = None
353
498
 
354
499
  def visitor(path: str, obj: Any) -> None:
500
+ """Visit HDF5 items to find dataset by name.
501
+
502
+ Args:
503
+ path: Dataset path within HDF5 file.
504
+ obj: HDF5 object (dataset or group).
505
+ """
355
506
  nonlocal result
356
507
  if result is not None:
357
508
  return
@@ -365,43 +516,45 @@ def _find_dataset_by_name(f: h5py.File, name: str) -> h5py.Dataset | None:
365
516
 
366
517
 
367
518
  def _find_sample_rate(f: h5py.File, ds: h5py.Dataset) -> float | None:
368
- """Find sample rate from HDF5 attributes."""
369
- # Check dataset attributes first
370
- for attr_name in SAMPLE_RATE_ATTRS:
371
- if attr_name in ds.attrs:
372
- value = ds.attrs[attr_name]
373
- if attr_name in ("sample_interval", "dt") and value > 0:
374
- return 1.0 / float(value)
375
- return float(value)
519
+ """Find sample rate from HDF5 attributes.
520
+
521
+ Args:
522
+ f: HDF5 file handle.
523
+ ds: Dataset to search for sample rate.
524
+
525
+ Returns:
526
+ Sample rate in Hz, or None if not found.
527
+ """
528
+ # Check dataset, then parent, then root, then metadata group
529
+ search_locations = [ds, ds.parent, f, f.get("metadata")]
530
+
531
+ for location in search_locations:
532
+ if location is None:
533
+ continue
534
+
535
+ rate = _extract_sample_rate_from_attrs(location)
536
+ if rate is not None:
537
+ return rate
538
+
539
+ return None
540
+
376
541
 
377
- # Check parent group attributes
378
- if ds.parent is not None:
379
- for attr_name in SAMPLE_RATE_ATTRS:
380
- if attr_name in ds.parent.attrs:
381
- value = ds.parent.attrs[attr_name]
382
- if attr_name in ("sample_interval", "dt") and value > 0:
383
- return 1.0 / float(value)
384
- return float(value)
542
+ def _extract_sample_rate_from_attrs(obj: h5py.Dataset | h5py.Group | h5py.File) -> float | None:
543
+ """Extract sample rate from HDF5 object attributes.
385
544
 
386
- # Check root attributes
545
+ Args:
546
+ obj: HDF5 object with attributes.
547
+
548
+ Returns:
549
+ Sample rate in Hz, or None if not found.
550
+ """
387
551
  for attr_name in SAMPLE_RATE_ATTRS:
388
- if attr_name in f.attrs:
389
- value = f.attrs[attr_name]
552
+ if attr_name in obj.attrs:
553
+ value = obj.attrs[attr_name]
390
554
  if attr_name in ("sample_interval", "dt") and value > 0:
391
555
  return 1.0 / float(value)
392
556
  return float(value)
393
557
 
394
- # Check for metadata group
395
- if "metadata" in f:
396
- meta = f["metadata"]
397
- if isinstance(meta, h5py.Group | h5py.Dataset):
398
- for attr_name in SAMPLE_RATE_ATTRS:
399
- if attr_name in meta.attrs:
400
- value = meta.attrs[attr_name]
401
- if attr_name in ("sample_interval", "dt") and value > 0:
402
- return 1.0 / float(value)
403
- return float(value)
404
-
405
558
  return None
406
559
 
407
560
 
@@ -446,6 +599,12 @@ def list_datasets(path: str | PathLike[str]) -> list[str]:
446
599
  datasets: list[str] = []
447
600
 
448
601
  def visitor(name: str, obj: Any) -> None:
602
+ """Visit HDF5 items to collect dataset paths.
603
+
604
+ Args:
605
+ name: Dataset path within HDF5 file.
606
+ obj: HDF5 object (dataset or group).
607
+ """
449
608
  if isinstance(obj, h5py.Dataset):
450
609
  datasets.append("/" + name)
451
610
 
oscura/loaders/lazy.py CHANGED
@@ -188,7 +188,7 @@ class LazyWaveformTrace:
188
188
 
189
189
  metadata = TraceMetadata(
190
190
  sample_rate=self._sample_rate,
191
- **self._metadata, # type: ignore[arg-type]
191
+ **self._metadata,
192
192
  )
193
193
  return WaveformTrace(data=sliced_data, metadata=metadata) # type: ignore[return-value]
194
194
 
@@ -221,7 +221,7 @@ class LazyWaveformTrace:
221
221
 
222
222
  metadata = TraceMetadata(
223
223
  sample_rate=self._sample_rate,
224
- **self._metadata, # type: ignore[arg-type]
224
+ **self._metadata,
225
225
  )
226
226
  return WaveformTrace(data=self.data, metadata=metadata)
227
227
 
@@ -299,72 +299,105 @@ def load_trace_lazy(
299
299
  if not file_path.exists():
300
300
  raise LoaderError(f"File not found: {file_path}")
301
301
 
302
- # Determine format
302
+ if sample_rate is None:
303
+ raise LoaderError("sample_rate is required")
304
+
305
+ # Determine format and extract metadata
303
306
  suffix = file_path.suffix.lower()
304
307
 
305
308
  if suffix == ".npy":
306
- # NumPy format - can read shape and dtype without loading data
307
- with open(file_path, "rb") as f:
308
- # Read NumPy header
309
- import numpy.lib.format as npf
309
+ length, dtype, offset = _extract_npy_metadata(file_path)
310
+ else:
311
+ length, dtype, offset = _extract_raw_metadata(file_path, kwargs)
312
+
313
+ # Return lazy or eager trace
314
+ if lazy:
315
+ return LazyWaveformTrace(
316
+ file_path=file_path,
317
+ sample_rate=sample_rate,
318
+ length=length,
319
+ dtype=dtype,
320
+ offset=offset,
321
+ )
322
+ else:
323
+ return _load_eager_trace(file_path, sample_rate, length, dtype, offset, suffix == ".npy")
310
324
 
311
- npf.read_magic(f) # type: ignore[no-untyped-call]
312
- shape, _fortran_order, dtype = npf.read_array_header_1_0(f) # type: ignore[no-untyped-call]
313
- offset = f.tell()
314
325
 
315
- if not isinstance(shape, tuple) or len(shape) != 1:
316
- raise LoaderError(f"Expected 1D array, got shape {shape}")
326
+ def _extract_npy_metadata(file_path: Path) -> tuple[int, DTypeLike, int]:
327
+ """Extract metadata from NumPy file without loading data.
317
328
 
318
- length = shape[0]
329
+ Args:
330
+ file_path: Path to .npy file.
319
331
 
320
- # Get sample rate from metadata or argument
321
- if sample_rate is None:
322
- raise LoaderError("sample_rate is required for .npy files")
332
+ Returns:
333
+ Tuple of (length, dtype, offset).
323
334
 
324
- if lazy:
325
- return LazyWaveformTrace(
326
- file_path=file_path,
327
- sample_rate=sample_rate,
328
- length=length,
329
- dtype=dtype,
330
- offset=offset,
331
- )
332
- else:
333
- # Load eagerly
334
- from oscura.core.types import TraceMetadata, WaveformTrace
335
+ Raises:
336
+ LoaderError: If file format is invalid.
337
+ """
338
+ with open(file_path, "rb") as f:
339
+ import numpy.lib.format as npf
335
340
 
336
- data = np.load(file_path).astype(np.float64)
337
- metadata = TraceMetadata(sample_rate=sample_rate)
338
- return WaveformTrace(data=data, metadata=metadata)
341
+ npf.read_magic(f) # type: ignore[no-untyped-call]
342
+ shape, _fortran_order, dtype = npf.read_array_header_1_0(f) # type: ignore[no-untyped-call]
343
+ offset = f.tell()
339
344
 
340
- else:
341
- # Raw binary - need sample rate and dtype
342
- if sample_rate is None:
343
- raise LoaderError("sample_rate is required for raw binary files")
345
+ if not isinstance(shape, tuple) or len(shape) != 1:
346
+ raise LoaderError(f"Expected 1D array, got shape {shape}")
344
347
 
345
- dtype = kwargs.get("dtype", np.float64)
346
- offset = kwargs.get("offset", 0)
348
+ return shape[0], dtype, offset
347
349
 
348
- # Compute length from file size
349
- file_size = file_path.stat().st_size - offset
350
- dtype_size = np.dtype(dtype).itemsize
351
- length = file_size // dtype_size
352
350
 
353
- if lazy:
354
- return LazyWaveformTrace(
355
- file_path=file_path,
356
- sample_rate=sample_rate,
357
- length=length,
358
- dtype=dtype,
359
- offset=offset,
360
- )
361
- else:
362
- # Load eagerly
363
- from oscura.core.types import TraceMetadata, WaveformTrace
351
+ def _extract_raw_metadata(file_path: Path, kwargs: dict[str, Any]) -> tuple[int, DTypeLike, int]:
352
+ """Extract metadata from raw binary file.
353
+
354
+ Args:
355
+ file_path: Path to raw binary file.
356
+ kwargs: Additional arguments (dtype, offset).
357
+
358
+ Returns:
359
+ Tuple of (length, dtype, offset).
360
+ """
361
+ dtype = kwargs.get("dtype", np.float64)
362
+ offset = kwargs.get("offset", 0)
363
+
364
+ file_size = file_path.stat().st_size - offset
365
+ dtype_size = np.dtype(dtype).itemsize
366
+ length = file_size // dtype_size
367
+
368
+ return length, dtype, offset
369
+
370
+
371
+ def _load_eager_trace(
372
+ file_path: Path,
373
+ sample_rate: float,
374
+ length: int,
375
+ dtype: DTypeLike,
376
+ offset: int,
377
+ is_npy: bool,
378
+ ) -> WaveformTrace:
379
+ """Load trace data eagerly into memory.
380
+
381
+ Args:
382
+ file_path: Path to trace file.
383
+ sample_rate: Sample rate in Hz.
384
+ length: Number of samples.
385
+ dtype: Data type.
386
+ offset: Byte offset in file.
387
+ is_npy: True if .npy format, False if raw binary.
388
+
389
+ Returns:
390
+ WaveformTrace with data loaded.
391
+ """
392
+ from oscura.core.types import TraceMetadata, WaveformTrace
393
+
394
+ if is_npy:
395
+ data = np.load(file_path).astype(np.float64)
396
+ else:
397
+ data = np.fromfile(file_path, dtype=dtype, count=length, offset=offset).astype(np.float64)
364
398
 
365
- data = np.fromfile(file_path, dtype=dtype, count=length, offset=offset)
366
- metadata = TraceMetadata(sample_rate=sample_rate)
367
- return WaveformTrace(data=data.astype(np.float64), metadata=metadata)
399
+ metadata = TraceMetadata(sample_rate=sample_rate)
400
+ return WaveformTrace(data=data, metadata=metadata)
368
401
 
369
402
 
370
403
  __all__ = ["LazyWaveformTrace", "load_trace_lazy"]
@@ -309,7 +309,7 @@ class MmapWaveformTrace:
309
309
  metadata = TraceMetadata(
310
310
  sample_rate=self._sample_rate,
311
311
  source_file=str(self._file_path),
312
- **self._metadata, # type: ignore[arg-type]
312
+ **self._metadata,
313
313
  )
314
314
 
315
315
  return WaveformTrace(data=data, metadata=metadata)