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,650 @@
|
|
|
1
|
+
"""Periodic pattern detection using multiple algorithms.
|
|
2
|
+
|
|
3
|
+
This module implements robust periodic pattern detection for digital signals
|
|
4
|
+
and binary data using autocorrelation, FFT spectral analysis, and suffix array
|
|
5
|
+
techniques.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
Author: Oscura Development Team
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import TYPE_CHECKING, Literal
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from numpy.typing import NDArray
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class PeriodResult:
|
|
24
|
+
"""Result of period detection.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
period_samples: Period in number of samples
|
|
28
|
+
period_seconds: Period in seconds (if sample_rate provided)
|
|
29
|
+
frequency_hz: Fundamental frequency in Hz
|
|
30
|
+
confidence: Detection confidence (0-1)
|
|
31
|
+
method: Detection method used
|
|
32
|
+
harmonics: List of detected harmonic frequencies (optional)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
period_samples: float
|
|
36
|
+
period_seconds: float
|
|
37
|
+
frequency_hz: float
|
|
38
|
+
confidence: float
|
|
39
|
+
method: str
|
|
40
|
+
harmonics: list[float] | None = field(default=None)
|
|
41
|
+
|
|
42
|
+
# Alias for compatibility with tests
|
|
43
|
+
@property
|
|
44
|
+
def period(self) -> float:
|
|
45
|
+
"""Alias for period_samples for test compatibility."""
|
|
46
|
+
return self.period_samples
|
|
47
|
+
|
|
48
|
+
def __post_init__(self) -> None:
|
|
49
|
+
"""Validate period result values."""
|
|
50
|
+
if self.period_samples <= 0:
|
|
51
|
+
raise ValueError("period_samples must be positive")
|
|
52
|
+
if self.confidence < 0 or self.confidence > 1:
|
|
53
|
+
raise ValueError("confidence must be in range [0, 1]")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def detect_period(
|
|
57
|
+
trace: NDArray[np.float64],
|
|
58
|
+
sample_rate: float = 1.0,
|
|
59
|
+
method: Literal["auto", "autocorr", "fft", "suffix"] = "auto",
|
|
60
|
+
min_period: int = 2,
|
|
61
|
+
max_period: int | None = None,
|
|
62
|
+
) -> PeriodResult | None:
|
|
63
|
+
"""Detect dominant period in signal using best available method.
|
|
64
|
+
|
|
65
|
+
: Periodic Pattern Detection
|
|
66
|
+
|
|
67
|
+
This function automatically selects the most appropriate algorithm based
|
|
68
|
+
on signal characteristics or uses the specified method.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
trace: Input signal array (1D)
|
|
72
|
+
sample_rate: Sampling rate in Hz (default: 1.0)
|
|
73
|
+
method: Detection method ('auto', 'autocorr', 'fft', 'suffix')
|
|
74
|
+
min_period: Minimum period to detect in samples
|
|
75
|
+
max_period: Maximum period to detect in samples (None = len(trace)//2)
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
PeriodResult with detected period information, or None if no period found
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
ValueError: If trace is empty, min_period invalid, or parameters inconsistent
|
|
82
|
+
|
|
83
|
+
Examples:
|
|
84
|
+
>>> signal = np.sin(2 * np.pi * 5 * np.linspace(0, 1, 1000))
|
|
85
|
+
>>> result = detect_period(signal, sample_rate=1000.0)
|
|
86
|
+
>>> print(f"Period: {result.period_seconds:.3f}s, Freq: {result.frequency_hz:.1f}Hz")
|
|
87
|
+
"""
|
|
88
|
+
# Input validation
|
|
89
|
+
if trace.size == 0:
|
|
90
|
+
raise ValueError("trace cannot be empty")
|
|
91
|
+
if min_period < 2:
|
|
92
|
+
raise ValueError("min_period must be at least 2")
|
|
93
|
+
if max_period is not None and max_period < min_period:
|
|
94
|
+
raise ValueError("max_period must be >= min_period")
|
|
95
|
+
if sample_rate <= 0:
|
|
96
|
+
raise ValueError("sample_rate must be positive")
|
|
97
|
+
|
|
98
|
+
# Ensure 1D array
|
|
99
|
+
trace = np.asarray(trace).flatten()
|
|
100
|
+
|
|
101
|
+
# Set default max_period
|
|
102
|
+
if max_period is None:
|
|
103
|
+
max_period = len(trace) // 2
|
|
104
|
+
max_period = min(max_period, len(trace) // 2)
|
|
105
|
+
|
|
106
|
+
# Auto-select method based on signal characteristics
|
|
107
|
+
if method == "auto":
|
|
108
|
+
# Use FFT for longer signals (more efficient)
|
|
109
|
+
# Use autocorr for shorter signals or binary data
|
|
110
|
+
if len(trace) > 10000:
|
|
111
|
+
method = "fft"
|
|
112
|
+
elif np.all(np.isin(trace, [0, 1])):
|
|
113
|
+
method = "autocorr" # Better for binary signals
|
|
114
|
+
else:
|
|
115
|
+
method = "fft"
|
|
116
|
+
|
|
117
|
+
# Dispatch to appropriate method
|
|
118
|
+
if method == "autocorr":
|
|
119
|
+
results = detect_periods_autocorr(trace, sample_rate, max_period, min_correlation=0.5)
|
|
120
|
+
return results[0] if results else None
|
|
121
|
+
|
|
122
|
+
elif method == "fft":
|
|
123
|
+
min_freq = sample_rate / max_period if max_period else None
|
|
124
|
+
max_freq = sample_rate / min_period if min_period > 0 else None
|
|
125
|
+
results = detect_periods_fft(trace, sample_rate, min_freq, max_freq, num_peaks=5)
|
|
126
|
+
return results[0] if results else None
|
|
127
|
+
|
|
128
|
+
elif method == "suffix":
|
|
129
|
+
# Suffix array method for exact repeats
|
|
130
|
+
period_samples = _detect_period_suffix(trace, min_period, max_period)
|
|
131
|
+
if period_samples is None:
|
|
132
|
+
return None
|
|
133
|
+
return PeriodResult(
|
|
134
|
+
period_samples=float(period_samples),
|
|
135
|
+
period_seconds=period_samples / sample_rate,
|
|
136
|
+
frequency_hz=sample_rate / period_samples,
|
|
137
|
+
confidence=0.9, # High confidence for exact matches
|
|
138
|
+
method="suffix",
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
raise ValueError(f"Unknown method: {method}")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def detect_periods_fft(
|
|
145
|
+
trace: NDArray[np.float64],
|
|
146
|
+
sample_rate: float = 1.0,
|
|
147
|
+
min_freq: float | None = None,
|
|
148
|
+
max_freq: float | None = None,
|
|
149
|
+
num_peaks: int = 5,
|
|
150
|
+
) -> list[PeriodResult]:
|
|
151
|
+
"""Detect periods using FFT spectral analysis.
|
|
152
|
+
|
|
153
|
+
: Periodic Pattern Detection via FFT
|
|
154
|
+
|
|
155
|
+
Uses power spectral density to identify dominant frequencies and their
|
|
156
|
+
harmonics. More efficient than autocorrelation for long signals.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
trace: Input signal array (1D)
|
|
160
|
+
sample_rate: Sampling rate in Hz
|
|
161
|
+
min_freq: Minimum frequency to consider (Hz)
|
|
162
|
+
max_freq: Maximum frequency to consider (Hz)
|
|
163
|
+
num_peaks: Maximum number of peaks to return
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
List of PeriodResult sorted by confidence (strongest first)
|
|
167
|
+
|
|
168
|
+
Examples:
|
|
169
|
+
>>> signal = np.sin(2*np.pi*10*np.linspace(0, 1, 1000))
|
|
170
|
+
>>> periods = detect_periods_fft(signal, sample_rate=1000.0, num_peaks=3)
|
|
171
|
+
"""
|
|
172
|
+
trace = np.asarray(trace).flatten()
|
|
173
|
+
|
|
174
|
+
if trace.size == 0:
|
|
175
|
+
return []
|
|
176
|
+
|
|
177
|
+
# Remove DC component
|
|
178
|
+
trace_centered = trace - np.mean(trace)
|
|
179
|
+
|
|
180
|
+
# Compute FFT
|
|
181
|
+
n = len(trace_centered)
|
|
182
|
+
fft_result = np.fft.rfft(trace_centered)
|
|
183
|
+
power = np.asarray(np.abs(fft_result) ** 2, dtype=np.float64)
|
|
184
|
+
freqs = np.fft.rfftfreq(n, 1.0 / sample_rate)
|
|
185
|
+
|
|
186
|
+
# Apply frequency range filtering
|
|
187
|
+
valid_mask = np.ones(len(freqs), dtype=bool)
|
|
188
|
+
if min_freq is not None:
|
|
189
|
+
valid_mask &= freqs >= min_freq
|
|
190
|
+
if max_freq is not None:
|
|
191
|
+
valid_mask &= freqs <= max_freq
|
|
192
|
+
|
|
193
|
+
# Exclude DC component
|
|
194
|
+
valid_mask[0] = False
|
|
195
|
+
|
|
196
|
+
# Find peaks in power spectrum
|
|
197
|
+
peak_indices = _find_spectral_peaks(power, min_distance=1)
|
|
198
|
+
peak_indices = peak_indices[valid_mask[peak_indices]]
|
|
199
|
+
|
|
200
|
+
if len(peak_indices) == 0:
|
|
201
|
+
return []
|
|
202
|
+
|
|
203
|
+
# Sort by power
|
|
204
|
+
peak_powers = power[peak_indices]
|
|
205
|
+
sorted_indices = np.argsort(peak_powers)[::-1][:num_peaks]
|
|
206
|
+
peak_indices = peak_indices[sorted_indices]
|
|
207
|
+
|
|
208
|
+
# Build results
|
|
209
|
+
results = []
|
|
210
|
+
max_power = np.max(power[peak_indices]) if len(peak_indices) > 0 else 1.0
|
|
211
|
+
|
|
212
|
+
for idx in peak_indices:
|
|
213
|
+
freq = freqs[idx]
|
|
214
|
+
if freq == 0:
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
period_seconds = 1.0 / freq
|
|
218
|
+
period_samples = sample_rate / freq
|
|
219
|
+
|
|
220
|
+
# Confidence based on relative power
|
|
221
|
+
confidence = float(power[idx] / max_power)
|
|
222
|
+
|
|
223
|
+
# Detect harmonics (simple approach: look for integer multiples)
|
|
224
|
+
harmonics = []
|
|
225
|
+
for mult in range(2, 6):
|
|
226
|
+
harmonic_freq = freq * mult
|
|
227
|
+
if harmonic_freq < freqs[-1]:
|
|
228
|
+
# Find closest frequency bin
|
|
229
|
+
harmonic_idx = np.argmin(np.abs(freqs - harmonic_freq))
|
|
230
|
+
if power[harmonic_idx] > 0.1 * power[idx]:
|
|
231
|
+
harmonics.append(harmonic_freq)
|
|
232
|
+
|
|
233
|
+
results.append(
|
|
234
|
+
PeriodResult(
|
|
235
|
+
period_samples=period_samples,
|
|
236
|
+
period_seconds=period_seconds,
|
|
237
|
+
frequency_hz=freq,
|
|
238
|
+
confidence=min(confidence, 1.0),
|
|
239
|
+
method="fft",
|
|
240
|
+
harmonics=harmonics if harmonics else None,
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return results
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def detect_periods_autocorr(
|
|
248
|
+
trace: NDArray[np.float64],
|
|
249
|
+
sample_rate: float = 1.0,
|
|
250
|
+
max_period: int | None = None,
|
|
251
|
+
min_correlation: float = 0.5,
|
|
252
|
+
) -> list[PeriodResult]:
|
|
253
|
+
"""Detect periods using autocorrelation.
|
|
254
|
+
|
|
255
|
+
: Periodic Pattern Detection via autocorrelation
|
|
256
|
+
|
|
257
|
+
Computes normalized autocorrelation and finds peaks corresponding to
|
|
258
|
+
periodic patterns. More robust to noise than simple pattern matching.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
trace: Input signal array (1D)
|
|
262
|
+
sample_rate: Sampling rate in Hz
|
|
263
|
+
max_period: Maximum period to search (samples)
|
|
264
|
+
min_correlation: Minimum correlation threshold (0-1)
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
List of PeriodResult sorted by confidence
|
|
268
|
+
|
|
269
|
+
Examples:
|
|
270
|
+
>>> signal = np.tile([1, 0, 1, 0], 100)
|
|
271
|
+
>>> periods = detect_periods_autocorr(signal, min_correlation=0.7)
|
|
272
|
+
"""
|
|
273
|
+
trace = np.asarray(trace).flatten()
|
|
274
|
+
|
|
275
|
+
if trace.size == 0:
|
|
276
|
+
return []
|
|
277
|
+
|
|
278
|
+
# Set default max_period
|
|
279
|
+
if max_period is None:
|
|
280
|
+
max_period = len(trace) // 2
|
|
281
|
+
max_period = min(max_period, len(trace) // 2)
|
|
282
|
+
|
|
283
|
+
# Compute normalized autocorrelation using FFT (efficient)
|
|
284
|
+
trace_centered = trace - np.mean(trace)
|
|
285
|
+
|
|
286
|
+
# Compute via FFT convolution
|
|
287
|
+
n = len(trace_centered)
|
|
288
|
+
fft_trace = np.fft.fft(trace_centered, n=2 * n)
|
|
289
|
+
autocorr = np.fft.ifft(fft_trace * np.conj(fft_trace)).real[:n]
|
|
290
|
+
|
|
291
|
+
# Normalize
|
|
292
|
+
if autocorr[0] > 0:
|
|
293
|
+
autocorr = autocorr / autocorr[0]
|
|
294
|
+
|
|
295
|
+
# Limit search range
|
|
296
|
+
autocorr = autocorr[: max_period + 1]
|
|
297
|
+
|
|
298
|
+
# Find peaks (skip lag 0)
|
|
299
|
+
peaks = _find_spectral_peaks(autocorr[1:], min_distance=1) + 1
|
|
300
|
+
|
|
301
|
+
# Filter by minimum correlation
|
|
302
|
+
peaks = peaks[autocorr[peaks] >= min_correlation]
|
|
303
|
+
|
|
304
|
+
if len(peaks) == 0:
|
|
305
|
+
return []
|
|
306
|
+
|
|
307
|
+
# Sort by correlation value
|
|
308
|
+
peak_corrs = autocorr[peaks]
|
|
309
|
+
sorted_indices = np.argsort(peak_corrs)[::-1]
|
|
310
|
+
peaks = peaks[sorted_indices]
|
|
311
|
+
|
|
312
|
+
# Build results
|
|
313
|
+
results = []
|
|
314
|
+
for lag in peaks[:5]: # Top 5 peaks
|
|
315
|
+
period_samples = float(lag)
|
|
316
|
+
period_seconds = period_samples / sample_rate
|
|
317
|
+
frequency_hz = sample_rate / period_samples
|
|
318
|
+
confidence = float(autocorr[lag])
|
|
319
|
+
|
|
320
|
+
results.append(
|
|
321
|
+
PeriodResult(
|
|
322
|
+
period_samples=period_samples,
|
|
323
|
+
period_seconds=period_seconds,
|
|
324
|
+
frequency_hz=frequency_hz,
|
|
325
|
+
confidence=confidence,
|
|
326
|
+
method="autocorr",
|
|
327
|
+
)
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
return results
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def validate_period(
|
|
334
|
+
trace: NDArray[np.float64], period: float, tolerance: float = 0.01
|
|
335
|
+
) -> tuple[bool, float]:
|
|
336
|
+
"""Validate detected period against signal.
|
|
337
|
+
|
|
338
|
+
: Period validation
|
|
339
|
+
|
|
340
|
+
Verifies that the signal actually repeats at the given period by measuring
|
|
341
|
+
correlation between shifted copies of the signal.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
trace: Input signal array
|
|
345
|
+
period: Period to validate (in samples)
|
|
346
|
+
tolerance: Allowed fractional deviation in period
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
Tuple of (is_valid, actual_confidence)
|
|
350
|
+
- is_valid: True if period is confirmed
|
|
351
|
+
- actual_confidence: Measured correlation strength (0-1)
|
|
352
|
+
|
|
353
|
+
Examples:
|
|
354
|
+
>>> signal = np.tile([1, 2, 3, 4], 50)
|
|
355
|
+
>>> is_valid, conf = validate_period(signal, period=4.0)
|
|
356
|
+
>>> assert is_valid and conf > 0.95
|
|
357
|
+
"""
|
|
358
|
+
trace = np.asarray(trace).flatten()
|
|
359
|
+
|
|
360
|
+
if trace.size == 0:
|
|
361
|
+
return False, 0.0
|
|
362
|
+
|
|
363
|
+
if period < 1 or period >= len(trace):
|
|
364
|
+
return False, 0.0
|
|
365
|
+
|
|
366
|
+
# Convert to integer lag for nearest-neighbor validation
|
|
367
|
+
lag = int(round(period))
|
|
368
|
+
|
|
369
|
+
# Check if lag is within tolerance
|
|
370
|
+
if abs(period - lag) > period * tolerance:
|
|
371
|
+
# Use interpolation for sub-sample periods
|
|
372
|
+
lag_low = int(np.floor(period))
|
|
373
|
+
lag_high = int(np.ceil(period))
|
|
374
|
+
alpha = period - lag_low
|
|
375
|
+
|
|
376
|
+
if lag_high >= len(trace):
|
|
377
|
+
lag = lag_low
|
|
378
|
+
else:
|
|
379
|
+
# Weighted average of both lags
|
|
380
|
+
corr_low = _compute_lag_correlation(trace, lag_low)
|
|
381
|
+
corr_high = _compute_lag_correlation(trace, lag_high)
|
|
382
|
+
confidence = (1 - alpha) * corr_low + alpha * corr_high
|
|
383
|
+
|
|
384
|
+
is_valid = confidence >= 0.5
|
|
385
|
+
return is_valid, float(confidence)
|
|
386
|
+
|
|
387
|
+
# Simple case: integer lag
|
|
388
|
+
confidence = _compute_lag_correlation(trace, lag)
|
|
389
|
+
is_valid = confidence >= 0.5
|
|
390
|
+
|
|
391
|
+
return is_valid, float(confidence)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _compute_lag_correlation(trace: NDArray[np.float64], lag: int) -> float:
|
|
395
|
+
"""Compute normalized correlation at specific lag.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
trace: Input signal
|
|
399
|
+
lag: Lag in samples
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
Normalized correlation coefficient (0-1)
|
|
403
|
+
"""
|
|
404
|
+
if lag <= 0 or lag >= len(trace):
|
|
405
|
+
return 0.0
|
|
406
|
+
|
|
407
|
+
# Center the signal
|
|
408
|
+
trace_centered = trace - np.mean(trace)
|
|
409
|
+
|
|
410
|
+
# Compute correlation
|
|
411
|
+
n_overlap = len(trace) - lag
|
|
412
|
+
part1 = trace_centered[:n_overlap]
|
|
413
|
+
part2 = trace_centered[lag : lag + n_overlap]
|
|
414
|
+
|
|
415
|
+
# Pearson correlation
|
|
416
|
+
std1 = np.std(part1)
|
|
417
|
+
std2 = np.std(part2)
|
|
418
|
+
|
|
419
|
+
if std1 == 0 or std2 == 0:
|
|
420
|
+
return 0.0
|
|
421
|
+
|
|
422
|
+
correlation = np.mean(part1 * part2) / (std1 * std2)
|
|
423
|
+
|
|
424
|
+
# Clamp to [0, 1] range
|
|
425
|
+
return float(np.clip(correlation, 0, 1))
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _find_spectral_peaks(data: NDArray[np.float64], min_distance: int = 1) -> NDArray[np.intp]:
|
|
429
|
+
"""Find peaks in 1D array.
|
|
430
|
+
|
|
431
|
+
Simple peak detection: point is peak if higher than neighbors.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
data: 1D array
|
|
435
|
+
min_distance: Minimum distance between peaks
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
Array of peak indices
|
|
439
|
+
"""
|
|
440
|
+
if len(data) < 3:
|
|
441
|
+
return np.array([], dtype=np.intp)
|
|
442
|
+
|
|
443
|
+
# Find local maxima
|
|
444
|
+
peaks_list: list[int] = []
|
|
445
|
+
for i in range(1, len(data) - 1):
|
|
446
|
+
if data[i] > data[i - 1] and data[i] > data[i + 1]:
|
|
447
|
+
peaks_list.append(i)
|
|
448
|
+
|
|
449
|
+
peaks: NDArray[np.intp] = np.array(peaks_list, dtype=np.intp)
|
|
450
|
+
|
|
451
|
+
# Apply minimum distance constraint
|
|
452
|
+
if len(peaks) > 0 and min_distance > 1:
|
|
453
|
+
# Keep highest peaks when too close
|
|
454
|
+
filtered_peaks_list: list[int] = []
|
|
455
|
+
last_peak = -min_distance
|
|
456
|
+
|
|
457
|
+
# Sort by height
|
|
458
|
+
sorted_indices = np.argsort(data[peaks])[::-1]
|
|
459
|
+
|
|
460
|
+
for idx in sorted_indices:
|
|
461
|
+
peak_pos = int(peaks[idx])
|
|
462
|
+
if peak_pos - last_peak >= min_distance:
|
|
463
|
+
filtered_peaks_list.append(peak_pos)
|
|
464
|
+
last_peak = peak_pos
|
|
465
|
+
|
|
466
|
+
peaks = np.array(np.sort(np.array(filtered_peaks_list, dtype=np.intp)), dtype=np.intp)
|
|
467
|
+
|
|
468
|
+
return peaks
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _detect_period_suffix(
|
|
472
|
+
trace: NDArray[np.float64], min_period: int, max_period: int
|
|
473
|
+
) -> int | None:
|
|
474
|
+
"""Detect period using suffix array (for exact repeats).
|
|
475
|
+
|
|
476
|
+
: Suffix array-based period detection
|
|
477
|
+
|
|
478
|
+
This method finds the longest exact repeating substring, which corresponds
|
|
479
|
+
to the period for perfectly periodic signals.
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
trace: Input signal (will be converted to bytes)
|
|
483
|
+
min_period: Minimum period length
|
|
484
|
+
max_period: Maximum period length
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
Period in samples, or None if not found
|
|
488
|
+
"""
|
|
489
|
+
# Convert to byte sequence for suffix array
|
|
490
|
+
if trace.dtype == np.bool_ or np.all(np.isin(trace, [0, 1])):
|
|
491
|
+
# Binary signal
|
|
492
|
+
trace_bytes = np.packbits(trace.astype(np.uint8))
|
|
493
|
+
else:
|
|
494
|
+
# Use raw bytes
|
|
495
|
+
trace_bytes = trace.astype(np.uint8)
|
|
496
|
+
|
|
497
|
+
n = len(trace_bytes)
|
|
498
|
+
|
|
499
|
+
# Simple period detection: check for repeating patterns
|
|
500
|
+
for period in range(min_period, min(max_period + 1, n // 2)):
|
|
501
|
+
# Check if trace repeats with this period
|
|
502
|
+
num_repeats = n // period
|
|
503
|
+
if num_repeats < 2:
|
|
504
|
+
continue
|
|
505
|
+
|
|
506
|
+
# Compare first period with subsequent periods
|
|
507
|
+
pattern = trace_bytes[:period]
|
|
508
|
+
matches = 0
|
|
509
|
+
|
|
510
|
+
for i in range(1, num_repeats):
|
|
511
|
+
segment = trace_bytes[i * period : (i + 1) * period]
|
|
512
|
+
if len(segment) == period and np.array_equal(pattern, segment):
|
|
513
|
+
matches += 1
|
|
514
|
+
|
|
515
|
+
# If most repetitions match, consider it valid
|
|
516
|
+
if matches >= num_repeats * 0.8 - 1:
|
|
517
|
+
return period
|
|
518
|
+
|
|
519
|
+
return None
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
class PeriodicPatternDetector:
|
|
523
|
+
"""Object-oriented wrapper for periodic pattern detection.
|
|
524
|
+
|
|
525
|
+
Provides a class-based interface for period detection operations,
|
|
526
|
+
wrapping the functional API for consistency with test expectations.
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
Example:
|
|
531
|
+
>>> detector = PeriodicPatternDetector()
|
|
532
|
+
>>> result = detector.detect_period(signal)
|
|
533
|
+
>>> print(f"Period: {result.period} samples, confidence: {result.confidence}")
|
|
534
|
+
"""
|
|
535
|
+
|
|
536
|
+
def __init__(
|
|
537
|
+
self,
|
|
538
|
+
method: Literal["auto", "autocorr", "fft", "autocorrelation"] = "auto",
|
|
539
|
+
sample_rate: float = 1.0,
|
|
540
|
+
min_period: int = 2,
|
|
541
|
+
max_period: int | None = None,
|
|
542
|
+
):
|
|
543
|
+
"""Initialize periodic pattern detector.
|
|
544
|
+
|
|
545
|
+
Args:
|
|
546
|
+
method: Detection method ('auto', 'autocorr', 'fft', 'autocorrelation').
|
|
547
|
+
sample_rate: Sample rate in Hz.
|
|
548
|
+
min_period: Minimum period in samples.
|
|
549
|
+
max_period: Maximum period in samples.
|
|
550
|
+
"""
|
|
551
|
+
# Map 'autocorrelation' to 'autocorr' for test compatibility
|
|
552
|
+
if method == "autocorrelation":
|
|
553
|
+
method = "autocorr"
|
|
554
|
+
self.method = method
|
|
555
|
+
self.sample_rate = sample_rate
|
|
556
|
+
self.min_period = min_period
|
|
557
|
+
self.max_period = max_period
|
|
558
|
+
|
|
559
|
+
def detect_period(self, trace: NDArray[np.float64]) -> PeriodResult:
|
|
560
|
+
"""Detect the dominant period in the signal.
|
|
561
|
+
|
|
562
|
+
Args:
|
|
563
|
+
trace: Input signal array (1D, boolean or numeric).
|
|
564
|
+
|
|
565
|
+
Returns:
|
|
566
|
+
PeriodResult with detected period information.
|
|
567
|
+
|
|
568
|
+
Raises:
|
|
569
|
+
ValueError: If trace is empty or too short.
|
|
570
|
+
|
|
571
|
+
Example:
|
|
572
|
+
>>> detector = PeriodicPatternDetector(method="autocorr")
|
|
573
|
+
>>> result = detector.detect_period(np.tile([1, 0], 100))
|
|
574
|
+
>>> result.period == 2
|
|
575
|
+
True
|
|
576
|
+
"""
|
|
577
|
+
trace = np.asarray(trace).flatten()
|
|
578
|
+
|
|
579
|
+
# Validate input
|
|
580
|
+
if trace.size == 0:
|
|
581
|
+
raise ValueError("trace cannot be empty")
|
|
582
|
+
if trace.size < 3:
|
|
583
|
+
raise ValueError("trace must have at least 3 elements for period detection")
|
|
584
|
+
|
|
585
|
+
result = detect_period(
|
|
586
|
+
trace,
|
|
587
|
+
sample_rate=self.sample_rate,
|
|
588
|
+
method=self.method,
|
|
589
|
+
min_period=self.min_period,
|
|
590
|
+
max_period=self.max_period,
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
if result is None:
|
|
594
|
+
# Return a low-confidence result if no period found
|
|
595
|
+
return PeriodResult(
|
|
596
|
+
period_samples=1.0,
|
|
597
|
+
period_seconds=1.0 / self.sample_rate,
|
|
598
|
+
frequency_hz=self.sample_rate,
|
|
599
|
+
confidence=0.0,
|
|
600
|
+
method=self.method,
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
return result
|
|
604
|
+
|
|
605
|
+
def detect_multiple_periods(
|
|
606
|
+
self, trace: NDArray[np.float64], num_periods: int = 5
|
|
607
|
+
) -> list[PeriodResult]:
|
|
608
|
+
"""Detect multiple periods in the signal.
|
|
609
|
+
|
|
610
|
+
Args:
|
|
611
|
+
trace: Input signal array.
|
|
612
|
+
num_periods: Maximum number of periods to return.
|
|
613
|
+
|
|
614
|
+
Returns:
|
|
615
|
+
List of PeriodResult sorted by confidence.
|
|
616
|
+
"""
|
|
617
|
+
trace = np.asarray(trace).flatten()
|
|
618
|
+
|
|
619
|
+
if self.method in ["fft", "auto"]:
|
|
620
|
+
min_freq = self.sample_rate / self.max_period if self.max_period else None
|
|
621
|
+
max_freq = self.sample_rate / self.min_period if self.min_period > 0 else None
|
|
622
|
+
return detect_periods_fft(trace, self.sample_rate, min_freq, max_freq, num_periods)
|
|
623
|
+
else:
|
|
624
|
+
return detect_periods_autocorr(
|
|
625
|
+
trace, self.sample_rate, self.max_period, min_correlation=0.3
|
|
626
|
+
)[:num_periods]
|
|
627
|
+
|
|
628
|
+
def validate(self, trace: NDArray[np.float64], period: float, tolerance: float = 0.01) -> bool:
|
|
629
|
+
"""Validate a detected period.
|
|
630
|
+
|
|
631
|
+
Args:
|
|
632
|
+
trace: Input signal array.
|
|
633
|
+
period: Period to validate.
|
|
634
|
+
tolerance: Tolerance for period matching.
|
|
635
|
+
|
|
636
|
+
Returns:
|
|
637
|
+
True if period is valid.
|
|
638
|
+
"""
|
|
639
|
+
is_valid, _ = validate_period(trace, period, tolerance)
|
|
640
|
+
return is_valid
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
__all__ = [
|
|
644
|
+
"PeriodResult",
|
|
645
|
+
"PeriodicPatternDetector",
|
|
646
|
+
"detect_period",
|
|
647
|
+
"detect_periods_autocorr",
|
|
648
|
+
"detect_periods_fft",
|
|
649
|
+
"validate_period",
|
|
650
|
+
]
|