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
@@ -79,13 +79,13 @@ def correct_timestamp_jitter(
79
79
  ) -> TimestampCorrection:
80
80
  """Correct timestamp jitter using filtering or PLL model.
81
81
 
82
- : Compensates for clock jitter in logic analyzer
82
+ Compensates for clock jitter in logic analyzer
83
83
  captures (e.g., USB transmission jitter) while preserving phase.
84
84
 
85
85
  Correction constraints (DAQ-003):
86
- - Max correction per sample: ±max_correction_factor × expected_period # noqa: RUF002, RUF003
87
- - Filter cutoff: expected_rate / 10 (removes 10× jitter frequency) # noqa: RUF002, RUF003
88
- - Target reduction: ≥5× for typical USB jitter # noqa: RUF002, RUF003
86
+ - Max correction per sample: ±max_correction_factor x expected_period
87
+ - Filter cutoff: expected_rate / 10 (removes 10x jitter frequency)
88
+ - Target reduction: >=5x for typical USB jitter
89
89
 
90
90
  Args:
91
91
  timestamps: Original jittery timestamps in seconds
@@ -113,109 +113,149 @@ def correct_timestamp_jitter(
113
113
  References:
114
114
  DAQ-003: Timestamp Jitter Compensation and Clock Correction
115
115
  """
116
- if len(timestamps) == 0:
117
- raise ValueError("Timestamps array cannot be empty")
118
-
119
- if expected_rate <= 0:
120
- raise ValueError("expected_rate must be positive")
121
-
122
- if max_correction_factor <= 0:
123
- raise ValueError("max_correction_factor must be positive")
116
+ # Validate inputs
117
+ _validate_jitter_correction_inputs(timestamps, expected_rate, max_correction_factor)
124
118
 
119
+ # Handle edge cases
125
120
  if len(timestamps) < 3:
126
- # Not enough data to filter
127
- return TimestampCorrection(
128
- corrected_timestamps=timestamps.copy(),
129
- original_jitter_rms=0.0,
130
- corrected_jitter_rms=0.0,
131
- reduction_ratio=1.0,
132
- samples_corrected=0,
133
- max_correction=0.0,
134
- )
121
+ return _create_no_correction_result(timestamps)
135
122
 
136
123
  expected_period = 1.0 / expected_rate
137
124
  max_correction = max_correction_factor * expected_period
138
125
 
139
126
  # Calculate original jitter
140
- diffs = np.diff(timestamps)
141
- original_jitter = diffs - expected_period
142
- original_jitter_rms = float(np.sqrt(np.mean(original_jitter**2)))
127
+ original_jitter_rms = _calculate_original_jitter_rms(timestamps, expected_period)
143
128
 
144
- # If jitter is negligible (below 1 ns), no correction needed
145
- # This avoids correcting floating-point rounding errors in perfect timestamps
129
+ # Skip correction if jitter is negligible
146
130
  if original_jitter_rms < 1e-9:
147
- return TimestampCorrection(
148
- corrected_timestamps=timestamps.copy(),
149
- original_jitter_rms=original_jitter_rms,
150
- corrected_jitter_rms=original_jitter_rms,
151
- reduction_ratio=1.0,
152
- samples_corrected=0,
153
- max_correction=0.0,
154
- )
131
+ return _create_negligible_jitter_result(timestamps, original_jitter_rms)
155
132
 
156
- if method == "lowpass":
157
- # Low-pass filter approach
158
- # Design Butterworth filter: cutoff at expected_rate / 10
159
- cutoff_freq = expected_rate / 10.0
160
- nyquist = 0.5 * expected_rate
133
+ # Apply correction method
134
+ corrected = _apply_jitter_correction(
135
+ timestamps, expected_rate, expected_period, max_correction, method
136
+ )
161
137
 
162
- # Ensure cutoff is valid
163
- if cutoff_freq >= nyquist:
164
- cutoff_freq = nyquist * 0.8
138
+ # Build and return result
139
+ return _build_jitter_correction_result(
140
+ timestamps, corrected, expected_period, original_jitter_rms, max_correction
141
+ )
165
142
 
166
- # Design 2nd order Butterworth
167
- sos = signal.butter(2, cutoff_freq / nyquist, btype="low", output="sos")
168
143
 
169
- # Filter the timestamps
170
- # Need to detrend first to avoid edge effects
171
- t_mean = np.mean(timestamps)
172
- t_detrended = timestamps - t_mean
144
+ def _validate_jitter_correction_inputs(
145
+ timestamps: NDArray[np.float64], expected_rate: float, max_correction_factor: float
146
+ ) -> None:
147
+ """Validate jitter correction input parameters."""
148
+ if len(timestamps) == 0:
149
+ raise ValueError("Timestamps array cannot be empty")
150
+ if expected_rate <= 0:
151
+ raise ValueError("expected_rate must be positive")
152
+ if max_correction_factor <= 0:
153
+ raise ValueError("max_correction_factor must be positive")
173
154
 
174
- # Apply filter
175
- filtered = signal.sosfiltfilt(sos, t_detrended)
176
- corrected = filtered + t_mean
177
155
 
178
- else: # pll
179
- # Phase-locked loop model
180
- # Simple PLL: track expected phase and correct deviations
181
- corrected = np.zeros_like(timestamps)
182
- corrected[0] = timestamps[0]
156
+ def _create_no_correction_result(timestamps: NDArray[np.float64]) -> TimestampCorrection:
157
+ """Create result for insufficient data case."""
158
+ return TimestampCorrection(
159
+ corrected_timestamps=timestamps.copy(),
160
+ original_jitter_rms=0.0,
161
+ corrected_jitter_rms=0.0,
162
+ reduction_ratio=1.0,
163
+ samples_corrected=0,
164
+ max_correction=0.0,
165
+ )
183
166
 
184
- # PLL state
185
- phase = 0.0
186
- phase_increment = 2 * np.pi * expected_rate
187
167
 
188
- for i in range(1, len(timestamps)):
189
- # Predict next timestamp based on expected rate
190
- predicted = corrected[i - 1] + expected_period
168
+ def _calculate_original_jitter_rms(
169
+ timestamps: NDArray[np.float64], expected_period: float
170
+ ) -> float:
171
+ """Calculate RMS jitter from timestamps."""
172
+ diffs = np.diff(timestamps)
173
+ original_jitter = diffs - expected_period
174
+ return float(np.sqrt(np.mean(original_jitter**2)))
191
175
 
192
- # Measure phase error
193
- actual = timestamps[i]
194
- error = actual - predicted
195
176
 
196
- # Apply correction with limiting
197
- correction = np.clip(error * 0.5, -max_correction, max_correction)
198
- corrected[i] = predicted + correction
177
+ def _create_negligible_jitter_result(
178
+ timestamps: NDArray[np.float64], original_jitter_rms: float
179
+ ) -> TimestampCorrection:
180
+ """Create result for negligible jitter case."""
181
+ return TimestampCorrection(
182
+ corrected_timestamps=timestamps.copy(),
183
+ original_jitter_rms=original_jitter_rms,
184
+ corrected_jitter_rms=original_jitter_rms,
185
+ reduction_ratio=1.0,
186
+ samples_corrected=0,
187
+ max_correction=0.0,
188
+ )
199
189
 
200
- # Update phase
201
- phase += phase_increment * (corrected[i] - corrected[i - 1])
190
+
191
+ def _apply_jitter_correction(
192
+ timestamps: NDArray[np.float64],
193
+ expected_rate: float,
194
+ expected_period: float,
195
+ max_correction: float,
196
+ method: Literal["lowpass", "pll"],
197
+ ) -> NDArray[np.float64]:
198
+ """Apply jitter correction using selected method."""
199
+ if method == "lowpass":
200
+ corrected = _apply_lowpass_correction(timestamps, expected_rate)
201
+ else: # pll
202
+ corrected = _apply_pll_correction(timestamps, expected_period, max_correction)
202
203
 
203
204
  # Limit corrections to max_correction
204
205
  corrections = corrected - timestamps
205
206
  exceeded = np.abs(corrections) > max_correction
206
207
  corrections[exceeded] = np.sign(corrections[exceeded]) * max_correction
207
- corrected = timestamps + corrections
208
+ return timestamps + corrections
209
+
210
+
211
+ def _apply_lowpass_correction(
212
+ timestamps: NDArray[np.float64], expected_rate: float
213
+ ) -> NDArray[np.float64]:
214
+ """Apply low-pass filter correction."""
215
+ cutoff_freq = expected_rate / 10.0
216
+ nyquist = 0.5 * expected_rate
217
+ if cutoff_freq >= nyquist:
218
+ cutoff_freq = nyquist * 0.8
219
+
220
+ sos = signal.butter(2, cutoff_freq / nyquist, btype="low", output="sos")
221
+ t_mean: np.floating[Any] = np.mean(timestamps)
222
+ t_detrended = timestamps - t_mean
223
+ filtered = signal.sosfiltfilt(sos, t_detrended)
224
+ result: NDArray[np.float64] = np.asarray(filtered + t_mean, dtype=np.float64)
225
+ return result
226
+
227
+
228
+ def _apply_pll_correction(
229
+ timestamps: NDArray[np.float64], expected_period: float, max_correction: float
230
+ ) -> NDArray[np.float64]:
231
+ """Apply phase-locked loop correction."""
232
+ corrected = np.zeros_like(timestamps)
233
+ corrected[0] = timestamps[0]
234
+
235
+ for i in range(1, len(timestamps)):
236
+ predicted = corrected[i - 1] + expected_period
237
+ error = timestamps[i] - predicted
238
+ correction = np.clip(error * 0.5, -max_correction, max_correction)
239
+ corrected[i] = predicted + correction
240
+
241
+ return corrected
208
242
 
209
- # Calculate corrected jitter
243
+
244
+ def _build_jitter_correction_result(
245
+ timestamps: NDArray[np.float64],
246
+ corrected: NDArray[np.float64],
247
+ expected_period: float,
248
+ original_jitter_rms: float,
249
+ max_correction: float,
250
+ ) -> TimestampCorrection:
251
+ """Build final jitter correction result."""
210
252
  corrected_diffs = np.diff(corrected)
211
253
  corrected_jitter = corrected_diffs - expected_period
212
254
  corrected_jitter_rms = float(np.sqrt(np.mean(corrected_jitter**2)))
213
255
 
214
- # Calculate metrics
256
+ corrections = corrected - timestamps
215
257
  samples_corrected = int(np.sum(np.abs(corrections) > 1e-12))
216
258
  max_correction_applied = float(np.max(np.abs(corrections)))
217
-
218
- # original_jitter_rms is always > 0 here (early return handles negligible jitter)
219
259
  reduction_ratio = original_jitter_rms / max(corrected_jitter_rms, 1e-15)
220
260
 
221
261
  return TimestampCorrection(
@@ -228,6 +268,52 @@ def correct_timestamp_jitter(
228
268
  )
229
269
 
230
270
 
271
+ def _decode_uart_tolerant(
272
+ data: NDArray[np.uint8],
273
+ tolerance: ErrorTolerance,
274
+ baud: float,
275
+ ) -> list[DecodedFrame]:
276
+ """Decode UART frames with error tolerance (simplified implementation)."""
277
+ frames: list[DecodedFrame] = []
278
+ pos = 0
279
+
280
+ while pos < len(data):
281
+ try:
282
+ if pos + 1 >= len(data):
283
+ break
284
+
285
+ frame_data = bytes([data[pos]])
286
+ timestamp = float(pos) / baud
287
+
288
+ # Validate frame (simplified - real decoder checks start/stop bits)
289
+ is_valid = data[pos] != 0xFF # Example error condition
290
+ error_type = "framing" if not is_valid else None
291
+
292
+ frames.append(
293
+ DecodedFrame(
294
+ data=frame_data,
295
+ timestamp=timestamp,
296
+ valid=is_valid,
297
+ error_type=error_type,
298
+ position=pos,
299
+ )
300
+ )
301
+
302
+ if not is_valid and tolerance == ErrorTolerance.STRICT:
303
+ break
304
+ elif not is_valid and tolerance == ErrorTolerance.TOLERANT:
305
+ pos += 1 # Skip error frame, resync
306
+ else:
307
+ pos += 1 # Permissive: record error, continue
308
+
309
+ except Exception:
310
+ if tolerance == ErrorTolerance.STRICT:
311
+ raise
312
+ pos += 1
313
+
314
+ return frames
315
+
316
+
231
317
  def decode_with_error_tolerance(
232
318
  data: NDArray[np.uint8],
233
319
  protocol: Literal["uart", "spi", "i2c", "can"],
@@ -279,82 +365,18 @@ def decode_with_error_tolerance(
279
365
  if protocol not in ("uart", "spi", "i2c", "can"):
280
366
  raise ValueError(f"Unsupported protocol: {protocol}")
281
367
 
282
- frames: list[DecodedFrame] = []
283
- pos = 0
284
-
285
368
  # Protocol-specific decode logic
286
369
  # This is a simplified implementation showing the error handling pattern
287
370
  # Full protocol decoders are in oscura.analyzers.protocols
288
371
 
289
372
  if protocol == "uart":
290
- # UART parameters
291
373
  if "baud" not in protocol_params:
292
374
  raise ValueError("UART requires 'baud' parameter")
375
+ return _decode_uart_tolerant(data, tolerance, protocol_params["baud"])
293
376
 
294
- # Simplified UART frame extraction with error tolerance
295
- while pos < len(data):
296
- try:
297
- # Try to decode frame at current position
298
- # This is simplified - real UART decoder would analyze bit timing
299
-
300
- # Check for valid frame (simplified)
301
- if pos + 1 >= len(data):
302
- break
303
-
304
- frame_data = bytes([data[pos]])
305
- timestamp = float(pos) / protocol_params["baud"]
306
-
307
- # Validate frame (simplified - would check start/stop bits)
308
- is_valid = True
309
- error_type = None
310
-
311
- # Example: detect framing error (no proper stop bit)
312
- if data[pos] == 0xFF: # Example error condition
313
- is_valid = False
314
- error_type = "framing"
315
-
316
- frames.append(
317
- DecodedFrame(
318
- data=frame_data,
319
- timestamp=timestamp,
320
- valid=is_valid,
321
- error_type=error_type,
322
- position=pos,
323
- )
324
- )
325
-
326
- if not is_valid and tolerance == ErrorTolerance.STRICT:
327
- # Strict mode: abort on error
328
- break
329
- elif not is_valid and tolerance == ErrorTolerance.TOLERANT:
330
- # Tolerant: skip error frame, resync
331
- # Search for next valid start bit
332
- pos += 1
333
- # In real implementation, would search for start bit pattern
334
- else:
335
- # Permissive: record error, continue
336
- pos += 1
337
-
338
- except Exception:
339
- if tolerance == ErrorTolerance.STRICT:
340
- raise
341
- else:
342
- # Log error and continue
343
- pos += 1
344
-
345
- elif protocol == "spi":
346
- # SPI: Re-align on CS edge
347
- # Simplified placeholder
348
- pass
349
-
350
- elif protocol == "i2c":
351
- # I2C: Search for START condition
352
- # Simplified placeholder
353
- pass
354
-
355
- elif protocol == "can":
356
- # CAN: Wait for SOF after error
357
- # Simplified placeholder
358
- pass
377
+ elif protocol in ("spi", "i2c", "can"):
378
+ # SPI/I2C/CAN: Simplified placeholders
379
+ # Real implementations would have protocol-specific resync logic
380
+ return []
359
381
 
360
- return frames
382
+ return []
@@ -113,102 +113,23 @@ def analyze_bit_errors(
113
113
  References:
114
114
  DAQ-005: Bit Error Pattern Analysis and Capture Diagnostics
115
115
  """
116
- if len(received) != len(expected):
117
- raise ValueError("Received and expected arrays must have same length")
118
-
119
- if len(received) == 0:
120
- raise ValueError("Arrays cannot be empty")
116
+ _validate_inputs(received, expected)
121
117
 
122
- # Find bit errors (XOR)
123
118
  errors = received != expected
124
119
  error_positions = np.where(errors)[0]
125
120
  error_count = len(error_positions)
126
121
  total_bits = len(received)
127
-
128
- # Calculate BER
129
122
  bit_error_rate = error_count / total_bits if total_bits > 0 else 0.0
130
123
 
131
124
  if error_count == 0:
132
- # No errors
133
- return ErrorAnalysis(
134
- bit_error_rate=0.0,
135
- error_count=0,
136
- total_bits=total_bits,
137
- pattern_type=ErrorPattern.RANDOM,
138
- mean_error_gap=float(total_bits),
139
- error_positions=error_positions,
140
- diagnosis="No errors detected - good capture quality",
141
- severity="low",
142
- )
143
-
144
- # Calculate error gaps
145
- if error_count > 1:
146
- error_gaps = np.diff(error_positions)
147
- mean_gap = float(np.mean(error_gaps))
148
- else:
149
- mean_gap = float(total_bits)
150
-
151
- # Classify error pattern
152
- pattern_type = ErrorPattern.UNKNOWN
153
- diagnosis = ""
125
+ return _create_no_errors_analysis(total_bits, error_positions)
154
126
 
155
- # Check for burst pattern (errors clustered)
156
- if error_count > 1 and mean_gap < burst_threshold:
157
- pattern_type = ErrorPattern.BURST
158
- diagnosis = "Burst errors detected - likely USB transmission issue"
159
-
160
- # Check for periodic pattern (FFT analysis)
161
- # Need at least 10 errors for reliable periodicity detection
162
- elif error_count >= 10:
163
- # Create binary error signal
164
- error_signal = errors.astype(float)
165
-
166
- # Compute FFT to detect periodicity
167
- fft = np.fft.rfft(error_signal)
168
- fft_mag = np.abs(fft[1:]) # Skip DC component
169
-
170
- if len(fft_mag) > 0:
171
- # Check if there's a strong peak relative to mean (not just max)
172
- mean_mag = np.mean(fft_mag)
173
- max_mag = np.max(fft_mag)
174
- peak_ratio = max_mag / (mean_mag + 1e-12)
175
-
176
- # Require strong peak (>10x mean) and exceeds threshold
177
- if peak_ratio > 10 and (max_mag / (np.max(fft_mag) + 1e-12)) > periodicity_threshold:
178
- pattern_type = ErrorPattern.PERIODIC
179
- diagnosis = "Periodic errors detected - likely clock jitter or interference"
180
-
181
- # If not burst or periodic, classify as random
182
- if pattern_type == ErrorPattern.UNKNOWN:
183
- # Check if errors are uniformly distributed
184
- if error_count > 2:
185
- # Use coefficient of variation of gaps
186
- if error_count > 1:
187
- gap_std = float(np.std(error_gaps))
188
- gap_cv = gap_std / (mean_gap + 1e-12)
189
-
190
- if gap_cv < 1.0: # Relatively uniform spacing
191
- pattern_type = ErrorPattern.RANDOM
192
- diagnosis = "Random errors detected - likely EMI or noise"
193
- else:
194
- diagnosis = "Mixed error pattern - multiple causes possible"
195
- else:
196
- pattern_type = ErrorPattern.RANDOM
197
- diagnosis = "Single error - insufficient data for classification"
198
- else:
199
- pattern_type = ErrorPattern.RANDOM
200
- diagnosis = "Few errors - likely random EMI or noise"
201
-
202
- # Determine severity based on BER
203
- if bit_error_rate > 0.01:
204
- severity = "severe"
205
- diagnosis += ". SEVERE: Check connections and hardware"
206
- elif bit_error_rate > 0.001:
207
- severity = "moderate"
208
- diagnosis += ". MODERATE: Consider reducing sample rate"
209
- else:
210
- severity = "low"
211
- diagnosis += ". Acceptable error rate"
127
+ mean_gap = _calculate_mean_gap(error_count, error_positions, total_bits)
128
+ pattern_type, diagnosis = _classify_error_pattern(
129
+ errors, error_count, error_positions, mean_gap, burst_threshold, periodicity_threshold
130
+ )
131
+ severity = _determine_severity(bit_error_rate)
132
+ diagnosis = _append_severity_message(diagnosis, severity)
212
133
 
213
134
  return ErrorAnalysis(
214
135
  bit_error_rate=bit_error_rate,
@@ -222,6 +143,117 @@ def analyze_bit_errors(
222
143
  )
223
144
 
224
145
 
146
+ def _validate_inputs(received: NDArray[np.uint8], expected: NDArray[np.uint8]) -> None:
147
+ """Validate input arrays for error analysis."""
148
+ if len(received) != len(expected):
149
+ raise ValueError("Received and expected arrays must have same length")
150
+ if len(received) == 0:
151
+ raise ValueError("Arrays cannot be empty")
152
+
153
+
154
+ def _create_no_errors_analysis(
155
+ total_bits: int, error_positions: NDArray[np.int64]
156
+ ) -> ErrorAnalysis:
157
+ """Create ErrorAnalysis for zero errors case."""
158
+ return ErrorAnalysis(
159
+ bit_error_rate=0.0,
160
+ error_count=0,
161
+ total_bits=total_bits,
162
+ pattern_type=ErrorPattern.RANDOM,
163
+ mean_error_gap=float(total_bits),
164
+ error_positions=error_positions,
165
+ diagnosis="No errors detected - good capture quality",
166
+ severity="low",
167
+ )
168
+
169
+
170
+ def _calculate_mean_gap(
171
+ error_count: int, error_positions: NDArray[np.int64], total_bits: int
172
+ ) -> float:
173
+ """Calculate mean gap between errors."""
174
+ if error_count > 1:
175
+ error_gaps = np.diff(error_positions)
176
+ return float(np.mean(error_gaps))
177
+ return float(total_bits)
178
+
179
+
180
+ def _classify_error_pattern(
181
+ errors: NDArray[np.bool_],
182
+ error_count: int,
183
+ error_positions: NDArray[np.int64],
184
+ mean_gap: float,
185
+ burst_threshold: int,
186
+ periodicity_threshold: float,
187
+ ) -> tuple[ErrorPattern, str]:
188
+ """Classify error pattern type and generate diagnosis."""
189
+ if error_count > 1 and mean_gap < burst_threshold:
190
+ return ErrorPattern.BURST, "Burst errors detected - likely USB transmission issue"
191
+
192
+ if error_count >= 10:
193
+ pattern, diagnosis = _check_periodic_pattern(errors, periodicity_threshold)
194
+ if pattern == ErrorPattern.PERIODIC:
195
+ return pattern, diagnosis
196
+
197
+ return _classify_random_pattern(error_count, error_positions, mean_gap)
198
+
199
+
200
+ def _check_periodic_pattern(
201
+ errors: NDArray[np.bool_], periodicity_threshold: float
202
+ ) -> tuple[ErrorPattern, str]:
203
+ """Check if errors show periodic pattern via FFT."""
204
+ error_signal = errors.astype(float)
205
+ fft = np.fft.rfft(error_signal)
206
+ fft_mag = np.abs(fft[1:])
207
+
208
+ if len(fft_mag) > 0:
209
+ mean_mag = np.mean(fft_mag)
210
+ max_mag = np.max(fft_mag)
211
+ peak_ratio = max_mag / (mean_mag + 1e-12)
212
+
213
+ if peak_ratio > 10 and (max_mag / (np.max(fft_mag) + 1e-12)) > periodicity_threshold:
214
+ return (
215
+ ErrorPattern.PERIODIC,
216
+ "Periodic errors detected - likely clock jitter or interference",
217
+ )
218
+
219
+ return ErrorPattern.UNKNOWN, ""
220
+
221
+
222
+ def _classify_random_pattern(
223
+ error_count: int, error_positions: NDArray[np.int64], mean_gap: float
224
+ ) -> tuple[ErrorPattern, str]:
225
+ """Classify as random pattern with appropriate diagnosis."""
226
+ if error_count <= 2:
227
+ return ErrorPattern.RANDOM, "Few errors - likely random EMI or noise"
228
+
229
+ error_gaps = np.diff(error_positions)
230
+ gap_std = float(np.std(error_gaps))
231
+ gap_cv = gap_std / (mean_gap + 1e-12)
232
+
233
+ if gap_cv < 1.0:
234
+ return ErrorPattern.RANDOM, "Random errors detected - likely EMI or noise"
235
+
236
+ return ErrorPattern.UNKNOWN, "Mixed error pattern - multiple causes possible"
237
+
238
+
239
+ def _determine_severity(bit_error_rate: float) -> str:
240
+ """Determine severity level based on BER."""
241
+ if bit_error_rate > 0.01:
242
+ return "severe"
243
+ if bit_error_rate > 0.001:
244
+ return "moderate"
245
+ return "low"
246
+
247
+
248
+ def _append_severity_message(diagnosis: str, severity: str) -> str:
249
+ """Append severity-specific message to diagnosis."""
250
+ if severity == "severe":
251
+ return diagnosis + ". SEVERE: Check connections and hardware"
252
+ if severity == "moderate":
253
+ return diagnosis + ". MODERATE: Consider reducing sample rate"
254
+ return diagnosis + ". Acceptable error rate"
255
+
256
+
225
257
  def generate_error_visualization_data(
226
258
  analysis: ErrorAnalysis,
227
259
  *,