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
@@ -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",
@@ -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