oscura 0.0.1__py3-none-any.whl → 0.1.1__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.1.dist-info/METADATA +300 -0
- oscura-0.1.1.dist-info/RECORD +463 -0
- oscura-0.1.1.dist-info/entry_points.txt +2 -0
- {oscura-0.0.1.dist-info → oscura-0.1.1.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.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
"""Correlation analysis for signal data.
|
|
2
|
+
|
|
3
|
+
This module provides autocorrelation, cross-correlation, and related
|
|
4
|
+
analysis functions for identifying signal relationships and periodicities.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.analyzers.statistics.correlation import (
|
|
9
|
+
... autocorrelation, cross_correlation, correlate_chunked
|
|
10
|
+
... )
|
|
11
|
+
>>> acf = autocorrelation(trace, max_lag=1000)
|
|
12
|
+
>>> xcorr, lag, coef = cross_correlation(trace1, trace2)
|
|
13
|
+
>>> # Memory-efficient correlation for large signals
|
|
14
|
+
>>> result = correlate_chunked(large_signal1, large_signal2)
|
|
15
|
+
|
|
16
|
+
References:
|
|
17
|
+
Oppenheim, A. V. & Schafer, R. W. (2009). Discrete-Time Signal Processing
|
|
18
|
+
IEEE 1241-2010: Standard for Terminology and Test Methods for ADCs
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from typing import TYPE_CHECKING, Any, Literal, cast
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
|
|
28
|
+
from oscura.core.types import WaveformTrace
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from numpy.typing import NDArray
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class CrossCorrelationResult:
|
|
36
|
+
"""Result of cross-correlation analysis.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
correlation: Full correlation array.
|
|
40
|
+
lags: Lag values in samples.
|
|
41
|
+
lag_times: Lag values in seconds.
|
|
42
|
+
peak_lag: Lag at maximum correlation (samples).
|
|
43
|
+
peak_lag_time: Lag at maximum correlation (seconds).
|
|
44
|
+
peak_coefficient: Maximum correlation coefficient.
|
|
45
|
+
sample_rate: Sample rate used for time conversion.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
correlation: NDArray[np.float64]
|
|
49
|
+
lags: NDArray[np.intp]
|
|
50
|
+
lag_times: NDArray[np.float64]
|
|
51
|
+
peak_lag: int
|
|
52
|
+
peak_lag_time: float
|
|
53
|
+
peak_coefficient: float
|
|
54
|
+
sample_rate: float
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def autocorrelation(
|
|
58
|
+
trace: WaveformTrace | NDArray[np.floating[Any]],
|
|
59
|
+
*,
|
|
60
|
+
max_lag: int | None = None,
|
|
61
|
+
normalized: bool = True,
|
|
62
|
+
sample_rate: float | None = None,
|
|
63
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
64
|
+
"""Compute autocorrelation of a signal.
|
|
65
|
+
|
|
66
|
+
Measures self-similarity of a signal at different time lags.
|
|
67
|
+
Useful for detecting periodicities and characteristic time scales.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
trace: Input trace or numpy array.
|
|
71
|
+
max_lag: Maximum lag to compute (samples). If None, uses n // 2.
|
|
72
|
+
normalized: If True, normalize to correlation coefficients [-1, 1].
|
|
73
|
+
sample_rate: Sample rate in Hz (for time axis). Required if trace is array.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Tuple of (lags_time, autocorrelation):
|
|
77
|
+
- lags_time: Time values for each lag in seconds
|
|
78
|
+
- autocorrelation: Normalized autocorrelation values
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
ValueError: If sample_rate is not provided when trace is array.
|
|
82
|
+
|
|
83
|
+
Example:
|
|
84
|
+
>>> lag_times, acf = autocorrelation(trace, max_lag=1000)
|
|
85
|
+
>>> # Find first zero crossing for decorrelation time
|
|
86
|
+
>>> zero_idx = np.where(acf[1:] < 0)[0][0]
|
|
87
|
+
>>> decorr_time = lag_times[zero_idx]
|
|
88
|
+
|
|
89
|
+
References:
|
|
90
|
+
Box, G. E. P. & Jenkins, G. M. (1976). Time Series Analysis
|
|
91
|
+
"""
|
|
92
|
+
if isinstance(trace, WaveformTrace):
|
|
93
|
+
data = trace.data
|
|
94
|
+
fs = trace.metadata.sample_rate
|
|
95
|
+
else:
|
|
96
|
+
data = trace
|
|
97
|
+
if sample_rate is None:
|
|
98
|
+
raise ValueError("sample_rate required when trace is array")
|
|
99
|
+
fs = sample_rate
|
|
100
|
+
|
|
101
|
+
n = len(data)
|
|
102
|
+
|
|
103
|
+
if max_lag is None:
|
|
104
|
+
max_lag = n // 2
|
|
105
|
+
|
|
106
|
+
max_lag = min(max_lag, n - 1)
|
|
107
|
+
|
|
108
|
+
# Remove mean for proper correlation
|
|
109
|
+
data_centered = data - np.mean(data)
|
|
110
|
+
|
|
111
|
+
# Compute autocorrelation via FFT (faster for large n)
|
|
112
|
+
if n > 256:
|
|
113
|
+
# Zero-pad for full correlation
|
|
114
|
+
nfft = int(2 ** np.ceil(np.log2(2 * n)))
|
|
115
|
+
fft_data = np.fft.rfft(data_centered, n=nfft)
|
|
116
|
+
acf_full = np.fft.irfft(fft_data * np.conj(fft_data), n=nfft)
|
|
117
|
+
acf = acf_full[: max_lag + 1]
|
|
118
|
+
else:
|
|
119
|
+
# Direct computation for small n
|
|
120
|
+
acf = np.correlate(data_centered, data_centered, mode="full")
|
|
121
|
+
acf = acf[n - 1 : n + max_lag]
|
|
122
|
+
|
|
123
|
+
# Normalize
|
|
124
|
+
if normalized and acf[0] > 0:
|
|
125
|
+
acf = acf / acf[0]
|
|
126
|
+
|
|
127
|
+
# Time axis
|
|
128
|
+
lags = np.arange(max_lag + 1)
|
|
129
|
+
lag_times = lags / fs
|
|
130
|
+
|
|
131
|
+
return lag_times, acf.astype(np.float64)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def cross_correlation(
|
|
135
|
+
trace1: WaveformTrace | NDArray[np.floating[Any]],
|
|
136
|
+
trace2: WaveformTrace | NDArray[np.floating[Any]],
|
|
137
|
+
*,
|
|
138
|
+
max_lag: int | None = None,
|
|
139
|
+
normalized: bool = True,
|
|
140
|
+
sample_rate: float | None = None,
|
|
141
|
+
) -> CrossCorrelationResult:
|
|
142
|
+
"""Compute cross-correlation between two signals.
|
|
143
|
+
|
|
144
|
+
Measures similarity between signals at different time lags.
|
|
145
|
+
Useful for finding time delays, alignments, and relationships.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
trace1: First input trace or numpy array (reference).
|
|
149
|
+
trace2: Second input trace or numpy array.
|
|
150
|
+
max_lag: Maximum lag to compute (samples). If None, uses min(n1, n2) // 2.
|
|
151
|
+
normalized: If True, normalize to correlation coefficients [-1, 1].
|
|
152
|
+
sample_rate: Sample rate in Hz. Required if traces are arrays.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
CrossCorrelationResult with correlation data and optimal lag.
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
ValueError: If sample_rate is not provided when traces are arrays.
|
|
159
|
+
|
|
160
|
+
Example:
|
|
161
|
+
>>> result = cross_correlation(trace1, trace2)
|
|
162
|
+
>>> print(f"Optimal lag: {result.peak_lag_time * 1e6:.1f} us")
|
|
163
|
+
>>> print(f"Correlation: {result.peak_coefficient:.3f}")
|
|
164
|
+
|
|
165
|
+
References:
|
|
166
|
+
Oppenheim, A. V. & Schafer, R. W. (2009). Discrete-Time Signal Processing
|
|
167
|
+
"""
|
|
168
|
+
if isinstance(trace1, WaveformTrace):
|
|
169
|
+
data1 = trace1.data
|
|
170
|
+
fs = trace1.metadata.sample_rate
|
|
171
|
+
else:
|
|
172
|
+
data1 = trace1
|
|
173
|
+
if sample_rate is None:
|
|
174
|
+
raise ValueError("sample_rate required when traces are arrays")
|
|
175
|
+
fs = sample_rate
|
|
176
|
+
|
|
177
|
+
if isinstance(trace2, WaveformTrace):
|
|
178
|
+
data2 = trace2.data
|
|
179
|
+
# Use trace2 sample rate if available and trace1 wasn't a WaveformTrace
|
|
180
|
+
if not isinstance(trace1, WaveformTrace):
|
|
181
|
+
fs = trace2.metadata.sample_rate
|
|
182
|
+
else:
|
|
183
|
+
data2 = trace2
|
|
184
|
+
|
|
185
|
+
n1, n2 = len(data1), len(data2)
|
|
186
|
+
|
|
187
|
+
if max_lag is None:
|
|
188
|
+
max_lag = min(n1, n2) // 2
|
|
189
|
+
|
|
190
|
+
# Center the data
|
|
191
|
+
data1_centered = data1 - np.mean(data1)
|
|
192
|
+
data2_centered = data2 - np.mean(data2)
|
|
193
|
+
|
|
194
|
+
# Full cross-correlation
|
|
195
|
+
# Note: np.correlate(a, b) computes sum(a[n+k] * conj(b[k]))
|
|
196
|
+
# For cross-correlation where we want to detect b delayed relative to a,
|
|
197
|
+
# we need correlate(b, a) so positive lag means b is delayed
|
|
198
|
+
xcorr_full = np.correlate(data2_centered, data1_centered, mode="full")
|
|
199
|
+
|
|
200
|
+
# Extract relevant portion around zero lag
|
|
201
|
+
# Full correlation has length n1 + n2 - 1, with zero lag at index n1 - 1
|
|
202
|
+
# (since we swapped the order above)
|
|
203
|
+
zero_lag_idx = n1 - 1
|
|
204
|
+
start_idx = max(0, zero_lag_idx - max_lag)
|
|
205
|
+
end_idx = min(len(xcorr_full), zero_lag_idx + max_lag + 1)
|
|
206
|
+
xcorr = xcorr_full[start_idx:end_idx]
|
|
207
|
+
|
|
208
|
+
# Create lag array
|
|
209
|
+
lags = np.arange(start_idx - zero_lag_idx, end_idx - zero_lag_idx)
|
|
210
|
+
|
|
211
|
+
# Normalize
|
|
212
|
+
if normalized:
|
|
213
|
+
norm1 = np.sqrt(np.sum(data1_centered**2))
|
|
214
|
+
norm2 = np.sqrt(np.sum(data2_centered**2))
|
|
215
|
+
if norm1 > 0 and norm2 > 0:
|
|
216
|
+
xcorr = xcorr / (norm1 * norm2)
|
|
217
|
+
|
|
218
|
+
# Find peak
|
|
219
|
+
peak_local_idx = np.argmax(np.abs(xcorr))
|
|
220
|
+
peak_lag = int(lags[peak_local_idx])
|
|
221
|
+
peak_coefficient = float(xcorr[peak_local_idx])
|
|
222
|
+
|
|
223
|
+
# Time values
|
|
224
|
+
lag_times = lags / fs
|
|
225
|
+
peak_lag_time = peak_lag / fs
|
|
226
|
+
|
|
227
|
+
return CrossCorrelationResult(
|
|
228
|
+
correlation=xcorr.astype(np.float64),
|
|
229
|
+
lags=lags,
|
|
230
|
+
lag_times=lag_times.astype(np.float64),
|
|
231
|
+
peak_lag=peak_lag,
|
|
232
|
+
peak_lag_time=peak_lag_time,
|
|
233
|
+
peak_coefficient=peak_coefficient,
|
|
234
|
+
sample_rate=fs,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def correlation_coefficient(
|
|
239
|
+
trace1: WaveformTrace | NDArray[np.floating[Any]],
|
|
240
|
+
trace2: WaveformTrace | NDArray[np.floating[Any]],
|
|
241
|
+
) -> float:
|
|
242
|
+
"""Compute Pearson correlation coefficient between two signals.
|
|
243
|
+
|
|
244
|
+
Simple measure of linear relationship between signals at zero lag.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
trace1: First input trace or numpy array.
|
|
248
|
+
trace2: Second input trace or numpy array.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Correlation coefficient in range [-1, 1].
|
|
252
|
+
|
|
253
|
+
Example:
|
|
254
|
+
>>> r = correlation_coefficient(trace1, trace2)
|
|
255
|
+
>>> print(f"Correlation: {r:.3f}")
|
|
256
|
+
"""
|
|
257
|
+
data1 = trace1.data if isinstance(trace1, WaveformTrace) else trace1
|
|
258
|
+
|
|
259
|
+
data2 = trace2.data if isinstance(trace2, WaveformTrace) else trace2
|
|
260
|
+
|
|
261
|
+
# Ensure same length
|
|
262
|
+
n = min(len(data1), len(data2))
|
|
263
|
+
data1 = data1[:n]
|
|
264
|
+
data2 = data2[:n]
|
|
265
|
+
|
|
266
|
+
# Compute correlation
|
|
267
|
+
return float(np.corrcoef(data1, data2)[0, 1])
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def find_periodicity(
|
|
271
|
+
trace: WaveformTrace | NDArray[np.floating[Any]],
|
|
272
|
+
*,
|
|
273
|
+
min_period_samples: int = 2,
|
|
274
|
+
max_period_samples: int | None = None,
|
|
275
|
+
sample_rate: float | None = None,
|
|
276
|
+
) -> dict[str, float | int | list[dict[str, int | float]]]:
|
|
277
|
+
"""Find dominant periodicity in signal using autocorrelation.
|
|
278
|
+
|
|
279
|
+
Detects the primary periodic component by finding the first
|
|
280
|
+
significant peak in the autocorrelation function.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
trace: Input trace or numpy array.
|
|
284
|
+
min_period_samples: Minimum period to consider (samples).
|
|
285
|
+
max_period_samples: Maximum period to consider (samples).
|
|
286
|
+
sample_rate: Sample rate in Hz (required for array input).
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Dictionary with periodicity analysis:
|
|
290
|
+
- period_samples: Period in samples
|
|
291
|
+
- period_time: Period in seconds
|
|
292
|
+
- frequency: Frequency in Hz
|
|
293
|
+
- strength: Autocorrelation at period (0-1)
|
|
294
|
+
- harmonics: List of detected harmonics
|
|
295
|
+
|
|
296
|
+
Raises:
|
|
297
|
+
ValueError: If sample_rate is not provided when trace is array.
|
|
298
|
+
|
|
299
|
+
Example:
|
|
300
|
+
>>> result = find_periodicity(trace)
|
|
301
|
+
>>> print(f"Period: {result['period_time']*1e6:.2f} us")
|
|
302
|
+
>>> print(f"Frequency: {result['frequency']/1e3:.1f} kHz")
|
|
303
|
+
"""
|
|
304
|
+
if isinstance(trace, WaveformTrace):
|
|
305
|
+
data = trace.data
|
|
306
|
+
fs = trace.metadata.sample_rate
|
|
307
|
+
else:
|
|
308
|
+
data = trace
|
|
309
|
+
if sample_rate is None:
|
|
310
|
+
raise ValueError("sample_rate required when trace is array")
|
|
311
|
+
fs = sample_rate
|
|
312
|
+
|
|
313
|
+
n = len(data)
|
|
314
|
+
|
|
315
|
+
if max_period_samples is None:
|
|
316
|
+
max_period_samples = n // 2
|
|
317
|
+
|
|
318
|
+
# Compute autocorrelation
|
|
319
|
+
_lag_times, acf = autocorrelation(
|
|
320
|
+
trace,
|
|
321
|
+
max_lag=max_period_samples,
|
|
322
|
+
sample_rate=sample_rate if sample_rate else fs,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# Find peaks in autocorrelation (after lag 0)
|
|
326
|
+
# Look for local maxima
|
|
327
|
+
acf_search = acf[min_period_samples:]
|
|
328
|
+
|
|
329
|
+
if len(acf_search) < 3:
|
|
330
|
+
return {
|
|
331
|
+
"period_samples": np.nan,
|
|
332
|
+
"period_time": np.nan,
|
|
333
|
+
"frequency": np.nan,
|
|
334
|
+
"strength": np.nan,
|
|
335
|
+
"harmonics": [],
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
# Find local maxima
|
|
339
|
+
local_max = (acf_search[1:-1] > acf_search[:-2]) & (acf_search[1:-1] > acf_search[2:])
|
|
340
|
+
max_indices = np.where(local_max)[0] + 1 # +1 for offset from [1:-1]
|
|
341
|
+
|
|
342
|
+
if len(max_indices) == 0:
|
|
343
|
+
# No local maxima found, use global max
|
|
344
|
+
primary_idx = int(np.argmax(acf_search)) + min_period_samples
|
|
345
|
+
strength = float(acf[primary_idx])
|
|
346
|
+
else:
|
|
347
|
+
# Find strongest peak
|
|
348
|
+
peak_values = acf_search[max_indices]
|
|
349
|
+
best_peak_idx = int(np.argmax(peak_values))
|
|
350
|
+
primary_idx = int(max_indices[best_peak_idx]) + min_period_samples
|
|
351
|
+
strength = float(acf[primary_idx])
|
|
352
|
+
|
|
353
|
+
period_samples = int(primary_idx)
|
|
354
|
+
period_time = period_samples / fs
|
|
355
|
+
frequency = 1.0 / period_time if period_time > 0 else np.nan
|
|
356
|
+
|
|
357
|
+
# Find harmonics (peaks at multiples of period)
|
|
358
|
+
harmonics: list[dict[str, int | float]] = []
|
|
359
|
+
for h in range(2, 6): # Check up to 5th harmonic
|
|
360
|
+
harmonic_lag = h * period_samples
|
|
361
|
+
if harmonic_lag < len(acf):
|
|
362
|
+
# Look for peak near expected harmonic
|
|
363
|
+
search_range = max(1, period_samples // 4)
|
|
364
|
+
start = int(max(0, harmonic_lag - search_range))
|
|
365
|
+
end = int(min(len(acf), harmonic_lag + search_range))
|
|
366
|
+
local_max_idx = int(start + int(np.argmax(acf[start:end])))
|
|
367
|
+
harmonic_strength = float(acf[local_max_idx])
|
|
368
|
+
|
|
369
|
+
if harmonic_strength > 0.3: # Threshold for significant harmonic
|
|
370
|
+
harmonics.append(
|
|
371
|
+
{
|
|
372
|
+
"harmonic": h,
|
|
373
|
+
"lag_samples": local_max_idx,
|
|
374
|
+
"strength": harmonic_strength,
|
|
375
|
+
}
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
"period_samples": period_samples,
|
|
380
|
+
"period_time": float(period_time),
|
|
381
|
+
"frequency": float(frequency),
|
|
382
|
+
"strength": strength,
|
|
383
|
+
"harmonics": harmonics,
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def coherence(
|
|
388
|
+
trace1: WaveformTrace | NDArray[np.floating[Any]],
|
|
389
|
+
trace2: WaveformTrace | NDArray[np.floating[Any]],
|
|
390
|
+
*,
|
|
391
|
+
nperseg: int | None = None,
|
|
392
|
+
sample_rate: float | None = None,
|
|
393
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
394
|
+
"""Compute magnitude-squared coherence between two signals.
|
|
395
|
+
|
|
396
|
+
Measures frequency-domain correlation between signals.
|
|
397
|
+
Coherence of 1 indicates perfect linear relationship at that frequency.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
trace1: First input trace or numpy array.
|
|
401
|
+
trace2: Second input trace or numpy array.
|
|
402
|
+
nperseg: Segment length for estimation. If None, auto-selected.
|
|
403
|
+
sample_rate: Sample rate in Hz (required for array input).
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
Tuple of (frequencies, coherence):
|
|
407
|
+
- frequencies: Frequency values in Hz
|
|
408
|
+
- coherence: Magnitude-squared coherence [0, 1]
|
|
409
|
+
|
|
410
|
+
Raises:
|
|
411
|
+
ValueError: If sample_rate is not provided when traces are arrays.
|
|
412
|
+
|
|
413
|
+
Example:
|
|
414
|
+
>>> freq, coh = coherence(trace1, trace2)
|
|
415
|
+
>>> # Find frequencies with high coherence
|
|
416
|
+
>>> high_coh_freqs = freq[coh > 0.8]
|
|
417
|
+
"""
|
|
418
|
+
from scipy import signal as sp_signal
|
|
419
|
+
|
|
420
|
+
if isinstance(trace1, WaveformTrace):
|
|
421
|
+
data1 = trace1.data
|
|
422
|
+
fs = trace1.metadata.sample_rate
|
|
423
|
+
else:
|
|
424
|
+
data1 = trace1
|
|
425
|
+
if sample_rate is None:
|
|
426
|
+
raise ValueError("sample_rate required when traces are arrays")
|
|
427
|
+
fs = sample_rate
|
|
428
|
+
|
|
429
|
+
data2 = trace2.data if isinstance(trace2, WaveformTrace) else trace2
|
|
430
|
+
|
|
431
|
+
# Ensure same length
|
|
432
|
+
n = min(len(data1), len(data2))
|
|
433
|
+
data1 = data1[:n]
|
|
434
|
+
data2 = data2[:n]
|
|
435
|
+
|
|
436
|
+
if nperseg is None:
|
|
437
|
+
nperseg = min(256, n // 4)
|
|
438
|
+
nperseg = max(nperseg, 16)
|
|
439
|
+
|
|
440
|
+
freq, coh = sp_signal.coherence(data1, data2, fs=fs, nperseg=nperseg, noverlap=nperseg // 2)
|
|
441
|
+
|
|
442
|
+
return freq, coh.astype(np.float64)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def correlate_chunked(
|
|
446
|
+
signal1: NDArray[np.floating[Any]],
|
|
447
|
+
signal2: NDArray[np.floating[Any]],
|
|
448
|
+
*,
|
|
449
|
+
mode: str = "same",
|
|
450
|
+
chunk_size: int | None = None,
|
|
451
|
+
) -> NDArray[np.float64]:
|
|
452
|
+
"""Memory-efficient cross-correlation using overlap-save FFT method.
|
|
453
|
+
|
|
454
|
+
Computes cross-correlation for large signals that don't fit in memory
|
|
455
|
+
by processing in chunks using the overlap-save method with FFT.
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
signal1: First input signal array.
|
|
459
|
+
signal2: Second input signal array (kernel/template).
|
|
460
|
+
mode: Correlation mode - 'same', 'valid', or 'full' (default 'same').
|
|
461
|
+
chunk_size: Size of chunks for processing. If None, auto-selected.
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
Cross-correlation result with same semantics as numpy.correlate.
|
|
465
|
+
|
|
466
|
+
Raises:
|
|
467
|
+
ValueError: If signals are empty or mode is invalid.
|
|
468
|
+
|
|
469
|
+
Example:
|
|
470
|
+
>>> import numpy as np
|
|
471
|
+
>>> # Large signals
|
|
472
|
+
>>> signal1 = np.random.randn(100_000_000)
|
|
473
|
+
>>> signal2 = np.random.randn(10_000)
|
|
474
|
+
>>> # Memory-efficient correlation
|
|
475
|
+
>>> result = correlate_chunked(signal1, signal2, mode='same')
|
|
476
|
+
>>> print(f"Result shape: {result.shape}")
|
|
477
|
+
|
|
478
|
+
Notes:
|
|
479
|
+
Uses overlap-save FFT-based convolution which is memory-efficient
|
|
480
|
+
and faster than direct correlation for large signals.
|
|
481
|
+
|
|
482
|
+
References:
|
|
483
|
+
MEM-008: Chunked Correlation
|
|
484
|
+
Oppenheim & Schafer (2009): Discrete-Time Signal Processing, Ch 8
|
|
485
|
+
"""
|
|
486
|
+
if len(signal1) == 0 or len(signal2) == 0:
|
|
487
|
+
raise ValueError("Input signals cannot be empty")
|
|
488
|
+
|
|
489
|
+
if mode not in ("same", "valid", "full"):
|
|
490
|
+
raise ValueError(f"Invalid mode: {mode}. Must be 'same', 'valid', or 'full'")
|
|
491
|
+
|
|
492
|
+
n1 = len(signal1)
|
|
493
|
+
n2 = len(signal2)
|
|
494
|
+
|
|
495
|
+
# Determine chunk size
|
|
496
|
+
if chunk_size is None:
|
|
497
|
+
# Auto-select: aim for ~100MB chunks
|
|
498
|
+
bytes_per_sample = 8 # float64
|
|
499
|
+
target_bytes = 100 * 1024 * 1024
|
|
500
|
+
chunk_size = min(target_bytes // bytes_per_sample, n1)
|
|
501
|
+
# Round to power of 2 for FFT efficiency
|
|
502
|
+
chunk_size = 2 ** int(np.log2(chunk_size))
|
|
503
|
+
|
|
504
|
+
# Ensure chunk_size is larger than filter length for overlap-save
|
|
505
|
+
# Otherwise overlap-save doesn't make sense
|
|
506
|
+
min_chunk_size = max(2 * n2, 64)
|
|
507
|
+
|
|
508
|
+
# For small signals or when chunk_size is too small, use direct method
|
|
509
|
+
if n1 <= min_chunk_size or n2 >= n1 or chunk_size < min_chunk_size:
|
|
510
|
+
mode_literal = cast("Literal['same', 'valid', 'full']", mode)
|
|
511
|
+
result = np.correlate(signal1, signal2, mode=mode_literal)
|
|
512
|
+
return result.astype(np.float64)
|
|
513
|
+
|
|
514
|
+
# For correlation, we need to flip signal2
|
|
515
|
+
signal2_flipped = signal2[::-1].copy()
|
|
516
|
+
|
|
517
|
+
# Overlap-save parameters
|
|
518
|
+
# L = chunk size, M = filter length
|
|
519
|
+
L = max(chunk_size, min_chunk_size)
|
|
520
|
+
M = n2
|
|
521
|
+
overlap = M - 1
|
|
522
|
+
|
|
523
|
+
# Ensure step size is positive (L must be > overlap)
|
|
524
|
+
step_size = L - overlap
|
|
525
|
+
if step_size <= 0:
|
|
526
|
+
# Fall back to direct method if chunk is too small
|
|
527
|
+
mode_literal = cast("Literal['same', 'valid', 'full']", mode)
|
|
528
|
+
result = np.correlate(signal1, signal2, mode=mode_literal)
|
|
529
|
+
return result.astype(np.float64)
|
|
530
|
+
|
|
531
|
+
# FFT size (power of 2, >= L + M - 1)
|
|
532
|
+
nfft = int(2 ** np.ceil(np.log2(L + M - 1)))
|
|
533
|
+
|
|
534
|
+
# Pre-compute FFT of flipped signal2 (kernel)
|
|
535
|
+
kernel_fft = np.fft.fft(signal2_flipped, n=nfft)
|
|
536
|
+
|
|
537
|
+
# Output length based on mode
|
|
538
|
+
if mode == "full":
|
|
539
|
+
output_len = n1 + n2 - 1
|
|
540
|
+
elif mode == "same":
|
|
541
|
+
output_len = n1
|
|
542
|
+
else: # valid
|
|
543
|
+
output_len = max(0, n1 - n2 + 1)
|
|
544
|
+
|
|
545
|
+
# Initialize output
|
|
546
|
+
output = np.zeros(output_len, dtype=np.float64)
|
|
547
|
+
|
|
548
|
+
# Process chunks with overlap-save
|
|
549
|
+
pos = 0 # Position in signal1
|
|
550
|
+
max_iterations = (n1 // step_size) + 2 # Safety limit
|
|
551
|
+
iteration = 0
|
|
552
|
+
|
|
553
|
+
while pos < n1 and iteration < max_iterations:
|
|
554
|
+
iteration += 1
|
|
555
|
+
|
|
556
|
+
# Extract chunk with overlap from previous chunk
|
|
557
|
+
if pos == 0:
|
|
558
|
+
# First chunk: no overlap needed
|
|
559
|
+
chunk_start = 0
|
|
560
|
+
chunk = signal1[0 : min(L, n1)]
|
|
561
|
+
else:
|
|
562
|
+
# Subsequent chunks: include overlap
|
|
563
|
+
chunk_start = max(0, pos - overlap)
|
|
564
|
+
chunk_end = min(chunk_start + L, n1)
|
|
565
|
+
chunk = signal1[chunk_start:chunk_end]
|
|
566
|
+
|
|
567
|
+
# Zero-pad chunk to FFT size
|
|
568
|
+
chunk_padded = np.zeros(nfft, dtype=np.float64)
|
|
569
|
+
chunk_padded[: len(chunk)] = chunk
|
|
570
|
+
|
|
571
|
+
# Perform FFT-based convolution
|
|
572
|
+
chunk_fft = np.fft.fft(chunk_padded)
|
|
573
|
+
conv_fft = chunk_fft * kernel_fft
|
|
574
|
+
conv_result = np.fft.ifft(conv_fft).real
|
|
575
|
+
|
|
576
|
+
# Extract valid portion (discard transient at start)
|
|
577
|
+
if pos == 0:
|
|
578
|
+
# First chunk
|
|
579
|
+
valid_start = 0
|
|
580
|
+
valid_end = min(L, len(conv_result))
|
|
581
|
+
else:
|
|
582
|
+
# Subsequent chunks: discard overlap region
|
|
583
|
+
valid_start = overlap
|
|
584
|
+
valid_end = min(len(chunk), len(conv_result))
|
|
585
|
+
|
|
586
|
+
valid_output = conv_result[valid_start:valid_end]
|
|
587
|
+
|
|
588
|
+
# Determine output range based on mode
|
|
589
|
+
if mode == "full":
|
|
590
|
+
# Full convolution includes all overlap
|
|
591
|
+
out_start = pos
|
|
592
|
+
out_end = min(out_start + len(valid_output), output_len)
|
|
593
|
+
elif mode == "same":
|
|
594
|
+
# Same mode: center-aligned
|
|
595
|
+
offset = (M - 1) // 2
|
|
596
|
+
out_start = max(0, pos - offset)
|
|
597
|
+
out_end = min(out_start + len(valid_output), output_len)
|
|
598
|
+
# Adjust valid_output if we're at boundaries
|
|
599
|
+
if pos == 0 and offset > 0:
|
|
600
|
+
valid_output = valid_output[offset:]
|
|
601
|
+
else: # valid
|
|
602
|
+
# Valid mode: only where signals fully overlap
|
|
603
|
+
offset = M - 1
|
|
604
|
+
if pos < offset:
|
|
605
|
+
# Skip this chunk, not in valid region yet
|
|
606
|
+
pos += step_size
|
|
607
|
+
continue
|
|
608
|
+
out_start = pos - offset
|
|
609
|
+
out_end = min(out_start + len(valid_output), output_len)
|
|
610
|
+
|
|
611
|
+
# Copy to output
|
|
612
|
+
copy_len = min(len(valid_output), out_end - out_start)
|
|
613
|
+
if copy_len > 0:
|
|
614
|
+
output[out_start : out_start + copy_len] = valid_output[:copy_len]
|
|
615
|
+
|
|
616
|
+
# Move to next chunk with guaranteed progress
|
|
617
|
+
pos += step_size
|
|
618
|
+
|
|
619
|
+
return output
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
__all__ = [
|
|
623
|
+
"CrossCorrelationResult",
|
|
624
|
+
"autocorrelation",
|
|
625
|
+
"coherence",
|
|
626
|
+
"correlate_chunked",
|
|
627
|
+
"correlation_coefficient",
|
|
628
|
+
"cross_correlation",
|
|
629
|
+
"find_periodicity",
|
|
630
|
+
]
|