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
@@ -11,7 +11,7 @@ Author: Oscura Development Team
11
11
  from __future__ import annotations
12
12
 
13
13
  from dataclasses import dataclass, field
14
- from typing import TYPE_CHECKING, Literal
14
+ from typing import TYPE_CHECKING, Any, Literal
15
15
 
16
16
  import numpy as np
17
17
 
@@ -150,8 +150,6 @@ def detect_periods_fft(
150
150
  ) -> list[PeriodResult]:
151
151
  """Detect periods using FFT spectral analysis.
152
152
 
153
- : Periodic Pattern Detection via FFT
154
-
155
153
  Uses power spectral density to identify dominant frequencies and their
156
154
  harmonics. More efficient than autocorrelation for long signals.
157
155
 
@@ -170,42 +168,99 @@ def detect_periods_fft(
170
168
  >>> periods = detect_periods_fft(signal, sample_rate=1000.0, num_peaks=3)
171
169
  """
172
170
  trace = np.asarray(trace).flatten()
173
-
174
171
  if trace.size == 0:
175
172
  return []
176
173
 
177
- # Remove DC component
178
- trace_centered = trace - np.mean(trace)
174
+ power, freqs = _compute_power_spectrum(trace, sample_rate)
175
+ peak_indices = _find_valid_peaks(power, freqs, min_freq, max_freq, num_peaks)
176
+
177
+ if len(peak_indices) == 0:
178
+ return []
179
179
 
180
- # Compute FFT
180
+ return _build_period_results(peak_indices, freqs, power, sample_rate)
181
+
182
+
183
+ def _compute_power_spectrum(
184
+ trace: NDArray[np.float64], sample_rate: float
185
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
186
+ """Compute power spectrum from FFT.
187
+
188
+ Args:
189
+ trace: Input signal array.
190
+ sample_rate: Sampling rate in Hz.
191
+
192
+ Returns:
193
+ Tuple of (power spectrum, frequencies).
194
+ """
195
+ trace_centered = trace - np.mean(trace)
181
196
  n = len(trace_centered)
182
- fft_result = np.fft.rfft(trace_centered)
197
+
198
+ # Apply Hanning window to reduce spectral leakage (improves noise robustness)
199
+ window = np.hanning(n)
200
+ trace_windowed = trace_centered * window
201
+
202
+ fft_result = np.fft.rfft(trace_windowed)
183
203
  power = np.asarray(np.abs(fft_result) ** 2, dtype=np.float64)
184
204
  freqs = np.fft.rfftfreq(n, 1.0 / sample_rate)
205
+ return power, freqs
206
+
207
+
208
+ def _find_valid_peaks(
209
+ power: NDArray[np.float64],
210
+ freqs: NDArray[np.float64],
211
+ min_freq: float | None,
212
+ max_freq: float | None,
213
+ num_peaks: int,
214
+ ) -> NDArray[np.intp]:
215
+ """Find valid spectral peaks within frequency range.
185
216
 
186
- # Apply frequency range filtering
217
+ Args:
218
+ power: Power spectrum array.
219
+ freqs: Frequency array.
220
+ min_freq: Minimum frequency.
221
+ max_freq: Maximum frequency.
222
+ num_peaks: Maximum peaks to return.
223
+
224
+ Returns:
225
+ Array of peak indices sorted by power.
226
+ """
187
227
  valid_mask = np.ones(len(freqs), dtype=bool)
188
228
  if min_freq is not None:
189
229
  valid_mask &= freqs >= min_freq
190
230
  if max_freq is not None:
191
231
  valid_mask &= freqs <= max_freq
232
+ valid_mask[0] = False # Exclude DC
192
233
 
193
- # Exclude DC component
194
- valid_mask[0] = False
195
-
196
- # Find peaks in power spectrum
197
234
  peak_indices = _find_spectral_peaks(power, min_distance=1)
198
235
  peak_indices = peak_indices[valid_mask[peak_indices]]
199
236
 
200
237
  if len(peak_indices) == 0:
201
- return []
238
+ empty_result: NDArray[np.signedinteger[Any]] = peak_indices
239
+ return empty_result
202
240
 
203
- # Sort by power
204
241
  peak_powers = power[peak_indices]
205
242
  sorted_indices = np.argsort(peak_powers)[::-1][:num_peaks]
206
- peak_indices = peak_indices[sorted_indices]
243
+ result: NDArray[np.signedinteger[Any]] = peak_indices[sorted_indices]
244
+ return result
207
245
 
208
- # Build results
246
+
247
+ def _build_period_results(
248
+ peak_indices: NDArray[np.intp],
249
+ freqs: NDArray[np.float64],
250
+ power: NDArray[np.float64],
251
+ sample_rate: float,
252
+ ) -> list[PeriodResult]:
253
+ """Build PeriodResult objects from spectral peaks.
254
+
255
+ Args:
256
+ peak_indices: Array of peak indices.
257
+ freqs: Frequency array.
258
+ power: Power spectrum array.
259
+ sample_rate: Sampling rate in Hz.
260
+
261
+ Returns:
262
+ List of PeriodResult objects.
263
+ """
209
264
  results = []
210
265
  max_power = np.max(power[peak_indices]) if len(peak_indices) > 0 else 1.0
211
266
 
@@ -214,28 +269,14 @@ def detect_periods_fft(
214
269
  if freq == 0:
215
270
  continue
216
271
 
217
- period_seconds = 1.0 / freq
218
- period_samples = sample_rate / freq
219
-
220
- # Confidence based on relative power
221
- confidence = float(power[idx] / max_power)
222
-
223
- # Detect harmonics (simple approach: look for integer multiples)
224
- harmonics = []
225
- for mult in range(2, 6):
226
- harmonic_freq = freq * mult
227
- if harmonic_freq < freqs[-1]:
228
- # Find closest frequency bin
229
- harmonic_idx = np.argmin(np.abs(freqs - harmonic_freq))
230
- if power[harmonic_idx] > 0.1 * power[idx]:
231
- harmonics.append(harmonic_freq)
272
+ harmonics = _detect_harmonics(freq, freqs, power, power[idx])
232
273
 
233
274
  results.append(
234
275
  PeriodResult(
235
- period_samples=period_samples,
236
- period_seconds=period_seconds,
276
+ period_samples=sample_rate / freq,
277
+ period_seconds=1.0 / freq,
237
278
  frequency_hz=freq,
238
- confidence=min(confidence, 1.0),
279
+ confidence=min(float(power[idx] / max_power), 1.0),
239
280
  method="fft",
240
281
  harmonics=harmonics if harmonics else None,
241
282
  )
@@ -244,6 +285,30 @@ def detect_periods_fft(
244
285
  return results
245
286
 
246
287
 
288
+ def _detect_harmonics(
289
+ freq: float, freqs: NDArray[np.float64], power: NDArray[np.float64], base_power: float
290
+ ) -> list[float]:
291
+ """Detect harmonic frequencies.
292
+
293
+ Args:
294
+ freq: Base frequency.
295
+ freqs: Frequency array.
296
+ power: Power spectrum.
297
+ base_power: Power at base frequency.
298
+
299
+ Returns:
300
+ List of harmonic frequencies.
301
+ """
302
+ harmonics = []
303
+ for mult in range(2, 6):
304
+ harmonic_freq = freq * mult
305
+ if harmonic_freq < freqs[-1]:
306
+ harmonic_idx = np.argmin(np.abs(freqs - harmonic_freq))
307
+ if power[harmonic_idx] > 0.1 * base_power:
308
+ harmonics.append(harmonic_freq)
309
+ return harmonics
310
+
311
+
247
312
  def detect_periods_autocorr(
248
313
  trace: NDArray[np.float64],
249
314
  sample_rate: float = 1.0,
@@ -429,6 +494,8 @@ def _find_spectral_peaks(data: NDArray[np.float64], min_distance: int = 1) -> ND
429
494
  """Find peaks in 1D array.
430
495
 
431
496
  Simple peak detection: point is peak if higher than neighbors.
497
+ Includes noise threshold to filter out spurious peaks.
498
+ Handles boundary conditions at edges of the array.
432
499
 
433
500
  Args:
434
501
  data: 1D array
@@ -437,15 +504,31 @@ def _find_spectral_peaks(data: NDArray[np.float64], min_distance: int = 1) -> ND
437
504
  Returns:
438
505
  Array of peak indices
439
506
  """
440
- if len(data) < 3:
507
+ if len(data) < 2:
441
508
  return np.array([], dtype=np.intp)
442
509
 
443
- # Find local maxima
510
+ # Calculate noise threshold (5% of max value to filter noise peaks)
511
+ threshold = 0.05 * np.max(data)
512
+
513
+ # Find local maxima above threshold
444
514
  peaks_list: list[int] = []
515
+
516
+ # Check interior points (only elements with both neighbors)
445
517
  for i in range(1, len(data) - 1):
446
- if data[i] > data[i - 1] and data[i] > data[i + 1]:
518
+ if data[i] > data[i - 1] and data[i] > data[i + 1] and data[i] > threshold:
447
519
  peaks_list.append(i)
448
520
 
521
+ # Check boundary elements (for Nyquist frequency peaks)
522
+ # Only consider boundary as peak if it's a TRUE local maximum (not just monotonic increase)
523
+ # Require it to be significantly higher than neighbor (at least 2x threshold)
524
+ if len(data) >= 3:
525
+ # Check first element
526
+ if data[0] > data[1] and data[0] > max(threshold * 2, data[1] * 1.5):
527
+ peaks_list.insert(0, 0)
528
+ # Check last element (important for Nyquist frequency in FFT)
529
+ if data[-1] > data[-2] and data[-1] > max(threshold * 2, data[-2] * 1.5):
530
+ peaks_list.append(len(data) - 1)
531
+
449
532
  peaks: NDArray[np.intp] = np.array(peaks_list, dtype=np.intp)
450
533
 
451
534
  # Apply minimum distance constraint
@@ -279,6 +279,155 @@ def find_longest_repeat(data: bytes | NDArray[np.uint8]) -> RepeatingSequence |
279
279
  )
280
280
 
281
281
 
282
+ def _extract_substrings(data_bytes: bytes, min_length: int) -> list[tuple[bytes, int]]:
283
+ """Extract all substrings of given length with their positions.
284
+
285
+ Args:
286
+ data_bytes: Input byte string
287
+ min_length: Length of substrings to extract
288
+
289
+ Returns:
290
+ List of (pattern, position) tuples
291
+ """
292
+ n = len(data_bytes)
293
+ substrings = []
294
+ for i in range(n - min_length + 1):
295
+ substrings.append((data_bytes[i : i + min_length], i))
296
+ return substrings
297
+
298
+
299
+ def _build_fuzzy_hash_buckets(
300
+ substrings: list[tuple[bytes, int]], min_length: int
301
+ ) -> dict[tuple[bytes, bytes], list[tuple[bytes, int]]]:
302
+ """Group substrings by fuzzy hash for efficient approximate matching.
303
+
304
+ Uses locality-sensitive hashing: hash of first few bytes + last few bytes.
305
+ Sequences with same prefix/suffix are likely similar.
306
+
307
+ Args:
308
+ substrings: List of (pattern, position) tuples
309
+ min_length: Minimum pattern length
310
+
311
+ Returns:
312
+ Dictionary mapping fuzzy hash to list of patterns
313
+ """
314
+ hash_buckets: dict[tuple[bytes, bytes], list[tuple[bytes, int]]] = defaultdict(list)
315
+ prefix_len = min(3, min_length // 3) # First 3 bytes or ~1/3 of length
316
+ suffix_len = min(3, min_length // 3) # Last 3 bytes
317
+
318
+ for pattern, pos in substrings:
319
+ prefix = pattern[:prefix_len]
320
+ suffix = pattern[-suffix_len:] if len(pattern) > suffix_len else pattern
321
+ fuzzy_hash = (prefix, suffix)
322
+ hash_buckets[fuzzy_hash].append((pattern, pos))
323
+
324
+ return hash_buckets
325
+
326
+
327
+ def _is_pattern_compatible(pattern: bytes, other_pattern: bytes, max_distance: int) -> bool:
328
+ """Check if two patterns can be within edit distance threshold.
329
+
330
+ Args:
331
+ pattern: First pattern
332
+ other_pattern: Second pattern
333
+ max_distance: Maximum allowed edit distance
334
+
335
+ Returns:
336
+ True if patterns might be within threshold (quick check)
337
+ """
338
+ return abs(len(pattern) - len(other_pattern)) <= max_distance
339
+
340
+
341
+ def _try_add_to_cluster(
342
+ pattern: bytes,
343
+ other_pattern: bytes,
344
+ other_pos: int,
345
+ max_distance: int,
346
+ cluster_patterns: list[bytes],
347
+ cluster_positions: list[int],
348
+ ) -> bool:
349
+ """Try to add a pattern to existing cluster if within distance threshold.
350
+
351
+ Args:
352
+ pattern: Representative pattern of cluster
353
+ other_pattern: Pattern to potentially add
354
+ other_pos: Position of other pattern
355
+ max_distance: Maximum edit distance allowed
356
+ cluster_patterns: Current cluster patterns (modified in place)
357
+ cluster_positions: Current cluster positions (modified in place)
358
+
359
+ Returns:
360
+ True if pattern was added to cluster
361
+ """
362
+ if not _is_pattern_compatible(pattern, other_pattern, max_distance):
363
+ return False
364
+
365
+ distance = _edit_distance_optimized(pattern, other_pattern, max_distance)
366
+ if distance <= max_distance:
367
+ cluster_patterns.append(other_pattern)
368
+ cluster_positions.append(other_pos)
369
+ return True
370
+ return False
371
+
372
+
373
+ def _cluster_bucket_patterns(
374
+ bucket_patterns: list[tuple[bytes, int]],
375
+ substrings: list[tuple[bytes, int]],
376
+ max_distance: int,
377
+ min_count: int,
378
+ global_used: set[int],
379
+ ) -> list[tuple[list[bytes], list[int]]]:
380
+ """Cluster patterns within a single hash bucket.
381
+
382
+ Args:
383
+ bucket_patterns: Patterns in this bucket
384
+ substrings: All substrings (for index lookup)
385
+ max_distance: Maximum edit distance
386
+ min_count: Minimum cluster size
387
+ global_used: Set of globally used indices (modified in place)
388
+
389
+ Returns:
390
+ List of (cluster_patterns, cluster_positions) tuples
391
+ """
392
+ clusters = []
393
+ bucket_used: set[int] = set()
394
+
395
+ for i, (pattern, pos) in enumerate(bucket_patterns):
396
+ # Check if already used globally
397
+ actual_idx = substrings.index((pattern, pos))
398
+ if actual_idx in global_used:
399
+ continue
400
+
401
+ # Start new cluster
402
+ cluster_patterns = [pattern]
403
+ cluster_positions = [pos]
404
+ bucket_used.add(i)
405
+ global_used.add(actual_idx)
406
+
407
+ # Compare within same bucket
408
+ for j in range(i + 1, len(bucket_patterns)):
409
+ if j in bucket_used:
410
+ continue
411
+
412
+ other_pattern, other_pos = bucket_patterns[j]
413
+ other_idx = substrings.index((other_pattern, other_pos))
414
+ if other_idx in global_used:
415
+ continue
416
+
417
+ # Try to add to cluster
418
+ if _try_add_to_cluster(
419
+ pattern, other_pattern, other_pos, max_distance, cluster_patterns, cluster_positions
420
+ ):
421
+ bucket_used.add(j)
422
+ global_used.add(other_idx)
423
+
424
+ # Add cluster if large enough
425
+ if len(cluster_patterns) >= min_count:
426
+ clusters.append((cluster_patterns, cluster_positions))
427
+
428
+ return clusters
429
+
430
+
282
431
  @memoize_analysis(maxsize=16)
283
432
  def find_approximate_repeats(
284
433
  data: bytes | NDArray[np.uint8],
@@ -328,98 +477,46 @@ def find_approximate_repeats(
328
477
  if n < min_length:
329
478
  return []
330
479
 
331
- # OPTIMIZATION 1: Extract substrings with numpy for better memory efficiency
332
- substrings = []
333
- for i in range(n - min_length + 1):
334
- substrings.append((data_bytes[i : i + min_length], i))
335
-
336
- # OPTIMIZATION 2: Hash-based pre-grouping
337
- # Group sequences by fuzzy hash to reduce comparisons
338
- # Use a locality-sensitive hash: hash of first few bytes + last few bytes
339
- hash_buckets: dict[tuple[bytes, bytes], list[tuple[bytes, int]]] = defaultdict(list)
340
- prefix_len = min(3, min_length // 3) # First 3 bytes or ~1/3 of length
341
- suffix_len = min(3, min_length // 3) # Last 3 bytes
480
+ # Extract all substrings
481
+ substrings = _extract_substrings(data_bytes, min_length)
342
482
 
343
- for pattern, pos in substrings:
344
- # Create fuzzy hash from prefix and suffix
345
- # Sequences with same prefix/suffix are likely similar
346
- prefix = pattern[:prefix_len]
347
- suffix = pattern[-suffix_len:] if len(pattern) > suffix_len else pattern
348
- fuzzy_hash = (prefix, suffix)
349
- hash_buckets[fuzzy_hash].append((pattern, pos))
483
+ # Group by fuzzy hash to reduce comparisons
484
+ hash_buckets = _build_fuzzy_hash_buckets(substrings, min_length)
350
485
 
351
- # OPTIMIZATION 3: Cluster within hash buckets only
352
- # This reduces O(n²) comparisons to O(k * m²) where k is number of buckets
353
- # and m is average bucket size (m << n)
354
- clusters = []
486
+ # Cluster within hash buckets
487
+ results = []
355
488
  global_used: set[int] = set()
356
489
 
357
490
  for bucket_patterns in hash_buckets.values():
358
491
  # Skip small buckets that can't form clusters
359
492
  if len(bucket_patterns) < min_count:
360
- # Still need to check if they can join other buckets
361
- # For now, skip - could optimize further by cross-bucket matching
362
493
  continue
363
494
 
364
- # Cluster within this bucket
365
- bucket_used: set[int] = set()
495
+ # Cluster patterns in this bucket
496
+ bucket_clusters = _cluster_bucket_patterns(
497
+ bucket_patterns, substrings, max_distance, min_count, global_used
498
+ )
366
499
 
367
- for i, (pattern, pos) in enumerate(bucket_patterns):
368
- # Check if already used globally
369
- actual_idx = substrings.index((pattern, pos))
370
- if actual_idx in global_used:
371
- continue
500
+ # Convert clusters to RepeatingSequence objects
501
+ for cluster_patterns, cluster_positions in bucket_clusters:
502
+ # Use most common pattern as representative
503
+ pattern_counter = Counter(cluster_patterns)
504
+ representative = pattern_counter.most_common(1)[0][0]
372
505
 
373
- # Start new cluster
374
- cluster_patterns = [pattern]
375
- cluster_positions = [pos]
376
- bucket_used.add(i)
377
- global_used.add(actual_idx)
378
-
379
- # OPTIMIZATION 4: Only compare within same bucket
380
- for j in range(i + 1, len(bucket_patterns)):
381
- if j in bucket_used:
382
- continue
383
-
384
- other_pattern, other_pos = bucket_patterns[j]
385
- other_idx = substrings.index((other_pattern, other_pos))
386
- if other_idx in global_used:
387
- continue
388
-
389
- # OPTIMIZATION 5: Early termination with quick checks
390
- # Check if lengths are compatible
391
- if abs(len(pattern) - len(other_pattern)) > max_distance:
392
- continue
393
-
394
- # OPTIMIZATION 6: Use optimized edit distance
395
- distance = _edit_distance_optimized(pattern, other_pattern, max_distance)
396
-
397
- if distance <= max_distance:
398
- cluster_patterns.append(other_pattern)
399
- cluster_positions.append(other_pos)
400
- bucket_used.add(j)
401
- global_used.add(other_idx)
402
-
403
- # Add cluster if large enough
404
- if len(cluster_patterns) >= min_count:
405
- # Use most common pattern as representative
406
- pattern_counter = Counter(cluster_patterns)
407
- representative = pattern_counter.most_common(1)[0][0]
408
-
409
- clusters.append(
410
- RepeatingSequence(
411
- pattern=representative,
412
- length=len(representative),
413
- count=len(cluster_patterns),
414
- positions=sorted(cluster_positions),
415
- frequency=len(cluster_patterns) / (n - min_length + 1),
416
- )
506
+ results.append(
507
+ RepeatingSequence(
508
+ pattern=representative,
509
+ length=len(representative),
510
+ count=len(cluster_patterns),
511
+ positions=sorted(cluster_positions),
512
+ frequency=len(cluster_patterns) / (n - min_length + 1),
417
513
  )
514
+ )
418
515
 
419
516
  # Sort by count (descending)
420
- clusters.sort(key=lambda x: x.count, reverse=True)
517
+ results.sort(key=lambda x: x.count, reverse=True)
421
518
 
422
- return clusters
519
+ return results
423
520
 
424
521
 
425
522
  def _to_bytes(data: bytes | NDArray[np.uint8] | memoryview | bytearray) -> bytes:
@@ -439,7 +536,7 @@ def _to_bytes(data: bytes | NDArray[np.uint8] | memoryview | bytearray) -> bytes
439
536
  elif isinstance(data, bytearray | memoryview):
440
537
  return bytes(data)
441
538
  elif isinstance(data, np.ndarray):
442
- return data.astype(np.uint8).tobytes() # type: ignore[no-any-return]
539
+ return data.astype(np.uint8).tobytes()
443
540
  else:
444
541
  raise TypeError(f"Unsupported data type: {type(data)}")
445
542
 
@@ -108,7 +108,7 @@ def on_resistance(
108
108
 
109
109
  mask = np.abs(i_data) >= min_current
110
110
  if not np.any(mask):
111
- return np.nan # type: ignore[no-any-return]
111
+ return np.nan
112
112
 
113
113
  v_on = v_data[mask]
114
114
  i_on = i_data[mask]
@@ -111,13 +111,13 @@ def soa_analysis(
111
111
  if pulse_width is None:
112
112
  pulse_width = np.inf
113
113
 
114
- applicable_limits = [l for l in limits if l.pulse_width >= pulse_width] # noqa: E741
114
+ applicable_limits = [l for l in limits if l.pulse_width >= pulse_width]
115
115
  if not applicable_limits:
116
116
  applicable_limits = limits # Use all if none match
117
117
 
118
118
  # Build SOA boundary (interpolate between limit points)
119
119
  # Sort by voltage
120
- sorted_limits = sorted(applicable_limits, key=lambda l: l.v_max) # noqa: E741
120
+ sorted_limits = sorted(applicable_limits, key=lambda l: l.v_max)
121
121
 
122
122
  violations: list[SOAViolation] = []
123
123
  margins: list[float] = []
@@ -171,7 +171,7 @@ def _interpolate_soa_limit(voltage: float, limits: list[SOALimit]) -> float:
171
171
  Interpolated current limit in Amps
172
172
  """
173
173
  if len(limits) == 0:
174
- return np.inf # type: ignore[no-any-return]
174
+ return np.inf
175
175
 
176
176
  if len(limits) == 1:
177
177
  if voltage <= limits[0].v_max:
@@ -259,9 +259,9 @@ def plot_soa(
259
259
  fig, ax = plt.subplots(figsize=figsize)
260
260
 
261
261
  # Plot SOA boundary
262
- sorted_limits = sorted(limits, key=lambda l: l.v_max) # noqa: E741
263
- v_boundary = [l.v_max for l in sorted_limits] # noqa: E741
264
- i_boundary = [l.i_max for l in sorted_limits] # noqa: E741
262
+ sorted_limits = sorted(limits, key=lambda l: l.v_max)
263
+ v_boundary = [l.v_max for l in sorted_limits]
264
+ i_boundary = [l.i_max for l in sorted_limits]
265
265
 
266
266
  # Add corner points for closed boundary
267
267
  v_boundary = [0, *v_boundary, v_boundary[-1], 0]