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.
- oscura/__init__.py +169 -167
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/classification.py +659 -0
- oscura/analyzers/digital/edges.py +325 -65
- oscura/analyzers/digital/quality.py +293 -166
- oscura/analyzers/digital/timing.py +260 -115
- oscura/analyzers/digital/timing_numba.py +334 -0
- oscura/analyzers/entropy.py +605 -0
- oscura/analyzers/eye/diagram.py +176 -109
- oscura/analyzers/eye/metrics.py +5 -5
- oscura/analyzers/jitter/__init__.py +6 -4
- oscura/analyzers/jitter/ber.py +52 -52
- oscura/analyzers/jitter/classification.py +156 -0
- oscura/analyzers/jitter/decomposition.py +163 -113
- oscura/analyzers/jitter/spectrum.py +80 -64
- oscura/analyzers/ml/__init__.py +39 -0
- oscura/analyzers/ml/features.py +600 -0
- oscura/analyzers/ml/signal_classifier.py +604 -0
- oscura/analyzers/packet/daq.py +246 -158
- oscura/analyzers/packet/parser.py +12 -1
- oscura/analyzers/packet/payload.py +50 -2110
- oscura/analyzers/packet/payload_analysis.py +361 -181
- oscura/analyzers/packet/payload_patterns.py +133 -70
- oscura/analyzers/packet/stream.py +84 -23
- oscura/analyzers/patterns/__init__.py +26 -5
- oscura/analyzers/patterns/anomaly_detection.py +908 -0
- oscura/analyzers/patterns/clustering.py +169 -108
- oscura/analyzers/patterns/clustering_optimized.py +227 -0
- oscura/analyzers/patterns/discovery.py +1 -1
- oscura/analyzers/patterns/matching.py +581 -197
- oscura/analyzers/patterns/pattern_mining.py +778 -0
- oscura/analyzers/patterns/periodic.py +121 -38
- oscura/analyzers/patterns/sequences.py +175 -78
- oscura/analyzers/power/conduction.py +1 -1
- oscura/analyzers/power/soa.py +6 -6
- oscura/analyzers/power/switching.py +250 -110
- oscura/analyzers/protocol/__init__.py +17 -1
- oscura/analyzers/protocols/base.py +6 -6
- oscura/analyzers/protocols/ble/__init__.py +38 -0
- oscura/analyzers/protocols/ble/analyzer.py +809 -0
- oscura/analyzers/protocols/ble/uuids.py +288 -0
- oscura/analyzers/protocols/can.py +257 -127
- oscura/analyzers/protocols/can_fd.py +107 -80
- oscura/analyzers/protocols/flexray.py +139 -80
- oscura/analyzers/protocols/hdlc.py +93 -58
- oscura/analyzers/protocols/i2c.py +247 -106
- oscura/analyzers/protocols/i2s.py +138 -86
- oscura/analyzers/protocols/industrial/__init__.py +40 -0
- oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
- oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
- oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
- oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
- oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
- oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
- oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
- oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
- oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
- oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
- oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
- oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
- oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
- oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
- oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
- oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
- oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
- oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
- oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
- oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
- oscura/analyzers/protocols/jtag.py +180 -98
- oscura/analyzers/protocols/lin.py +219 -114
- oscura/analyzers/protocols/manchester.py +4 -4
- oscura/analyzers/protocols/onewire.py +253 -149
- oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
- oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
- oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
- oscura/analyzers/protocols/spi.py +192 -95
- oscura/analyzers/protocols/swd.py +321 -167
- oscura/analyzers/protocols/uart.py +267 -125
- oscura/analyzers/protocols/usb.py +235 -131
- oscura/analyzers/side_channel/power.py +17 -12
- oscura/analyzers/signal/__init__.py +15 -0
- oscura/analyzers/signal/timing_analysis.py +1086 -0
- oscura/analyzers/signal_integrity/__init__.py +4 -1
- oscura/analyzers/signal_integrity/sparams.py +2 -19
- oscura/analyzers/spectral/chunked.py +129 -60
- oscura/analyzers/spectral/chunked_fft.py +300 -94
- oscura/analyzers/spectral/chunked_wavelet.py +100 -80
- oscura/analyzers/statistical/checksum.py +376 -217
- oscura/analyzers/statistical/classification.py +229 -107
- oscura/analyzers/statistical/entropy.py +78 -53
- oscura/analyzers/statistics/correlation.py +407 -211
- oscura/analyzers/statistics/outliers.py +2 -2
- oscura/analyzers/statistics/streaming.py +30 -5
- oscura/analyzers/validation.py +216 -101
- oscura/analyzers/waveform/measurements.py +9 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
- oscura/analyzers/waveform/spectral.py +500 -228
- oscura/api/__init__.py +31 -5
- oscura/api/dsl/__init__.py +582 -0
- oscura/{dsl → api/dsl}/commands.py +43 -76
- oscura/{dsl → api/dsl}/interpreter.py +26 -51
- oscura/{dsl → api/dsl}/parser.py +107 -77
- oscura/{dsl → api/dsl}/repl.py +2 -2
- oscura/api/dsl.py +1 -1
- oscura/{integrations → api/integrations}/__init__.py +1 -1
- oscura/{integrations → api/integrations}/llm.py +201 -102
- oscura/api/operators.py +3 -3
- oscura/api/optimization.py +144 -30
- oscura/api/rest_server.py +921 -0
- oscura/api/server/__init__.py +17 -0
- oscura/api/server/dashboard.py +850 -0
- oscura/api/server/static/README.md +34 -0
- oscura/api/server/templates/base.html +181 -0
- oscura/api/server/templates/export.html +120 -0
- oscura/api/server/templates/home.html +284 -0
- oscura/api/server/templates/protocols.html +58 -0
- oscura/api/server/templates/reports.html +43 -0
- oscura/api/server/templates/session_detail.html +89 -0
- oscura/api/server/templates/sessions.html +83 -0
- oscura/api/server/templates/waveforms.html +73 -0
- oscura/automotive/__init__.py +8 -1
- oscura/automotive/can/__init__.py +10 -0
- oscura/automotive/can/checksum.py +3 -1
- oscura/automotive/can/dbc_generator.py +590 -0
- oscura/automotive/can/message_wrapper.py +121 -74
- oscura/automotive/can/patterns.py +98 -21
- oscura/automotive/can/session.py +292 -56
- oscura/automotive/can/state_machine.py +6 -3
- oscura/automotive/can/stimulus_response.py +97 -75
- oscura/automotive/dbc/__init__.py +10 -2
- oscura/automotive/dbc/generator.py +84 -56
- oscura/automotive/dbc/parser.py +6 -6
- oscura/automotive/dtc/data.json +17 -102
- oscura/automotive/dtc/database.py +2 -2
- oscura/automotive/flexray/__init__.py +31 -0
- oscura/automotive/flexray/analyzer.py +504 -0
- oscura/automotive/flexray/crc.py +185 -0
- oscura/automotive/flexray/fibex.py +449 -0
- oscura/automotive/j1939/__init__.py +45 -8
- oscura/automotive/j1939/analyzer.py +605 -0
- oscura/automotive/j1939/spns.py +326 -0
- oscura/automotive/j1939/transport.py +306 -0
- oscura/automotive/lin/__init__.py +47 -0
- oscura/automotive/lin/analyzer.py +612 -0
- oscura/automotive/loaders/blf.py +13 -2
- oscura/automotive/loaders/csv_can.py +143 -72
- oscura/automotive/loaders/dispatcher.py +50 -2
- oscura/automotive/loaders/mdf.py +86 -45
- oscura/automotive/loaders/pcap.py +111 -61
- oscura/automotive/uds/__init__.py +4 -0
- oscura/automotive/uds/analyzer.py +725 -0
- oscura/automotive/uds/decoder.py +140 -58
- oscura/automotive/uds/models.py +7 -1
- oscura/automotive/visualization.py +1 -1
- oscura/cli/analyze.py +348 -0
- oscura/cli/batch.py +142 -122
- oscura/cli/benchmark.py +275 -0
- oscura/cli/characterize.py +137 -82
- oscura/cli/compare.py +224 -131
- oscura/cli/completion.py +250 -0
- oscura/cli/config_cmd.py +361 -0
- oscura/cli/decode.py +164 -87
- oscura/cli/export.py +286 -0
- oscura/cli/main.py +115 -31
- oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
- oscura/{onboarding → cli/onboarding}/help.py +80 -58
- oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
- oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
- oscura/cli/progress.py +147 -0
- oscura/cli/shell.py +157 -135
- oscura/cli/validate_cmd.py +204 -0
- oscura/cli/visualize.py +158 -0
- oscura/convenience.py +125 -79
- oscura/core/__init__.py +4 -2
- oscura/core/backend_selector.py +3 -3
- oscura/core/cache.py +126 -15
- oscura/core/cancellation.py +1 -1
- oscura/{config → core/config}/__init__.py +20 -11
- oscura/{config → core/config}/defaults.py +1 -1
- oscura/{config → core/config}/loader.py +7 -5
- oscura/{config → core/config}/memory.py +5 -5
- oscura/{config → core/config}/migration.py +1 -1
- oscura/{config → core/config}/pipeline.py +99 -23
- oscura/{config → core/config}/preferences.py +1 -1
- oscura/{config → core/config}/protocol.py +3 -3
- oscura/{config → core/config}/schema.py +426 -272
- oscura/{config → core/config}/settings.py +1 -1
- oscura/{config → core/config}/thresholds.py +195 -153
- oscura/core/correlation.py +5 -6
- oscura/core/cross_domain.py +0 -2
- oscura/core/debug.py +9 -5
- oscura/{extensibility → core/extensibility}/docs.py +158 -70
- oscura/{extensibility → core/extensibility}/extensions.py +160 -76
- oscura/{extensibility → core/extensibility}/logging.py +1 -1
- oscura/{extensibility → core/extensibility}/measurements.py +1 -1
- oscura/{extensibility → core/extensibility}/plugins.py +1 -1
- oscura/{extensibility → core/extensibility}/templates.py +73 -3
- oscura/{extensibility → core/extensibility}/validation.py +1 -1
- oscura/core/gpu_backend.py +11 -7
- oscura/core/log_query.py +101 -11
- oscura/core/logging.py +126 -54
- oscura/core/logging_advanced.py +5 -5
- oscura/core/memory_limits.py +108 -70
- oscura/core/memory_monitor.py +2 -2
- oscura/core/memory_progress.py +7 -7
- oscura/core/memory_warnings.py +1 -1
- oscura/core/numba_backend.py +13 -13
- oscura/{plugins → core/plugins}/__init__.py +9 -9
- oscura/{plugins → core/plugins}/base.py +7 -7
- oscura/{plugins → core/plugins}/cli.py +3 -3
- oscura/{plugins → core/plugins}/discovery.py +186 -106
- oscura/{plugins → core/plugins}/lifecycle.py +1 -1
- oscura/{plugins → core/plugins}/manager.py +7 -7
- oscura/{plugins → core/plugins}/registry.py +3 -3
- oscura/{plugins → core/plugins}/versioning.py +1 -1
- oscura/core/progress.py +16 -1
- oscura/core/provenance.py +8 -2
- oscura/{schemas → core/schemas}/__init__.py +2 -2
- oscura/{schemas → core/schemas}/device_mapping.json +2 -8
- oscura/{schemas → core/schemas}/packet_format.json +4 -24
- oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
- oscura/core/types.py +4 -0
- oscura/core/uncertainty.py +3 -3
- oscura/correlation/__init__.py +52 -0
- oscura/correlation/multi_protocol.py +811 -0
- oscura/discovery/auto_decoder.py +117 -35
- oscura/discovery/comparison.py +191 -86
- oscura/discovery/quality_validator.py +155 -68
- oscura/discovery/signal_detector.py +196 -79
- oscura/export/__init__.py +18 -8
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- oscura/export/wireshark/generator.py +1 -1
- oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
- oscura/export/wireshark_dissector.py +746 -0
- oscura/guidance/wizard.py +207 -111
- oscura/hardware/__init__.py +19 -0
- oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
- oscura/{acquisition → hardware/acquisition}/file.py +2 -2
- oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
- oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
- oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
- oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
- oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
- oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
- oscura/hardware/firmware/__init__.py +29 -0
- oscura/hardware/firmware/pattern_recognition.py +874 -0
- oscura/hardware/hal_detector.py +736 -0
- oscura/hardware/security/__init__.py +37 -0
- oscura/hardware/security/side_channel_detector.py +1126 -0
- oscura/inference/__init__.py +4 -0
- oscura/inference/active_learning/observation_table.py +4 -1
- oscura/inference/alignment.py +216 -123
- oscura/inference/bayesian.py +113 -33
- oscura/inference/crc_reverse.py +101 -55
- oscura/inference/logic.py +6 -2
- oscura/inference/message_format.py +342 -183
- oscura/inference/protocol.py +95 -44
- oscura/inference/protocol_dsl.py +180 -82
- oscura/inference/signal_intelligence.py +1439 -706
- oscura/inference/spectral.py +99 -57
- oscura/inference/state_machine.py +810 -158
- oscura/inference/stream.py +270 -110
- oscura/iot/__init__.py +34 -0
- oscura/iot/coap/__init__.py +32 -0
- oscura/iot/coap/analyzer.py +668 -0
- oscura/iot/coap/options.py +212 -0
- oscura/iot/lorawan/__init__.py +21 -0
- oscura/iot/lorawan/crypto.py +206 -0
- oscura/iot/lorawan/decoder.py +801 -0
- oscura/iot/lorawan/mac_commands.py +341 -0
- oscura/iot/mqtt/__init__.py +27 -0
- oscura/iot/mqtt/analyzer.py +999 -0
- oscura/iot/mqtt/properties.py +315 -0
- oscura/iot/zigbee/__init__.py +31 -0
- oscura/iot/zigbee/analyzer.py +615 -0
- oscura/iot/zigbee/security.py +153 -0
- oscura/iot/zigbee/zcl.py +349 -0
- oscura/jupyter/display.py +125 -45
- oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
- oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
- oscura/jupyter/exploratory/fuzzy.py +746 -0
- oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
- oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
- oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
- oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
- oscura/jupyter/exploratory/sync.py +612 -0
- oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
- oscura/jupyter/magic.py +4 -4
- oscura/{ui → jupyter/ui}/__init__.py +2 -2
- oscura/{ui → jupyter/ui}/formatters.py +3 -3
- oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
- oscura/loaders/__init__.py +183 -67
- oscura/loaders/binary.py +88 -1
- oscura/loaders/chipwhisperer.py +153 -137
- oscura/loaders/configurable.py +208 -86
- oscura/loaders/csv_loader.py +458 -215
- oscura/loaders/hdf5_loader.py +278 -119
- oscura/loaders/lazy.py +87 -54
- oscura/loaders/mmap_loader.py +1 -1
- oscura/loaders/numpy_loader.py +253 -116
- oscura/loaders/pcap.py +226 -151
- oscura/loaders/rigol.py +110 -49
- oscura/loaders/sigrok.py +201 -78
- oscura/loaders/tdms.py +81 -58
- oscura/loaders/tektronix.py +291 -174
- oscura/loaders/touchstone.py +182 -87
- oscura/loaders/tss.py +456 -0
- oscura/loaders/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -0
- oscura/reporting/analyze.py +352 -146
- oscura/reporting/argument_preparer.py +69 -14
- oscura/reporting/auto_report.py +97 -61
- oscura/reporting/batch.py +131 -58
- oscura/reporting/chart_selection.py +57 -45
- oscura/reporting/comparison.py +63 -17
- oscura/reporting/content/executive.py +76 -24
- oscura/reporting/core_formats/multi_format.py +11 -8
- oscura/reporting/engine.py +312 -158
- oscura/reporting/enhanced_reports.py +949 -0
- oscura/reporting/export.py +86 -43
- oscura/reporting/formatting/numbers.py +69 -42
- oscura/reporting/html.py +139 -58
- oscura/reporting/index.py +137 -65
- oscura/reporting/output.py +158 -67
- oscura/reporting/pdf.py +67 -102
- oscura/reporting/plots.py +191 -112
- oscura/reporting/sections.py +88 -47
- oscura/reporting/standards.py +104 -61
- oscura/reporting/summary_generator.py +75 -55
- oscura/reporting/tables.py +138 -54
- oscura/reporting/templates/enhanced/protocol_re.html +525 -0
- oscura/sessions/__init__.py +14 -23
- oscura/sessions/base.py +3 -3
- oscura/sessions/blackbox.py +106 -10
- oscura/sessions/generic.py +2 -2
- oscura/sessions/legacy.py +783 -0
- oscura/side_channel/__init__.py +63 -0
- oscura/side_channel/dpa.py +1025 -0
- oscura/utils/__init__.py +15 -1
- oscura/utils/bitwise.py +118 -0
- oscura/{builders → utils/builders}/__init__.py +1 -1
- oscura/{comparison → utils/comparison}/__init__.py +6 -6
- oscura/{comparison → utils/comparison}/compare.py +202 -101
- oscura/{comparison → utils/comparison}/golden.py +83 -63
- oscura/{comparison → utils/comparison}/limits.py +313 -89
- oscura/{comparison → utils/comparison}/mask.py +151 -45
- oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
- oscura/{comparison → utils/comparison}/visualization.py +147 -89
- oscura/{component → utils/component}/__init__.py +3 -3
- oscura/{component → utils/component}/impedance.py +122 -58
- oscura/{component → utils/component}/reactive.py +165 -168
- oscura/{component → utils/component}/transmission_line.py +3 -3
- oscura/{filtering → utils/filtering}/__init__.py +6 -6
- oscura/{filtering → utils/filtering}/base.py +1 -1
- oscura/{filtering → utils/filtering}/convenience.py +2 -2
- oscura/{filtering → utils/filtering}/design.py +169 -93
- oscura/{filtering → utils/filtering}/filters.py +2 -2
- oscura/{filtering → utils/filtering}/introspection.py +2 -2
- oscura/utils/geometry.py +31 -0
- oscura/utils/imports.py +184 -0
- oscura/utils/lazy.py +1 -1
- oscura/{math → utils/math}/__init__.py +2 -2
- oscura/{math → utils/math}/arithmetic.py +114 -48
- oscura/{math → utils/math}/interpolation.py +139 -106
- oscura/utils/memory.py +129 -66
- oscura/utils/memory_advanced.py +92 -9
- oscura/utils/memory_extensions.py +10 -8
- oscura/{optimization → utils/optimization}/__init__.py +1 -1
- oscura/{optimization → utils/optimization}/search.py +2 -2
- oscura/utils/performance/__init__.py +58 -0
- oscura/utils/performance/caching.py +889 -0
- oscura/utils/performance/lsh_clustering.py +333 -0
- oscura/utils/performance/memory_optimizer.py +699 -0
- oscura/utils/performance/optimizations.py +675 -0
- oscura/utils/performance/parallel.py +654 -0
- oscura/utils/performance/profiling.py +661 -0
- oscura/{pipeline → utils/pipeline}/base.py +1 -1
- oscura/{pipeline → utils/pipeline}/composition.py +1 -1
- oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
- oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
- oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
- oscura/{search → utils/search}/__init__.py +3 -3
- oscura/{search → utils/search}/anomaly.py +188 -58
- oscura/utils/search/context.py +294 -0
- oscura/{search → utils/search}/pattern.py +138 -10
- oscura/utils/serial.py +51 -0
- oscura/utils/storage/__init__.py +61 -0
- oscura/utils/storage/database.py +1166 -0
- oscura/{streaming → utils/streaming}/chunked.py +302 -143
- oscura/{streaming → utils/streaming}/progressive.py +1 -1
- oscura/{streaming → utils/streaming}/realtime.py +3 -2
- oscura/{triggering → utils/triggering}/__init__.py +6 -6
- oscura/{triggering → utils/triggering}/base.py +6 -6
- oscura/{triggering → utils/triggering}/edge.py +2 -2
- oscura/{triggering → utils/triggering}/pattern.py +2 -2
- oscura/{triggering → utils/triggering}/pulse.py +115 -74
- oscura/{triggering → utils/triggering}/window.py +2 -2
- oscura/utils/validation.py +32 -0
- oscura/validation/__init__.py +121 -0
- oscura/{compliance → validation/compliance}/__init__.py +5 -5
- oscura/{compliance → validation/compliance}/advanced.py +5 -5
- oscura/{compliance → validation/compliance}/masks.py +1 -1
- oscura/{compliance → validation/compliance}/reporting.py +127 -53
- oscura/{compliance → validation/compliance}/testing.py +114 -52
- oscura/validation/compliance_tests.py +915 -0
- oscura/validation/fuzzer.py +990 -0
- oscura/validation/grammar_tests.py +596 -0
- oscura/validation/grammar_validator.py +904 -0
- oscura/validation/hil_testing.py +977 -0
- oscura/{quality → validation/quality}/__init__.py +4 -4
- oscura/{quality → validation/quality}/ensemble.py +251 -171
- oscura/{quality → validation/quality}/explainer.py +3 -3
- oscura/{quality → validation/quality}/scoring.py +1 -1
- oscura/{quality → validation/quality}/warnings.py +4 -4
- oscura/validation/regression_suite.py +808 -0
- oscura/validation/replay.py +788 -0
- oscura/{testing → validation/testing}/__init__.py +2 -2
- oscura/{testing → validation/testing}/synthetic.py +5 -5
- oscura/visualization/__init__.py +9 -0
- oscura/visualization/accessibility.py +1 -1
- oscura/visualization/annotations.py +64 -67
- oscura/visualization/colors.py +7 -7
- oscura/visualization/digital.py +180 -81
- oscura/visualization/eye.py +236 -85
- oscura/visualization/interactive.py +320 -143
- oscura/visualization/jitter.py +587 -247
- oscura/visualization/layout.py +169 -134
- oscura/visualization/optimization.py +103 -52
- oscura/visualization/palettes.py +1 -1
- oscura/visualization/power.py +427 -211
- oscura/visualization/power_extended.py +626 -297
- oscura/visualization/presets.py +2 -0
- oscura/visualization/protocols.py +495 -181
- oscura/visualization/render.py +79 -63
- oscura/visualization/reverse_engineering.py +171 -124
- oscura/visualization/signal_integrity.py +460 -279
- oscura/visualization/specialized.py +190 -100
- oscura/visualization/spectral.py +670 -255
- oscura/visualization/thumbnails.py +166 -137
- oscura/visualization/waveform.py +150 -63
- oscura/workflows/__init__.py +3 -0
- oscura/{batch → workflows/batch}/__init__.py +5 -5
- oscura/{batch → workflows/batch}/advanced.py +150 -75
- oscura/workflows/batch/aggregate.py +531 -0
- oscura/workflows/batch/analyze.py +236 -0
- oscura/{batch → workflows/batch}/logging.py +2 -2
- oscura/{batch → workflows/batch}/metrics.py +1 -1
- oscura/workflows/complete_re.py +1144 -0
- oscura/workflows/compliance.py +44 -54
- oscura/workflows/digital.py +197 -51
- oscura/workflows/legacy/__init__.py +12 -0
- oscura/{workflow → workflows/legacy}/dag.py +4 -1
- oscura/workflows/multi_trace.py +9 -9
- oscura/workflows/power.py +42 -62
- oscura/workflows/protocol.py +82 -49
- oscura/workflows/reverse_engineering.py +351 -150
- oscura/workflows/signal_integrity.py +157 -82
- oscura-0.7.0.dist-info/METADATA +661 -0
- oscura-0.7.0.dist-info/RECORD +591 -0
- oscura/batch/aggregate.py +0 -300
- oscura/batch/analyze.py +0 -139
- oscura/dsl/__init__.py +0 -73
- oscura/exceptions.py +0 -59
- oscura/exploratory/fuzzy.py +0 -513
- oscura/exploratory/sync.py +0 -384
- oscura/exporters/__init__.py +0 -94
- oscura/exporters/csv.py +0 -303
- oscura/exporters/exporters.py +0 -44
- oscura/exporters/hdf5.py +0 -217
- oscura/exporters/html_export.py +0 -701
- oscura/exporters/json_export.py +0 -291
- oscura/exporters/markdown_export.py +0 -367
- oscura/exporters/matlab_export.py +0 -354
- oscura/exporters/npz_export.py +0 -219
- oscura/exporters/spice_export.py +0 -210
- oscura/search/context.py +0 -149
- oscura/session/__init__.py +0 -34
- oscura/session/annotations.py +0 -289
- oscura/session/history.py +0 -313
- oscura/session/session.py +0 -520
- oscura/workflow/__init__.py +0 -13
- oscura-0.5.1.dist-info/METADATA +0 -583
- oscura-0.5.1.dist-info/RECORD +0 -481
- /oscura/core/{config.py → config/legacy.py} +0 -0
- /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
- /oscura/{extensibility → core/extensibility}/registry.py +0 -0
- /oscura/{plugins → core/plugins}/isolation.py +0 -0
- /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
- /oscura/{builders → utils/builders}/signal_builder.py +0 -0
- /oscura/{optimization → utils/optimization}/parallel.py +0 -0
- /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
- /oscura/{streaming → utils/streaming}/__init__.py +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
oscura/inference/__init__.py
CHANGED
|
@@ -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=
|
|
228
|
+
alphabet=alphabet_union,
|
|
226
229
|
initial_state=initial_state,
|
|
227
230
|
accepting_states=accepting_states,
|
|
228
231
|
)
|
oscura/inference/alignment.py
CHANGED
|
@@ -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
|
-
|
|
200
|
-
|
|
201
|
-
arr_a
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
oscura/inference/bayesian.py
CHANGED
|
@@ -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"]))
|
|
145
|
+
return float(stats.norm.pdf(x, loc=self.params["mean"], scale=self.params["std"]))
|
|
146
146
|
elif self.distribution == "uniform":
|
|
147
|
-
return float(
|
|
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
|
|
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"]))
|
|
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"]))
|
|
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"]))
|
|
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"]))
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
404
|
-
|
|
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
|
|
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
|
|
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
|
|