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,611 @@
|
|
|
1
|
+
"""Streaming APIs for chunk-by-chunk processing of large files.
|
|
2
|
+
|
|
3
|
+
This module implements memory-efficient streaming analysis for huge waveform
|
|
4
|
+
files that don't fit in memory. Uses generator-based chunk loading and
|
|
5
|
+
accumulator pattern for rolling statistics.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
from scipy import signal
|
|
15
|
+
|
|
16
|
+
from ..core.types import WaveformTrace
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from collections.abc import Callable, Generator
|
|
20
|
+
|
|
21
|
+
from numpy.typing import NDArray
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_trace_chunks(
|
|
25
|
+
file_path: str | Path,
|
|
26
|
+
chunk_size: int | float = 100e6,
|
|
27
|
+
overlap: int = 0,
|
|
28
|
+
loader: Callable[[str | Path], WaveformTrace] | None = None,
|
|
29
|
+
progress_callback: Callable[[int, int], None] | None = None,
|
|
30
|
+
) -> Generator[WaveformTrace, None, None]:
|
|
31
|
+
"""Load large trace files chunk-by-chunk without loading into memory.
|
|
32
|
+
|
|
33
|
+
Yields chunks of the trace for memory-efficient processing. Supports
|
|
34
|
+
overlap between chunks for windowed operations that need continuity.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
file_path: Path to trace file.
|
|
38
|
+
chunk_size: Size of each chunk in samples (if int) or bytes (if float).
|
|
39
|
+
Default 100e6 (100 MB).
|
|
40
|
+
overlap: Number of samples to overlap between chunks. Useful for
|
|
41
|
+
windowed operations like FFT. Default 0.
|
|
42
|
+
loader: Optional custom loader function. If None, uses default loader.
|
|
43
|
+
progress_callback: Optional callback(chunk_num, total_chunks) for
|
|
44
|
+
progress reporting.
|
|
45
|
+
|
|
46
|
+
Yields:
|
|
47
|
+
WaveformTrace chunks.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ValueError: If failed to load trace metadata.
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
>>> # Stream 10 GB file in 100 MB chunks
|
|
54
|
+
>>> for chunk in tk.load_trace_chunks('huge_trace.bin', chunk_size=100e6):
|
|
55
|
+
... mean = chunk.data.mean()
|
|
56
|
+
... std = chunk.data.std()
|
|
57
|
+
... print(f"Chunk stats: mean={mean:.3f}, std={std:.3f}")
|
|
58
|
+
|
|
59
|
+
Advanced Example:
|
|
60
|
+
>>> # Process with overlap for FFT continuity
|
|
61
|
+
>>> for chunk in tk.load_trace_chunks(
|
|
62
|
+
... 'large_trace.bin',
|
|
63
|
+
... chunk_size=50e6,
|
|
64
|
+
... overlap=8192 # Overlap for continuity
|
|
65
|
+
... ):
|
|
66
|
+
... fft_result = tk.fft(chunk, nfft=8192)
|
|
67
|
+
... # Process FFT result...
|
|
68
|
+
|
|
69
|
+
References:
|
|
70
|
+
API-003: Streaming/Generator API for Large Files
|
|
71
|
+
"""
|
|
72
|
+
file_path = Path(file_path)
|
|
73
|
+
|
|
74
|
+
# Import loader here to avoid circular dependency
|
|
75
|
+
from ..loaders import load
|
|
76
|
+
|
|
77
|
+
# Use provided loader or default
|
|
78
|
+
load_func = loader if loader is not None else load
|
|
79
|
+
|
|
80
|
+
# Load full trace metadata to get total size
|
|
81
|
+
# For memory-mapped files, this doesn't load data
|
|
82
|
+
try:
|
|
83
|
+
full_trace = load_func(file_path)
|
|
84
|
+
except Exception as e:
|
|
85
|
+
raise ValueError(f"Failed to load trace metadata: {e}") from e
|
|
86
|
+
|
|
87
|
+
total_samples = len(full_trace.data) # type: ignore[union-attr]
|
|
88
|
+
chunk_samples = int(chunk_size) if chunk_size < 1e6 else int(chunk_size / 8)
|
|
89
|
+
|
|
90
|
+
# Calculate number of chunks
|
|
91
|
+
num_chunks = (total_samples - overlap) // (chunk_samples - overlap)
|
|
92
|
+
if (total_samples - overlap) % (chunk_samples - overlap) != 0:
|
|
93
|
+
num_chunks += 1
|
|
94
|
+
|
|
95
|
+
# Yield chunks
|
|
96
|
+
chunk_num = 0
|
|
97
|
+
start_idx = 0
|
|
98
|
+
|
|
99
|
+
while start_idx < total_samples:
|
|
100
|
+
end_idx = min(start_idx + chunk_samples, total_samples)
|
|
101
|
+
|
|
102
|
+
# Extract chunk
|
|
103
|
+
chunk_data = full_trace.data[start_idx:end_idx] # type: ignore[union-attr]
|
|
104
|
+
|
|
105
|
+
# Create chunk trace with same metadata
|
|
106
|
+
# Cast needed for mypy: slicing a floating array returns a floating array
|
|
107
|
+
chunk_trace = WaveformTrace(
|
|
108
|
+
data=cast("NDArray[np.floating[Any]]", chunk_data),
|
|
109
|
+
metadata=full_trace.metadata,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Call progress callback if provided
|
|
113
|
+
if progress_callback is not None:
|
|
114
|
+
progress_callback(chunk_num, num_chunks)
|
|
115
|
+
|
|
116
|
+
yield chunk_trace
|
|
117
|
+
|
|
118
|
+
# Move to next chunk, accounting for overlap
|
|
119
|
+
start_idx = end_idx - overlap
|
|
120
|
+
chunk_num += 1
|
|
121
|
+
|
|
122
|
+
# Break if we've reached the end
|
|
123
|
+
if end_idx >= total_samples:
|
|
124
|
+
break
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class StreamingAnalyzer:
|
|
128
|
+
"""Accumulator for streaming analysis of large files.
|
|
129
|
+
|
|
130
|
+
Processes traces chunk-by-chunk, accumulating statistics and measurements
|
|
131
|
+
without loading entire file into memory. Supports streaming PSD estimation
|
|
132
|
+
using Welch's method and other rolling statistics.
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
>>> # Create streaming analyzer
|
|
136
|
+
>>> analyzer = tk.StreamingAnalyzer()
|
|
137
|
+
>>> # Process file in chunks
|
|
138
|
+
>>> for chunk in tk.load_trace_chunks('large_trace.bin', chunk_size=50e6):
|
|
139
|
+
... analyzer.accumulate_psd(chunk, nperseg=4096, window='hann')
|
|
140
|
+
>>> # Get aggregated result
|
|
141
|
+
>>> psd_result = analyzer.get_psd()
|
|
142
|
+
|
|
143
|
+
Advanced Example:
|
|
144
|
+
>>> # Compute multiple statistics in streaming fashion
|
|
145
|
+
>>> analyzer = tk.StreamingAnalyzer()
|
|
146
|
+
>>> for chunk in tk.load_trace_chunks('huge_file.bin'):
|
|
147
|
+
... analyzer.accumulate_statistics(chunk)
|
|
148
|
+
... analyzer.accumulate_psd(chunk, nperseg=8192)
|
|
149
|
+
>>> stats = analyzer.get_statistics()
|
|
150
|
+
>>> psd = analyzer.get_psd()
|
|
151
|
+
>>> print(f"Mean: {stats['mean']:.3f}, PSD shape: {psd.shape}")
|
|
152
|
+
|
|
153
|
+
References:
|
|
154
|
+
API-003: Streaming/Generator API for Large Files
|
|
155
|
+
scipy.signal.welch for streaming PSD
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
def __init__(self) -> None:
|
|
159
|
+
"""Initialize streaming analyzer."""
|
|
160
|
+
# Statistics accumulators
|
|
161
|
+
self._n_samples = 0
|
|
162
|
+
self._sum = 0.0
|
|
163
|
+
self._sum_sq = 0.0
|
|
164
|
+
self._min = float("inf")
|
|
165
|
+
self._max = float("-inf")
|
|
166
|
+
|
|
167
|
+
# PSD accumulators
|
|
168
|
+
self._psd_sum: NDArray[np.float64] | None = None
|
|
169
|
+
self._psd_freqs: NDArray[np.float64] | None = None
|
|
170
|
+
self._psd_count = 0
|
|
171
|
+
self._sample_rate: float | None = None
|
|
172
|
+
|
|
173
|
+
# Histogram accumulators
|
|
174
|
+
self._hist_counts: NDArray[np.int64] | None = None
|
|
175
|
+
self._hist_edges: NDArray[np.float64] | None = None
|
|
176
|
+
|
|
177
|
+
def accumulate_statistics(self, chunk: WaveformTrace) -> None:
|
|
178
|
+
"""Accumulate basic statistics from chunk.
|
|
179
|
+
|
|
180
|
+
Updates running mean, std, min, max using Welford's online algorithm.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
chunk: WaveformTrace chunk to process.
|
|
184
|
+
|
|
185
|
+
Example:
|
|
186
|
+
>>> analyzer.accumulate_statistics(chunk)
|
|
187
|
+
"""
|
|
188
|
+
chunk_data = chunk.data
|
|
189
|
+
self._n_samples += len(chunk_data)
|
|
190
|
+
self._sum += float(chunk_data.sum())
|
|
191
|
+
self._sum_sq += float((chunk_data**2).sum())
|
|
192
|
+
self._min = min(self._min, float(chunk_data.min()))
|
|
193
|
+
self._max = max(self._max, float(chunk_data.max()))
|
|
194
|
+
|
|
195
|
+
def accumulate_psd(
|
|
196
|
+
self,
|
|
197
|
+
chunk: WaveformTrace,
|
|
198
|
+
nperseg: int = 4096,
|
|
199
|
+
window: str = "hann",
|
|
200
|
+
**welch_kwargs: Any,
|
|
201
|
+
) -> None:
|
|
202
|
+
"""Accumulate PSD estimate from chunk using Welch's method.
|
|
203
|
+
|
|
204
|
+
Computes PSD for chunk and accumulates with running average.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
chunk: WaveformTrace chunk to process.
|
|
208
|
+
nperseg: Length of each segment for Welch's method.
|
|
209
|
+
window: Window function name (default 'hann').
|
|
210
|
+
**welch_kwargs: Additional arguments for scipy.signal.welch.
|
|
211
|
+
|
|
212
|
+
Example:
|
|
213
|
+
>>> analyzer.accumulate_psd(chunk, nperseg=4096, window='hann')
|
|
214
|
+
|
|
215
|
+
References:
|
|
216
|
+
scipy.signal.welch
|
|
217
|
+
https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.welch.html
|
|
218
|
+
"""
|
|
219
|
+
# Store sample rate from first chunk
|
|
220
|
+
if self._sample_rate is None:
|
|
221
|
+
self._sample_rate = chunk.metadata.sample_rate
|
|
222
|
+
|
|
223
|
+
# Compute PSD for this chunk using Welch's method
|
|
224
|
+
freqs, psd = signal.welch(
|
|
225
|
+
chunk.data,
|
|
226
|
+
fs=chunk.metadata.sample_rate,
|
|
227
|
+
nperseg=nperseg,
|
|
228
|
+
window=window,
|
|
229
|
+
**welch_kwargs,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Initialize or accumulate
|
|
233
|
+
if self._psd_sum is None:
|
|
234
|
+
self._psd_sum = psd
|
|
235
|
+
self._psd_freqs = freqs
|
|
236
|
+
else:
|
|
237
|
+
# Accumulate PSD estimates
|
|
238
|
+
self._psd_sum += psd
|
|
239
|
+
|
|
240
|
+
self._psd_count += 1
|
|
241
|
+
|
|
242
|
+
def accumulate_histogram(
|
|
243
|
+
self,
|
|
244
|
+
chunk: WaveformTrace,
|
|
245
|
+
bins: int | NDArray[np.float64] = 100,
|
|
246
|
+
range: tuple[float, float] | None = None,
|
|
247
|
+
) -> None:
|
|
248
|
+
"""Accumulate histogram from chunk.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
chunk: WaveformTrace chunk to process.
|
|
252
|
+
bins: Number of bins or bin edges.
|
|
253
|
+
range: Range of histogram (min, max).
|
|
254
|
+
|
|
255
|
+
Example:
|
|
256
|
+
>>> analyzer.accumulate_histogram(chunk, bins=100)
|
|
257
|
+
"""
|
|
258
|
+
counts, edges = np.histogram(chunk.data, bins=bins, range=range)
|
|
259
|
+
|
|
260
|
+
if self._hist_counts is None:
|
|
261
|
+
self._hist_counts = counts.astype(np.int64)
|
|
262
|
+
self._hist_edges = edges
|
|
263
|
+
else:
|
|
264
|
+
self._hist_counts += counts.astype(np.int64)
|
|
265
|
+
|
|
266
|
+
def get_statistics(self) -> dict[str, float]:
|
|
267
|
+
"""Get accumulated statistics.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Dictionary with mean, std, min, max, and sample count.
|
|
271
|
+
|
|
272
|
+
Raises:
|
|
273
|
+
ValueError: If no data accumulated yet.
|
|
274
|
+
|
|
275
|
+
Example:
|
|
276
|
+
>>> stats = analyzer.get_statistics()
|
|
277
|
+
>>> print(f"Mean: {stats['mean']:.3f}, Std: {stats['std']:.3f}")
|
|
278
|
+
"""
|
|
279
|
+
if self._n_samples == 0:
|
|
280
|
+
raise ValueError("No data accumulated yet")
|
|
281
|
+
|
|
282
|
+
mean = self._sum / self._n_samples
|
|
283
|
+
variance = (self._sum_sq / self._n_samples) - (mean**2)
|
|
284
|
+
std = np.sqrt(max(0, variance)) # Avoid negative due to numerical errors
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
"mean": mean,
|
|
288
|
+
"std": std,
|
|
289
|
+
"min": self._min,
|
|
290
|
+
"max": self._max,
|
|
291
|
+
"n_samples": self._n_samples,
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
def get_psd(self) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
295
|
+
"""Get accumulated PSD estimate.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Tuple of (frequencies, psd) where psd is averaged over all chunks.
|
|
299
|
+
|
|
300
|
+
Raises:
|
|
301
|
+
ValueError: If no PSD data accumulated.
|
|
302
|
+
|
|
303
|
+
Example:
|
|
304
|
+
>>> freqs, psd = analyzer.get_psd()
|
|
305
|
+
>>> print(f"PSD shape: {psd.shape}")
|
|
306
|
+
"""
|
|
307
|
+
if self._psd_sum is None or self._psd_freqs is None:
|
|
308
|
+
raise ValueError("No PSD data accumulated yet")
|
|
309
|
+
|
|
310
|
+
# Return averaged PSD
|
|
311
|
+
psd_avg = self._psd_sum / self._psd_count
|
|
312
|
+
return self._psd_freqs, psd_avg
|
|
313
|
+
|
|
314
|
+
def get_histogram(self) -> tuple[NDArray[np.int64], NDArray[np.float64]]:
|
|
315
|
+
"""Get accumulated histogram.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
Tuple of (counts, edges).
|
|
319
|
+
|
|
320
|
+
Raises:
|
|
321
|
+
ValueError: If no histogram data accumulated.
|
|
322
|
+
|
|
323
|
+
Example:
|
|
324
|
+
>>> counts, edges = analyzer.get_histogram()
|
|
325
|
+
"""
|
|
326
|
+
if self._hist_counts is None or self._hist_edges is None:
|
|
327
|
+
raise ValueError("No histogram data accumulated yet")
|
|
328
|
+
|
|
329
|
+
return self._hist_counts, self._hist_edges
|
|
330
|
+
|
|
331
|
+
def reset(self) -> None:
|
|
332
|
+
"""Reset all accumulators.
|
|
333
|
+
|
|
334
|
+
Example:
|
|
335
|
+
>>> analyzer.reset()
|
|
336
|
+
"""
|
|
337
|
+
self._n_samples = 0
|
|
338
|
+
self._sum = 0.0
|
|
339
|
+
self._sum_sq = 0.0
|
|
340
|
+
self._min = float("inf")
|
|
341
|
+
self._max = float("-inf")
|
|
342
|
+
self._psd_sum = None
|
|
343
|
+
self._psd_freqs = None
|
|
344
|
+
self._psd_count = 0
|
|
345
|
+
self._sample_rate = None
|
|
346
|
+
self._hist_counts = None
|
|
347
|
+
self._hist_edges = None
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def chunked_spectrogram(
|
|
351
|
+
data: NDArray[np.float64],
|
|
352
|
+
sample_rate: float,
|
|
353
|
+
*,
|
|
354
|
+
chunk_size: int = 10_000_000,
|
|
355
|
+
overlap: int = 0,
|
|
356
|
+
nperseg: int = 256,
|
|
357
|
+
noverlap: int | None = None,
|
|
358
|
+
window: str = "hann",
|
|
359
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]:
|
|
360
|
+
"""Compute spectrogram for large signals using chunked processing.
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
Processes large signals in overlapping chunks to compute spectrograms
|
|
364
|
+
without loading entire signal into memory. Stitches STFT results from
|
|
365
|
+
chunks with proper boundary handling.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
data: Input signal array (can be memory-mapped).
|
|
369
|
+
sample_rate: Sample rate in Hz.
|
|
370
|
+
chunk_size: Maximum samples per chunk (default 10M).
|
|
371
|
+
overlap: Overlap samples between chunks for continuity (default 0).
|
|
372
|
+
Should be at least 2*nperseg for proper STFT boundary handling.
|
|
373
|
+
nperseg: Segment length for STFT (default 256).
|
|
374
|
+
noverlap: Overlap between STFT segments within chunk (default nperseg//2).
|
|
375
|
+
window: Window function name (default "hann").
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
(times, frequencies, Sxx_db) - Time axis, frequency axis, and
|
|
379
|
+
spectrogram magnitude in dB as 2D array (frequencies x time).
|
|
380
|
+
|
|
381
|
+
Raises:
|
|
382
|
+
ValueError: If no valid chunks produced.
|
|
383
|
+
|
|
384
|
+
Example:
|
|
385
|
+
>>> # Memory-efficient spectrogram on 1 GB signal
|
|
386
|
+
>>> import numpy as np
|
|
387
|
+
>>> data = np.memmap('huge_trace.dat', dtype='float64', mode='r')
|
|
388
|
+
>>> t, f, Sxx = chunked_spectrogram(data, sample_rate=1e9, chunk_size=10_000_000)
|
|
389
|
+
>>> print(f"Spectrogram shape: {Sxx.shape}")
|
|
390
|
+
|
|
391
|
+
References:
|
|
392
|
+
MEM-004: Chunked Spectrogram requirement
|
|
393
|
+
scipy.signal.spectrogram
|
|
394
|
+
"""
|
|
395
|
+
n = len(data)
|
|
396
|
+
|
|
397
|
+
# Handle empty input
|
|
398
|
+
if n == 0:
|
|
399
|
+
return np.array([]), np.array([]), np.array([]).reshape(0, 0)
|
|
400
|
+
|
|
401
|
+
if noverlap is None:
|
|
402
|
+
noverlap = nperseg // 2
|
|
403
|
+
|
|
404
|
+
# Auto-adjust overlap if not specified to ensure continuity
|
|
405
|
+
if overlap == 0:
|
|
406
|
+
overlap = 2 * nperseg
|
|
407
|
+
|
|
408
|
+
# If data fits in one chunk, use scipy directly
|
|
409
|
+
if n <= chunk_size:
|
|
410
|
+
freq, times, Sxx = signal.spectrogram(
|
|
411
|
+
data,
|
|
412
|
+
fs=sample_rate,
|
|
413
|
+
window=window,
|
|
414
|
+
nperseg=nperseg,
|
|
415
|
+
noverlap=noverlap,
|
|
416
|
+
scaling="spectrum",
|
|
417
|
+
)
|
|
418
|
+
# Convert to dB
|
|
419
|
+
Sxx = np.maximum(Sxx, 1e-20)
|
|
420
|
+
Sxx_db = 10 * np.log10(Sxx)
|
|
421
|
+
return times, freq, Sxx_db
|
|
422
|
+
|
|
423
|
+
# Process chunks
|
|
424
|
+
chunks_stft = []
|
|
425
|
+
chunks_times = []
|
|
426
|
+
chunk_start = 0
|
|
427
|
+
|
|
428
|
+
while chunk_start < n:
|
|
429
|
+
# Determine chunk boundaries with overlap
|
|
430
|
+
chunk_end = min(chunk_start + chunk_size, n)
|
|
431
|
+
|
|
432
|
+
# Extract chunk with overlap extension on both sides
|
|
433
|
+
extended_start = max(0, chunk_start - overlap)
|
|
434
|
+
extended_end = min(n, chunk_end + overlap)
|
|
435
|
+
|
|
436
|
+
chunk_data = data[extended_start:extended_end]
|
|
437
|
+
|
|
438
|
+
# Compute spectrogram for chunk
|
|
439
|
+
freq, times_chunk, Sxx_chunk = signal.spectrogram(
|
|
440
|
+
chunk_data,
|
|
441
|
+
fs=sample_rate,
|
|
442
|
+
window=window,
|
|
443
|
+
nperseg=nperseg,
|
|
444
|
+
noverlap=noverlap,
|
|
445
|
+
scaling="spectrum",
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# Adjust time axis for chunk position
|
|
449
|
+
time_offset = extended_start / sample_rate
|
|
450
|
+
times_chunk_adjusted = times_chunk + time_offset
|
|
451
|
+
|
|
452
|
+
# Trim overlap regions to avoid duplication
|
|
453
|
+
valid_time_start = chunk_start / sample_rate
|
|
454
|
+
valid_time_end = chunk_end / sample_rate
|
|
455
|
+
|
|
456
|
+
valid_mask = (times_chunk_adjusted >= valid_time_start) & (
|
|
457
|
+
times_chunk_adjusted < valid_time_end
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
if np.any(valid_mask):
|
|
461
|
+
Sxx_chunk = Sxx_chunk[:, valid_mask]
|
|
462
|
+
times_chunk_adjusted = times_chunk_adjusted[valid_mask]
|
|
463
|
+
|
|
464
|
+
chunks_stft.append(Sxx_chunk)
|
|
465
|
+
chunks_times.append(times_chunk_adjusted)
|
|
466
|
+
|
|
467
|
+
# Move to next chunk
|
|
468
|
+
chunk_start = chunk_end
|
|
469
|
+
|
|
470
|
+
# Concatenate all chunks
|
|
471
|
+
if len(chunks_stft) == 0:
|
|
472
|
+
raise ValueError("No valid chunks produced")
|
|
473
|
+
|
|
474
|
+
Sxx = np.concatenate(chunks_stft, axis=1)
|
|
475
|
+
times = np.concatenate(chunks_times)
|
|
476
|
+
|
|
477
|
+
# Convert to dB
|
|
478
|
+
Sxx = np.maximum(Sxx, 1e-20)
|
|
479
|
+
Sxx_db = 10 * np.log10(Sxx)
|
|
480
|
+
|
|
481
|
+
return times, freq, Sxx_db
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def chunked_fft(
|
|
485
|
+
data: NDArray[np.float64],
|
|
486
|
+
sample_rate: float,
|
|
487
|
+
*,
|
|
488
|
+
chunk_size: int = 10_000_000,
|
|
489
|
+
overlap: float = 50.0,
|
|
490
|
+
window: str = "hann",
|
|
491
|
+
nfft: int | None = None,
|
|
492
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
493
|
+
"""Compute FFT for very long signals using segmented averaging.
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
Divides signal into overlapping segments, computes FFT for each,
|
|
497
|
+
and averages magnitude spectra. This is memory-bounded by chunk_size
|
|
498
|
+
and provides variance reduction through averaging (similar to Welch's method).
|
|
499
|
+
|
|
500
|
+
Args:
|
|
501
|
+
data: Input signal array (can be memory-mapped).
|
|
502
|
+
sample_rate: Sample rate in Hz.
|
|
503
|
+
chunk_size: Size of each segment in samples (default 10M).
|
|
504
|
+
overlap: Percentage overlap between segments, 0-100 (default 50%).
|
|
505
|
+
window: Window function name (default "hann").
|
|
506
|
+
nfft: FFT length. If None, uses next power of 2 >= chunk_size.
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
(frequencies, magnitude_db) - Frequency axis and averaged magnitude in dB.
|
|
510
|
+
|
|
511
|
+
Example:
|
|
512
|
+
>>> # Memory-efficient FFT on 1 GB signal with 50% overlap
|
|
513
|
+
>>> import numpy as np
|
|
514
|
+
>>> data = np.memmap('huge_trace.dat', dtype='float64', mode='r')
|
|
515
|
+
>>> freq, mag = chunked_fft(data, sample_rate=1e9, chunk_size=1_000_000)
|
|
516
|
+
>>> print(f"Frequency resolution: {freq[1] - freq[0]:.3f} Hz")
|
|
517
|
+
|
|
518
|
+
References:
|
|
519
|
+
MEM-006: Chunked FFT requirement
|
|
520
|
+
Welch's method for spectral estimation
|
|
521
|
+
"""
|
|
522
|
+
from ..utils.windowing import get_window
|
|
523
|
+
|
|
524
|
+
n = len(data)
|
|
525
|
+
|
|
526
|
+
# Handle empty input
|
|
527
|
+
if n == 0:
|
|
528
|
+
return np.array([]), np.array([])
|
|
529
|
+
|
|
530
|
+
# If data fits in one chunk, compute single FFT
|
|
531
|
+
if n <= chunk_size:
|
|
532
|
+
if nfft is None:
|
|
533
|
+
nfft = int(2 ** np.ceil(np.log2(n)))
|
|
534
|
+
|
|
535
|
+
# Apply window
|
|
536
|
+
w = get_window(window, n)
|
|
537
|
+
data_windowed = data * w
|
|
538
|
+
|
|
539
|
+
# Compute FFT
|
|
540
|
+
spectrum = np.fft.rfft(data_windowed, n=nfft)
|
|
541
|
+
|
|
542
|
+
# Frequency axis
|
|
543
|
+
freq = np.fft.rfftfreq(nfft, d=1.0 / sample_rate)
|
|
544
|
+
|
|
545
|
+
# Magnitude in dB (normalized by window gain)
|
|
546
|
+
window_gain = np.sum(w) / n
|
|
547
|
+
magnitude = np.abs(spectrum) / (n * window_gain)
|
|
548
|
+
magnitude = np.maximum(magnitude, 1e-20)
|
|
549
|
+
magnitude_db = 20 * np.log10(magnitude)
|
|
550
|
+
|
|
551
|
+
return freq, magnitude_db
|
|
552
|
+
|
|
553
|
+
# Calculate overlap
|
|
554
|
+
overlap_samples = int(chunk_size * overlap / 100.0)
|
|
555
|
+
hop = chunk_size - overlap_samples
|
|
556
|
+
|
|
557
|
+
# Determine number of segments
|
|
558
|
+
num_segments = max(1, (n - overlap_samples) // hop)
|
|
559
|
+
|
|
560
|
+
if nfft is None:
|
|
561
|
+
nfft = int(2 ** np.ceil(np.log2(chunk_size)))
|
|
562
|
+
|
|
563
|
+
# Prepare window
|
|
564
|
+
w = get_window(window, chunk_size)
|
|
565
|
+
window_gain = np.sum(w) / chunk_size
|
|
566
|
+
|
|
567
|
+
# Accumulate magnitude spectra
|
|
568
|
+
freq = np.fft.rfftfreq(nfft, d=1.0 / sample_rate)
|
|
569
|
+
magnitude_sum = np.zeros(len(freq))
|
|
570
|
+
|
|
571
|
+
for i in range(num_segments):
|
|
572
|
+
start = i * hop
|
|
573
|
+
end = min(start + chunk_size, n)
|
|
574
|
+
|
|
575
|
+
# Extract segment
|
|
576
|
+
if end - start < chunk_size:
|
|
577
|
+
# Last segment: pad with zeros
|
|
578
|
+
segment = np.zeros(chunk_size)
|
|
579
|
+
segment[: end - start] = data[start:end]
|
|
580
|
+
else:
|
|
581
|
+
segment = data[start:end]
|
|
582
|
+
|
|
583
|
+
# Detrend (remove mean)
|
|
584
|
+
segment = segment - np.mean(segment)
|
|
585
|
+
|
|
586
|
+
# Window
|
|
587
|
+
segment_windowed = segment * w
|
|
588
|
+
|
|
589
|
+
# FFT
|
|
590
|
+
spectrum = np.fft.rfft(segment_windowed, n=nfft)
|
|
591
|
+
|
|
592
|
+
# Accumulate magnitude
|
|
593
|
+
magnitude = np.abs(spectrum) / (chunk_size * window_gain)
|
|
594
|
+
magnitude_sum += magnitude
|
|
595
|
+
|
|
596
|
+
# Average
|
|
597
|
+
magnitude_avg = magnitude_sum / num_segments
|
|
598
|
+
|
|
599
|
+
# Convert to dB
|
|
600
|
+
magnitude_avg = np.maximum(magnitude_avg, 1e-20)
|
|
601
|
+
magnitude_db = 20 * np.log10(magnitude_avg)
|
|
602
|
+
|
|
603
|
+
return freq, magnitude_db
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
__all__ = [
|
|
607
|
+
"StreamingAnalyzer",
|
|
608
|
+
"chunked_fft",
|
|
609
|
+
"chunked_spectrogram",
|
|
610
|
+
"load_trace_chunks",
|
|
611
|
+
]
|