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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Signal quality analysis module for Oscura."""
|
|
2
2
|
|
|
3
|
-
from oscura.quality.ensemble import (
|
|
3
|
+
from oscura.validation.quality.ensemble import (
|
|
4
4
|
AMPLITUDE_ENSEMBLE,
|
|
5
5
|
EDGE_DETECTION_ENSEMBLE,
|
|
6
6
|
FREQUENCY_ENSEMBLE,
|
|
@@ -10,12 +10,12 @@ from oscura.quality.ensemble import (
|
|
|
10
10
|
create_edge_ensemble,
|
|
11
11
|
create_frequency_ensemble,
|
|
12
12
|
)
|
|
13
|
-
from oscura.quality.explainer import (
|
|
13
|
+
from oscura.validation.quality.explainer import (
|
|
14
14
|
ResultExplainer,
|
|
15
15
|
ResultExplanation,
|
|
16
16
|
explain_result,
|
|
17
17
|
)
|
|
18
|
-
from oscura.quality.scoring import (
|
|
18
|
+
from oscura.validation.quality.scoring import (
|
|
19
19
|
AnalysisQualityScore,
|
|
20
20
|
DataQualityMetrics,
|
|
21
21
|
ReliabilityCategory,
|
|
@@ -24,7 +24,7 @@ from oscura.quality.scoring import (
|
|
|
24
24
|
combine_quality_scores,
|
|
25
25
|
score_analysis_result,
|
|
26
26
|
)
|
|
27
|
-
from oscura.quality.warnings import (
|
|
27
|
+
from oscura.validation.quality.warnings import (
|
|
28
28
|
QualityWarning,
|
|
29
29
|
SignalQualityAnalyzer,
|
|
30
30
|
check_clipping,
|
|
@@ -6,8 +6,8 @@ bias, handle outliers, and provide confidence bounds for more reliable measureme
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
Example:
|
|
9
|
-
>>> from oscura.quality.ensemble import EnsembleAggregator, AggregationMethod
|
|
10
|
-
>>> from oscura.quality.ensemble import create_frequency_ensemble
|
|
9
|
+
>>> from oscura.validation.quality.ensemble import EnsembleAggregator, AggregationMethod
|
|
10
|
+
>>> from oscura.validation.quality.ensemble import create_frequency_ensemble
|
|
11
11
|
>>> # Combine multiple frequency measurements
|
|
12
12
|
>>> result = create_frequency_ensemble(signal, sample_rate=1e9)
|
|
13
13
|
>>> print(f"Frequency: {result.value:.2f} Hz ± {result.confidence*100:.1f}%")
|
|
@@ -38,7 +38,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
38
38
|
import numpy as np
|
|
39
39
|
from scipy import stats
|
|
40
40
|
|
|
41
|
-
from oscura.quality.scoring import AnalysisQualityScore, combine_quality_scores
|
|
41
|
+
from oscura.validation.quality.scoring import AnalysisQualityScore, combine_quality_scores
|
|
42
42
|
|
|
43
43
|
if TYPE_CHECKING:
|
|
44
44
|
from numpy.typing import NDArray
|
|
@@ -233,118 +233,143 @@ class EnsembleAggregator:
|
|
|
233
233
|
if not values:
|
|
234
234
|
raise ValueError("Cannot aggregate empty values list")
|
|
235
235
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
236
|
+
original_results = original_results or [
|
|
237
|
+
{"value": v, "confidence": c} for v, c in zip(values, confidences, strict=False)
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
# Filter outliers
|
|
241
|
+
valid_values, valid_confidences, outlier_indices = self._filter_outliers(
|
|
242
|
+
values, confidences, original_results
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Compute aggregation
|
|
246
|
+
aggregated_value, std_dev = self._compute_aggregated_value(valid_values, valid_confidences)
|
|
247
|
+
|
|
248
|
+
# Compute confidence bounds and agreement
|
|
249
|
+
lower_bound, upper_bound = self._compute_confidence_bounds(
|
|
250
|
+
aggregated_value, std_dev, len(valid_values)
|
|
251
|
+
)
|
|
252
|
+
method_agreement = self._compute_method_agreement(valid_values, aggregated_value, std_dev)
|
|
253
|
+
|
|
254
|
+
# Overall confidence
|
|
255
|
+
overall_confidence = self._compute_overall_confidence(valid_confidences, method_agreement)
|
|
256
|
+
|
|
257
|
+
# Quality scores
|
|
258
|
+
ensemble_quality = self._combine_quality_scores(original_results, confidences)
|
|
259
|
+
|
|
260
|
+
return EnsembleResult(
|
|
261
|
+
value=aggregated_value,
|
|
262
|
+
confidence=overall_confidence,
|
|
263
|
+
lower_bound=lower_bound,
|
|
264
|
+
upper_bound=upper_bound,
|
|
265
|
+
method_agreement=method_agreement,
|
|
266
|
+
individual_results=original_results,
|
|
267
|
+
aggregation_method=self.method,
|
|
268
|
+
quality_score=ensemble_quality,
|
|
269
|
+
outlier_methods=outlier_indices,
|
|
270
|
+
)
|
|
240
271
|
|
|
272
|
+
def _filter_outliers(
|
|
273
|
+
self,
|
|
274
|
+
values: list[float],
|
|
275
|
+
confidences: list[float],
|
|
276
|
+
original_results: list[dict[str, Any]],
|
|
277
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64], list[int]]:
|
|
278
|
+
"""Filter outliers from values and return valid subsets."""
|
|
241
279
|
values_arr = np.array(values, dtype=np.float64)
|
|
242
280
|
confidences_arr = np.array(confidences, dtype=np.float64)
|
|
243
281
|
|
|
244
|
-
# Detect outliers
|
|
245
282
|
outlier_indices = self.detect_outlier_methods(original_results)
|
|
246
283
|
|
|
247
|
-
# Create mask for non-outlier values
|
|
248
284
|
valid_mask = np.ones(len(values), dtype=bool)
|
|
249
285
|
valid_mask[outlier_indices] = False
|
|
250
286
|
|
|
251
|
-
# Use only non-outliers for aggregation
|
|
252
287
|
valid_values = values_arr[valid_mask]
|
|
253
288
|
valid_confidences = confidences_arr[valid_mask]
|
|
254
289
|
|
|
255
290
|
if len(valid_values) == 0:
|
|
256
|
-
# All values are outliers, use all with warning
|
|
257
291
|
logger.warning("All methods detected as outliers, using all values")
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
292
|
+
return values_arr, confidences_arr, []
|
|
293
|
+
|
|
294
|
+
return valid_values, valid_confidences, outlier_indices
|
|
261
295
|
|
|
262
|
-
|
|
296
|
+
def _compute_aggregated_value(
|
|
297
|
+
self, valid_values: NDArray[np.float64], valid_confidences: NDArray[np.float64]
|
|
298
|
+
) -> tuple[float, float]:
|
|
299
|
+
"""Compute aggregated value and standard deviation."""
|
|
263
300
|
if self.method == AggregationMethod.WEIGHTED_AVERAGE:
|
|
264
|
-
# Normalize weights
|
|
265
301
|
weights = valid_confidences / np.sum(valid_confidences)
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
std_dev = np.sqrt(variance)
|
|
302
|
+
value = float(np.sum(valid_values * weights))
|
|
303
|
+
variance = float(np.sum(weights * (valid_values - value) ** 2))
|
|
304
|
+
return value, float(np.sqrt(variance))
|
|
270
305
|
|
|
271
306
|
elif self.method == AggregationMethod.MEDIAN:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
mad
|
|
275
|
-
std_dev = mad * 1.4826 # Scale factor for normal distribution
|
|
307
|
+
value = float(np.median(valid_values))
|
|
308
|
+
mad = float(np.median(np.abs(valid_values - value)))
|
|
309
|
+
return value, mad * 1.4826
|
|
276
310
|
|
|
277
311
|
elif self.method == AggregationMethod.BAYESIAN:
|
|
278
|
-
|
|
279
|
-
# Prior: uniform over range
|
|
280
|
-
# Likelihood: Gaussian with confidence-based variance
|
|
281
|
-
precisions = valid_confidences**2 # Higher confidence = lower variance
|
|
312
|
+
precisions = valid_confidences**2
|
|
282
313
|
total_precision = np.sum(precisions)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
std_dev = float(np.sqrt(variance))
|
|
287
|
-
|
|
288
|
-
else:
|
|
289
|
-
# Fallback to simple average
|
|
290
|
-
aggregated_value = float(np.mean(valid_values))
|
|
291
|
-
std_dev = float(np.std(valid_values))
|
|
292
|
-
|
|
293
|
-
# Compute confidence bounds (95% confidence interval)
|
|
294
|
-
if len(valid_values) > 1:
|
|
295
|
-
# Use t-distribution for small samples
|
|
296
|
-
dof = len(valid_values) - 1
|
|
297
|
-
t_value = stats.t.ppf(0.975, dof) # 95% CI
|
|
298
|
-
margin = t_value * std_dev / np.sqrt(len(valid_values))
|
|
299
|
-
lower_bound = aggregated_value - margin
|
|
300
|
-
upper_bound = aggregated_value + margin
|
|
301
|
-
else:
|
|
302
|
-
lower_bound = aggregated_value
|
|
303
|
-
upper_bound = aggregated_value
|
|
314
|
+
value = float(np.sum(valid_values * precisions) / total_precision)
|
|
315
|
+
std_dev = float(np.sqrt(1.0 / total_precision))
|
|
316
|
+
return value, std_dev
|
|
304
317
|
|
|
305
|
-
# Compute method agreement (inverse of coefficient of variation)
|
|
306
|
-
if len(valid_values) > 1 and aggregated_value != 0:
|
|
307
|
-
cv = std_dev / abs(aggregated_value)
|
|
308
|
-
method_agreement = float(np.clip(1.0 - cv, 0.0, 1.0))
|
|
309
318
|
else:
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
319
|
+
return float(np.mean(valid_values)), float(np.std(valid_values))
|
|
320
|
+
|
|
321
|
+
def _compute_confidence_bounds(
|
|
322
|
+
self, value: float, std_dev: float, n_values: int
|
|
323
|
+
) -> tuple[float, float]:
|
|
324
|
+
"""Compute 95% confidence interval bounds."""
|
|
325
|
+
if n_values <= 1:
|
|
326
|
+
return value, value
|
|
327
|
+
|
|
328
|
+
dof = n_values - 1
|
|
329
|
+
t_value = stats.t.ppf(0.975, dof)
|
|
330
|
+
margin = t_value * std_dev / np.sqrt(n_values)
|
|
331
|
+
return value - margin, value + margin
|
|
332
|
+
|
|
333
|
+
def _compute_method_agreement(
|
|
334
|
+
self, valid_values: NDArray[np.float64], value: float, std_dev: float
|
|
335
|
+
) -> float:
|
|
336
|
+
"""Compute method agreement from coefficient of variation."""
|
|
337
|
+
if len(valid_values) <= 1 or value == 0:
|
|
338
|
+
return 1.0
|
|
339
|
+
|
|
340
|
+
cv = std_dev / abs(value)
|
|
341
|
+
return float(np.clip(1.0 - cv, 0.0, 1.0))
|
|
342
|
+
|
|
343
|
+
def _compute_overall_confidence(
|
|
344
|
+
self, valid_confidences: NDArray[np.float64], method_agreement: float
|
|
345
|
+
) -> float:
|
|
346
|
+
"""Compute overall confidence with agreement penalty."""
|
|
347
|
+
confidence = float(np.mean(valid_confidences))
|
|
314
348
|
|
|
315
|
-
# Penalize confidence if agreement is low
|
|
316
349
|
if method_agreement < self.min_agreement:
|
|
317
|
-
|
|
350
|
+
confidence *= method_agreement
|
|
318
351
|
logger.warning(
|
|
319
352
|
f"Low method agreement ({method_agreement:.2f}), "
|
|
320
|
-
f"reduced confidence to {
|
|
353
|
+
f"reduced confidence to {confidence:.2f}"
|
|
321
354
|
)
|
|
322
355
|
|
|
323
|
-
|
|
356
|
+
return confidence
|
|
357
|
+
|
|
358
|
+
def _combine_quality_scores(
|
|
359
|
+
self, original_results: list[dict[str, Any]], confidences: list[float]
|
|
360
|
+
) -> AnalysisQualityScore | None:
|
|
361
|
+
"""Combine quality scores if available."""
|
|
324
362
|
quality_scores_raw = [
|
|
325
363
|
r.get("quality_score") for r in original_results if "quality_score" in r
|
|
326
364
|
]
|
|
327
|
-
|
|
328
|
-
if quality_scores_raw
|
|
365
|
+
|
|
366
|
+
if not quality_scores_raw or not all(
|
|
329
367
|
isinstance(q, AnalysisQualityScore) for q in quality_scores_raw
|
|
330
368
|
):
|
|
331
|
-
|
|
332
|
-
quality_scores: list[AnalysisQualityScore] = quality_scores_raw # type: ignore[assignment]
|
|
333
|
-
ensemble_quality = combine_quality_scores(
|
|
334
|
-
quality_scores, weights=confidences[: len(quality_scores)]
|
|
335
|
-
)
|
|
369
|
+
return None
|
|
336
370
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
confidence=overall_confidence,
|
|
340
|
-
lower_bound=lower_bound,
|
|
341
|
-
upper_bound=upper_bound,
|
|
342
|
-
method_agreement=method_agreement,
|
|
343
|
-
individual_results=original_results,
|
|
344
|
-
aggregation_method=self.method,
|
|
345
|
-
quality_score=ensemble_quality,
|
|
346
|
-
outlier_methods=outlier_indices,
|
|
347
|
-
)
|
|
371
|
+
quality_scores: list[AnalysisQualityScore] = quality_scores_raw # type: ignore[assignment]
|
|
372
|
+
return combine_quality_scores(quality_scores, weights=confidences[: len(quality_scores)])
|
|
348
373
|
|
|
349
374
|
def aggregate_categorical(
|
|
350
375
|
self,
|
|
@@ -517,7 +542,7 @@ def create_frequency_ensemble(
|
|
|
517
542
|
EnsembleResult with combined frequency estimate.
|
|
518
543
|
|
|
519
544
|
Raises:
|
|
520
|
-
ValueError: If all frequency detection methods
|
|
545
|
+
ValueError: If all frequency detection methods failed.
|
|
521
546
|
|
|
522
547
|
Example:
|
|
523
548
|
>>> import numpy as np
|
|
@@ -531,80 +556,96 @@ def create_frequency_ensemble(
|
|
|
531
556
|
method_weights = FREQUENCY_ENSEMBLE
|
|
532
557
|
|
|
533
558
|
results = []
|
|
559
|
+
results.extend(_try_fft_frequency(signal, sample_rate, method_weights[0][1]))
|
|
560
|
+
results.extend(_try_zero_crossing_frequency(signal, sample_rate, method_weights[1][1]))
|
|
561
|
+
results.extend(_try_autocorr_frequency(signal, sample_rate, method_weights[2][1]))
|
|
562
|
+
|
|
563
|
+
if not results:
|
|
564
|
+
raise ValueError("All frequency detection methods failed")
|
|
565
|
+
|
|
566
|
+
aggregator = EnsembleAggregator(method=AggregationMethod.WEIGHTED_AVERAGE)
|
|
567
|
+
return aggregator.aggregate(results)
|
|
534
568
|
|
|
535
|
-
|
|
569
|
+
|
|
570
|
+
def _try_fft_frequency(
|
|
571
|
+
signal: NDArray[np.float64], sample_rate: float, weight: float
|
|
572
|
+
) -> list[dict[str, Any]]:
|
|
573
|
+
"""Try FFT peak detection for frequency estimation."""
|
|
536
574
|
try:
|
|
537
575
|
fft_result = np.fft.rfft(signal)
|
|
538
576
|
freqs = np.fft.rfftfreq(len(signal), d=1.0 / sample_rate)
|
|
539
577
|
peak_idx = np.argmax(np.abs(fft_result[1:])) + 1 # Skip DC
|
|
540
578
|
freq_fft = float(freqs[peak_idx])
|
|
579
|
+
|
|
541
580
|
# Confidence based on peak prominence
|
|
542
581
|
peak_magnitude = np.abs(fft_result[peak_idx])
|
|
543
582
|
mean_magnitude = np.mean(np.abs(fft_result[1:]))
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
"value": freq_fft,
|
|
548
|
-
"confidence": confidence_fft * method_weights[0][1],
|
|
549
|
-
"method": "fft_peak",
|
|
550
|
-
}
|
|
551
|
-
)
|
|
583
|
+
confidence = min(1.0, peak_magnitude / (mean_magnitude * 10))
|
|
584
|
+
|
|
585
|
+
return [{"value": freq_fft, "confidence": confidence * weight, "method": "fft_peak"}]
|
|
552
586
|
except Exception as e:
|
|
553
587
|
logger.debug(f"FFT peak detection failed: {e}")
|
|
588
|
+
return []
|
|
554
589
|
|
|
555
|
-
|
|
590
|
+
|
|
591
|
+
def _try_zero_crossing_frequency(
|
|
592
|
+
signal: NDArray[np.float64], sample_rate: float, weight: float
|
|
593
|
+
) -> list[dict[str, Any]]:
|
|
594
|
+
"""Try zero crossing rate for frequency estimation."""
|
|
556
595
|
try:
|
|
557
596
|
zero_crossings = np.where(np.diff(np.sign(signal)))[0]
|
|
558
|
-
if len(zero_crossings)
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
597
|
+
if len(zero_crossings) <= 1:
|
|
598
|
+
return []
|
|
599
|
+
|
|
600
|
+
# Average time between zero crossings (half period)
|
|
601
|
+
avg_half_period = np.mean(np.diff(zero_crossings)) / sample_rate
|
|
602
|
+
freq_zc = 1.0 / (2.0 * avg_half_period)
|
|
603
|
+
|
|
604
|
+
# Confidence based on regularity of crossings
|
|
605
|
+
std_half_period = np.std(np.diff(zero_crossings)) / sample_rate
|
|
606
|
+
confidence = max(0.0, 1.0 - std_half_period / avg_half_period)
|
|
607
|
+
|
|
608
|
+
return [
|
|
609
|
+
{"value": float(freq_zc), "confidence": confidence * weight, "method": "zero_crossing"}
|
|
610
|
+
]
|
|
572
611
|
except Exception as e:
|
|
573
612
|
logger.debug(f"Zero crossing detection failed: {e}")
|
|
613
|
+
return []
|
|
574
614
|
|
|
575
|
-
|
|
615
|
+
|
|
616
|
+
def _try_autocorr_frequency(
|
|
617
|
+
signal: NDArray[np.float64], sample_rate: float, weight: float
|
|
618
|
+
) -> list[dict[str, Any]]:
|
|
619
|
+
"""Try autocorrelation for frequency estimation."""
|
|
576
620
|
try:
|
|
577
|
-
# Compute autocorrelation
|
|
578
621
|
autocorr = np.correlate(signal, signal, mode="full")
|
|
579
622
|
autocorr = autocorr[len(autocorr) // 2 :]
|
|
580
|
-
# Find first peak after zero lag (skip DC)
|
|
581
|
-
peaks = []
|
|
582
|
-
for i in range(1, min(len(autocorr) - 1, len(signal) // 2)):
|
|
583
|
-
if autocorr[i] > autocorr[i - 1] and autocorr[i] > autocorr[i + 1]:
|
|
584
|
-
peaks.append(i)
|
|
585
|
-
if peaks:
|
|
586
|
-
first_peak = peaks[0]
|
|
587
|
-
period_samples = first_peak
|
|
588
|
-
freq_ac = sample_rate / period_samples
|
|
589
|
-
# Confidence based on peak strength
|
|
590
|
-
peak_strength = autocorr[first_peak] / autocorr[0]
|
|
591
|
-
confidence_ac = float(np.clip(peak_strength, 0.0, 1.0))
|
|
592
|
-
results.append(
|
|
593
|
-
{
|
|
594
|
-
"value": float(freq_ac),
|
|
595
|
-
"confidence": confidence_ac * method_weights[2][1],
|
|
596
|
-
"method": "autocorrelation",
|
|
597
|
-
}
|
|
598
|
-
)
|
|
599
|
-
except Exception as e:
|
|
600
|
-
logger.debug(f"Autocorrelation detection failed: {e}")
|
|
601
623
|
|
|
602
|
-
|
|
603
|
-
|
|
624
|
+
# Find first peak after zero lag
|
|
625
|
+
peaks = [
|
|
626
|
+
i
|
|
627
|
+
for i in range(1, min(len(autocorr) - 1, len(signal) // 2))
|
|
628
|
+
if autocorr[i] > autocorr[i - 1] and autocorr[i] > autocorr[i + 1]
|
|
629
|
+
]
|
|
604
630
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
631
|
+
if not peaks:
|
|
632
|
+
return []
|
|
633
|
+
|
|
634
|
+
first_peak = peaks[0]
|
|
635
|
+
freq_ac = sample_rate / first_peak
|
|
636
|
+
peak_strength = autocorr[first_peak] / autocorr[0]
|
|
637
|
+
confidence = float(np.clip(peak_strength, 0.0, 1.0))
|
|
638
|
+
|
|
639
|
+
return [
|
|
640
|
+
{
|
|
641
|
+
"value": float(freq_ac),
|
|
642
|
+
"confidence": confidence * weight,
|
|
643
|
+
"method": "autocorrelation",
|
|
644
|
+
}
|
|
645
|
+
]
|
|
646
|
+
except Exception as e:
|
|
647
|
+
logger.debug(f"Autocorrelation detection failed: {e}")
|
|
648
|
+
return []
|
|
608
649
|
|
|
609
650
|
|
|
610
651
|
def create_edge_ensemble(
|
|
@@ -643,89 +684,128 @@ def create_edge_ensemble(
|
|
|
643
684
|
threshold = float((np.max(signal) + np.min(signal)) / 2.0)
|
|
644
685
|
|
|
645
686
|
results = []
|
|
687
|
+
results.extend(_detect_threshold_crossing(signal, threshold, method_weights[0][1]))
|
|
688
|
+
results.extend(_detect_derivative_edges(signal, method_weights[1][1]))
|
|
689
|
+
results.extend(_detect_schmitt_trigger(signal, threshold, method_weights[2][1]))
|
|
646
690
|
|
|
647
|
-
|
|
691
|
+
if not results:
|
|
692
|
+
raise ValueError("All edge detection methods failed")
|
|
693
|
+
|
|
694
|
+
aggregator = EnsembleAggregator(method=AggregationMethod.MEDIAN)
|
|
695
|
+
return aggregator.aggregate(results)
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def _detect_threshold_crossing(
|
|
699
|
+
signal: NDArray[np.float64], threshold: float, weight: float
|
|
700
|
+
) -> list[dict[str, Any]]:
|
|
701
|
+
"""Detect edges via threshold crossing.
|
|
702
|
+
|
|
703
|
+
Args:
|
|
704
|
+
signal: Input signal.
|
|
705
|
+
threshold: Detection threshold.
|
|
706
|
+
weight: Method weight for confidence.
|
|
707
|
+
|
|
708
|
+
Returns:
|
|
709
|
+
List with single result dict or empty list if failed.
|
|
710
|
+
"""
|
|
648
711
|
try:
|
|
649
712
|
crossings = np.where(np.diff(np.sign(signal - threshold)))[0]
|
|
650
|
-
|
|
651
|
-
|
|
713
|
+
edge_count = len(crossings)
|
|
714
|
+
|
|
652
715
|
signal_range = np.ptp(signal)
|
|
653
716
|
noise_estimate = np.std(np.diff(signal))
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
results.append(
|
|
717
|
+
confidence = min(1.0, signal_range / (noise_estimate * 10)) if noise_estimate > 0 else 0.5
|
|
718
|
+
|
|
719
|
+
return [
|
|
658
720
|
{
|
|
659
|
-
"value":
|
|
660
|
-
"confidence":
|
|
721
|
+
"value": edge_count,
|
|
722
|
+
"confidence": confidence * weight,
|
|
661
723
|
"method": "threshold_crossing",
|
|
662
724
|
}
|
|
663
|
-
|
|
725
|
+
]
|
|
664
726
|
except Exception as e:
|
|
665
727
|
logger.debug(f"Threshold crossing detection failed: {e}")
|
|
728
|
+
return []
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def _detect_derivative_edges(signal: NDArray[np.float64], weight: float) -> list[dict[str, Any]]:
|
|
732
|
+
"""Detect edges via derivative peak detection.
|
|
733
|
+
|
|
734
|
+
Args:
|
|
735
|
+
signal: Input signal.
|
|
736
|
+
weight: Method weight for confidence.
|
|
666
737
|
|
|
667
|
-
|
|
738
|
+
Returns:
|
|
739
|
+
List with single result dict or empty list if failed.
|
|
740
|
+
"""
|
|
668
741
|
try:
|
|
669
742
|
derivative = np.diff(signal)
|
|
670
|
-
# Find peaks in absolute derivative
|
|
671
743
|
deriv_std = np.std(derivative)
|
|
672
744
|
deriv_threshold = deriv_std * 2
|
|
673
745
|
edge_indices = np.where(np.abs(derivative) > deriv_threshold)[0]
|
|
746
|
+
|
|
674
747
|
# Remove consecutive detections (within 2 samples)
|
|
675
748
|
filtered_edges = []
|
|
676
749
|
for i, idx in enumerate(edge_indices):
|
|
677
750
|
if i == 0 or idx - edge_indices[i - 1] > 2:
|
|
678
751
|
filtered_edges.append(idx)
|
|
679
|
-
|
|
680
|
-
# Confidence based on peak derivative prominence above threshold
|
|
681
|
-
# Higher max derivative relative to threshold means clearer edges
|
|
752
|
+
|
|
682
753
|
max_deriv = np.max(np.abs(derivative)) if len(derivative) > 0 else 0.0
|
|
683
754
|
prominence_ratio = (max_deriv / deriv_threshold) if deriv_threshold > 0 else 0.0
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
results.append(
|
|
755
|
+
confidence = float(np.clip(prominence_ratio / 3.0, 0.0, 1.0))
|
|
756
|
+
|
|
757
|
+
return [
|
|
688
758
|
{
|
|
689
|
-
"value":
|
|
690
|
-
"confidence":
|
|
759
|
+
"value": len(filtered_edges),
|
|
760
|
+
"confidence": confidence * weight,
|
|
691
761
|
"method": "derivative",
|
|
692
762
|
}
|
|
693
|
-
|
|
763
|
+
]
|
|
694
764
|
except Exception as e:
|
|
695
765
|
logger.debug(f"Derivative edge detection failed: {e}")
|
|
766
|
+
return []
|
|
767
|
+
|
|
696
768
|
|
|
697
|
-
|
|
769
|
+
def _detect_schmitt_trigger(
|
|
770
|
+
signal: NDArray[np.float64], threshold: float, weight: float
|
|
771
|
+
) -> list[dict[str, Any]]:
|
|
772
|
+
"""Detect edges via Schmitt trigger with hysteresis.
|
|
773
|
+
|
|
774
|
+
Args:
|
|
775
|
+
signal: Input signal.
|
|
776
|
+
threshold: Base threshold.
|
|
777
|
+
weight: Method weight for confidence.
|
|
778
|
+
|
|
779
|
+
Returns:
|
|
780
|
+
List with single result dict or empty list if failed.
|
|
781
|
+
"""
|
|
698
782
|
try:
|
|
699
783
|
hysteresis = float(np.std(signal) * 0.1)
|
|
700
784
|
thresh_high = threshold + hysteresis
|
|
701
785
|
thresh_low = threshold - hysteresis
|
|
702
786
|
state = signal[0] > threshold
|
|
703
|
-
|
|
787
|
+
edge_count = 0
|
|
788
|
+
|
|
704
789
|
for val in signal:
|
|
705
790
|
if not state and val > thresh_high:
|
|
706
|
-
|
|
791
|
+
edge_count += 1
|
|
707
792
|
state = True
|
|
708
793
|
elif state and val < thresh_low:
|
|
709
|
-
|
|
794
|
+
edge_count += 1
|
|
710
795
|
state = False
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
796
|
+
|
|
797
|
+
confidence = 0.7 # Lower base confidence due to hysteresis delay
|
|
798
|
+
|
|
799
|
+
return [
|
|
714
800
|
{
|
|
715
|
-
"value":
|
|
716
|
-
"confidence":
|
|
801
|
+
"value": edge_count,
|
|
802
|
+
"confidence": confidence * weight,
|
|
717
803
|
"method": "schmitt_trigger",
|
|
718
804
|
}
|
|
719
|
-
|
|
805
|
+
]
|
|
720
806
|
except Exception as e:
|
|
721
807
|
logger.debug(f"Schmitt trigger detection failed: {e}")
|
|
722
|
-
|
|
723
|
-
if not results:
|
|
724
|
-
raise ValueError("All edge detection methods failed")
|
|
725
|
-
|
|
726
|
-
# Aggregate results (use median for integer counts)
|
|
727
|
-
aggregator = EnsembleAggregator(method=AggregationMethod.MEDIAN)
|
|
728
|
-
return aggregator.aggregate(results)
|
|
808
|
+
return []
|
|
729
809
|
|
|
730
810
|
|
|
731
811
|
__all__ = [
|
|
@@ -4,8 +4,8 @@ Generates human-readable explanations for why analysis results
|
|
|
4
4
|
are reliable or unreliable.
|
|
5
5
|
|
|
6
6
|
Example:
|
|
7
|
-
>>> from oscura.quality.explainer import explain_result
|
|
8
|
-
>>> from oscura.quality.scoring import calculate_quality_score
|
|
7
|
+
>>> from oscura.validation.quality.explainer import explain_result
|
|
8
|
+
>>> from oscura.validation.quality.scoring import calculate_quality_score
|
|
9
9
|
>>> score = calculate_quality_score(0.9, 0.8, 0.85)
|
|
10
10
|
>>> explanation = explain_result("frequency", 10.5e6, score, "fft")
|
|
11
11
|
>>> print(explanation)
|
|
@@ -20,7 +20,7 @@ from dataclasses import dataclass, field
|
|
|
20
20
|
from typing import TYPE_CHECKING, Any, ClassVar
|
|
21
21
|
|
|
22
22
|
if TYPE_CHECKING:
|
|
23
|
-
from oscura.quality.scoring import AnalysisQualityScore
|
|
23
|
+
from oscura.validation.quality.scoring import AnalysisQualityScore
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
@dataclass
|
|
@@ -5,7 +5,7 @@ analysis results, enabling users to assess confidence in automated findings.
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
Example:
|
|
8
|
-
>>> from oscura.quality.scoring import AnalysisQualityScore, ReliabilityCategory
|
|
8
|
+
>>> from oscura.validation.quality.scoring import AnalysisQualityScore, ReliabilityCategory
|
|
9
9
|
>>> score = AnalysisQualityScore(
|
|
10
10
|
... confidence=0.85,
|
|
11
11
|
... category=ReliabilityCategory.HIGH,
|
|
@@ -5,7 +5,7 @@ including clipping, noise, saturation, and undersampling.
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
Example:
|
|
8
|
-
>>> from oscura.quality.warnings import SignalQualityAnalyzer
|
|
8
|
+
>>> from oscura.validation.quality.warnings import SignalQualityAnalyzer
|
|
9
9
|
>>> analyzer = SignalQualityAnalyzer()
|
|
10
10
|
>>> warnings = analyzer.analyze(trace)
|
|
11
11
|
>>> for warning in warnings:
|
|
@@ -91,7 +91,7 @@ class SignalQualityAnalyzer:
|
|
|
91
91
|
nyquist_factor: Factor for Nyquist frequency check (default: 2.0)
|
|
92
92
|
|
|
93
93
|
Example:
|
|
94
|
-
>>> from oscura.quality.warnings import SignalQualityAnalyzer
|
|
94
|
+
>>> from oscura.validation.quality.warnings import SignalQualityAnalyzer
|
|
95
95
|
>>> analyzer = SignalQualityAnalyzer(clip_threshold=0.95)
|
|
96
96
|
>>> warnings = analyzer.analyze(trace)
|
|
97
97
|
>>> if warnings:
|
|
@@ -152,9 +152,9 @@ class SignalQualityAnalyzer:
|
|
|
152
152
|
"""
|
|
153
153
|
# Extract data and sample rate
|
|
154
154
|
if hasattr(trace, "data"):
|
|
155
|
-
data = trace.data
|
|
155
|
+
data = trace.data
|
|
156
156
|
if sample_rate is None and hasattr(trace, "metadata"):
|
|
157
|
-
sample_rate = trace.metadata.sample_rate
|
|
157
|
+
sample_rate = trace.metadata.sample_rate
|
|
158
158
|
else:
|
|
159
159
|
data = trace # type: ignore[assignment]
|
|
160
160
|
|