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,571 @@
|
|
|
1
|
+
"""Chunked FFT computation for very long signals.
|
|
2
|
+
|
|
3
|
+
This module implements FFT computation for signals larger than memory
|
|
4
|
+
using overlapping segments with result aggregation.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.analyzers.spectral.chunked_fft import fft_chunked
|
|
9
|
+
>>> freqs, spectrum = fft_chunked('huge_signal.bin', segment_size=1e6, overlap_pct=50)
|
|
10
|
+
>>> print(f"FFT shape: {spectrum.shape}")
|
|
11
|
+
|
|
12
|
+
References:
|
|
13
|
+
scipy.fft for FFT computation
|
|
14
|
+
Welch's method for spectral averaging
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import TYPE_CHECKING, Any
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
from scipy import fft, signal
|
|
24
|
+
|
|
25
|
+
from oscura.core.memoize import memoize_analysis
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from collections.abc import Callable, Iterator
|
|
29
|
+
|
|
30
|
+
from numpy.typing import NDArray
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@memoize_analysis(maxsize=16)
|
|
34
|
+
def fft_chunked(
|
|
35
|
+
file_path: str | Path,
|
|
36
|
+
segment_size: int | float,
|
|
37
|
+
overlap_pct: float = 50.0,
|
|
38
|
+
*,
|
|
39
|
+
window: str | NDArray[np.float64] = "hann",
|
|
40
|
+
nfft: int | None = None,
|
|
41
|
+
detrend: str | bool = False,
|
|
42
|
+
scaling: str = "density",
|
|
43
|
+
average_method: str = "mean",
|
|
44
|
+
sample_rate: float = 1.0,
|
|
45
|
+
dtype: str = "float32",
|
|
46
|
+
preserve_phase: bool = False,
|
|
47
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64] | NDArray[np.complex128]]:
|
|
48
|
+
"""Compute FFT for very long signals using overlapping segments.
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
Processes signal in segments with overlap, computes FFT per segment,
|
|
52
|
+
and aggregates using specified method. Handles window edge effects.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
file_path: Path to signal file (binary format).
|
|
56
|
+
segment_size: Segment size in samples.
|
|
57
|
+
overlap_pct: Overlap percentage between segments (0-100).
|
|
58
|
+
window: Window function name or array.
|
|
59
|
+
nfft: FFT length (default: segment_size, zero-padded if larger).
|
|
60
|
+
detrend: Detrend type ('constant', 'linear', False).
|
|
61
|
+
scaling: Scaling mode ('density' or 'spectrum').
|
|
62
|
+
average_method: Aggregation method ('mean', 'median', 'max').
|
|
63
|
+
sample_rate: Sample rate in Hz (for frequency axis).
|
|
64
|
+
dtype: Data type of input file ('float32' or 'float64').
|
|
65
|
+
preserve_phase: If True, preserve phase information (complex output).
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Tuple of (frequencies, spectrum) where:
|
|
69
|
+
- frequencies: Frequency bins in Hz.
|
|
70
|
+
- spectrum: Averaged FFT magnitude (or complex if preserve_phase=True).
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
ValueError: If overlap_pct not in [0, 100] or file cannot be read.
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
>>> # Process 10 GB file with 1M sample segments, 50% overlap
|
|
77
|
+
>>> freqs, spectrum = fft_chunked(
|
|
78
|
+
... 'huge_signal.bin',
|
|
79
|
+
... segment_size=1e6,
|
|
80
|
+
... overlap_pct=50,
|
|
81
|
+
... window='hann',
|
|
82
|
+
... sample_rate=1e9,
|
|
83
|
+
... dtype='float32'
|
|
84
|
+
... )
|
|
85
|
+
>>> print(f"Spectrum shape: {spectrum.shape}")
|
|
86
|
+
|
|
87
|
+
References:
|
|
88
|
+
MEM-006: Chunked FFT for Very Long Signals
|
|
89
|
+
"""
|
|
90
|
+
if not 0 <= overlap_pct < 100:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"overlap_pct must be in [0, 100), got {overlap_pct}. Note: 100% overlap would create an infinite loop."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
segment_size = int(segment_size)
|
|
96
|
+
if nfft is None:
|
|
97
|
+
nfft = segment_size
|
|
98
|
+
|
|
99
|
+
# Calculate overlap in samples
|
|
100
|
+
noverlap = int(segment_size * overlap_pct / 100)
|
|
101
|
+
|
|
102
|
+
# Determine dtype
|
|
103
|
+
np_dtype = np.float32 if dtype == "float32" else np.float64
|
|
104
|
+
bytes_per_sample = 4 if dtype == "float32" else 8
|
|
105
|
+
|
|
106
|
+
# Open file and get total size
|
|
107
|
+
file_path = Path(file_path)
|
|
108
|
+
file_size_bytes = file_path.stat().st_size
|
|
109
|
+
total_samples = file_size_bytes // bytes_per_sample
|
|
110
|
+
|
|
111
|
+
# Generate window
|
|
112
|
+
if isinstance(window, str):
|
|
113
|
+
window_arr = signal.get_window(window, segment_size)
|
|
114
|
+
else:
|
|
115
|
+
window_arr = np.asarray(window)
|
|
116
|
+
|
|
117
|
+
# Initialize accumulators
|
|
118
|
+
fft_accum: list[NDArray[np.float64] | NDArray[np.complex128]] = []
|
|
119
|
+
|
|
120
|
+
# Process segments
|
|
121
|
+
for segment in _generate_segments(file_path, total_samples, segment_size, noverlap, np_dtype):
|
|
122
|
+
# Apply detrending
|
|
123
|
+
if detrend:
|
|
124
|
+
segment = signal.detrend(segment, type=detrend)
|
|
125
|
+
|
|
126
|
+
# Apply window
|
|
127
|
+
windowed = segment * window_arr[: len(segment)]
|
|
128
|
+
|
|
129
|
+
# Zero-pad if needed
|
|
130
|
+
if len(windowed) < nfft:
|
|
131
|
+
windowed = np.pad(windowed, (0, nfft - len(windowed)), mode="constant")
|
|
132
|
+
|
|
133
|
+
# Compute FFT
|
|
134
|
+
fft_result = fft.rfft(windowed, n=nfft)
|
|
135
|
+
|
|
136
|
+
# Store result (magnitude or complex)
|
|
137
|
+
if preserve_phase:
|
|
138
|
+
fft_accum.append(fft_result)
|
|
139
|
+
else:
|
|
140
|
+
fft_accum.append(np.abs(fft_result))
|
|
141
|
+
|
|
142
|
+
# Aggregate results
|
|
143
|
+
if len(fft_accum) == 0:
|
|
144
|
+
raise ValueError(f"No segments processed from {file_path}")
|
|
145
|
+
|
|
146
|
+
if average_method == "mean":
|
|
147
|
+
spectrum = np.mean(fft_accum, axis=0)
|
|
148
|
+
elif average_method == "median":
|
|
149
|
+
spectrum = np.median(fft_accum, axis=0)
|
|
150
|
+
elif average_method == "max":
|
|
151
|
+
spectrum = np.max(fft_accum, axis=0)
|
|
152
|
+
else:
|
|
153
|
+
raise ValueError(
|
|
154
|
+
f"Unknown average_method: {average_method}. Use 'mean', 'median', or 'max'."
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Apply scaling
|
|
158
|
+
if scaling == "density" and not preserve_phase:
|
|
159
|
+
# Convert to PSD-like scaling
|
|
160
|
+
spectrum = spectrum**2 / (sample_rate * np.sum(window_arr**2))
|
|
161
|
+
elif scaling == "spectrum" and not preserve_phase:
|
|
162
|
+
# RMS scaling
|
|
163
|
+
spectrum = spectrum / len(window_arr)
|
|
164
|
+
|
|
165
|
+
# Frequency axis
|
|
166
|
+
frequencies = fft.rfftfreq(nfft, d=1 / sample_rate)
|
|
167
|
+
|
|
168
|
+
return frequencies, spectrum
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _generate_segments(
|
|
172
|
+
file_path: Path,
|
|
173
|
+
total_samples: int,
|
|
174
|
+
segment_size: int,
|
|
175
|
+
noverlap: int,
|
|
176
|
+
dtype: type,
|
|
177
|
+
) -> Iterator[NDArray[np.float64]]:
|
|
178
|
+
"""Generate overlapping segments from file.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
file_path: Path to binary file.
|
|
182
|
+
total_samples: Total number of samples in file.
|
|
183
|
+
segment_size: Samples per segment.
|
|
184
|
+
noverlap: Overlap samples between segments.
|
|
185
|
+
dtype: NumPy dtype for data.
|
|
186
|
+
|
|
187
|
+
Yields:
|
|
188
|
+
Segment arrays.
|
|
189
|
+
"""
|
|
190
|
+
hop = segment_size - noverlap
|
|
191
|
+
offset = 0
|
|
192
|
+
|
|
193
|
+
with open(file_path, "rb") as f:
|
|
194
|
+
while offset < total_samples:
|
|
195
|
+
# Read segment
|
|
196
|
+
f.seek(offset * dtype().itemsize)
|
|
197
|
+
segment_data: NDArray[np.float64] = np.fromfile(f, dtype=dtype, count=segment_size)
|
|
198
|
+
|
|
199
|
+
if len(segment_data) == 0:
|
|
200
|
+
break
|
|
201
|
+
|
|
202
|
+
yield segment_data
|
|
203
|
+
|
|
204
|
+
offset += hop
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def welch_psd_chunked(
|
|
208
|
+
file_path: str | Path,
|
|
209
|
+
segment_size: int | float = 256,
|
|
210
|
+
overlap_pct: float = 50.0,
|
|
211
|
+
*,
|
|
212
|
+
window: str | NDArray[np.float64] = "hann",
|
|
213
|
+
nfft: int | None = None,
|
|
214
|
+
detrend: str | bool = "constant",
|
|
215
|
+
scaling: str = "density",
|
|
216
|
+
sample_rate: float = 1.0,
|
|
217
|
+
dtype: str = "float32",
|
|
218
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
219
|
+
"""Compute Welch PSD estimate for very long signals.
|
|
220
|
+
|
|
221
|
+
Similar to fft_chunked but specifically implements Welch's method
|
|
222
|
+
for power spectral density estimation.
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
file_path: Path to signal file.
|
|
227
|
+
segment_size: Segment size for Welch's method.
|
|
228
|
+
overlap_pct: Overlap percentage (typically 50%).
|
|
229
|
+
window: Window function.
|
|
230
|
+
nfft: FFT length.
|
|
231
|
+
detrend: Detrend type.
|
|
232
|
+
scaling: Scaling mode ('density' or 'spectrum').
|
|
233
|
+
sample_rate: Sample rate in Hz.
|
|
234
|
+
dtype: Data type of input file.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Tuple of (frequencies, psd).
|
|
238
|
+
|
|
239
|
+
Example:
|
|
240
|
+
>>> freqs, psd = welch_psd_chunked('signal.bin', segment_size=1024, sample_rate=1e6)
|
|
241
|
+
>>> print(f"PSD shape: {psd.shape}")
|
|
242
|
+
|
|
243
|
+
References:
|
|
244
|
+
MEM-005: Chunked Welch PSD
|
|
245
|
+
Welch, P.D. (1967). "The use of fast Fourier transform for the
|
|
246
|
+
estimation of power spectra"
|
|
247
|
+
"""
|
|
248
|
+
freqs, spectrum = fft_chunked(
|
|
249
|
+
file_path,
|
|
250
|
+
segment_size=segment_size,
|
|
251
|
+
overlap_pct=overlap_pct,
|
|
252
|
+
window=window,
|
|
253
|
+
nfft=nfft,
|
|
254
|
+
detrend=detrend,
|
|
255
|
+
scaling=scaling,
|
|
256
|
+
average_method="mean",
|
|
257
|
+
sample_rate=sample_rate,
|
|
258
|
+
dtype=dtype,
|
|
259
|
+
preserve_phase=False,
|
|
260
|
+
)
|
|
261
|
+
# preserve_phase=False guarantees float64 output, not complex128
|
|
262
|
+
return freqs, spectrum # type: ignore[return-value]
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def fft_chunked_parallel(
|
|
266
|
+
file_path: str | Path,
|
|
267
|
+
segment_size: int | float,
|
|
268
|
+
overlap_pct: float = 50.0,
|
|
269
|
+
*,
|
|
270
|
+
n_workers: int = 4,
|
|
271
|
+
**kwargs: Any,
|
|
272
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
273
|
+
"""Compute chunked FFT with parallel processing.
|
|
274
|
+
|
|
275
|
+
Similar to fft_chunked but uses multiple workers for parallel
|
|
276
|
+
segment processing. Useful for very large files on multi-core systems.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
file_path: Path to signal file.
|
|
280
|
+
segment_size: Segment size in samples.
|
|
281
|
+
overlap_pct: Overlap percentage.
|
|
282
|
+
n_workers: Number of parallel workers.
|
|
283
|
+
**kwargs: Additional arguments passed to fft_chunked.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Tuple of (frequencies, spectrum).
|
|
287
|
+
|
|
288
|
+
Note:
|
|
289
|
+
FUTURE ENHANCEMENT: Parallel processing with multiprocessing/joblib.
|
|
290
|
+
Currently uses serial processing (n_workers parameter is reserved
|
|
291
|
+
for future implementation). The serial fallback provides correct
|
|
292
|
+
results; parallelization is an optimization opportunity.
|
|
293
|
+
|
|
294
|
+
Example:
|
|
295
|
+
>>> freqs, spectrum = fft_chunked_parallel(
|
|
296
|
+
... 'signal.bin',
|
|
297
|
+
... segment_size=1e6,
|
|
298
|
+
... overlap_pct=50,
|
|
299
|
+
... n_workers=8
|
|
300
|
+
... )
|
|
301
|
+
"""
|
|
302
|
+
# Future: Implement parallel processing with multiprocessing or joblib
|
|
303
|
+
# For now, fall back to serial processing
|
|
304
|
+
freqs, spectrum = fft_chunked(file_path, segment_size, overlap_pct, **kwargs)
|
|
305
|
+
# kwargs may contain preserve_phase, handle both float64 and complex128
|
|
306
|
+
return freqs, spectrum # type: ignore[return-value]
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def streaming_fft(
|
|
310
|
+
file_path: str | Path,
|
|
311
|
+
segment_size: int | float,
|
|
312
|
+
overlap_pct: float = 50.0,
|
|
313
|
+
*,
|
|
314
|
+
window: str | NDArray[np.float64] = "hann",
|
|
315
|
+
nfft: int | None = None,
|
|
316
|
+
detrend: str | bool = False,
|
|
317
|
+
sample_rate: float = 1.0,
|
|
318
|
+
dtype: str = "float32",
|
|
319
|
+
progress_callback: Callable[[int, int], None] | None = None,
|
|
320
|
+
) -> Iterator[tuple[NDArray[np.float64], NDArray[np.float64]]]:
|
|
321
|
+
"""Stream FFT computation yielding frequency bins as computed.
|
|
322
|
+
|
|
323
|
+
Implements streaming/generator API for memory-efficient FFT computation
|
|
324
|
+
on very large files. Yields frequency bins as they are computed, allowing
|
|
325
|
+
downstream processing before all segments are complete.
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
file_path: Path to signal file (binary format).
|
|
330
|
+
segment_size: Segment size in samples.
|
|
331
|
+
overlap_pct: Overlap percentage between segments (0-100).
|
|
332
|
+
window: Window function name or array.
|
|
333
|
+
nfft: FFT length (default: segment_size).
|
|
334
|
+
detrend: Detrend type ('constant', 'linear', False).
|
|
335
|
+
sample_rate: Sample rate in Hz (for frequency axis).
|
|
336
|
+
dtype: Data type of input file ('float32' or 'float64').
|
|
337
|
+
progress_callback: Optional callback(current, total) to report progress.
|
|
338
|
+
|
|
339
|
+
Yields:
|
|
340
|
+
Tuple of (frequencies, fft_magnitude) for each segment.
|
|
341
|
+
|
|
342
|
+
Raises:
|
|
343
|
+
ValueError: If overlap_pct not in valid range.
|
|
344
|
+
|
|
345
|
+
Example:
|
|
346
|
+
>>> # Stream FFT results as computed
|
|
347
|
+
>>> def on_progress(current, total):
|
|
348
|
+
... print(f"Progress: {current}/{total} ({current/total*100:.1f}%)")
|
|
349
|
+
>>>
|
|
350
|
+
>>> for frequencies, magnitude in streaming_fft(
|
|
351
|
+
... 'huge_signal.bin',
|
|
352
|
+
... segment_size=1e6,
|
|
353
|
+
... overlap_pct=50,
|
|
354
|
+
... progress_callback=on_progress
|
|
355
|
+
... ):
|
|
356
|
+
... # Process each segment immediately
|
|
357
|
+
... peak_freq = frequencies[magnitude.argmax()]
|
|
358
|
+
... print(f"Peak frequency: {peak_freq:.2e} Hz")
|
|
359
|
+
|
|
360
|
+
References:
|
|
361
|
+
API-003: Streaming/Generator API for Large Files
|
|
362
|
+
"""
|
|
363
|
+
if not 0 <= overlap_pct < 100:
|
|
364
|
+
raise ValueError(
|
|
365
|
+
f"overlap_pct must be in [0, 100), got {overlap_pct}. Note: 100% overlap would create an infinite loop."
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
segment_size = int(segment_size)
|
|
369
|
+
if nfft is None:
|
|
370
|
+
nfft = segment_size
|
|
371
|
+
|
|
372
|
+
# Calculate overlap in samples
|
|
373
|
+
noverlap = int(segment_size * overlap_pct / 100)
|
|
374
|
+
|
|
375
|
+
# Determine dtype
|
|
376
|
+
np_dtype = np.float32 if dtype == "float32" else np.float64
|
|
377
|
+
bytes_per_sample = 4 if dtype == "float32" else 8
|
|
378
|
+
|
|
379
|
+
# Open file and get total size
|
|
380
|
+
file_path = Path(file_path)
|
|
381
|
+
file_size_bytes = file_path.stat().st_size
|
|
382
|
+
total_samples = file_size_bytes // bytes_per_sample
|
|
383
|
+
|
|
384
|
+
# Calculate total segments for progress reporting
|
|
385
|
+
hop = segment_size - noverlap
|
|
386
|
+
total_segments = max(1, (total_samples - segment_size) // hop + 1)
|
|
387
|
+
|
|
388
|
+
# Generate window
|
|
389
|
+
if isinstance(window, str):
|
|
390
|
+
window_arr = signal.get_window(window, segment_size)
|
|
391
|
+
else:
|
|
392
|
+
window_arr = np.asarray(window)
|
|
393
|
+
|
|
394
|
+
# Frequency axis (computed once)
|
|
395
|
+
frequencies = fft.rfftfreq(nfft, d=1 / sample_rate)
|
|
396
|
+
|
|
397
|
+
# Process and yield segments
|
|
398
|
+
segment_count = 0
|
|
399
|
+
for segment in _generate_segments(file_path, total_samples, segment_size, noverlap, np_dtype):
|
|
400
|
+
# Apply detrending
|
|
401
|
+
if detrend:
|
|
402
|
+
segment = signal.detrend(segment, type=detrend)
|
|
403
|
+
|
|
404
|
+
# Apply window
|
|
405
|
+
windowed = segment * window_arr[: len(segment)]
|
|
406
|
+
|
|
407
|
+
# Zero-pad if needed
|
|
408
|
+
if len(windowed) < nfft:
|
|
409
|
+
windowed = np.pad(windowed, (0, nfft - len(windowed)), mode="constant")
|
|
410
|
+
|
|
411
|
+
# Compute FFT
|
|
412
|
+
fft_result = fft.rfft(windowed, n=nfft)
|
|
413
|
+
magnitude = np.abs(fft_result)
|
|
414
|
+
|
|
415
|
+
# Yield result immediately
|
|
416
|
+
yield frequencies, magnitude
|
|
417
|
+
|
|
418
|
+
# Update progress
|
|
419
|
+
segment_count += 1 # noqa: SIM113
|
|
420
|
+
if progress_callback is not None:
|
|
421
|
+
progress_callback(segment_count, total_segments)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
class StreamingAnalyzer:
|
|
425
|
+
"""Accumulator for streaming analysis across chunks.
|
|
426
|
+
|
|
427
|
+
Enables processing of huge files chunk-by-chunk with accumulation
|
|
428
|
+
of statistics, PSD estimates, and other aggregated measurements.
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
Attributes:
|
|
432
|
+
chunk_count: Number of chunks processed.
|
|
433
|
+
accumulated_psd: Accumulated PSD estimate (if accumulate_psd called).
|
|
434
|
+
accumulated_stats: Dictionary of accumulated statistics.
|
|
435
|
+
|
|
436
|
+
Example:
|
|
437
|
+
>>> analyzer = StreamingAnalyzer()
|
|
438
|
+
>>> for chunk in load_trace_chunks('large.bin', chunk_size=50e6):
|
|
439
|
+
... analyzer.accumulate_psd(chunk, nperseg=4096)
|
|
440
|
+
... analyzer.accumulate_stats(chunk)
|
|
441
|
+
>>> psd = analyzer.get_psd()
|
|
442
|
+
>>> stats = analyzer.get_stats()
|
|
443
|
+
|
|
444
|
+
References:
|
|
445
|
+
API-003: Streaming/Generator API for Large Files
|
|
446
|
+
"""
|
|
447
|
+
|
|
448
|
+
def __init__(self) -> None:
|
|
449
|
+
"""Initialize streaming analyzer."""
|
|
450
|
+
self.chunk_count: int = 0
|
|
451
|
+
self._psd_accumulator: list[NDArray[Any]] = []
|
|
452
|
+
self._psd_frequencies: NDArray[Any] | None = None
|
|
453
|
+
self._psd_config: dict[str, Any] = {}
|
|
454
|
+
self._stats_accumulator: dict[str, list[float]] = {
|
|
455
|
+
"mean": [],
|
|
456
|
+
"std": [],
|
|
457
|
+
"min": [],
|
|
458
|
+
"max": [],
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
def accumulate_psd(
|
|
462
|
+
self,
|
|
463
|
+
chunk: NDArray[Any],
|
|
464
|
+
nperseg: int = 256,
|
|
465
|
+
window: str = "hann",
|
|
466
|
+
sample_rate: float = 1.0,
|
|
467
|
+
) -> None:
|
|
468
|
+
"""Accumulate PSD estimate from chunk using Welch's method.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
chunk: Data chunk to process.
|
|
472
|
+
nperseg: Length of each segment for Welch's method.
|
|
473
|
+
window: Window function name.
|
|
474
|
+
sample_rate: Sample rate in Hz.
|
|
475
|
+
|
|
476
|
+
Example:
|
|
477
|
+
>>> analyzer.accumulate_psd(chunk, nperseg=4096, window='hann')
|
|
478
|
+
"""
|
|
479
|
+
# Compute Welch PSD for this chunk
|
|
480
|
+
freqs, psd = signal.welch(chunk, fs=sample_rate, nperseg=nperseg, window=window)
|
|
481
|
+
|
|
482
|
+
# Store frequencies on first call
|
|
483
|
+
if self._psd_frequencies is None:
|
|
484
|
+
self._psd_frequencies = freqs
|
|
485
|
+
self._psd_config = {
|
|
486
|
+
"nperseg": nperseg,
|
|
487
|
+
"window": window,
|
|
488
|
+
"sample_rate": sample_rate,
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
# Accumulate PSD
|
|
492
|
+
self._psd_accumulator.append(psd)
|
|
493
|
+
self.chunk_count += 1
|
|
494
|
+
|
|
495
|
+
def accumulate_stats(self, chunk: NDArray[np.float64]) -> None:
|
|
496
|
+
"""Accumulate basic statistics from chunk.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
chunk: Data chunk to process.
|
|
500
|
+
|
|
501
|
+
Example:
|
|
502
|
+
>>> analyzer.accumulate_stats(chunk)
|
|
503
|
+
"""
|
|
504
|
+
self._stats_accumulator["mean"].append(float(np.mean(chunk)))
|
|
505
|
+
self._stats_accumulator["std"].append(float(np.std(chunk)))
|
|
506
|
+
self._stats_accumulator["min"].append(float(np.min(chunk)))
|
|
507
|
+
self._stats_accumulator["max"].append(float(np.max(chunk)))
|
|
508
|
+
|
|
509
|
+
def get_psd(self) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
510
|
+
"""Get aggregated PSD estimate.
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
Tuple of (frequencies, psd) with averaged PSD across chunks.
|
|
514
|
+
|
|
515
|
+
Raises:
|
|
516
|
+
ValueError: If no PSD data accumulated.
|
|
517
|
+
|
|
518
|
+
Example:
|
|
519
|
+
>>> freqs, psd = analyzer.get_psd()
|
|
520
|
+
"""
|
|
521
|
+
if not self._psd_accumulator:
|
|
522
|
+
raise ValueError("No PSD data accumulated. Call accumulate_psd() first.")
|
|
523
|
+
|
|
524
|
+
if self._psd_frequencies is None:
|
|
525
|
+
raise ValueError("PSD frequencies not initialized. Call accumulate_psd() first.")
|
|
526
|
+
|
|
527
|
+
# Average PSDs across all chunks
|
|
528
|
+
avg_psd = np.mean(self._psd_accumulator, axis=0)
|
|
529
|
+
return self._psd_frequencies, avg_psd
|
|
530
|
+
|
|
531
|
+
def get_stats(self) -> dict[str, float]:
|
|
532
|
+
"""Get aggregated statistics.
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
Dictionary with overall mean, std, min, max.
|
|
536
|
+
|
|
537
|
+
Example:
|
|
538
|
+
>>> stats = analyzer.get_stats()
|
|
539
|
+
>>> print(f"Overall mean: {stats['mean']:.3f}")
|
|
540
|
+
"""
|
|
541
|
+
if not self._stats_accumulator["mean"]:
|
|
542
|
+
return {"mean": 0.0, "std": 0.0, "min": 0.0, "max": 0.0}
|
|
543
|
+
|
|
544
|
+
return {
|
|
545
|
+
"mean": float(np.mean(self._stats_accumulator["mean"])),
|
|
546
|
+
"std": float(np.mean(self._stats_accumulator["std"])),
|
|
547
|
+
"min": float(np.min(self._stats_accumulator["min"])),
|
|
548
|
+
"max": float(np.max(self._stats_accumulator["max"])),
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
def reset(self) -> None:
|
|
552
|
+
"""Reset all accumulated data.
|
|
553
|
+
|
|
554
|
+
Example:
|
|
555
|
+
>>> analyzer.reset()
|
|
556
|
+
"""
|
|
557
|
+
self.chunk_count = 0
|
|
558
|
+
self._psd_accumulator.clear()
|
|
559
|
+
self._psd_frequencies = None
|
|
560
|
+
self._psd_config.clear()
|
|
561
|
+
for key in self._stats_accumulator:
|
|
562
|
+
self._stats_accumulator[key].clear()
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
__all__ = [
|
|
566
|
+
"StreamingAnalyzer",
|
|
567
|
+
"fft_chunked",
|
|
568
|
+
"fft_chunked_parallel",
|
|
569
|
+
"streaming_fft",
|
|
570
|
+
"welch_psd_chunked",
|
|
571
|
+
]
|