oscura 0.5.1__py3-none-any.whl → 0.7.0__py3-none-any.whl

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