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
@@ -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.