oscura 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (513) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/__init__.py +0 -48
  5. oscura/analyzers/digital/edges.py +325 -65
  6. oscura/analyzers/digital/extraction.py +0 -195
  7. oscura/analyzers/digital/quality.py +293 -166
  8. oscura/analyzers/digital/timing.py +260 -115
  9. oscura/analyzers/digital/timing_numba.py +334 -0
  10. oscura/analyzers/entropy.py +605 -0
  11. oscura/analyzers/eye/diagram.py +176 -109
  12. oscura/analyzers/eye/metrics.py +5 -5
  13. oscura/analyzers/jitter/__init__.py +6 -4
  14. oscura/analyzers/jitter/ber.py +52 -52
  15. oscura/analyzers/jitter/classification.py +156 -0
  16. oscura/analyzers/jitter/decomposition.py +163 -113
  17. oscura/analyzers/jitter/spectrum.py +80 -64
  18. oscura/analyzers/ml/__init__.py +39 -0
  19. oscura/analyzers/ml/features.py +600 -0
  20. oscura/analyzers/ml/signal_classifier.py +604 -0
  21. oscura/analyzers/packet/daq.py +246 -158
  22. oscura/analyzers/packet/parser.py +12 -1
  23. oscura/analyzers/packet/payload.py +50 -2110
  24. oscura/analyzers/packet/payload_analysis.py +361 -181
  25. oscura/analyzers/packet/payload_patterns.py +133 -70
  26. oscura/analyzers/packet/stream.py +84 -23
  27. oscura/analyzers/patterns/__init__.py +26 -5
  28. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  29. oscura/analyzers/patterns/clustering.py +169 -108
  30. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  31. oscura/analyzers/patterns/discovery.py +1 -1
  32. oscura/analyzers/patterns/matching.py +581 -197
  33. oscura/analyzers/patterns/pattern_mining.py +778 -0
  34. oscura/analyzers/patterns/periodic.py +121 -38
  35. oscura/analyzers/patterns/sequences.py +175 -78
  36. oscura/analyzers/power/conduction.py +1 -1
  37. oscura/analyzers/power/soa.py +6 -6
  38. oscura/analyzers/power/switching.py +250 -110
  39. oscura/analyzers/protocol/__init__.py +17 -1
  40. oscura/analyzers/protocols/__init__.py +1 -22
  41. oscura/analyzers/protocols/base.py +6 -6
  42. oscura/analyzers/protocols/ble/__init__.py +38 -0
  43. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  44. oscura/analyzers/protocols/ble/uuids.py +288 -0
  45. oscura/analyzers/protocols/can.py +257 -127
  46. oscura/analyzers/protocols/can_fd.py +107 -80
  47. oscura/analyzers/protocols/flexray.py +139 -80
  48. oscura/analyzers/protocols/hdlc.py +93 -58
  49. oscura/analyzers/protocols/i2c.py +247 -106
  50. oscura/analyzers/protocols/i2s.py +138 -86
  51. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  52. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  53. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  54. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  55. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  56. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  57. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  58. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  59. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  60. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  61. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  62. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  63. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  64. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  65. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  66. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  67. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  68. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  69. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  70. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  71. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  72. oscura/analyzers/protocols/jtag.py +180 -98
  73. oscura/analyzers/protocols/lin.py +219 -114
  74. oscura/analyzers/protocols/manchester.py +4 -4
  75. oscura/analyzers/protocols/onewire.py +253 -149
  76. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  77. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  78. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  79. oscura/analyzers/protocols/spi.py +192 -95
  80. oscura/analyzers/protocols/swd.py +321 -167
  81. oscura/analyzers/protocols/uart.py +267 -125
  82. oscura/analyzers/protocols/usb.py +235 -131
  83. oscura/analyzers/side_channel/power.py +17 -12
  84. oscura/analyzers/signal/__init__.py +15 -0
  85. oscura/analyzers/signal/timing_analysis.py +1086 -0
  86. oscura/analyzers/signal_integrity/__init__.py +4 -1
  87. oscura/analyzers/signal_integrity/sparams.py +2 -19
  88. oscura/analyzers/spectral/chunked.py +129 -60
  89. oscura/analyzers/spectral/chunked_fft.py +300 -94
  90. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  91. oscura/analyzers/statistical/checksum.py +376 -217
  92. oscura/analyzers/statistical/classification.py +229 -107
  93. oscura/analyzers/statistical/entropy.py +78 -53
  94. oscura/analyzers/statistics/correlation.py +407 -211
  95. oscura/analyzers/statistics/outliers.py +2 -2
  96. oscura/analyzers/statistics/streaming.py +30 -5
  97. oscura/analyzers/validation.py +216 -101
  98. oscura/analyzers/waveform/measurements.py +9 -0
  99. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  100. oscura/analyzers/waveform/spectral.py +500 -228
  101. oscura/api/__init__.py +31 -5
  102. oscura/api/dsl/__init__.py +582 -0
  103. oscura/{dsl → api/dsl}/commands.py +43 -76
  104. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  105. oscura/{dsl → api/dsl}/parser.py +107 -77
  106. oscura/{dsl → api/dsl}/repl.py +2 -2
  107. oscura/api/dsl.py +1 -1
  108. oscura/{integrations → api/integrations}/__init__.py +1 -1
  109. oscura/{integrations → api/integrations}/llm.py +201 -102
  110. oscura/api/operators.py +3 -3
  111. oscura/api/optimization.py +144 -30
  112. oscura/api/rest_server.py +921 -0
  113. oscura/api/server/__init__.py +17 -0
  114. oscura/api/server/dashboard.py +850 -0
  115. oscura/api/server/static/README.md +34 -0
  116. oscura/api/server/templates/base.html +181 -0
  117. oscura/api/server/templates/export.html +120 -0
  118. oscura/api/server/templates/home.html +284 -0
  119. oscura/api/server/templates/protocols.html +58 -0
  120. oscura/api/server/templates/reports.html +43 -0
  121. oscura/api/server/templates/session_detail.html +89 -0
  122. oscura/api/server/templates/sessions.html +83 -0
  123. oscura/api/server/templates/waveforms.html +73 -0
  124. oscura/automotive/__init__.py +8 -1
  125. oscura/automotive/can/__init__.py +10 -0
  126. oscura/automotive/can/checksum.py +3 -1
  127. oscura/automotive/can/dbc_generator.py +590 -0
  128. oscura/automotive/can/message_wrapper.py +121 -74
  129. oscura/automotive/can/patterns.py +98 -21
  130. oscura/automotive/can/session.py +292 -56
  131. oscura/automotive/can/state_machine.py +6 -3
  132. oscura/automotive/can/stimulus_response.py +97 -75
  133. oscura/automotive/dbc/__init__.py +10 -2
  134. oscura/automotive/dbc/generator.py +84 -56
  135. oscura/automotive/dbc/parser.py +6 -6
  136. oscura/automotive/dtc/data.json +2763 -0
  137. oscura/automotive/dtc/database.py +2 -2
  138. oscura/automotive/flexray/__init__.py +31 -0
  139. oscura/automotive/flexray/analyzer.py +504 -0
  140. oscura/automotive/flexray/crc.py +185 -0
  141. oscura/automotive/flexray/fibex.py +449 -0
  142. oscura/automotive/j1939/__init__.py +45 -8
  143. oscura/automotive/j1939/analyzer.py +605 -0
  144. oscura/automotive/j1939/spns.py +326 -0
  145. oscura/automotive/j1939/transport.py +306 -0
  146. oscura/automotive/lin/__init__.py +47 -0
  147. oscura/automotive/lin/analyzer.py +612 -0
  148. oscura/automotive/loaders/blf.py +13 -2
  149. oscura/automotive/loaders/csv_can.py +143 -72
  150. oscura/automotive/loaders/dispatcher.py +50 -2
  151. oscura/automotive/loaders/mdf.py +86 -45
  152. oscura/automotive/loaders/pcap.py +111 -61
  153. oscura/automotive/uds/__init__.py +4 -0
  154. oscura/automotive/uds/analyzer.py +725 -0
  155. oscura/automotive/uds/decoder.py +140 -58
  156. oscura/automotive/uds/models.py +7 -1
  157. oscura/automotive/visualization.py +1 -1
  158. oscura/cli/analyze.py +348 -0
  159. oscura/cli/batch.py +142 -122
  160. oscura/cli/benchmark.py +275 -0
  161. oscura/cli/characterize.py +137 -82
  162. oscura/cli/compare.py +224 -131
  163. oscura/cli/completion.py +250 -0
  164. oscura/cli/config_cmd.py +361 -0
  165. oscura/cli/decode.py +164 -87
  166. oscura/cli/export.py +286 -0
  167. oscura/cli/main.py +115 -31
  168. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  169. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  170. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  171. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  172. oscura/cli/progress.py +147 -0
  173. oscura/cli/shell.py +157 -135
  174. oscura/cli/validate_cmd.py +204 -0
  175. oscura/cli/visualize.py +158 -0
  176. oscura/convenience.py +125 -79
  177. oscura/core/__init__.py +4 -2
  178. oscura/core/backend_selector.py +3 -3
  179. oscura/core/cache.py +126 -15
  180. oscura/core/cancellation.py +1 -1
  181. oscura/{config → core/config}/__init__.py +20 -11
  182. oscura/{config → core/config}/defaults.py +1 -1
  183. oscura/{config → core/config}/loader.py +7 -5
  184. oscura/{config → core/config}/memory.py +5 -5
  185. oscura/{config → core/config}/migration.py +1 -1
  186. oscura/{config → core/config}/pipeline.py +99 -23
  187. oscura/{config → core/config}/preferences.py +1 -1
  188. oscura/{config → core/config}/protocol.py +3 -3
  189. oscura/{config → core/config}/schema.py +426 -272
  190. oscura/{config → core/config}/settings.py +1 -1
  191. oscura/{config → core/config}/thresholds.py +195 -153
  192. oscura/core/correlation.py +5 -6
  193. oscura/core/cross_domain.py +0 -2
  194. oscura/core/debug.py +9 -5
  195. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  196. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  197. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  198. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  199. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  200. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  201. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  202. oscura/core/gpu_backend.py +11 -7
  203. oscura/core/log_query.py +101 -11
  204. oscura/core/logging.py +126 -54
  205. oscura/core/logging_advanced.py +5 -5
  206. oscura/core/memory_limits.py +108 -70
  207. oscura/core/memory_monitor.py +2 -2
  208. oscura/core/memory_progress.py +7 -7
  209. oscura/core/memory_warnings.py +1 -1
  210. oscura/core/numba_backend.py +13 -13
  211. oscura/{plugins → core/plugins}/__init__.py +9 -9
  212. oscura/{plugins → core/plugins}/base.py +7 -7
  213. oscura/{plugins → core/plugins}/cli.py +3 -3
  214. oscura/{plugins → core/plugins}/discovery.py +186 -106
  215. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  216. oscura/{plugins → core/plugins}/manager.py +7 -7
  217. oscura/{plugins → core/plugins}/registry.py +3 -3
  218. oscura/{plugins → core/plugins}/versioning.py +1 -1
  219. oscura/core/progress.py +16 -1
  220. oscura/core/provenance.py +8 -2
  221. oscura/{schemas → core/schemas}/__init__.py +2 -2
  222. oscura/core/schemas/bus_configuration.json +322 -0
  223. oscura/core/schemas/device_mapping.json +182 -0
  224. oscura/core/schemas/packet_format.json +418 -0
  225. oscura/core/schemas/protocol_definition.json +363 -0
  226. oscura/core/types.py +4 -0
  227. oscura/core/uncertainty.py +3 -3
  228. oscura/correlation/__init__.py +52 -0
  229. oscura/correlation/multi_protocol.py +811 -0
  230. oscura/discovery/auto_decoder.py +117 -35
  231. oscura/discovery/comparison.py +191 -86
  232. oscura/discovery/quality_validator.py +155 -68
  233. oscura/discovery/signal_detector.py +196 -79
  234. oscura/export/__init__.py +18 -20
  235. oscura/export/kaitai_struct.py +513 -0
  236. oscura/export/scapy_layer.py +801 -0
  237. oscura/export/wireshark/README.md +15 -15
  238. oscura/export/wireshark/generator.py +1 -1
  239. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  240. oscura/export/wireshark_dissector.py +746 -0
  241. oscura/guidance/wizard.py +207 -111
  242. oscura/hardware/__init__.py +19 -0
  243. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  244. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  245. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  246. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  247. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  248. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  249. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  250. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  251. oscura/hardware/firmware/__init__.py +29 -0
  252. oscura/hardware/firmware/pattern_recognition.py +874 -0
  253. oscura/hardware/hal_detector.py +736 -0
  254. oscura/hardware/security/__init__.py +37 -0
  255. oscura/hardware/security/side_channel_detector.py +1126 -0
  256. oscura/inference/__init__.py +4 -0
  257. oscura/inference/active_learning/README.md +7 -7
  258. oscura/inference/active_learning/observation_table.py +4 -1
  259. oscura/inference/alignment.py +216 -123
  260. oscura/inference/bayesian.py +113 -33
  261. oscura/inference/crc_reverse.py +101 -55
  262. oscura/inference/logic.py +6 -2
  263. oscura/inference/message_format.py +342 -183
  264. oscura/inference/protocol.py +95 -44
  265. oscura/inference/protocol_dsl.py +180 -82
  266. oscura/inference/signal_intelligence.py +1439 -706
  267. oscura/inference/spectral.py +99 -57
  268. oscura/inference/state_machine.py +810 -158
  269. oscura/inference/stream.py +270 -110
  270. oscura/iot/__init__.py +34 -0
  271. oscura/iot/coap/__init__.py +32 -0
  272. oscura/iot/coap/analyzer.py +668 -0
  273. oscura/iot/coap/options.py +212 -0
  274. oscura/iot/lorawan/__init__.py +21 -0
  275. oscura/iot/lorawan/crypto.py +206 -0
  276. oscura/iot/lorawan/decoder.py +801 -0
  277. oscura/iot/lorawan/mac_commands.py +341 -0
  278. oscura/iot/mqtt/__init__.py +27 -0
  279. oscura/iot/mqtt/analyzer.py +999 -0
  280. oscura/iot/mqtt/properties.py +315 -0
  281. oscura/iot/zigbee/__init__.py +31 -0
  282. oscura/iot/zigbee/analyzer.py +615 -0
  283. oscura/iot/zigbee/security.py +153 -0
  284. oscura/iot/zigbee/zcl.py +349 -0
  285. oscura/jupyter/display.py +125 -45
  286. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  287. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  288. oscura/jupyter/exploratory/fuzzy.py +746 -0
  289. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  290. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  291. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  292. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  293. oscura/jupyter/exploratory/sync.py +612 -0
  294. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  295. oscura/jupyter/magic.py +4 -4
  296. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  297. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  298. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  299. oscura/loaders/__init__.py +171 -63
  300. oscura/loaders/binary.py +88 -1
  301. oscura/loaders/chipwhisperer.py +153 -137
  302. oscura/loaders/configurable.py +208 -86
  303. oscura/loaders/csv_loader.py +458 -215
  304. oscura/loaders/hdf5_loader.py +278 -119
  305. oscura/loaders/lazy.py +87 -54
  306. oscura/loaders/mmap_loader.py +1 -1
  307. oscura/loaders/numpy_loader.py +253 -116
  308. oscura/loaders/pcap.py +226 -151
  309. oscura/loaders/rigol.py +110 -49
  310. oscura/loaders/sigrok.py +201 -78
  311. oscura/loaders/tdms.py +81 -58
  312. oscura/loaders/tektronix.py +291 -174
  313. oscura/loaders/touchstone.py +182 -87
  314. oscura/loaders/vcd.py +215 -117
  315. oscura/loaders/wav.py +155 -68
  316. oscura/reporting/__init__.py +9 -7
  317. oscura/reporting/analyze.py +352 -146
  318. oscura/reporting/argument_preparer.py +69 -14
  319. oscura/reporting/auto_report.py +97 -61
  320. oscura/reporting/batch.py +131 -58
  321. oscura/reporting/chart_selection.py +57 -45
  322. oscura/reporting/comparison.py +63 -17
  323. oscura/reporting/content/executive.py +76 -24
  324. oscura/reporting/core_formats/multi_format.py +11 -8
  325. oscura/reporting/engine.py +312 -158
  326. oscura/reporting/enhanced_reports.py +949 -0
  327. oscura/reporting/export.py +86 -43
  328. oscura/reporting/formatting/numbers.py +69 -42
  329. oscura/reporting/html.py +139 -58
  330. oscura/reporting/index.py +137 -65
  331. oscura/reporting/output.py +158 -67
  332. oscura/reporting/pdf.py +67 -102
  333. oscura/reporting/plots.py +191 -112
  334. oscura/reporting/sections.py +88 -47
  335. oscura/reporting/standards.py +104 -61
  336. oscura/reporting/summary_generator.py +75 -55
  337. oscura/reporting/tables.py +138 -54
  338. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  339. oscura/reporting/templates/index.md +13 -13
  340. oscura/sessions/__init__.py +14 -23
  341. oscura/sessions/base.py +3 -3
  342. oscura/sessions/blackbox.py +106 -10
  343. oscura/sessions/generic.py +2 -2
  344. oscura/sessions/legacy.py +783 -0
  345. oscura/side_channel/__init__.py +63 -0
  346. oscura/side_channel/dpa.py +1025 -0
  347. oscura/utils/__init__.py +15 -1
  348. oscura/utils/autodetect.py +1 -5
  349. oscura/utils/bitwise.py +118 -0
  350. oscura/{builders → utils/builders}/__init__.py +1 -1
  351. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  352. oscura/{comparison → utils/comparison}/compare.py +202 -101
  353. oscura/{comparison → utils/comparison}/golden.py +83 -63
  354. oscura/{comparison → utils/comparison}/limits.py +313 -89
  355. oscura/{comparison → utils/comparison}/mask.py +151 -45
  356. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  357. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  358. oscura/{component → utils/component}/__init__.py +3 -3
  359. oscura/{component → utils/component}/impedance.py +122 -58
  360. oscura/{component → utils/component}/reactive.py +165 -168
  361. oscura/{component → utils/component}/transmission_line.py +3 -3
  362. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  363. oscura/{filtering → utils/filtering}/base.py +1 -1
  364. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  365. oscura/{filtering → utils/filtering}/design.py +169 -93
  366. oscura/{filtering → utils/filtering}/filters.py +2 -2
  367. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  368. oscura/utils/geometry.py +31 -0
  369. oscura/utils/imports.py +184 -0
  370. oscura/utils/lazy.py +1 -1
  371. oscura/{math → utils/math}/__init__.py +2 -2
  372. oscura/{math → utils/math}/arithmetic.py +114 -48
  373. oscura/{math → utils/math}/interpolation.py +139 -106
  374. oscura/utils/memory.py +129 -66
  375. oscura/utils/memory_advanced.py +92 -9
  376. oscura/utils/memory_extensions.py +10 -8
  377. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  378. oscura/{optimization → utils/optimization}/search.py +2 -2
  379. oscura/utils/performance/__init__.py +58 -0
  380. oscura/utils/performance/caching.py +889 -0
  381. oscura/utils/performance/lsh_clustering.py +333 -0
  382. oscura/utils/performance/memory_optimizer.py +699 -0
  383. oscura/utils/performance/optimizations.py +675 -0
  384. oscura/utils/performance/parallel.py +654 -0
  385. oscura/utils/performance/profiling.py +661 -0
  386. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  387. oscura/{pipeline → utils/pipeline}/composition.py +11 -3
  388. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  389. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  390. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  391. oscura/{search → utils/search}/__init__.py +3 -3
  392. oscura/{search → utils/search}/anomaly.py +188 -58
  393. oscura/utils/search/context.py +294 -0
  394. oscura/{search → utils/search}/pattern.py +138 -10
  395. oscura/utils/serial.py +51 -0
  396. oscura/utils/storage/__init__.py +61 -0
  397. oscura/utils/storage/database.py +1166 -0
  398. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  399. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  400. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  401. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  402. oscura/{triggering → utils/triggering}/base.py +6 -6
  403. oscura/{triggering → utils/triggering}/edge.py +2 -2
  404. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  405. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  406. oscura/{triggering → utils/triggering}/window.py +2 -2
  407. oscura/utils/validation.py +32 -0
  408. oscura/validation/__init__.py +121 -0
  409. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  410. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  411. oscura/{compliance → validation/compliance}/masks.py +1 -1
  412. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  413. oscura/{compliance → validation/compliance}/testing.py +114 -52
  414. oscura/validation/compliance_tests.py +915 -0
  415. oscura/validation/fuzzer.py +990 -0
  416. oscura/validation/grammar_tests.py +596 -0
  417. oscura/validation/grammar_validator.py +904 -0
  418. oscura/validation/hil_testing.py +977 -0
  419. oscura/{quality → validation/quality}/__init__.py +4 -4
  420. oscura/{quality → validation/quality}/ensemble.py +251 -171
  421. oscura/{quality → validation/quality}/explainer.py +3 -3
  422. oscura/{quality → validation/quality}/scoring.py +1 -1
  423. oscura/{quality → validation/quality}/warnings.py +4 -4
  424. oscura/validation/regression_suite.py +808 -0
  425. oscura/validation/replay.py +788 -0
  426. oscura/{testing → validation/testing}/__init__.py +2 -2
  427. oscura/{testing → validation/testing}/synthetic.py +5 -5
  428. oscura/visualization/__init__.py +9 -0
  429. oscura/visualization/accessibility.py +1 -1
  430. oscura/visualization/annotations.py +64 -67
  431. oscura/visualization/colors.py +7 -7
  432. oscura/visualization/digital.py +180 -81
  433. oscura/visualization/eye.py +236 -85
  434. oscura/visualization/interactive.py +320 -143
  435. oscura/visualization/jitter.py +587 -247
  436. oscura/visualization/layout.py +169 -134
  437. oscura/visualization/optimization.py +103 -52
  438. oscura/visualization/palettes.py +1 -1
  439. oscura/visualization/power.py +427 -211
  440. oscura/visualization/power_extended.py +626 -297
  441. oscura/visualization/presets.py +2 -0
  442. oscura/visualization/protocols.py +495 -181
  443. oscura/visualization/render.py +79 -63
  444. oscura/visualization/reverse_engineering.py +171 -124
  445. oscura/visualization/signal_integrity.py +460 -279
  446. oscura/visualization/specialized.py +190 -100
  447. oscura/visualization/spectral.py +670 -255
  448. oscura/visualization/thumbnails.py +166 -137
  449. oscura/visualization/waveform.py +150 -63
  450. oscura/workflows/__init__.py +3 -0
  451. oscura/{batch → workflows/batch}/__init__.py +5 -5
  452. oscura/{batch → workflows/batch}/advanced.py +150 -75
  453. oscura/workflows/batch/aggregate.py +531 -0
  454. oscura/workflows/batch/analyze.py +236 -0
  455. oscura/{batch → workflows/batch}/logging.py +2 -2
  456. oscura/{batch → workflows/batch}/metrics.py +1 -1
  457. oscura/workflows/complete_re.py +1144 -0
  458. oscura/workflows/compliance.py +44 -54
  459. oscura/workflows/digital.py +197 -51
  460. oscura/workflows/legacy/__init__.py +12 -0
  461. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  462. oscura/workflows/multi_trace.py +9 -9
  463. oscura/workflows/power.py +42 -62
  464. oscura/workflows/protocol.py +82 -49
  465. oscura/workflows/reverse_engineering.py +351 -150
  466. oscura/workflows/signal_integrity.py +157 -82
  467. oscura-0.6.0.dist-info/METADATA +643 -0
  468. oscura-0.6.0.dist-info/RECORD +590 -0
  469. oscura/analyzers/digital/ic_database.py +0 -498
  470. oscura/analyzers/digital/timing_paths.py +0 -339
  471. oscura/analyzers/digital/vintage.py +0 -377
  472. oscura/analyzers/digital/vintage_result.py +0 -148
  473. oscura/analyzers/protocols/parallel_bus.py +0 -449
  474. oscura/batch/aggregate.py +0 -300
  475. oscura/batch/analyze.py +0 -139
  476. oscura/dsl/__init__.py +0 -73
  477. oscura/exceptions.py +0 -59
  478. oscura/exploratory/fuzzy.py +0 -513
  479. oscura/exploratory/sync.py +0 -384
  480. oscura/export/wavedrom.py +0 -430
  481. oscura/exporters/__init__.py +0 -94
  482. oscura/exporters/csv.py +0 -303
  483. oscura/exporters/exporters.py +0 -44
  484. oscura/exporters/hdf5.py +0 -217
  485. oscura/exporters/html_export.py +0 -701
  486. oscura/exporters/json_export.py +0 -338
  487. oscura/exporters/markdown_export.py +0 -367
  488. oscura/exporters/matlab_export.py +0 -354
  489. oscura/exporters/npz_export.py +0 -219
  490. oscura/exporters/spice_export.py +0 -210
  491. oscura/exporters/vintage_logic_csv.py +0 -247
  492. oscura/reporting/vintage_logic_report.py +0 -523
  493. oscura/search/context.py +0 -149
  494. oscura/session/__init__.py +0 -34
  495. oscura/session/annotations.py +0 -289
  496. oscura/session/history.py +0 -313
  497. oscura/session/session.py +0 -520
  498. oscura/visualization/digital_advanced.py +0 -718
  499. oscura/visualization/figure_manager.py +0 -156
  500. oscura/workflow/__init__.py +0 -13
  501. oscura-0.5.0.dist-info/METADATA +0 -407
  502. oscura-0.5.0.dist-info/RECORD +0 -486
  503. /oscura/core/{config.py → config/legacy.py} +0 -0
  504. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  505. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  506. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  507. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  508. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  509. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  510. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  511. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
  512. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
  513. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -123,6 +123,8 @@ from oscura.inference.spectral import auto_spectral_config
123
123
  from oscura.inference.state_machine import (
124
124
  FiniteAutomaton,
125
125
  State,
126
+ StateMachine,
127
+ StateMachineExtractor,
126
128
  StateMachineInferrer,
127
129
  Transition,
128
130
  infer_rpni,
@@ -196,6 +198,8 @@ __all__ = [
196
198
  "SequentialBayesian",
197
199
  "SimulatorTeacher",
198
200
  "State",
201
+ "StateMachine",
202
+ "StateMachineExtractor",
199
203
  "StateMachineInferrer",
200
204
  "StreamSegment",
201
205
  "TCPStreamReassembler",
@@ -140,13 +140,13 @@ Information and Computation, 75(2), 87-106.
140
140
 
141
141
  ## Comparison with RPNI
142
142
 
143
- |Feature|L\* (Active)|RPNI (Passive)|
144
- |---|---|---|---|---|---|---|
145
- |Learning type|Active (queries oracle)|Passive (fixed dataset)|
146
- |Minimal DFA|Yes|No (may have extra states)|
147
- |Negative examples|Not required|Optional|
148
- |Live learning|Yes|No|
149
- |Query complexity|O(|Q|²|Σ|)|N/A|## See Also
143
+ | Feature | L\* (Active) | RPNI (Passive) |
144
+ | ----------------- | ----------------------- | -------------------------- | --- | --- | --- | --- | ----------- |
145
+ | Learning type | Active (queries oracle) | Passive (fixed dataset) |
146
+ | Minimal DFA | Yes | No (may have extra states) |
147
+ | Negative examples | Not required | Optional |
148
+ | Live learning | Yes | No |
149
+ | Query complexity | O( | Q | ² | Σ | ) | N/A | ## See Also |
150
150
 
151
151
  - `oscura.inference.state_machine`: RPNI passive learning
152
152
  - `examples/lstar_demo.py`: Complete usage examples
@@ -219,10 +219,13 @@ class ObservationTable:
219
219
  initial_row = self.row(())
220
220
  initial_state = row_to_state[initial_row]
221
221
 
222
+ # Type cast alphabet from set[str] to set[str | int] for FiniteAutomaton
223
+ alphabet_union: set[str | int] = set(self.alphabet)
224
+
222
225
  return FiniteAutomaton(
223
226
  states=states,
224
227
  transitions=transitions,
225
- alphabet=self.alphabet.copy(),
228
+ alphabet=alphabet_union,
226
229
  initial_state=initial_state,
227
230
  accepting_states=accepting_states,
228
231
  )
@@ -122,42 +122,12 @@ def align_global(
122
122
  traceback[i, j] = 2 # Left
123
123
 
124
124
  # Traceback to get alignment
125
- aligned_a = []
126
- aligned_b = []
127
-
128
- i, j = n, m
129
- while i > 0 or j > 0:
130
- if traceback[i, j] == 0: # Diagonal
131
- aligned_a.append(int(arr_a[i - 1]))
132
- aligned_b.append(int(arr_b[j - 1]))
133
- i -= 1
134
- j -= 1
135
- elif traceback[i, j] == 1: # Up
136
- aligned_a.append(int(arr_a[i - 1]))
137
- aligned_b.append(-1) # Gap
138
- i -= 1
139
- else: # Left
140
- aligned_a.append(-1) # Gap
141
- aligned_b.append(int(arr_b[j - 1]))
142
- j -= 1
143
-
144
- # Reverse (we traced backwards)
145
- aligned_a = list(reversed(aligned_a))
146
- aligned_b = list(reversed(aligned_b))
125
+ aligned_a, aligned_b = _traceback_alignment(traceback, arr_a, arr_b, n, m)
147
126
 
148
127
  # Calculate statistics
149
128
  final_score = float(score_matrix[n, m])
150
129
  similarity = compute_similarity(aligned_a, aligned_b)
151
-
152
- # Handle empty alignments
153
- if len(aligned_a) == 0:
154
- identity = 0.0
155
- gaps = 0
156
- else:
157
- identity = sum(
158
- 1 for a, b in zip(aligned_a, aligned_b, strict=True) if a == b and a != -1
159
- ) / len(aligned_a)
160
- gaps = sum(1 for a, b in zip(aligned_a, aligned_b, strict=True) if a == -1 or b == -1)
130
+ identity, gaps = _calculate_alignment_stats(aligned_a, aligned_b)
161
131
 
162
132
  # Find conserved and variable regions
163
133
  conserved = _find_conserved_simple(aligned_a, aligned_b)
@@ -196,97 +166,12 @@ def align_local(
196
166
  Returns:
197
167
  AlignmentResult with best local alignment
198
168
  """
199
- # Convert to arrays
200
- if isinstance(seq_a, bytes):
201
- arr_a = np.frombuffer(seq_a, dtype=np.uint8)
202
- else:
203
- arr_a = np.array(seq_a, dtype=np.uint8)
204
-
205
- if isinstance(seq_b, bytes):
206
- arr_b = np.frombuffer(seq_b, dtype=np.uint8)
207
- else:
208
- arr_b = np.array(seq_b, dtype=np.uint8)
209
-
210
- n, m = len(arr_a), len(arr_b)
211
-
212
- # Initialize scoring matrix and traceback matrix
213
- score_matrix = np.zeros((n + 1, m + 1), dtype=np.float32)
214
- traceback = np.zeros((n + 1, m + 1), dtype=np.int8)
215
-
216
- # Track maximum score position
217
- max_score = 0.0
218
- max_i, max_j = 0, 0
219
-
220
- # Fill the matrices (Smith-Waterman: no negative scores)
221
- for i in range(1, n + 1):
222
- for j in range(1, m + 1):
223
- # Match/mismatch
224
- if arr_a[i - 1] == arr_b[j - 1]:
225
- diag_score = score_matrix[i - 1, j - 1] + match_score
226
- else:
227
- diag_score = score_matrix[i - 1, j - 1] + mismatch_penalty
228
-
229
- # Gap in seq_b (up)
230
- up_score = score_matrix[i - 1, j] + gap_penalty
231
-
232
- # Gap in seq_a (left)
233
- left_score = score_matrix[i, j - 1] + gap_penalty
234
-
235
- # Smith-Waterman: can start fresh (score = 0)
236
- cell_score = max(0.0, diag_score, up_score, left_score)
237
- score_matrix[i, j] = cell_score
238
-
239
- if cell_score == 0:
240
- traceback[i, j] = -1 # Stop
241
- elif cell_score == diag_score:
242
- traceback[i, j] = 0 # Diagonal
243
- elif cell_score == up_score:
244
- traceback[i, j] = 1 # Up
245
- else:
246
- traceback[i, j] = 2 # Left
247
-
248
- # Track maximum
249
- if cell_score > max_score:
250
- max_score = cell_score
251
- max_i, max_j = i, j
252
-
253
- # Traceback from max position
254
- aligned_a = []
255
- aligned_b = []
256
-
257
- i, j = max_i, max_j
258
- while i > 0 and j > 0 and traceback[i, j] != -1:
259
- if traceback[i, j] == 0: # Diagonal
260
- aligned_a.append(int(arr_a[i - 1]))
261
- aligned_b.append(int(arr_b[j - 1]))
262
- i -= 1
263
- j -= 1
264
- elif traceback[i, j] == 1: # Up
265
- aligned_a.append(int(arr_a[i - 1]))
266
- aligned_b.append(-1) # Gap
267
- i -= 1
268
- else: # Left
269
- aligned_a.append(-1) # Gap
270
- aligned_b.append(int(arr_b[j - 1]))
271
- j -= 1
272
-
273
- # Reverse
274
- aligned_a = list(reversed(aligned_a))
275
- aligned_b = list(reversed(aligned_b))
276
-
277
- # Calculate statistics
278
- if len(aligned_a) > 0:
279
- similarity = compute_similarity(aligned_a, aligned_b)
280
- identity = sum(
281
- 1 for a, b in zip(aligned_a, aligned_b, strict=True) if a == b and a != -1
282
- ) / len(aligned_a)
283
- gaps = sum(1 for a, b in zip(aligned_a, aligned_b, strict=True) if a == -1 or b == -1)
284
- else:
285
- similarity = 0.0
286
- identity = 0.0
287
- gaps = 0
288
-
289
- # Find conserved and variable regions
169
+ arr_a, arr_b = _convert_to_arrays(seq_a, seq_b)
170
+ score_matrix, traceback, max_score, max_pos = _build_sw_matrix(
171
+ arr_a, arr_b, gap_penalty, match_score, mismatch_penalty
172
+ )
173
+ aligned_a, aligned_b = _traceback_local(traceback, arr_a, arr_b, max_pos)
174
+ similarity, identity, gaps = _compute_local_stats(aligned_a, aligned_b)
290
175
  conserved = _find_conserved_simple(aligned_a, aligned_b)
291
176
  variable = _find_variable_simple(aligned_a, aligned_b)
292
177
 
@@ -532,6 +417,214 @@ def find_variable_regions(
532
417
  return regions
533
418
 
534
419
 
420
+ def _convert_to_arrays(
421
+ seq_a: bytes | NDArray[Any], seq_b: bytes | NDArray[Any]
422
+ ) -> tuple[NDArray[np.uint8], NDArray[np.uint8]]:
423
+ """Convert input sequences to numpy arrays.
424
+
425
+ Args:
426
+ seq_a: First sequence (bytes or array).
427
+ seq_b: Second sequence (bytes or array).
428
+
429
+ Returns:
430
+ Tuple of (arr_a, arr_b) as uint8 arrays.
431
+ """
432
+ if isinstance(seq_a, bytes):
433
+ arr_a = np.frombuffer(seq_a, dtype=np.uint8)
434
+ else:
435
+ arr_a = np.array(seq_a, dtype=np.uint8)
436
+
437
+ if isinstance(seq_b, bytes):
438
+ arr_b = np.frombuffer(seq_b, dtype=np.uint8)
439
+ else:
440
+ arr_b = np.array(seq_b, dtype=np.uint8)
441
+
442
+ return arr_a, arr_b
443
+
444
+
445
+ def _build_sw_matrix(
446
+ arr_a: NDArray[np.uint8],
447
+ arr_b: NDArray[np.uint8],
448
+ gap_penalty: float,
449
+ match_score: float,
450
+ mismatch_penalty: float,
451
+ ) -> tuple[NDArray[np.float32], NDArray[np.int8], float, tuple[int, int]]:
452
+ """Build Smith-Waterman scoring and traceback matrices.
453
+
454
+ Args:
455
+ arr_a: First sequence array.
456
+ arr_b: Second sequence array.
457
+ gap_penalty: Penalty for gaps.
458
+ match_score: Score for matches.
459
+ mismatch_penalty: Penalty for mismatches.
460
+
461
+ Returns:
462
+ (score_matrix, traceback, max_score, max_position).
463
+ """
464
+ n, m = len(arr_a), len(arr_b)
465
+ score_matrix = np.zeros((n + 1, m + 1), dtype=np.float32)
466
+ traceback = np.zeros((n + 1, m + 1), dtype=np.int8)
467
+ max_score = 0.0
468
+ max_i, max_j = 0, 0
469
+
470
+ for i in range(1, n + 1):
471
+ for j in range(1, m + 1):
472
+ # Match/mismatch
473
+ if arr_a[i - 1] == arr_b[j - 1]:
474
+ diag_score = score_matrix[i - 1, j - 1] + match_score
475
+ else:
476
+ diag_score = score_matrix[i - 1, j - 1] + mismatch_penalty
477
+
478
+ up_score = score_matrix[i - 1, j] + gap_penalty
479
+ left_score = score_matrix[i, j - 1] + gap_penalty
480
+
481
+ # Smith-Waterman: can start fresh (score = 0)
482
+ cell_score = max(0.0, diag_score, up_score, left_score)
483
+ score_matrix[i, j] = cell_score
484
+
485
+ if cell_score == 0:
486
+ traceback[i, j] = -1 # Stop
487
+ elif cell_score == diag_score:
488
+ traceback[i, j] = 0 # Diagonal
489
+ elif cell_score == up_score:
490
+ traceback[i, j] = 1 # Up
491
+ else:
492
+ traceback[i, j] = 2 # Left
493
+
494
+ if cell_score > max_score:
495
+ max_score = cell_score
496
+ max_i, max_j = i, j
497
+
498
+ return score_matrix, traceback, max_score, (max_i, max_j)
499
+
500
+
501
+ def _traceback_local(
502
+ traceback: NDArray[np.int8],
503
+ arr_a: NDArray[np.uint8],
504
+ arr_b: NDArray[np.uint8],
505
+ max_pos: tuple[int, int],
506
+ ) -> tuple[list[int], list[int]]:
507
+ """Perform traceback from max position for local alignment.
508
+
509
+ Args:
510
+ traceback: Traceback matrix.
511
+ arr_a: First sequence array.
512
+ arr_b: Second sequence array.
513
+ max_pos: (i, j) position of maximum score.
514
+
515
+ Returns:
516
+ Tuple of (aligned_a, aligned_b) with -1 for gaps.
517
+ """
518
+ aligned_a = []
519
+ aligned_b = []
520
+
521
+ i, j = max_pos
522
+ while i > 0 and j > 0 and traceback[i, j] != -1:
523
+ if traceback[i, j] == 0: # Diagonal
524
+ aligned_a.append(int(arr_a[i - 1]))
525
+ aligned_b.append(int(arr_b[j - 1]))
526
+ i -= 1
527
+ j -= 1
528
+ elif traceback[i, j] == 1: # Up
529
+ aligned_a.append(int(arr_a[i - 1]))
530
+ aligned_b.append(-1) # Gap
531
+ i -= 1
532
+ else: # Left
533
+ aligned_a.append(-1) # Gap
534
+ aligned_b.append(int(arr_b[j - 1]))
535
+ j -= 1
536
+
537
+ return list(reversed(aligned_a)), list(reversed(aligned_b))
538
+
539
+
540
+ def _compute_local_stats(aligned_a: list[int], aligned_b: list[int]) -> tuple[float, float, int]:
541
+ """Compute statistics for local alignment.
542
+
543
+ Args:
544
+ aligned_a: First aligned sequence.
545
+ aligned_b: Second aligned sequence.
546
+
547
+ Returns:
548
+ (similarity, identity, gaps).
549
+ """
550
+ if len(aligned_a) == 0:
551
+ return 0.0, 0.0, 0
552
+
553
+ similarity = compute_similarity(aligned_a, aligned_b)
554
+ identity = sum(
555
+ 1 for a, b in zip(aligned_a, aligned_b, strict=True) if a == b and a != -1
556
+ ) / len(aligned_a)
557
+ gaps = sum(1 for a, b in zip(aligned_a, aligned_b, strict=True) if a == -1 or b == -1)
558
+
559
+ return similarity, identity, gaps
560
+
561
+
562
+ def _traceback_alignment(
563
+ traceback: NDArray[np.int8],
564
+ arr_a: NDArray[np.uint8],
565
+ arr_b: NDArray[np.uint8],
566
+ n: int,
567
+ m: int,
568
+ ) -> tuple[list[int], list[int]]:
569
+ """Perform traceback to extract aligned sequences.
570
+
571
+ Args:
572
+ traceback: Traceback matrix (0=diagonal, 1=up, 2=left).
573
+ arr_a: First sequence array.
574
+ arr_b: Second sequence array.
575
+ n: Length of first sequence.
576
+ m: Length of second sequence.
577
+
578
+ Returns:
579
+ Tuple of (aligned_a, aligned_b) with -1 for gaps.
580
+ """
581
+ aligned_a = []
582
+ aligned_b = []
583
+
584
+ i, j = n, m
585
+ while i > 0 or j > 0:
586
+ if traceback[i, j] == 0: # Diagonal (match/mismatch)
587
+ aligned_a.append(int(arr_a[i - 1]))
588
+ aligned_b.append(int(arr_b[j - 1]))
589
+ i -= 1
590
+ j -= 1
591
+ elif traceback[i, j] == 1: # Up (gap in seq_b)
592
+ aligned_a.append(int(arr_a[i - 1]))
593
+ aligned_b.append(-1) # Gap
594
+ i -= 1
595
+ else: # Left (gap in seq_a)
596
+ aligned_a.append(-1) # Gap
597
+ aligned_b.append(int(arr_b[j - 1]))
598
+ j -= 1
599
+
600
+ # Reverse (we traced backwards)
601
+ return list(reversed(aligned_a)), list(reversed(aligned_b))
602
+
603
+
604
+ def _calculate_alignment_stats(
605
+ aligned_a: list[int],
606
+ aligned_b: list[int],
607
+ ) -> tuple[float, int]:
608
+ """Calculate identity and gap statistics for alignment.
609
+
610
+ Args:
611
+ aligned_a: First aligned sequence (-1 for gaps).
612
+ aligned_b: Second aligned sequence (-1 for gaps).
613
+
614
+ Returns:
615
+ Tuple of (identity, gaps).
616
+ """
617
+ if len(aligned_a) == 0:
618
+ return 0.0, 0
619
+
620
+ identity = sum(
621
+ 1 for a, b in zip(aligned_a, aligned_b, strict=True) if a == b and a != -1
622
+ ) / len(aligned_a)
623
+ gaps = sum(1 for a, b in zip(aligned_a, aligned_b, strict=True) if a == -1 or b == -1)
624
+
625
+ return identity, gaps
626
+
627
+
535
628
  def _find_conserved_simple(aligned_a: list[int], aligned_b: list[int]) -> list[tuple[int, int]]:
536
629
  """Find conserved regions in pairwise alignment.
537
630
 
@@ -142,9 +142,9 @@ class Prior:
142
142
  ValueError: If distribution is not recognized.
143
143
  """
144
144
  if self.distribution == "normal":
145
- return float(stats.norm.pdf(x, loc=self.params["mean"], scale=self.params["std"])) # type: ignore[no-any-return]
145
+ return float(stats.norm.pdf(x, loc=self.params["mean"], scale=self.params["std"]))
146
146
  elif self.distribution == "uniform":
147
- return float( # type: ignore[no-any-return]
147
+ return float(
148
148
  stats.uniform.pdf(
149
149
  x, loc=self.params["low"], scale=self.params["high"] - self.params["low"]
150
150
  )
@@ -156,16 +156,16 @@ class Prior:
156
156
  log_x = np.log(np.maximum(x, 1e-100)) # Avoid log(0)
157
157
  density = stats.uniform.pdf(log_x, loc=log_low, scale=log_high - log_low)
158
158
  # Jacobian correction: d(log x)/dx = 1/x
159
- result = density / np.maximum(x, 1e-100)
160
- return result # type: ignore[return-value, no-any-return]
159
+ result: float | NDArray[np.floating[Any]] = density / np.maximum(x, 1e-100)
160
+ return result
161
161
  elif self.distribution == "beta":
162
- return float(stats.beta.pdf(x, a=self.params["a"], b=self.params["b"])) # type: ignore[no-any-return]
162
+ return float(stats.beta.pdf(x, a=self.params["a"], b=self.params["b"]))
163
163
  elif self.distribution == "gamma":
164
- return float(stats.gamma.pdf(x, a=self.params["shape"], scale=self.params["scale"])) # type: ignore[no-any-return]
164
+ return float(stats.gamma.pdf(x, a=self.params["shape"], scale=self.params["scale"]))
165
165
  elif self.distribution == "half_normal":
166
- return float(stats.halfnorm.pdf(x, scale=self.params["scale"])) # type: ignore[no-any-return]
166
+ return float(stats.halfnorm.pdf(x, scale=self.params["scale"]))
167
167
  elif self.distribution == "geometric":
168
- return float(stats.geom.pmf(x, p=self.params["p"])) # type: ignore[no-any-return]
168
+ return float(stats.geom.pmf(x, p=self.params["p"]))
169
169
  else:
170
170
  raise ValueError(f"PDF not implemented for {self.distribution}")
171
171
 
@@ -191,7 +191,7 @@ class Prior:
191
191
  # Sample uniformly on log scale, then exponentiate
192
192
  log_low = np.log(self.params["low"])
193
193
  log_high = np.log(self.params["high"])
194
- log_samples = stats.uniform.rvs(loc=log_low, scale=log_high - log_low, size=n) # type: ignore[no-any-return]
194
+ log_samples = stats.uniform.rvs(loc=log_low, scale=log_high - log_low, size=n)
195
195
  return np.exp(log_samples) # type: ignore[no-any-return]
196
196
  elif self.distribution == "beta":
197
197
  return stats.beta.rvs(a=self.params["a"], b=self.params["b"], size=n) # type: ignore[no-any-return]
@@ -364,7 +364,25 @@ class BayesianInference:
364
364
  >>> inference = BayesianInference()
365
365
  >>> posterior = inference.update("frequency", likelihood)
366
366
  """
367
- # Get prior
367
+ prior = self._get_prior(param, prior)
368
+ samples = self._sample_from_prior(prior, param, num_samples)
369
+ likelihoods = self._compute_likelihoods(samples, likelihood_fn, param)
370
+ weights = self._normalize_weights(likelihoods, param)
371
+ return self._build_posterior(samples, weights)
372
+
373
+ def _get_prior(self, param: str, prior: Prior | None) -> Prior:
374
+ """Get prior distribution for parameter.
375
+
376
+ Args:
377
+ param: Parameter name.
378
+ prior: Optional explicit prior.
379
+
380
+ Returns:
381
+ Prior distribution to use.
382
+
383
+ Raises:
384
+ ValueError: If parameter unknown and no prior provided.
385
+ """
368
386
  if prior is None:
369
387
  if param not in self.priors:
370
388
  raise ValueError(
@@ -372,17 +390,51 @@ class BayesianInference:
372
390
  f"Known parameters: {list(self.priors.keys())}"
373
391
  )
374
392
  prior = self.priors[param]
393
+ return prior
394
+
395
+ def _sample_from_prior(
396
+ self, prior: Prior, param: str, num_samples: int
397
+ ) -> NDArray[np.floating[Any]]:
398
+ """Sample from prior distribution.
375
399
 
376
- # Sample from prior
400
+ Args:
401
+ prior: Prior distribution.
402
+ param: Parameter name for error messages.
403
+ num_samples: Number of samples.
404
+
405
+ Returns:
406
+ Array of prior samples.
407
+
408
+ Raises:
409
+ AnalysisError: If sampling fails.
410
+ """
377
411
  try:
378
- samples = prior.sample(num_samples)
412
+ return prior.sample(num_samples)
379
413
  except Exception as e:
380
414
  raise AnalysisError(
381
415
  f"Failed to sample from prior for '{param}'",
382
416
  details=str(e),
383
417
  ) from e
384
418
 
385
- # Compute likelihood for each sample
419
+ def _compute_likelihoods(
420
+ self,
421
+ samples: NDArray[np.floating[Any]],
422
+ likelihood_fn: Callable[[float], float],
423
+ param: str,
424
+ ) -> NDArray[np.floating[Any]]:
425
+ """Compute likelihoods for all samples.
426
+
427
+ Args:
428
+ samples: Prior samples.
429
+ likelihood_fn: Likelihood function.
430
+ param: Parameter name for error messages.
431
+
432
+ Returns:
433
+ Array of likelihood values.
434
+
435
+ Raises:
436
+ AnalysisError: If likelihood computation fails or all zeros.
437
+ """
386
438
  try:
387
439
  likelihoods = np.array([likelihood_fn(s) for s in samples])
388
440
  except Exception as e:
@@ -392,7 +444,6 @@ class BayesianInference:
392
444
  fix_hint="Check that likelihood_fn is compatible with prior samples",
393
445
  ) from e
394
446
 
395
- # Check for valid likelihoods (before numerical stability fixes)
396
447
  if np.all(likelihoods == 0):
397
448
  raise AnalysisError(
398
449
  f"All likelihood values are zero for '{param}'",
@@ -400,42 +451,71 @@ class BayesianInference:
400
451
  fix_hint="Adjust prior range or check likelihood function",
401
452
  )
402
453
 
403
- # Numerical stability: normalize likelihoods to prevent underflow
404
- # Use log-space computation if likelihoods are very small
454
+ return likelihoods
455
+
456
+ def _normalize_weights(
457
+ self, likelihoods: NDArray[np.floating[Any]], param: str
458
+ ) -> NDArray[np.floating[Any]]:
459
+ """Normalize likelihoods to importance weights.
460
+
461
+ Uses numerical stability techniques for extreme values.
462
+
463
+ Args:
464
+ likelihoods: Likelihood values.
465
+ param: Parameter name for error messages.
466
+
467
+ Returns:
468
+ Normalized importance weights.
469
+
470
+ Raises:
471
+ AnalysisError: If all likelihoods are zero.
472
+ """
405
473
  max_likelihood = np.max(likelihoods)
406
- if max_likelihood > 0:
407
- # Normalize by max to prevent overflow/underflow
408
- normalized_likelihoods = likelihoods / max_likelihood
409
- # Check if we need log-space (very small values)
410
- if max_likelihood < 1e-300:
411
- # Use log-space for extreme underflow
412
- log_likelihoods = np.log(np.maximum(likelihoods, 1e-300))
413
- log_likelihoods -= np.max(log_likelihoods) # Normalize
414
- weights = np.exp(log_likelihoods)
415
- weights /= np.sum(weights)
416
- else:
417
- # Standard normalization
418
- weights = normalized_likelihoods / np.sum(normalized_likelihoods)
419
- else:
420
- # All likelihoods are zero - this should have been caught above
474
+ if max_likelihood <= 0:
421
475
  raise AnalysisError(
422
476
  f"All likelihood values are zero for '{param}'",
423
477
  details="Observation may be incompatible with prior range",
424
478
  fix_hint="Adjust prior range or check likelihood function",
425
479
  )
426
480
 
481
+ # Normalize by max to prevent overflow/underflow
482
+ normalized_likelihoods = likelihoods / max_likelihood
483
+
484
+ # Use log-space for extreme underflow
485
+ if max_likelihood < 1e-300:
486
+ log_likelihoods = np.log(np.maximum(likelihoods, 1e-300))
487
+ log_likelihoods -= np.max(log_likelihoods)
488
+ weights = np.exp(log_likelihoods)
489
+ weights /= np.sum(weights)
490
+ else:
491
+ weights = normalized_likelihoods / np.sum(normalized_likelihoods)
492
+
493
+ result: NDArray[np.floating[Any]] = np.asarray(weights, dtype=np.float64)
494
+ return result
495
+
496
+ def _build_posterior(
497
+ self, samples: NDArray[np.floating[Any]], weights: NDArray[np.floating[Any]]
498
+ ) -> Posterior:
499
+ """Build posterior distribution from weighted samples.
500
+
501
+ Args:
502
+ samples: Prior samples.
503
+ weights: Importance weights.
504
+
505
+ Returns:
506
+ Posterior distribution with statistics.
507
+ """
427
508
  # Compute posterior statistics
428
509
  mean = float(np.sum(samples * weights))
429
510
  variance = float(np.sum(weights * (samples - mean) ** 2))
430
511
  std = float(np.sqrt(variance))
431
512
 
432
- # Compute 95% credible interval via weighted percentiles
513
+ # Compute 95% credible interval
433
514
  sorted_indices = np.argsort(samples)
434
515
  sorted_samples = samples[sorted_indices]
435
516
  sorted_weights = weights[sorted_indices]
436
517
  cumsum = np.cumsum(sorted_weights)
437
518
 
438
- # Find 2.5th and 97.5th percentiles
439
519
  ci_lower = float(sorted_samples[np.searchsorted(cumsum, 0.025)])
440
520
  ci_upper = float(sorted_samples[np.searchsorted(cumsum, 0.975)])
441
521