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
@@ -10,18 +10,18 @@ legacy hardware, and corrupted or noisy data.
10
10
  - UNKNOWN-005: Reverse Engineering Workflow
11
11
  """
12
12
 
13
- from oscura.exploratory.error_recovery import (
13
+ from oscura.jupyter.exploratory.error_recovery import (
14
14
  ErrorContext,
15
15
  partial_decode,
16
16
  recover_corrupted_data,
17
17
  retry_with_adjustment,
18
18
  )
19
- from oscura.exploratory.fuzzy import (
19
+ from oscura.jupyter.exploratory.fuzzy import (
20
20
  fuzzy_pattern_match,
21
21
  fuzzy_protocol_detect,
22
22
  fuzzy_timing_match,
23
23
  )
24
- from oscura.exploratory.fuzzy_advanced import (
24
+ from oscura.jupyter.exploratory.fuzzy_advanced import (
25
25
  AlignmentResult,
26
26
  PositionAnalysis,
27
27
  VariantCharacterization,
@@ -30,33 +30,33 @@ from oscura.exploratory.fuzzy_advanced import (
30
30
  characterize_variants,
31
31
  compute_conservation_scores,
32
32
  )
33
- from oscura.exploratory.legacy import (
33
+ from oscura.jupyter.exploratory.legacy import (
34
34
  assess_signal_quality,
35
35
  characterize_test_points,
36
36
  cross_correlate_multi_reference,
37
37
  detect_logic_families_multi_channel,
38
38
  )
39
- from oscura.exploratory.parse import (
39
+ from oscura.jupyter.exploratory.parse import (
40
40
  DecodedFrame,
41
41
  ErrorTolerance,
42
42
  TimestampCorrection,
43
43
  correct_timestamp_jitter,
44
44
  decode_with_error_tolerance,
45
45
  )
46
- from oscura.exploratory.recovery import (
46
+ from oscura.jupyter.exploratory.recovery import (
47
47
  ErrorAnalysis,
48
48
  ErrorPattern,
49
49
  analyze_bit_errors,
50
50
  generate_error_visualization_data,
51
51
  )
52
- from oscura.exploratory.sync import (
52
+ from oscura.jupyter.exploratory.sync import (
53
53
  PacketParseResult,
54
54
  RecoveryStrategy,
55
55
  SyncMatch,
56
56
  fuzzy_sync_search,
57
57
  parse_variable_length_packets,
58
58
  )
59
- from oscura.exploratory.unknown import (
59
+ from oscura.jupyter.exploratory.unknown import (
60
60
  analyze_pattern_frequency,
61
61
  characterize_unknown_signal,
62
62
  detect_binary_fields,
@@ -5,7 +5,7 @@ noisy, or incomplete signal data.
5
5
 
6
6
 
7
7
  Example:
8
- >>> from oscura.exploratory.error_recovery import recover_corrupted_data
8
+ >>> from oscura.jupyter.exploratory.error_recovery import recover_corrupted_data
9
9
  >>> recovered, stats = recover_corrupted_data(trace)
10
10
  >>> print(f"Recovered {stats.recovered_samples} samples")
11
11
  """
@@ -83,7 +83,64 @@ def recover_corrupted_data(
83
83
  data = trace.data.copy()
84
84
  n = len(data)
85
85
 
86
- # Detect corrupted samples using statistical outlier detection
86
+ # Detect corrupted samples
87
+ corrupted_mask, corrupted_indices = _detect_corrupted_samples(data, corruption_threshold)
88
+ n_corrupted = len(corrupted_indices)
89
+
90
+ if n_corrupted == 0:
91
+ return trace, RecoveryStats(
92
+ total_samples=n,
93
+ corrupted_samples=0,
94
+ recovered_samples=0,
95
+ unrecoverable_samples=0,
96
+ recovery_method="none",
97
+ confidence=1.0,
98
+ )
99
+
100
+ # Group corrupted samples into contiguous regions (gaps)
101
+ gaps = _find_corruption_gaps(corrupted_indices)
102
+
103
+ # Attempt recovery for each gap
104
+ recovered, unrecoverable = _recover_gaps(
105
+ data, gaps, corrupted_mask, n, recovery_method, max_gap_samples
106
+ )
107
+
108
+ # Create recovered trace
109
+ recovered_trace = WaveformTrace(
110
+ data=data,
111
+ metadata=trace.metadata,
112
+ )
113
+
114
+ # Calculate confidence
115
+ recovery_ratio = recovered / max(n_corrupted, 1)
116
+ gap_sizes = [end - start + 1 for start, end in gaps]
117
+ avg_gap_size = np.mean(gap_sizes) if gap_sizes else 0
118
+ confidence = recovery_ratio * (1 - avg_gap_size / max_gap_samples)
119
+
120
+ confidence_clamped: float = float(max(0.0, min(1.0, confidence)))
121
+ return recovered_trace, RecoveryStats(
122
+ total_samples=n,
123
+ corrupted_samples=n_corrupted,
124
+ recovered_samples=recovered,
125
+ unrecoverable_samples=unrecoverable,
126
+ recovery_method=recovery_method,
127
+ confidence=confidence_clamped,
128
+ )
129
+
130
+
131
+ def _detect_corrupted_samples(
132
+ data: NDArray[np.float64],
133
+ corruption_threshold: float,
134
+ ) -> tuple[NDArray[np.bool_], NDArray[np.int_]]:
135
+ """Detect corrupted samples using statistical outlier detection.
136
+
137
+ Args:
138
+ data: Waveform data array.
139
+ corruption_threshold: Threshold in standard deviations.
140
+
141
+ Returns:
142
+ Tuple of (corrupted_mask, corrupted_indices).
143
+ """
87
144
  # Filter out nan/inf for initial statistics calculation
88
145
  valid_mask = np.isfinite(data)
89
146
  valid_data = data[valid_mask] if np.any(valid_mask) else data
@@ -94,46 +151,70 @@ def recover_corrupted_data(
94
151
  if mad < 1e-10:
95
152
  mad = np.std(valid_data) if len(valid_data) > 0 else 1.0
96
153
 
97
- # Z-score based on MAD
154
+ # Z-score based on MAD (Median Absolute Deviation)
98
155
  z_scores = np.abs(data - median) / (1.4826 * mad + 1e-10)
99
156
 
100
- # Find corrupted samples
157
+ # Find corrupted samples (statistical outliers or invalid values)
101
158
  corrupted_mask = z_scores > corruption_threshold
102
-
103
- # Also detect NaN and Inf
104
159
  corrupted_mask |= np.isnan(data)
105
160
  corrupted_mask |= np.isinf(data)
106
161
 
107
162
  corrupted_indices = np.where(corrupted_mask)[0]
108
- n_corrupted = len(corrupted_indices)
109
163
 
110
- if n_corrupted == 0:
111
- return trace, RecoveryStats(
112
- total_samples=n,
113
- corrupted_samples=0,
114
- recovered_samples=0,
115
- unrecoverable_samples=0,
116
- recovery_method="none",
117
- confidence=1.0,
118
- )
164
+ return corrupted_mask, corrupted_indices
165
+
166
+
167
+ def _find_corruption_gaps(
168
+ corrupted_indices: NDArray[np.int_],
169
+ ) -> list[tuple[int, int]]:
170
+ """Group corrupted indices into contiguous regions (gaps).
171
+
172
+ Args:
173
+ corrupted_indices: Indices of corrupted samples.
174
+
175
+ Returns:
176
+ List of (start, end) tuples for each gap.
177
+ """
178
+ if len(corrupted_indices) == 0:
179
+ return []
119
180
 
120
- # Group corrupted samples into contiguous regions
121
181
  gaps = []
122
- if len(corrupted_indices) > 0:
123
- gap_start = corrupted_indices[0]
124
- gap_end = corrupted_indices[0]
182
+ gap_start = corrupted_indices[0]
183
+ gap_end = corrupted_indices[0]
125
184
 
126
- for idx in corrupted_indices[1:]:
127
- if idx == gap_end + 1:
128
- gap_end = idx
129
- else:
130
- gaps.append((gap_start, gap_end))
131
- gap_start = idx
132
- gap_end = idx
185
+ for idx in corrupted_indices[1:]:
186
+ if idx == gap_end + 1:
187
+ gap_end = idx
188
+ else:
189
+ gaps.append((gap_start, gap_end))
190
+ gap_start = idx
191
+ gap_end = idx
133
192
 
134
- gaps.append((gap_start, gap_end))
193
+ gaps.append((gap_start, gap_end))
194
+ return gaps
135
195
 
136
- # Attempt recovery
196
+
197
+ def _recover_gaps(
198
+ data: NDArray[np.float64],
199
+ gaps: list[tuple[int, int]],
200
+ corrupted_mask: NDArray[np.bool_],
201
+ n: int,
202
+ recovery_method: str,
203
+ max_gap_samples: int,
204
+ ) -> tuple[int, int]:
205
+ """Attempt recovery for each corruption gap.
206
+
207
+ Args:
208
+ data: Waveform data array (modified in place).
209
+ gaps: List of (start, end) tuples for gaps.
210
+ corrupted_mask: Boolean mask of corrupted samples.
211
+ n: Total number of samples.
212
+ recovery_method: Recovery method ('interpolate', 'median', 'zero').
213
+ max_gap_samples: Maximum recoverable gap size.
214
+
215
+ Returns:
216
+ Tuple of (recovered_count, unrecoverable_count).
217
+ """
137
218
  recovered = 0
138
219
  unrecoverable = 0
139
220
 
@@ -145,68 +226,93 @@ def recover_corrupted_data(
145
226
  continue
146
227
 
147
228
  if recovery_method == "interpolate":
148
- # Linear interpolation from surrounding samples
149
- left_idx = max(0, start - 1)
150
- right_idx = min(n - 1, end + 1)
151
-
152
- if left_idx < start and right_idx > end:
153
- # Can interpolate
154
- left_val = data[left_idx]
155
- right_val = data[right_idx]
156
- for i, idx in enumerate(range(start, end + 1)):
157
- t = (i + 1) / (gap_length + 1)
158
- data[idx] = left_val * (1 - t) + right_val * t
159
- recovered += gap_length
160
- else:
161
- # Edge case - use nearest valid value
162
- if left_idx >= start:
163
- data[start : end + 1] = data[right_idx]
164
- else:
165
- data[start : end + 1] = data[left_idx]
166
- recovered += gap_length
167
-
229
+ recovered += _recover_gap_interpolate(data, start, end, gap_length, n)
168
230
  elif recovery_method == "median":
169
- # Replace with local median
170
- window_start = max(0, start - 50)
171
- window_end = min(n, end + 50)
172
- window_data = data[window_start:window_end]
173
- valid_data = window_data[~corrupted_mask[window_start:window_end]]
174
-
175
- if len(valid_data) > 0:
176
- fill_value = np.median(valid_data)
177
- data[start : end + 1] = fill_value
231
+ result = _recover_gap_median(data, corrupted_mask, start, end, gap_length, n)
232
+ if result:
178
233
  recovered += gap_length
179
234
  else:
180
235
  unrecoverable += gap_length
181
-
182
236
  elif recovery_method == "zero":
183
- # Replace with zero
184
237
  data[start : end + 1] = 0
185
238
  recovered += gap_length
186
-
187
239
  else:
188
240
  unrecoverable += gap_length
189
241
 
190
- # Create recovered trace
191
- recovered_trace = WaveformTrace(
192
- data=data,
193
- metadata=trace.metadata,
194
- )
242
+ return recovered, unrecoverable
195
243
 
196
- # Calculate confidence
197
- recovery_ratio = recovered / max(n_corrupted, 1)
198
- gap_sizes = [end - start + 1 for start, end in gaps]
199
- avg_gap_size = np.mean(gap_sizes) if gap_sizes else 0
200
- confidence = recovery_ratio * (1 - avg_gap_size / max_gap_samples)
201
244
 
202
- return recovered_trace, RecoveryStats(
203
- total_samples=n,
204
- corrupted_samples=n_corrupted,
205
- recovered_samples=recovered,
206
- unrecoverable_samples=unrecoverable,
207
- recovery_method=recovery_method,
208
- confidence=max(0.0, min(1.0, confidence)),
209
- )
245
+ def _recover_gap_interpolate(
246
+ data: NDArray[np.float64],
247
+ start: int,
248
+ end: int,
249
+ gap_length: int,
250
+ n: int,
251
+ ) -> int:
252
+ """Recover gap using linear interpolation.
253
+
254
+ Args:
255
+ data: Waveform data array (modified in place).
256
+ start: Gap start index.
257
+ end: Gap end index.
258
+ gap_length: Gap length.
259
+ n: Total number of samples.
260
+
261
+ Returns:
262
+ Number of samples recovered.
263
+ """
264
+ left_idx = max(0, start - 1)
265
+ right_idx = min(n - 1, end + 1)
266
+
267
+ if left_idx < start and right_idx > end:
268
+ # Can interpolate between valid samples
269
+ left_val = data[left_idx]
270
+ right_val = data[right_idx]
271
+ for i, idx in enumerate(range(start, end + 1)):
272
+ t = (i + 1) / (gap_length + 1)
273
+ data[idx] = left_val * (1 - t) + right_val * t
274
+ else:
275
+ # Edge case - use nearest valid value
276
+ if left_idx >= start:
277
+ data[start : end + 1] = data[right_idx]
278
+ else:
279
+ data[start : end + 1] = data[left_idx]
280
+
281
+ return gap_length
282
+
283
+
284
+ def _recover_gap_median(
285
+ data: NDArray[np.float64],
286
+ corrupted_mask: NDArray[np.bool_],
287
+ start: int,
288
+ end: int,
289
+ gap_length: int,
290
+ n: int,
291
+ ) -> bool:
292
+ """Recover gap using local median value.
293
+
294
+ Args:
295
+ data: Waveform data array (modified in place).
296
+ corrupted_mask: Boolean mask of corrupted samples.
297
+ start: Gap start index.
298
+ end: Gap end index.
299
+ gap_length: Gap length.
300
+ n: Total number of samples.
301
+
302
+ Returns:
303
+ True if recovery succeeded, False otherwise.
304
+ """
305
+ window_start = max(0, start - 50)
306
+ window_end = min(n, end + 50)
307
+ window_data = data[window_start:window_end]
308
+ valid_data = window_data[~corrupted_mask[window_start:window_end]]
309
+
310
+ if len(valid_data) > 0:
311
+ fill_value = np.median(valid_data)
312
+ data[start : end + 1] = fill_value
313
+ return True
314
+ else:
315
+ return False
210
316
 
211
317
 
212
318
  @dataclass
@@ -358,73 +464,20 @@ def partial_decode(
358
464
  References:
359
465
  ERROR-003: Partial Decode Support
360
466
  """
361
- data = trace.data
362
- n = len(data)
363
-
364
- complete_packets: list[dict[str, Any]] = []
365
- partial_packets: list[dict[str, Any]] = []
366
- error_regions: list[dict[str, Any]] = []
367
-
368
- total_samples = 0
369
- decoded_samples = 0
370
-
371
- # Try to decode entire trace first
372
- try:
373
- full_result = decode_func(trace)
374
- if full_result:
375
- complete_packets.extend(full_result)
376
- decoded_samples = n
377
- total_samples = n
378
- except Exception as e:
379
- logger.info("Full decode failed, falling back to segment decode: %s", e)
380
- # Fall back to segment-by-segment decode
381
- for start in range(0, n, segment_size):
382
- end = min(start + segment_size, n)
383
- segment_data = data[start:end]
384
-
385
- # Create segment trace
386
- segment_trace = WaveformTrace(
387
- data=segment_data,
388
- metadata=trace.metadata,
389
- )
467
+ n = len(trace.data)
390
468
 
391
- total_samples += len(segment_data)
469
+ # Try full decode first
470
+ full_decode_result = _try_full_decode(trace, decode_func, n)
471
+ if full_decode_result is not None:
472
+ return full_decode_result
392
473
 
393
- try:
394
- segment_result = decode_func(segment_trace)
395
-
396
- if segment_result:
397
- # Adjust timestamps
398
- for packet in segment_result:
399
- if "timestamp" in packet:
400
- packet["timestamp"] += start / trace.metadata.sample_rate
401
- if "sample" in packet:
402
- packet["sample"] += start
403
-
404
- # Check if segment is valid
405
- valid_ratio = len(segment_result) / max(len(segment_data) / 100, 1)
406
-
407
- if valid_ratio >= min_valid_ratio:
408
- complete_packets.extend(segment_result)
409
- decoded_samples += len(segment_data)
410
- else:
411
- partial_packets.extend(segment_result)
412
- decoded_samples += len(segment_data) // 2
413
-
414
- except Exception as e:
415
- logger.debug("Segment decode failed at sample %d: %s", start, e)
416
- error_regions.append(
417
- {
418
- "start_sample": start,
419
- "end_sample": end,
420
- "error": str(e),
421
- }
422
- )
474
+ # Fall back to segment decode
475
+ complete_packets, partial_packets, error_regions, decoded_samples, total_samples = (
476
+ _segment_decode(trace, decode_func, segment_size, min_valid_ratio, n)
477
+ )
423
478
 
424
479
  # Calculate statistics
425
480
  decode_rate = decoded_samples / max(total_samples, 1)
426
-
427
- # Calculate confidence
428
481
  error_ratio = len(error_regions) / max((n // segment_size), 1)
429
482
  confidence = decode_rate * (1 - error_ratio)
430
483
 
@@ -437,6 +490,110 @@ def partial_decode(
437
490
  )
438
491
 
439
492
 
493
+ def _try_full_decode(
494
+ trace: WaveformTrace,
495
+ decode_func: Callable[[WaveformTrace], list[dict[str, Any]]],
496
+ n: int,
497
+ ) -> PartialDecodeResult | None:
498
+ """Try to decode entire trace at once.
499
+
500
+ Args:
501
+ trace: Trace to decode.
502
+ decode_func: Protocol decode function.
503
+ n: Number of samples.
504
+
505
+ Returns:
506
+ PartialDecodeResult if successful, None if failed.
507
+ """
508
+ try:
509
+ full_result = decode_func(trace)
510
+ if full_result:
511
+ return PartialDecodeResult(
512
+ complete_packets=full_result,
513
+ partial_packets=[],
514
+ error_regions=[],
515
+ decode_rate=1.0,
516
+ confidence=1.0,
517
+ )
518
+ except Exception as e:
519
+ logger.info("Full decode failed, falling back to segment decode: %s", e)
520
+
521
+ return None
522
+
523
+
524
+ def _segment_decode(
525
+ trace: WaveformTrace,
526
+ decode_func: Callable[[WaveformTrace], list[dict[str, Any]]],
527
+ segment_size: int,
528
+ min_valid_ratio: float,
529
+ n: int,
530
+ ) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]], int, int]:
531
+ """Decode trace segment by segment.
532
+
533
+ Args:
534
+ trace: Trace to decode.
535
+ decode_func: Protocol decode function.
536
+ segment_size: Size of segments to try independently.
537
+ min_valid_ratio: Minimum valid ratio to accept segment.
538
+ n: Number of samples.
539
+
540
+ Returns:
541
+ Tuple of (complete_packets, partial_packets, error_regions, decoded_samples, total_samples).
542
+ """
543
+ complete_packets: list[dict[str, Any]] = []
544
+ partial_packets: list[dict[str, Any]] = []
545
+ error_regions: list[dict[str, Any]] = []
546
+ total_samples = 0
547
+ decoded_samples = 0
548
+
549
+ for start in range(0, n, segment_size):
550
+ end = min(start + segment_size, n)
551
+ segment_data = trace.data[start:end]
552
+
553
+ segment_trace = WaveformTrace(data=segment_data, metadata=trace.metadata)
554
+ total_samples += len(segment_data)
555
+
556
+ try:
557
+ segment_result = decode_func(segment_trace)
558
+
559
+ if segment_result:
560
+ _adjust_packet_timestamps(segment_result, start, trace.metadata.sample_rate)
561
+
562
+ # Classify segment as complete or partial
563
+ valid_ratio = len(segment_result) / max(len(segment_data) / 100, 1)
564
+ if valid_ratio >= min_valid_ratio:
565
+ complete_packets.extend(segment_result)
566
+ decoded_samples += len(segment_data)
567
+ else:
568
+ partial_packets.extend(segment_result)
569
+ decoded_samples += len(segment_data) // 2
570
+
571
+ except Exception as e:
572
+ logger.debug("Segment decode failed at sample %d: %s", start, e)
573
+ error_regions.append({"start_sample": start, "end_sample": end, "error": str(e)})
574
+
575
+ return complete_packets, partial_packets, error_regions, decoded_samples, total_samples
576
+
577
+
578
+ def _adjust_packet_timestamps(
579
+ packets: list[dict[str, Any]],
580
+ start_sample: int,
581
+ sample_rate: float,
582
+ ) -> None:
583
+ """Adjust packet timestamps to account for segment offset.
584
+
585
+ Args:
586
+ packets: List of decoded packets to adjust.
587
+ start_sample: Start sample of this segment.
588
+ sample_rate: Sample rate in Hz.
589
+ """
590
+ for packet in packets:
591
+ if "timestamp" in packet:
592
+ packet["timestamp"] += start_sample / sample_rate
593
+ if "sample" in packet:
594
+ packet["sample"] += start_sample
595
+
596
+
440
597
  @dataclass
441
598
  class ErrorContext:
442
599
  """Preserved error context for debugging.