oscura 0.0.1__py3-none-any.whl → 0.1.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 +813 -8
- oscura/__main__.py +392 -0
- oscura/analyzers/__init__.py +37 -0
- oscura/analyzers/digital/__init__.py +177 -0
- oscura/analyzers/digital/bus.py +691 -0
- oscura/analyzers/digital/clock.py +805 -0
- oscura/analyzers/digital/correlation.py +720 -0
- oscura/analyzers/digital/edges.py +632 -0
- oscura/analyzers/digital/extraction.py +413 -0
- oscura/analyzers/digital/quality.py +878 -0
- oscura/analyzers/digital/signal_quality.py +877 -0
- oscura/analyzers/digital/thresholds.py +708 -0
- oscura/analyzers/digital/timing.py +1104 -0
- oscura/analyzers/eye/__init__.py +46 -0
- oscura/analyzers/eye/diagram.py +434 -0
- oscura/analyzers/eye/metrics.py +555 -0
- oscura/analyzers/jitter/__init__.py +83 -0
- oscura/analyzers/jitter/ber.py +333 -0
- oscura/analyzers/jitter/decomposition.py +759 -0
- oscura/analyzers/jitter/measurements.py +413 -0
- oscura/analyzers/jitter/spectrum.py +220 -0
- oscura/analyzers/measurements.py +40 -0
- oscura/analyzers/packet/__init__.py +171 -0
- oscura/analyzers/packet/daq.py +1077 -0
- oscura/analyzers/packet/metrics.py +437 -0
- oscura/analyzers/packet/parser.py +327 -0
- oscura/analyzers/packet/payload.py +2156 -0
- oscura/analyzers/packet/payload_analysis.py +1312 -0
- oscura/analyzers/packet/payload_extraction.py +236 -0
- oscura/analyzers/packet/payload_patterns.py +670 -0
- oscura/analyzers/packet/stream.py +359 -0
- oscura/analyzers/patterns/__init__.py +266 -0
- oscura/analyzers/patterns/clustering.py +1036 -0
- oscura/analyzers/patterns/discovery.py +539 -0
- oscura/analyzers/patterns/learning.py +797 -0
- oscura/analyzers/patterns/matching.py +1091 -0
- oscura/analyzers/patterns/periodic.py +650 -0
- oscura/analyzers/patterns/sequences.py +767 -0
- oscura/analyzers/power/__init__.py +116 -0
- oscura/analyzers/power/ac_power.py +391 -0
- oscura/analyzers/power/basic.py +383 -0
- oscura/analyzers/power/conduction.py +314 -0
- oscura/analyzers/power/efficiency.py +297 -0
- oscura/analyzers/power/ripple.py +356 -0
- oscura/analyzers/power/soa.py +372 -0
- oscura/analyzers/power/switching.py +479 -0
- oscura/analyzers/protocol/__init__.py +150 -0
- oscura/analyzers/protocols/__init__.py +150 -0
- oscura/analyzers/protocols/base.py +500 -0
- oscura/analyzers/protocols/can.py +620 -0
- oscura/analyzers/protocols/can_fd.py +448 -0
- oscura/analyzers/protocols/flexray.py +405 -0
- oscura/analyzers/protocols/hdlc.py +399 -0
- oscura/analyzers/protocols/i2c.py +368 -0
- oscura/analyzers/protocols/i2s.py +296 -0
- oscura/analyzers/protocols/jtag.py +393 -0
- oscura/analyzers/protocols/lin.py +445 -0
- oscura/analyzers/protocols/manchester.py +333 -0
- oscura/analyzers/protocols/onewire.py +501 -0
- oscura/analyzers/protocols/spi.py +334 -0
- oscura/analyzers/protocols/swd.py +325 -0
- oscura/analyzers/protocols/uart.py +393 -0
- oscura/analyzers/protocols/usb.py +495 -0
- oscura/analyzers/signal_integrity/__init__.py +63 -0
- oscura/analyzers/signal_integrity/embedding.py +294 -0
- oscura/analyzers/signal_integrity/equalization.py +370 -0
- oscura/analyzers/signal_integrity/sparams.py +484 -0
- oscura/analyzers/spectral/__init__.py +53 -0
- oscura/analyzers/spectral/chunked.py +273 -0
- oscura/analyzers/spectral/chunked_fft.py +571 -0
- oscura/analyzers/spectral/chunked_wavelet.py +391 -0
- oscura/analyzers/spectral/fft.py +92 -0
- oscura/analyzers/statistical/__init__.py +250 -0
- oscura/analyzers/statistical/checksum.py +923 -0
- oscura/analyzers/statistical/chunked_corr.py +228 -0
- oscura/analyzers/statistical/classification.py +778 -0
- oscura/analyzers/statistical/entropy.py +1113 -0
- oscura/analyzers/statistical/ngrams.py +614 -0
- oscura/analyzers/statistics/__init__.py +119 -0
- oscura/analyzers/statistics/advanced.py +885 -0
- oscura/analyzers/statistics/basic.py +263 -0
- oscura/analyzers/statistics/correlation.py +630 -0
- oscura/analyzers/statistics/distribution.py +298 -0
- oscura/analyzers/statistics/outliers.py +463 -0
- oscura/analyzers/statistics/streaming.py +93 -0
- oscura/analyzers/statistics/trend.py +520 -0
- oscura/analyzers/validation.py +598 -0
- oscura/analyzers/waveform/__init__.py +36 -0
- oscura/analyzers/waveform/measurements.py +943 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
- oscura/analyzers/waveform/spectral.py +1689 -0
- oscura/analyzers/waveform/wavelets.py +298 -0
- oscura/api/__init__.py +62 -0
- oscura/api/dsl.py +538 -0
- oscura/api/fluent.py +571 -0
- oscura/api/operators.py +498 -0
- oscura/api/optimization.py +392 -0
- oscura/api/profiling.py +396 -0
- oscura/automotive/__init__.py +73 -0
- oscura/automotive/can/__init__.py +52 -0
- oscura/automotive/can/analysis.py +356 -0
- oscura/automotive/can/checksum.py +250 -0
- oscura/automotive/can/correlation.py +212 -0
- oscura/automotive/can/discovery.py +355 -0
- oscura/automotive/can/message_wrapper.py +375 -0
- oscura/automotive/can/models.py +385 -0
- oscura/automotive/can/patterns.py +381 -0
- oscura/automotive/can/session.py +452 -0
- oscura/automotive/can/state_machine.py +300 -0
- oscura/automotive/can/stimulus_response.py +461 -0
- oscura/automotive/dbc/__init__.py +15 -0
- oscura/automotive/dbc/generator.py +156 -0
- oscura/automotive/dbc/parser.py +146 -0
- oscura/automotive/dtc/__init__.py +30 -0
- oscura/automotive/dtc/database.py +3036 -0
- oscura/automotive/j1939/__init__.py +14 -0
- oscura/automotive/j1939/decoder.py +745 -0
- oscura/automotive/loaders/__init__.py +35 -0
- oscura/automotive/loaders/asc.py +98 -0
- oscura/automotive/loaders/blf.py +77 -0
- oscura/automotive/loaders/csv_can.py +136 -0
- oscura/automotive/loaders/dispatcher.py +136 -0
- oscura/automotive/loaders/mdf.py +331 -0
- oscura/automotive/loaders/pcap.py +132 -0
- oscura/automotive/obd/__init__.py +14 -0
- oscura/automotive/obd/decoder.py +707 -0
- oscura/automotive/uds/__init__.py +48 -0
- oscura/automotive/uds/decoder.py +265 -0
- oscura/automotive/uds/models.py +64 -0
- oscura/automotive/visualization.py +369 -0
- oscura/batch/__init__.py +55 -0
- oscura/batch/advanced.py +627 -0
- oscura/batch/aggregate.py +300 -0
- oscura/batch/analyze.py +139 -0
- oscura/batch/logging.py +487 -0
- oscura/batch/metrics.py +556 -0
- oscura/builders/__init__.py +41 -0
- oscura/builders/signal_builder.py +1131 -0
- oscura/cli/__init__.py +14 -0
- oscura/cli/batch.py +339 -0
- oscura/cli/characterize.py +273 -0
- oscura/cli/compare.py +775 -0
- oscura/cli/decode.py +551 -0
- oscura/cli/main.py +247 -0
- oscura/cli/shell.py +350 -0
- oscura/comparison/__init__.py +66 -0
- oscura/comparison/compare.py +397 -0
- oscura/comparison/golden.py +487 -0
- oscura/comparison/limits.py +391 -0
- oscura/comparison/mask.py +434 -0
- oscura/comparison/trace_diff.py +30 -0
- oscura/comparison/visualization.py +481 -0
- oscura/compliance/__init__.py +70 -0
- oscura/compliance/advanced.py +756 -0
- oscura/compliance/masks.py +363 -0
- oscura/compliance/reporting.py +483 -0
- oscura/compliance/testing.py +298 -0
- oscura/component/__init__.py +38 -0
- oscura/component/impedance.py +365 -0
- oscura/component/reactive.py +598 -0
- oscura/component/transmission_line.py +312 -0
- oscura/config/__init__.py +191 -0
- oscura/config/defaults.py +254 -0
- oscura/config/loader.py +348 -0
- oscura/config/memory.py +271 -0
- oscura/config/migration.py +458 -0
- oscura/config/pipeline.py +1077 -0
- oscura/config/preferences.py +530 -0
- oscura/config/protocol.py +875 -0
- oscura/config/schema.py +713 -0
- oscura/config/settings.py +420 -0
- oscura/config/thresholds.py +599 -0
- oscura/convenience.py +457 -0
- oscura/core/__init__.py +299 -0
- oscura/core/audit.py +457 -0
- oscura/core/backend_selector.py +405 -0
- oscura/core/cache.py +590 -0
- oscura/core/cancellation.py +439 -0
- oscura/core/confidence.py +225 -0
- oscura/core/config.py +506 -0
- oscura/core/correlation.py +216 -0
- oscura/core/cross_domain.py +422 -0
- oscura/core/debug.py +301 -0
- oscura/core/edge_cases.py +541 -0
- oscura/core/exceptions.py +535 -0
- oscura/core/gpu_backend.py +523 -0
- oscura/core/lazy.py +832 -0
- oscura/core/log_query.py +540 -0
- oscura/core/logging.py +931 -0
- oscura/core/logging_advanced.py +952 -0
- oscura/core/memoize.py +171 -0
- oscura/core/memory_check.py +274 -0
- oscura/core/memory_guard.py +290 -0
- oscura/core/memory_limits.py +336 -0
- oscura/core/memory_monitor.py +453 -0
- oscura/core/memory_progress.py +465 -0
- oscura/core/memory_warnings.py +315 -0
- oscura/core/numba_backend.py +362 -0
- oscura/core/performance.py +352 -0
- oscura/core/progress.py +524 -0
- oscura/core/provenance.py +358 -0
- oscura/core/results.py +331 -0
- oscura/core/types.py +504 -0
- oscura/core/uncertainty.py +383 -0
- oscura/discovery/__init__.py +52 -0
- oscura/discovery/anomaly_detector.py +672 -0
- oscura/discovery/auto_decoder.py +415 -0
- oscura/discovery/comparison.py +497 -0
- oscura/discovery/quality_validator.py +528 -0
- oscura/discovery/signal_detector.py +769 -0
- oscura/dsl/__init__.py +73 -0
- oscura/dsl/commands.py +246 -0
- oscura/dsl/interpreter.py +455 -0
- oscura/dsl/parser.py +689 -0
- oscura/dsl/repl.py +172 -0
- oscura/exceptions.py +59 -0
- oscura/exploratory/__init__.py +111 -0
- oscura/exploratory/error_recovery.py +642 -0
- oscura/exploratory/fuzzy.py +513 -0
- oscura/exploratory/fuzzy_advanced.py +786 -0
- oscura/exploratory/legacy.py +831 -0
- oscura/exploratory/parse.py +358 -0
- oscura/exploratory/recovery.py +275 -0
- oscura/exploratory/sync.py +382 -0
- oscura/exploratory/unknown.py +707 -0
- oscura/export/__init__.py +25 -0
- oscura/export/wireshark/README.md +265 -0
- oscura/export/wireshark/__init__.py +47 -0
- oscura/export/wireshark/generator.py +312 -0
- oscura/export/wireshark/lua_builder.py +159 -0
- oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
- oscura/export/wireshark/type_mapping.py +165 -0
- oscura/export/wireshark/validator.py +105 -0
- oscura/exporters/__init__.py +94 -0
- oscura/exporters/csv.py +303 -0
- oscura/exporters/exporters.py +44 -0
- oscura/exporters/hdf5.py +219 -0
- oscura/exporters/html_export.py +701 -0
- oscura/exporters/json_export.py +291 -0
- oscura/exporters/markdown_export.py +367 -0
- oscura/exporters/matlab_export.py +354 -0
- oscura/exporters/npz_export.py +219 -0
- oscura/exporters/spice_export.py +210 -0
- oscura/extensibility/__init__.py +131 -0
- oscura/extensibility/docs.py +752 -0
- oscura/extensibility/extensions.py +1125 -0
- oscura/extensibility/logging.py +259 -0
- oscura/extensibility/measurements.py +485 -0
- oscura/extensibility/plugins.py +414 -0
- oscura/extensibility/registry.py +346 -0
- oscura/extensibility/templates.py +913 -0
- oscura/extensibility/validation.py +651 -0
- oscura/filtering/__init__.py +89 -0
- oscura/filtering/base.py +563 -0
- oscura/filtering/convenience.py +564 -0
- oscura/filtering/design.py +725 -0
- oscura/filtering/filters.py +32 -0
- oscura/filtering/introspection.py +605 -0
- oscura/guidance/__init__.py +24 -0
- oscura/guidance/recommender.py +429 -0
- oscura/guidance/wizard.py +518 -0
- oscura/inference/__init__.py +251 -0
- oscura/inference/active_learning/README.md +153 -0
- oscura/inference/active_learning/__init__.py +38 -0
- oscura/inference/active_learning/lstar.py +257 -0
- oscura/inference/active_learning/observation_table.py +230 -0
- oscura/inference/active_learning/oracle.py +78 -0
- oscura/inference/active_learning/teachers/__init__.py +15 -0
- oscura/inference/active_learning/teachers/simulator.py +192 -0
- oscura/inference/adaptive_tuning.py +453 -0
- oscura/inference/alignment.py +653 -0
- oscura/inference/bayesian.py +943 -0
- oscura/inference/binary.py +1016 -0
- oscura/inference/crc_reverse.py +711 -0
- oscura/inference/logic.py +288 -0
- oscura/inference/message_format.py +1305 -0
- oscura/inference/protocol.py +417 -0
- oscura/inference/protocol_dsl.py +1084 -0
- oscura/inference/protocol_library.py +1230 -0
- oscura/inference/sequences.py +809 -0
- oscura/inference/signal_intelligence.py +1509 -0
- oscura/inference/spectral.py +215 -0
- oscura/inference/state_machine.py +634 -0
- oscura/inference/stream.py +918 -0
- oscura/integrations/__init__.py +59 -0
- oscura/integrations/llm.py +1827 -0
- oscura/jupyter/__init__.py +32 -0
- oscura/jupyter/display.py +268 -0
- oscura/jupyter/magic.py +334 -0
- oscura/loaders/__init__.py +526 -0
- oscura/loaders/binary.py +69 -0
- oscura/loaders/configurable.py +1255 -0
- oscura/loaders/csv.py +26 -0
- oscura/loaders/csv_loader.py +473 -0
- oscura/loaders/hdf5.py +9 -0
- oscura/loaders/hdf5_loader.py +510 -0
- oscura/loaders/lazy.py +370 -0
- oscura/loaders/mmap_loader.py +583 -0
- oscura/loaders/numpy_loader.py +436 -0
- oscura/loaders/pcap.py +432 -0
- oscura/loaders/preprocessing.py +368 -0
- oscura/loaders/rigol.py +287 -0
- oscura/loaders/sigrok.py +321 -0
- oscura/loaders/tdms.py +367 -0
- oscura/loaders/tektronix.py +711 -0
- oscura/loaders/validation.py +584 -0
- oscura/loaders/vcd.py +464 -0
- oscura/loaders/wav.py +233 -0
- oscura/math/__init__.py +45 -0
- oscura/math/arithmetic.py +824 -0
- oscura/math/interpolation.py +413 -0
- oscura/onboarding/__init__.py +39 -0
- oscura/onboarding/help.py +498 -0
- oscura/onboarding/tutorials.py +405 -0
- oscura/onboarding/wizard.py +466 -0
- oscura/optimization/__init__.py +19 -0
- oscura/optimization/parallel.py +440 -0
- oscura/optimization/search.py +532 -0
- oscura/pipeline/__init__.py +43 -0
- oscura/pipeline/base.py +338 -0
- oscura/pipeline/composition.py +242 -0
- oscura/pipeline/parallel.py +448 -0
- oscura/pipeline/pipeline.py +375 -0
- oscura/pipeline/reverse_engineering.py +1119 -0
- oscura/plugins/__init__.py +122 -0
- oscura/plugins/base.py +272 -0
- oscura/plugins/cli.py +497 -0
- oscura/plugins/discovery.py +411 -0
- oscura/plugins/isolation.py +418 -0
- oscura/plugins/lifecycle.py +959 -0
- oscura/plugins/manager.py +493 -0
- oscura/plugins/registry.py +421 -0
- oscura/plugins/versioning.py +372 -0
- oscura/py.typed +0 -0
- oscura/quality/__init__.py +65 -0
- oscura/quality/ensemble.py +740 -0
- oscura/quality/explainer.py +338 -0
- oscura/quality/scoring.py +616 -0
- oscura/quality/warnings.py +456 -0
- oscura/reporting/__init__.py +248 -0
- oscura/reporting/advanced.py +1234 -0
- oscura/reporting/analyze.py +448 -0
- oscura/reporting/argument_preparer.py +596 -0
- oscura/reporting/auto_report.py +507 -0
- oscura/reporting/batch.py +615 -0
- oscura/reporting/chart_selection.py +223 -0
- oscura/reporting/comparison.py +330 -0
- oscura/reporting/config.py +615 -0
- oscura/reporting/content/__init__.py +39 -0
- oscura/reporting/content/executive.py +127 -0
- oscura/reporting/content/filtering.py +191 -0
- oscura/reporting/content/minimal.py +257 -0
- oscura/reporting/content/verbosity.py +162 -0
- oscura/reporting/core.py +508 -0
- oscura/reporting/core_formats/__init__.py +17 -0
- oscura/reporting/core_formats/multi_format.py +210 -0
- oscura/reporting/engine.py +836 -0
- oscura/reporting/export.py +366 -0
- oscura/reporting/formatting/__init__.py +129 -0
- oscura/reporting/formatting/emphasis.py +81 -0
- oscura/reporting/formatting/numbers.py +403 -0
- oscura/reporting/formatting/standards.py +55 -0
- oscura/reporting/formatting.py +466 -0
- oscura/reporting/html.py +578 -0
- oscura/reporting/index.py +590 -0
- oscura/reporting/multichannel.py +296 -0
- oscura/reporting/output.py +379 -0
- oscura/reporting/pdf.py +373 -0
- oscura/reporting/plots.py +731 -0
- oscura/reporting/pptx_export.py +360 -0
- oscura/reporting/renderers/__init__.py +11 -0
- oscura/reporting/renderers/pdf.py +94 -0
- oscura/reporting/sections.py +471 -0
- oscura/reporting/standards.py +680 -0
- oscura/reporting/summary_generator.py +368 -0
- oscura/reporting/tables.py +397 -0
- oscura/reporting/template_system.py +724 -0
- oscura/reporting/templates/__init__.py +15 -0
- oscura/reporting/templates/definition.py +205 -0
- oscura/reporting/templates/index.html +649 -0
- oscura/reporting/templates/index.md +173 -0
- oscura/schemas/__init__.py +158 -0
- oscura/schemas/bus_configuration.json +322 -0
- oscura/schemas/device_mapping.json +182 -0
- oscura/schemas/packet_format.json +418 -0
- oscura/schemas/protocol_definition.json +363 -0
- oscura/search/__init__.py +16 -0
- oscura/search/anomaly.py +292 -0
- oscura/search/context.py +149 -0
- oscura/search/pattern.py +160 -0
- oscura/session/__init__.py +34 -0
- oscura/session/annotations.py +289 -0
- oscura/session/history.py +313 -0
- oscura/session/session.py +445 -0
- oscura/streaming/__init__.py +43 -0
- oscura/streaming/chunked.py +611 -0
- oscura/streaming/progressive.py +393 -0
- oscura/streaming/realtime.py +622 -0
- oscura/testing/__init__.py +54 -0
- oscura/testing/synthetic.py +808 -0
- oscura/triggering/__init__.py +68 -0
- oscura/triggering/base.py +229 -0
- oscura/triggering/edge.py +353 -0
- oscura/triggering/pattern.py +344 -0
- oscura/triggering/pulse.py +581 -0
- oscura/triggering/window.py +453 -0
- oscura/ui/__init__.py +48 -0
- oscura/ui/formatters.py +526 -0
- oscura/ui/progressive_display.py +340 -0
- oscura/utils/__init__.py +99 -0
- oscura/utils/autodetect.py +338 -0
- oscura/utils/buffer.py +389 -0
- oscura/utils/lazy.py +407 -0
- oscura/utils/lazy_imports.py +147 -0
- oscura/utils/memory.py +836 -0
- oscura/utils/memory_advanced.py +1326 -0
- oscura/utils/memory_extensions.py +465 -0
- oscura/utils/progressive.py +352 -0
- oscura/utils/windowing.py +362 -0
- oscura/visualization/__init__.py +321 -0
- oscura/visualization/accessibility.py +526 -0
- oscura/visualization/annotations.py +374 -0
- oscura/visualization/axis_scaling.py +305 -0
- oscura/visualization/colors.py +453 -0
- oscura/visualization/digital.py +337 -0
- oscura/visualization/eye.py +420 -0
- oscura/visualization/histogram.py +281 -0
- oscura/visualization/interactive.py +858 -0
- oscura/visualization/jitter.py +702 -0
- oscura/visualization/keyboard.py +394 -0
- oscura/visualization/layout.py +365 -0
- oscura/visualization/optimization.py +1028 -0
- oscura/visualization/palettes.py +446 -0
- oscura/visualization/plot.py +92 -0
- oscura/visualization/power.py +290 -0
- oscura/visualization/power_extended.py +626 -0
- oscura/visualization/presets.py +467 -0
- oscura/visualization/protocols.py +932 -0
- oscura/visualization/render.py +207 -0
- oscura/visualization/rendering.py +444 -0
- oscura/visualization/reverse_engineering.py +791 -0
- oscura/visualization/signal_integrity.py +808 -0
- oscura/visualization/specialized.py +553 -0
- oscura/visualization/spectral.py +811 -0
- oscura/visualization/styles.py +381 -0
- oscura/visualization/thumbnails.py +311 -0
- oscura/visualization/time_axis.py +351 -0
- oscura/visualization/waveform.py +367 -0
- oscura/workflow/__init__.py +13 -0
- oscura/workflow/dag.py +377 -0
- oscura/workflows/__init__.py +58 -0
- oscura/workflows/compliance.py +280 -0
- oscura/workflows/digital.py +272 -0
- oscura/workflows/multi_trace.py +502 -0
- oscura/workflows/power.py +178 -0
- oscura/workflows/protocol.py +492 -0
- oscura/workflows/reverse_engineering.py +639 -0
- oscura/workflows/signal_integrity.py +227 -0
- oscura-0.1.0.dist-info/METADATA +300 -0
- oscura-0.1.0.dist-info/RECORD +463 -0
- oscura-0.1.0.dist-info/entry_points.txt +2 -0
- {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/licenses/LICENSE +1 -1
- oscura-0.0.1.dist-info/METADATA +0 -63
- oscura-0.0.1.dist-info/RECORD +0 -5
- {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
"""Ensemble methods for combining multiple analysis algorithms.
|
|
2
|
+
|
|
3
|
+
This module provides robust analysis by combining results from multiple algorithms
|
|
4
|
+
using various aggregation strategies. Ensemble methods reduce individual algorithm
|
|
5
|
+
bias, handle outliers, and provide confidence bounds for more reliable measurements.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
>>> from oscura.quality.ensemble import EnsembleAggregator, AggregationMethod
|
|
10
|
+
>>> from oscura.quality.ensemble import create_frequency_ensemble
|
|
11
|
+
>>> # Combine multiple frequency measurements
|
|
12
|
+
>>> result = create_frequency_ensemble(signal, sample_rate=1e9)
|
|
13
|
+
>>> print(f"Frequency: {result.value:.2f} Hz ± {result.confidence*100:.1f}%")
|
|
14
|
+
>>> print(f"Methods agree: {result.method_agreement*100:.1f}%")
|
|
15
|
+
>>> # Use custom ensemble
|
|
16
|
+
>>> aggregator = EnsembleAggregator(method=AggregationMethod.WEIGHTED_AVERAGE)
|
|
17
|
+
>>> results = [
|
|
18
|
+
... {"value": 1000.0, "confidence": 0.9, "method": "fft"},
|
|
19
|
+
... {"value": 1005.0, "confidence": 0.8, "method": "autocorr"},
|
|
20
|
+
... {"value": 995.0, "confidence": 0.85, "method": "zero_crossing"},
|
|
21
|
+
... ]
|
|
22
|
+
>>> ensemble_result = aggregator.aggregate(results)
|
|
23
|
+
|
|
24
|
+
References:
|
|
25
|
+
- Kuncheva, L.I.: "Combining Pattern Classifiers" (2nd Ed), Wiley, 2014
|
|
26
|
+
- Polikar, R.: "Ensemble Learning", Scholarpedia, 2009
|
|
27
|
+
- Dietterich, T.G.: "Ensemble Methods in Machine Learning", 2000
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import logging
|
|
33
|
+
from collections import Counter
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from enum import Enum
|
|
36
|
+
from typing import TYPE_CHECKING, Any
|
|
37
|
+
|
|
38
|
+
import numpy as np
|
|
39
|
+
from scipy import stats
|
|
40
|
+
|
|
41
|
+
from oscura.quality.scoring import AnalysisQualityScore, combine_quality_scores
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from numpy.typing import NDArray
|
|
45
|
+
|
|
46
|
+
logger = logging.getLogger(__name__)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AggregationMethod(Enum):
|
|
50
|
+
"""Strategy for combining multiple analysis results.
|
|
51
|
+
|
|
52
|
+
Attributes:
|
|
53
|
+
WEIGHTED_AVERAGE: Weight results by confidence (best for numeric values).
|
|
54
|
+
VOTING: Majority voting (best for categorical results).
|
|
55
|
+
MEDIAN: Robust to outliers (best when outliers expected).
|
|
56
|
+
BAYESIAN: Bayesian combination with prior (best when prior knowledge available).
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
WEIGHTED_AVERAGE = "weighted_average"
|
|
60
|
+
VOTING = "voting"
|
|
61
|
+
MEDIAN = "median"
|
|
62
|
+
BAYESIAN = "bayesian"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class EnsembleResult:
|
|
67
|
+
"""Combined result from multiple analysis methods.
|
|
68
|
+
|
|
69
|
+
Attributes:
|
|
70
|
+
value: Aggregated value (numeric or categorical).
|
|
71
|
+
confidence: Overall confidence in combined result (0-1).
|
|
72
|
+
lower_bound: Lower confidence bound (None for categorical).
|
|
73
|
+
upper_bound: Upper confidence bound (None for categorical).
|
|
74
|
+
method_agreement: Agreement between methods (0-1, higher is better).
|
|
75
|
+
individual_results: List of individual method results.
|
|
76
|
+
aggregation_method: Method used for aggregation.
|
|
77
|
+
quality_score: Optional quality score for the ensemble result.
|
|
78
|
+
outlier_methods: Indices of methods producing outlier results.
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
>>> if result.method_agreement > 0.8:
|
|
82
|
+
... print(f"High agreement: {result.value}")
|
|
83
|
+
>>> else:
|
|
84
|
+
... print(f"Methods disagree, confidence: {result.confidence}")
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
value: Any
|
|
88
|
+
confidence: float
|
|
89
|
+
lower_bound: float | None = None
|
|
90
|
+
upper_bound: float | None = None
|
|
91
|
+
method_agreement: float = 1.0
|
|
92
|
+
individual_results: list[dict[str, Any]] = field(default_factory=list)
|
|
93
|
+
aggregation_method: AggregationMethod = AggregationMethod.WEIGHTED_AVERAGE
|
|
94
|
+
quality_score: AnalysisQualityScore | None = None
|
|
95
|
+
outlier_methods: list[int] = field(default_factory=list)
|
|
96
|
+
|
|
97
|
+
def __post_init__(self) -> None:
|
|
98
|
+
"""Validate confidence and agreement values."""
|
|
99
|
+
if not 0 <= self.confidence <= 1:
|
|
100
|
+
raise ValueError(f"Confidence must be in [0, 1], got {self.confidence}")
|
|
101
|
+
if not 0 <= self.method_agreement <= 1:
|
|
102
|
+
raise ValueError(f"Method agreement must be in [0, 1], got {self.method_agreement}")
|
|
103
|
+
|
|
104
|
+
def to_dict(self) -> dict[str, Any]:
|
|
105
|
+
"""Convert to dictionary for serialization.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Dictionary representation of ensemble result.
|
|
109
|
+
"""
|
|
110
|
+
return {
|
|
111
|
+
"value": self.value,
|
|
112
|
+
"confidence": self.confidence,
|
|
113
|
+
"lower_bound": self.lower_bound,
|
|
114
|
+
"upper_bound": self.upper_bound,
|
|
115
|
+
"method_agreement": self.method_agreement,
|
|
116
|
+
"aggregation_method": self.aggregation_method.value,
|
|
117
|
+
"individual_results": self.individual_results,
|
|
118
|
+
"outlier_methods": self.outlier_methods,
|
|
119
|
+
"quality_score": self.quality_score.to_dict() if self.quality_score else None,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class EnsembleAggregator:
|
|
124
|
+
"""Combines multiple analysis results for robust estimation.
|
|
125
|
+
|
|
126
|
+
Supports various aggregation strategies optimized for different data types
|
|
127
|
+
and analysis scenarios. Automatically detects and handles outliers, computes
|
|
128
|
+
confidence bounds, and measures inter-method agreement.
|
|
129
|
+
|
|
130
|
+
QUAL-004: Ensemble Methods for Robust Analysis
|
|
131
|
+
QUAL-005: Disagreement Detection and Handling
|
|
132
|
+
QUAL-006: Confidence Bound Estimation
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
>>> aggregator = EnsembleAggregator(method=AggregationMethod.WEIGHTED_AVERAGE)
|
|
136
|
+
>>> results = [
|
|
137
|
+
... {"value": 100.0, "confidence": 0.9},
|
|
138
|
+
... {"value": 102.0, "confidence": 0.85},
|
|
139
|
+
... {"value": 98.0, "confidence": 0.8},
|
|
140
|
+
... ]
|
|
141
|
+
>>> ensemble = aggregator.aggregate(results)
|
|
142
|
+
>>> print(f"Result: {ensemble.value:.2f} ± {ensemble.confidence*100:.1f}%")
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
def __init__(
|
|
146
|
+
self,
|
|
147
|
+
method: AggregationMethod = AggregationMethod.WEIGHTED_AVERAGE,
|
|
148
|
+
outlier_threshold: float = 3.0,
|
|
149
|
+
min_agreement: float = 0.5,
|
|
150
|
+
):
|
|
151
|
+
"""Initialize ensemble aggregator.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
method: Aggregation strategy to use.
|
|
155
|
+
outlier_threshold: Z-score threshold for outlier detection (default 3.0).
|
|
156
|
+
min_agreement: Minimum agreement threshold to warn (default 0.5).
|
|
157
|
+
"""
|
|
158
|
+
self.method = method
|
|
159
|
+
self.outlier_threshold = outlier_threshold
|
|
160
|
+
self.min_agreement = min_agreement
|
|
161
|
+
|
|
162
|
+
def aggregate(self, results: list[dict[str, Any]]) -> EnsembleResult:
|
|
163
|
+
"""Combine multiple results into one robust estimate.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
results: List of result dictionaries with keys:
|
|
167
|
+
- value: Measured value (numeric or categorical)
|
|
168
|
+
- confidence: Confidence score (0-1)
|
|
169
|
+
- method: Optional method name
|
|
170
|
+
- quality_score: Optional AnalysisQualityScore
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
EnsembleResult with combined value and metadata.
|
|
174
|
+
|
|
175
|
+
Raises:
|
|
176
|
+
ValueError: If results list is empty or invalid.
|
|
177
|
+
|
|
178
|
+
Example:
|
|
179
|
+
>>> results = [
|
|
180
|
+
... {"value": 1000, "confidence": 0.9, "method": "fft"},
|
|
181
|
+
... {"value": 1005, "confidence": 0.85, "method": "autocorr"},
|
|
182
|
+
... ]
|
|
183
|
+
>>> ensemble = aggregator.aggregate(results)
|
|
184
|
+
"""
|
|
185
|
+
if not results:
|
|
186
|
+
raise ValueError("Cannot aggregate empty results list")
|
|
187
|
+
|
|
188
|
+
# Extract values and confidences
|
|
189
|
+
values = [r["value"] for r in results]
|
|
190
|
+
confidences = [r.get("confidence", 1.0) for r in results]
|
|
191
|
+
|
|
192
|
+
# Determine if values are numeric or categorical
|
|
193
|
+
is_numeric = all(isinstance(v, int | float | np.number) for v in values)
|
|
194
|
+
|
|
195
|
+
if is_numeric:
|
|
196
|
+
return self.aggregate_numeric(
|
|
197
|
+
[float(v) for v in values],
|
|
198
|
+
confidences,
|
|
199
|
+
original_results=results,
|
|
200
|
+
)
|
|
201
|
+
else:
|
|
202
|
+
return self.aggregate_categorical(
|
|
203
|
+
[str(v) for v in values],
|
|
204
|
+
confidences,
|
|
205
|
+
original_results=results,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def aggregate_numeric(
|
|
209
|
+
self,
|
|
210
|
+
values: list[float],
|
|
211
|
+
confidences: list[float],
|
|
212
|
+
original_results: list[dict[str, Any]] | None = None,
|
|
213
|
+
) -> EnsembleResult:
|
|
214
|
+
"""Combine numeric values with confidence weighting.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
values: List of numeric values to combine.
|
|
218
|
+
confidences: Confidence scores for each value (0-1).
|
|
219
|
+
original_results: Optional original result dictionaries.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
EnsembleResult with aggregated numeric value.
|
|
223
|
+
|
|
224
|
+
Raises:
|
|
225
|
+
ValueError: If values list is empty.
|
|
226
|
+
|
|
227
|
+
Example:
|
|
228
|
+
>>> values = [100.0, 102.0, 98.0, 150.0] # 150 is outlier
|
|
229
|
+
>>> confidences = [0.9, 0.85, 0.8, 0.7]
|
|
230
|
+
>>> result = aggregator.aggregate_numeric(values, confidences)
|
|
231
|
+
>>> # Outlier detected and handled
|
|
232
|
+
"""
|
|
233
|
+
if not values:
|
|
234
|
+
raise ValueError("Cannot aggregate empty values list")
|
|
235
|
+
|
|
236
|
+
if original_results is None:
|
|
237
|
+
original_results = [
|
|
238
|
+
{"value": v, "confidence": c} for v, c in zip(values, confidences, strict=False)
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
values_arr = np.array(values, dtype=np.float64)
|
|
242
|
+
confidences_arr = np.array(confidences, dtype=np.float64)
|
|
243
|
+
|
|
244
|
+
# Detect outliers
|
|
245
|
+
outlier_indices = self.detect_outlier_methods(original_results)
|
|
246
|
+
|
|
247
|
+
# Create mask for non-outlier values
|
|
248
|
+
valid_mask = np.ones(len(values), dtype=bool)
|
|
249
|
+
valid_mask[outlier_indices] = False
|
|
250
|
+
|
|
251
|
+
# Use only non-outliers for aggregation
|
|
252
|
+
valid_values = values_arr[valid_mask]
|
|
253
|
+
valid_confidences = confidences_arr[valid_mask]
|
|
254
|
+
|
|
255
|
+
if len(valid_values) == 0:
|
|
256
|
+
# All values are outliers, use all with warning
|
|
257
|
+
logger.warning("All methods detected as outliers, using all values")
|
|
258
|
+
valid_values = values_arr
|
|
259
|
+
valid_confidences = confidences_arr
|
|
260
|
+
outlier_indices = []
|
|
261
|
+
|
|
262
|
+
# Compute aggregated value based on method
|
|
263
|
+
if self.method == AggregationMethod.WEIGHTED_AVERAGE:
|
|
264
|
+
# Normalize weights
|
|
265
|
+
weights = valid_confidences / np.sum(valid_confidences)
|
|
266
|
+
aggregated_value = float(np.sum(valid_values * weights))
|
|
267
|
+
# Weighted variance
|
|
268
|
+
variance = float(np.sum(weights * (valid_values - aggregated_value) ** 2))
|
|
269
|
+
std_dev = np.sqrt(variance)
|
|
270
|
+
|
|
271
|
+
elif self.method == AggregationMethod.MEDIAN:
|
|
272
|
+
aggregated_value = float(np.median(valid_values))
|
|
273
|
+
# Use MAD (Median Absolute Deviation) for robust std estimate
|
|
274
|
+
mad = float(np.median(np.abs(valid_values - aggregated_value)))
|
|
275
|
+
std_dev = mad * 1.4826 # Scale factor for normal distribution
|
|
276
|
+
|
|
277
|
+
elif self.method == AggregationMethod.BAYESIAN:
|
|
278
|
+
# Bayesian combination with Gaussian likelihood
|
|
279
|
+
# Prior: uniform over range
|
|
280
|
+
# Likelihood: Gaussian with confidence-based variance
|
|
281
|
+
precisions = valid_confidences**2 # Higher confidence = lower variance
|
|
282
|
+
total_precision = np.sum(precisions)
|
|
283
|
+
aggregated_value = float(np.sum(valid_values * precisions) / total_precision)
|
|
284
|
+
# Posterior variance
|
|
285
|
+
variance = 1.0 / total_precision
|
|
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
|
|
304
|
+
|
|
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
|
+
else:
|
|
310
|
+
method_agreement = 1.0
|
|
311
|
+
|
|
312
|
+
# Overall confidence (weighted average of individual confidences)
|
|
313
|
+
overall_confidence = float(np.mean(valid_confidences))
|
|
314
|
+
|
|
315
|
+
# Penalize confidence if agreement is low
|
|
316
|
+
if method_agreement < self.min_agreement:
|
|
317
|
+
overall_confidence *= method_agreement
|
|
318
|
+
logger.warning(
|
|
319
|
+
f"Low method agreement ({method_agreement:.2f}), "
|
|
320
|
+
f"reduced confidence to {overall_confidence:.2f}"
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# Combine quality scores if available
|
|
324
|
+
quality_scores_raw = [
|
|
325
|
+
r.get("quality_score") for r in original_results if "quality_score" in r
|
|
326
|
+
]
|
|
327
|
+
ensemble_quality = None
|
|
328
|
+
if quality_scores_raw and all(
|
|
329
|
+
isinstance(q, AnalysisQualityScore) for q in quality_scores_raw
|
|
330
|
+
):
|
|
331
|
+
# Type narrowing - we know all are AnalysisQualityScore at this point
|
|
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
|
+
)
|
|
336
|
+
|
|
337
|
+
return EnsembleResult(
|
|
338
|
+
value=aggregated_value,
|
|
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
|
+
)
|
|
348
|
+
|
|
349
|
+
def aggregate_categorical(
|
|
350
|
+
self,
|
|
351
|
+
values: list[str],
|
|
352
|
+
confidences: list[float],
|
|
353
|
+
original_results: list[dict[str, Any]] | None = None,
|
|
354
|
+
) -> EnsembleResult:
|
|
355
|
+
"""Combine categorical values via weighted voting.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
values: List of categorical values to combine.
|
|
359
|
+
confidences: Confidence scores for each value (0-1).
|
|
360
|
+
original_results: Optional original result dictionaries.
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
EnsembleResult with majority vote value.
|
|
364
|
+
|
|
365
|
+
Raises:
|
|
366
|
+
ValueError: If values list is empty.
|
|
367
|
+
|
|
368
|
+
Example:
|
|
369
|
+
>>> values = ["rising", "rising", "falling", "rising"]
|
|
370
|
+
>>> confidences = [0.9, 0.85, 0.6, 0.8]
|
|
371
|
+
>>> result = aggregator.aggregate_categorical(values, confidences)
|
|
372
|
+
>>> # "rising" wins by weighted vote
|
|
373
|
+
"""
|
|
374
|
+
if not values:
|
|
375
|
+
raise ValueError("Cannot aggregate empty values list")
|
|
376
|
+
|
|
377
|
+
if original_results is None:
|
|
378
|
+
original_results = [
|
|
379
|
+
{"value": v, "confidence": c} for v, c in zip(values, confidences, strict=False)
|
|
380
|
+
]
|
|
381
|
+
|
|
382
|
+
# Weighted voting
|
|
383
|
+
vote_weights: dict[str, float] = {}
|
|
384
|
+
for value, confidence in zip(values, confidences, strict=False):
|
|
385
|
+
vote_weights[value] = vote_weights.get(value, 0.0) + confidence
|
|
386
|
+
|
|
387
|
+
# Get winner
|
|
388
|
+
winner = max(vote_weights.items(), key=lambda x: x[1])
|
|
389
|
+
aggregated_value = winner[0]
|
|
390
|
+
total_weight = sum(vote_weights.values())
|
|
391
|
+
|
|
392
|
+
# Confidence is the fraction of votes for winner
|
|
393
|
+
overall_confidence = winner[1] / total_weight if total_weight > 0 else 0.0
|
|
394
|
+
|
|
395
|
+
# Agreement is measured by vote concentration
|
|
396
|
+
# Higher agreement when votes are concentrated on one option
|
|
397
|
+
vote_counts = Counter(values)
|
|
398
|
+
total_votes = len(values)
|
|
399
|
+
max_count = vote_counts.most_common(1)[0][1]
|
|
400
|
+
method_agreement = max_count / total_votes
|
|
401
|
+
|
|
402
|
+
# Combine quality scores if available
|
|
403
|
+
quality_scores_raw = [
|
|
404
|
+
r.get("quality_score") for r in original_results if "quality_score" in r
|
|
405
|
+
]
|
|
406
|
+
ensemble_quality = None
|
|
407
|
+
if quality_scores_raw and all(
|
|
408
|
+
isinstance(q, AnalysisQualityScore) for q in quality_scores_raw
|
|
409
|
+
):
|
|
410
|
+
# Type narrowing - we know all are AnalysisQualityScore at this point
|
|
411
|
+
quality_scores: list[AnalysisQualityScore] = quality_scores_raw # type: ignore[assignment]
|
|
412
|
+
ensemble_quality = combine_quality_scores(
|
|
413
|
+
quality_scores, weights=confidences[: len(quality_scores)]
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
return EnsembleResult(
|
|
417
|
+
value=aggregated_value,
|
|
418
|
+
confidence=overall_confidence,
|
|
419
|
+
lower_bound=None,
|
|
420
|
+
upper_bound=None,
|
|
421
|
+
method_agreement=method_agreement,
|
|
422
|
+
individual_results=original_results,
|
|
423
|
+
aggregation_method=self.method,
|
|
424
|
+
quality_score=ensemble_quality,
|
|
425
|
+
outlier_methods=[], # No outlier detection for categorical
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
def detect_outlier_methods(self, results: list[dict[str, Any]]) -> list[int]:
|
|
429
|
+
"""Identify methods producing outlier results.
|
|
430
|
+
|
|
431
|
+
Uses modified Z-score (based on MAD) for robust outlier detection.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
results: List of result dictionaries with "value" key.
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
List of indices corresponding to outlier methods.
|
|
438
|
+
|
|
439
|
+
Example:
|
|
440
|
+
>>> results = [
|
|
441
|
+
... {"value": 100}, {"value": 102}, {"value": 98}, {"value": 500}
|
|
442
|
+
... ]
|
|
443
|
+
>>> outliers = aggregator.detect_outlier_methods(results)
|
|
444
|
+
>>> # Returns [3] - the 500 value is an outlier
|
|
445
|
+
"""
|
|
446
|
+
values = [r["value"] for r in results]
|
|
447
|
+
|
|
448
|
+
# Only works for numeric values
|
|
449
|
+
if not all(isinstance(v, int | float | np.number) for v in values):
|
|
450
|
+
return []
|
|
451
|
+
|
|
452
|
+
if len(values) < 3:
|
|
453
|
+
# Need at least 3 values for outlier detection
|
|
454
|
+
return []
|
|
455
|
+
|
|
456
|
+
values_arr = np.array(values, dtype=np.float64)
|
|
457
|
+
|
|
458
|
+
# Use modified Z-score based on MAD (robust to outliers)
|
|
459
|
+
median = np.median(values_arr)
|
|
460
|
+
mad = np.median(np.abs(values_arr - median))
|
|
461
|
+
|
|
462
|
+
if mad == 0:
|
|
463
|
+
# All values are identical
|
|
464
|
+
return []
|
|
465
|
+
|
|
466
|
+
# Modified Z-score
|
|
467
|
+
modified_z_scores = 0.6745 * (values_arr - median) / mad
|
|
468
|
+
|
|
469
|
+
# Identify outliers
|
|
470
|
+
outlier_mask = np.abs(modified_z_scores) > self.outlier_threshold
|
|
471
|
+
outlier_indices: list[int] = np.where(outlier_mask)[0].tolist()
|
|
472
|
+
|
|
473
|
+
if outlier_indices:
|
|
474
|
+
logger.info(f"Detected {len(outlier_indices)} outlier method(s): {outlier_indices}")
|
|
475
|
+
|
|
476
|
+
return outlier_indices
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
# Pre-configured ensembles for common analysis types
|
|
480
|
+
# Weights represent the relative reliability of each method
|
|
481
|
+
|
|
482
|
+
FREQUENCY_ENSEMBLE: list[tuple[str, float]] = [
|
|
483
|
+
("fft_peak", 0.4), # FFT peak is generally most reliable
|
|
484
|
+
("zero_crossing", 0.3), # Zero crossing is robust but can be noisy
|
|
485
|
+
("autocorrelation", 0.3), # Autocorrelation handles noise well
|
|
486
|
+
]
|
|
487
|
+
|
|
488
|
+
EDGE_DETECTION_ENSEMBLE: list[tuple[str, float]] = [
|
|
489
|
+
("threshold_crossing", 0.5), # Most direct method
|
|
490
|
+
("derivative", 0.3), # Good for clean signals
|
|
491
|
+
("schmitt_trigger", 0.2), # Noise immunity but less precise
|
|
492
|
+
]
|
|
493
|
+
|
|
494
|
+
AMPLITUDE_ENSEMBLE: list[tuple[str, float]] = [
|
|
495
|
+
("peak_to_peak", 0.4), # Direct measurement
|
|
496
|
+
("rms", 0.3), # Robust to noise
|
|
497
|
+
("percentile_99", 0.3), # Outlier resistant
|
|
498
|
+
]
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def create_frequency_ensemble(
|
|
502
|
+
signal: NDArray[np.float64],
|
|
503
|
+
sample_rate: float,
|
|
504
|
+
method_weights: list[tuple[str, float]] | None = None,
|
|
505
|
+
) -> EnsembleResult:
|
|
506
|
+
"""Run multiple frequency detection methods and combine results.
|
|
507
|
+
|
|
508
|
+
Applies FFT peak detection, zero-crossing rate, and autocorrelation-based
|
|
509
|
+
frequency estimation, then combines using weighted averaging.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
signal: Input signal array.
|
|
513
|
+
sample_rate: Sample rate in Hz.
|
|
514
|
+
method_weights: Optional custom method weights. Defaults to FREQUENCY_ENSEMBLE.
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
EnsembleResult with combined frequency estimate.
|
|
518
|
+
|
|
519
|
+
Raises:
|
|
520
|
+
ValueError: If all frequency detection methods fail.
|
|
521
|
+
|
|
522
|
+
Example:
|
|
523
|
+
>>> import numpy as np
|
|
524
|
+
>>> t = np.linspace(0, 1, 1000)
|
|
525
|
+
>>> signal = np.sin(2 * np.pi * 10 * t) # 10 Hz sine
|
|
526
|
+
>>> result = create_frequency_ensemble(signal, sample_rate=1000)
|
|
527
|
+
>>> print(f"Frequency: {result.value:.2f} Hz")
|
|
528
|
+
>>> print(f"Confidence: {result.confidence:.2%}")
|
|
529
|
+
"""
|
|
530
|
+
if method_weights is None:
|
|
531
|
+
method_weights = FREQUENCY_ENSEMBLE
|
|
532
|
+
|
|
533
|
+
results = []
|
|
534
|
+
|
|
535
|
+
# Method 1: FFT peak detection
|
|
536
|
+
try:
|
|
537
|
+
fft_result = np.fft.rfft(signal)
|
|
538
|
+
freqs = np.fft.rfftfreq(len(signal), d=1.0 / sample_rate)
|
|
539
|
+
peak_idx = np.argmax(np.abs(fft_result[1:])) + 1 # Skip DC
|
|
540
|
+
freq_fft = float(freqs[peak_idx])
|
|
541
|
+
# Confidence based on peak prominence
|
|
542
|
+
peak_magnitude = np.abs(fft_result[peak_idx])
|
|
543
|
+
mean_magnitude = np.mean(np.abs(fft_result[1:]))
|
|
544
|
+
confidence_fft = min(1.0, peak_magnitude / (mean_magnitude * 10))
|
|
545
|
+
results.append(
|
|
546
|
+
{
|
|
547
|
+
"value": freq_fft,
|
|
548
|
+
"confidence": confidence_fft * method_weights[0][1],
|
|
549
|
+
"method": "fft_peak",
|
|
550
|
+
}
|
|
551
|
+
)
|
|
552
|
+
except Exception as e:
|
|
553
|
+
logger.debug(f"FFT peak detection failed: {e}")
|
|
554
|
+
|
|
555
|
+
# Method 2: Zero crossing rate
|
|
556
|
+
try:
|
|
557
|
+
zero_crossings = np.where(np.diff(np.sign(signal)))[0]
|
|
558
|
+
if len(zero_crossings) > 1:
|
|
559
|
+
# Average time between zero crossings (half period)
|
|
560
|
+
avg_half_period = np.mean(np.diff(zero_crossings)) / sample_rate
|
|
561
|
+
freq_zc = 1.0 / (2.0 * avg_half_period)
|
|
562
|
+
# Confidence based on regularity of crossings
|
|
563
|
+
std_half_period = np.std(np.diff(zero_crossings)) / sample_rate
|
|
564
|
+
confidence_zc = max(0.0, 1.0 - std_half_period / avg_half_period)
|
|
565
|
+
results.append(
|
|
566
|
+
{
|
|
567
|
+
"value": float(freq_zc),
|
|
568
|
+
"confidence": confidence_zc * method_weights[1][1],
|
|
569
|
+
"method": "zero_crossing",
|
|
570
|
+
}
|
|
571
|
+
)
|
|
572
|
+
except Exception as e:
|
|
573
|
+
logger.debug(f"Zero crossing detection failed: {e}")
|
|
574
|
+
|
|
575
|
+
# Method 3: Autocorrelation
|
|
576
|
+
try:
|
|
577
|
+
# Compute autocorrelation
|
|
578
|
+
autocorr = np.correlate(signal, signal, mode="full")
|
|
579
|
+
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
|
+
|
|
602
|
+
if not results:
|
|
603
|
+
raise ValueError("All frequency detection methods failed")
|
|
604
|
+
|
|
605
|
+
# Aggregate results
|
|
606
|
+
aggregator = EnsembleAggregator(method=AggregationMethod.WEIGHTED_AVERAGE)
|
|
607
|
+
return aggregator.aggregate(results)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def create_edge_ensemble(
|
|
611
|
+
signal: NDArray[np.float64],
|
|
612
|
+
sample_rate: float,
|
|
613
|
+
threshold: float | None = None,
|
|
614
|
+
method_weights: list[tuple[str, float]] | None = None,
|
|
615
|
+
) -> EnsembleResult:
|
|
616
|
+
"""Run multiple edge detection methods and combine results.
|
|
617
|
+
|
|
618
|
+
Applies threshold crossing, derivative-based, and Schmitt trigger edge
|
|
619
|
+
detection, then combines results using weighted voting or averaging.
|
|
620
|
+
|
|
621
|
+
Args:
|
|
622
|
+
signal: Input signal array.
|
|
623
|
+
sample_rate: Sample rate in Hz.
|
|
624
|
+
threshold: Detection threshold. If None, uses signal midpoint.
|
|
625
|
+
method_weights: Optional custom method weights. Defaults to EDGE_DETECTION_ENSEMBLE.
|
|
626
|
+
|
|
627
|
+
Returns:
|
|
628
|
+
EnsembleResult with combined edge detection results.
|
|
629
|
+
|
|
630
|
+
Raises:
|
|
631
|
+
ValueError: If all edge detection methods fail.
|
|
632
|
+
|
|
633
|
+
Example:
|
|
634
|
+
>>> signal = np.array([0, 0, 1, 1, 0, 0, 1, 1])
|
|
635
|
+
>>> result = create_edge_ensemble(signal, sample_rate=1000)
|
|
636
|
+
>>> print(f"Edge count: {result.value}")
|
|
637
|
+
>>> print(f"Agreement: {result.method_agreement:.2%}")
|
|
638
|
+
"""
|
|
639
|
+
if method_weights is None:
|
|
640
|
+
method_weights = EDGE_DETECTION_ENSEMBLE
|
|
641
|
+
|
|
642
|
+
if threshold is None:
|
|
643
|
+
threshold = float((np.max(signal) + np.min(signal)) / 2.0)
|
|
644
|
+
|
|
645
|
+
results = []
|
|
646
|
+
|
|
647
|
+
# Method 1: Threshold crossing
|
|
648
|
+
try:
|
|
649
|
+
crossings = np.where(np.diff(np.sign(signal - threshold)))[0]
|
|
650
|
+
edge_count_tc = len(crossings)
|
|
651
|
+
# Confidence based on signal quality (SNR proxy)
|
|
652
|
+
signal_range = np.ptp(signal)
|
|
653
|
+
noise_estimate = np.std(np.diff(signal))
|
|
654
|
+
confidence_tc = (
|
|
655
|
+
min(1.0, signal_range / (noise_estimate * 10)) if noise_estimate > 0 else 0.5
|
|
656
|
+
)
|
|
657
|
+
results.append(
|
|
658
|
+
{
|
|
659
|
+
"value": edge_count_tc,
|
|
660
|
+
"confidence": confidence_tc * method_weights[0][1],
|
|
661
|
+
"method": "threshold_crossing",
|
|
662
|
+
}
|
|
663
|
+
)
|
|
664
|
+
except Exception as e:
|
|
665
|
+
logger.debug(f"Threshold crossing detection failed: {e}")
|
|
666
|
+
|
|
667
|
+
# Method 2: Derivative-based
|
|
668
|
+
try:
|
|
669
|
+
derivative = np.diff(signal)
|
|
670
|
+
# Find peaks in absolute derivative
|
|
671
|
+
deriv_std = np.std(derivative)
|
|
672
|
+
deriv_threshold = deriv_std * 2
|
|
673
|
+
edge_indices = np.where(np.abs(derivative) > deriv_threshold)[0]
|
|
674
|
+
# Remove consecutive detections (within 2 samples)
|
|
675
|
+
filtered_edges = []
|
|
676
|
+
for i, idx in enumerate(edge_indices):
|
|
677
|
+
if i == 0 or idx - edge_indices[i - 1] > 2:
|
|
678
|
+
filtered_edges.append(idx)
|
|
679
|
+
edge_count_deriv = len(filtered_edges)
|
|
680
|
+
# Confidence based on peak derivative prominence above threshold
|
|
681
|
+
# Higher max derivative relative to threshold means clearer edges
|
|
682
|
+
max_deriv = np.max(np.abs(derivative)) if len(derivative) > 0 else 0.0
|
|
683
|
+
prominence_ratio = (max_deriv / deriv_threshold) if deriv_threshold > 0 else 0.0
|
|
684
|
+
confidence_deriv = float(
|
|
685
|
+
np.clip(prominence_ratio / 3.0, 0.0, 1.0)
|
|
686
|
+
) # Normalize: 3x threshold = 100%
|
|
687
|
+
results.append(
|
|
688
|
+
{
|
|
689
|
+
"value": edge_count_deriv,
|
|
690
|
+
"confidence": confidence_deriv * method_weights[1][1],
|
|
691
|
+
"method": "derivative",
|
|
692
|
+
}
|
|
693
|
+
)
|
|
694
|
+
except Exception as e:
|
|
695
|
+
logger.debug(f"Derivative edge detection failed: {e}")
|
|
696
|
+
|
|
697
|
+
# Method 3: Schmitt trigger (hysteresis)
|
|
698
|
+
try:
|
|
699
|
+
hysteresis = float(np.std(signal) * 0.1)
|
|
700
|
+
thresh_high = threshold + hysteresis
|
|
701
|
+
thresh_low = threshold - hysteresis
|
|
702
|
+
state = signal[0] > threshold
|
|
703
|
+
edge_count_schmitt = 0
|
|
704
|
+
for val in signal:
|
|
705
|
+
if not state and val > thresh_high:
|
|
706
|
+
edge_count_schmitt += 1
|
|
707
|
+
state = True
|
|
708
|
+
elif state and val < thresh_low:
|
|
709
|
+
edge_count_schmitt += 1
|
|
710
|
+
state = False
|
|
711
|
+
# Confidence based on hysteresis effectiveness
|
|
712
|
+
confidence_schmitt = 0.7 # Lower base confidence due to hysteresis delay
|
|
713
|
+
results.append(
|
|
714
|
+
{
|
|
715
|
+
"value": edge_count_schmitt,
|
|
716
|
+
"confidence": confidence_schmitt * method_weights[2][1],
|
|
717
|
+
"method": "schmitt_trigger",
|
|
718
|
+
}
|
|
719
|
+
)
|
|
720
|
+
except Exception as e:
|
|
721
|
+
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)
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
__all__ = [
|
|
732
|
+
"AMPLITUDE_ENSEMBLE",
|
|
733
|
+
"EDGE_DETECTION_ENSEMBLE",
|
|
734
|
+
"FREQUENCY_ENSEMBLE",
|
|
735
|
+
"AggregationMethod",
|
|
736
|
+
"EnsembleAggregator",
|
|
737
|
+
"EnsembleResult",
|
|
738
|
+
"create_edge_ensemble",
|
|
739
|
+
"create_frequency_ensemble",
|
|
740
|
+
]
|