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,352 @@
|
|
|
1
|
+
"""Progressive resolution analysis for memory-constrained scenarios.
|
|
2
|
+
|
|
3
|
+
This module provides multi-pass analysis capabilities: preview first,
|
|
4
|
+
then zoom into regions of interest for detailed analysis.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.utils.progressive import create_preview, analyze_roi
|
|
9
|
+
>>> preview = create_preview(trace, downsample_factor=10)
|
|
10
|
+
>>> # User inspects preview, selects ROI
|
|
11
|
+
>>> roi_result = analyze_roi(trace, start_time=0.001, end_time=0.002)
|
|
12
|
+
|
|
13
|
+
References:
|
|
14
|
+
Multi-resolution analysis techniques
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import TYPE_CHECKING, Any
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
|
|
27
|
+
from numpy.typing import NDArray
|
|
28
|
+
|
|
29
|
+
from oscura.core.types import WaveformTrace
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class PreviewResult:
|
|
34
|
+
"""Result of preview analysis.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
downsampled_data: Downsampled waveform data.
|
|
39
|
+
downsample_factor: Downsampling factor applied.
|
|
40
|
+
original_length: Length of original signal.
|
|
41
|
+
preview_length: Length of preview signal.
|
|
42
|
+
sample_rate: Sample rate of preview (original / factor).
|
|
43
|
+
time_vector: Time axis for preview.
|
|
44
|
+
basic_stats: Basic statistics from preview.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
downsampled_data: NDArray[np.float64]
|
|
48
|
+
downsample_factor: int
|
|
49
|
+
original_length: int
|
|
50
|
+
preview_length: int
|
|
51
|
+
sample_rate: float
|
|
52
|
+
time_vector: NDArray[np.float64]
|
|
53
|
+
basic_stats: dict[str, float]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class ROISelection:
|
|
58
|
+
"""Region of interest selection.
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
Attributes:
|
|
62
|
+
start_time: Start time in seconds.
|
|
63
|
+
end_time: End time in seconds.
|
|
64
|
+
start_index: Start sample index in original signal.
|
|
65
|
+
end_index: End sample index in original signal.
|
|
66
|
+
duration: Duration in seconds.
|
|
67
|
+
num_samples: Number of samples in ROI.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
start_time: float
|
|
71
|
+
end_time: float
|
|
72
|
+
start_index: int
|
|
73
|
+
end_index: int
|
|
74
|
+
duration: float
|
|
75
|
+
num_samples: int
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def create_preview(
|
|
79
|
+
trace: WaveformTrace,
|
|
80
|
+
*,
|
|
81
|
+
downsample_factor: int | None = None,
|
|
82
|
+
max_samples: int = 10_000,
|
|
83
|
+
apply_antialiasing: bool = True,
|
|
84
|
+
) -> PreviewResult:
|
|
85
|
+
"""Create downsampled preview of waveform for quick inspection.
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
trace: Input waveform trace.
|
|
90
|
+
downsample_factor: Downsampling factor (auto-computed if None).
|
|
91
|
+
max_samples: Target maximum samples in preview.
|
|
92
|
+
apply_antialiasing: Apply anti-aliasing lowpass filter before decimation.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
PreviewResult with downsampled data and metadata.
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
>>> preview = create_preview(large_trace, downsample_factor=10)
|
|
99
|
+
>>> print(f"Preview: {preview.preview_length} samples (factor {preview.downsample_factor}x)")
|
|
100
|
+
>>> # Inspect preview.basic_stats
|
|
101
|
+
"""
|
|
102
|
+
from scipy import signal as sp_signal
|
|
103
|
+
|
|
104
|
+
data = trace.data
|
|
105
|
+
original_length = len(data)
|
|
106
|
+
sample_rate = trace.metadata.sample_rate
|
|
107
|
+
|
|
108
|
+
# Auto-compute downsample factor
|
|
109
|
+
if downsample_factor is None:
|
|
110
|
+
downsample_factor = max(1, original_length // max_samples)
|
|
111
|
+
# Round to nearest power of 2 for efficiency
|
|
112
|
+
downsample_factor = 2 ** int(np.ceil(np.log2(downsample_factor)))
|
|
113
|
+
downsample_factor = max(1, downsample_factor)
|
|
114
|
+
|
|
115
|
+
# Apply anti-aliasing filter if requested
|
|
116
|
+
if apply_antialiasing and downsample_factor > 1:
|
|
117
|
+
# Lowpass filter at Nyquist frequency of downsampled rate
|
|
118
|
+
nyquist_freq = (sample_rate / downsample_factor) / 2
|
|
119
|
+
sos = sp_signal.butter(8, nyquist_freq, btype="low", fs=sample_rate, output="sos")
|
|
120
|
+
filtered = sp_signal.sosfilt(sos, data)
|
|
121
|
+
downsampled = filtered[::downsample_factor]
|
|
122
|
+
else:
|
|
123
|
+
# Simple decimation without filtering
|
|
124
|
+
downsampled = data[::downsample_factor]
|
|
125
|
+
|
|
126
|
+
preview_length = len(downsampled)
|
|
127
|
+
preview_sample_rate = sample_rate / downsample_factor
|
|
128
|
+
|
|
129
|
+
# Create time vector
|
|
130
|
+
time_vector = np.arange(preview_length) / preview_sample_rate
|
|
131
|
+
|
|
132
|
+
# Compute basic statistics
|
|
133
|
+
basic_stats = {
|
|
134
|
+
"mean": float(np.mean(downsampled)),
|
|
135
|
+
"std": float(np.std(downsampled)),
|
|
136
|
+
"min": float(np.min(downsampled)),
|
|
137
|
+
"max": float(np.max(downsampled)),
|
|
138
|
+
"rms": float(np.sqrt(np.mean(downsampled**2))),
|
|
139
|
+
"peak_to_peak": float(np.ptp(downsampled)),
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return PreviewResult(
|
|
143
|
+
downsampled_data=downsampled,
|
|
144
|
+
downsample_factor=downsample_factor,
|
|
145
|
+
original_length=original_length,
|
|
146
|
+
preview_length=preview_length,
|
|
147
|
+
sample_rate=preview_sample_rate,
|
|
148
|
+
time_vector=time_vector,
|
|
149
|
+
basic_stats=basic_stats,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def select_roi(
|
|
154
|
+
trace: WaveformTrace,
|
|
155
|
+
start_time: float,
|
|
156
|
+
end_time: float,
|
|
157
|
+
) -> ROISelection:
|
|
158
|
+
"""Create ROI selection from time range.
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
trace: Input waveform trace.
|
|
163
|
+
start_time: Start time in seconds.
|
|
164
|
+
end_time: End time in seconds.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
ROISelection with sample indices and metadata.
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
ValueError: If time range is invalid.
|
|
171
|
+
|
|
172
|
+
Example:
|
|
173
|
+
>>> roi = select_roi(trace, start_time=0.001, end_time=0.002)
|
|
174
|
+
>>> print(f"ROI: {roi.num_samples} samples ({roi.duration*1e6:.1f} µs)")
|
|
175
|
+
"""
|
|
176
|
+
sample_rate = trace.metadata.sample_rate
|
|
177
|
+
total_length = len(trace.data)
|
|
178
|
+
total_duration = total_length / sample_rate
|
|
179
|
+
|
|
180
|
+
# Validate time range
|
|
181
|
+
if start_time < 0 or end_time > total_duration:
|
|
182
|
+
raise ValueError(
|
|
183
|
+
f"Time range [{start_time}, {end_time}] outside signal duration [0, {total_duration}]"
|
|
184
|
+
)
|
|
185
|
+
if start_time >= end_time:
|
|
186
|
+
raise ValueError(f"start_time ({start_time}) must be < end_time ({end_time})")
|
|
187
|
+
|
|
188
|
+
# Convert to sample indices
|
|
189
|
+
start_index = int(start_time * sample_rate)
|
|
190
|
+
end_index = int(end_time * sample_rate)
|
|
191
|
+
|
|
192
|
+
# Clamp to valid range
|
|
193
|
+
start_index = max(0, min(start_index, total_length - 1))
|
|
194
|
+
end_index = max(start_index + 1, min(end_index, total_length))
|
|
195
|
+
|
|
196
|
+
duration = end_time - start_time
|
|
197
|
+
num_samples = end_index - start_index
|
|
198
|
+
|
|
199
|
+
return ROISelection(
|
|
200
|
+
start_time=start_time,
|
|
201
|
+
end_time=end_time,
|
|
202
|
+
start_index=start_index,
|
|
203
|
+
end_index=end_index,
|
|
204
|
+
duration=duration,
|
|
205
|
+
num_samples=num_samples,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def analyze_roi(
|
|
210
|
+
trace: WaveformTrace,
|
|
211
|
+
roi: ROISelection,
|
|
212
|
+
*,
|
|
213
|
+
analysis_func: Callable[[WaveformTrace], Any],
|
|
214
|
+
**analysis_kwargs: Any,
|
|
215
|
+
) -> Any:
|
|
216
|
+
"""Analyze region of interest with high resolution.
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
trace: Input waveform trace.
|
|
221
|
+
roi: ROI selection.
|
|
222
|
+
analysis_func: Analysis function to apply to ROI.
|
|
223
|
+
**analysis_kwargs: Additional arguments for analysis function.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Result of analysis function on ROI.
|
|
227
|
+
|
|
228
|
+
Example:
|
|
229
|
+
>>> from oscura.analyzers.waveform.spectral import fft
|
|
230
|
+
>>> roi = select_roi(trace, 0.001, 0.002)
|
|
231
|
+
>>> freq, mag = analyze_roi(trace, roi, analysis_func=fft, window='hann')
|
|
232
|
+
"""
|
|
233
|
+
from oscura.core.types import TraceMetadata, WaveformTrace
|
|
234
|
+
|
|
235
|
+
# Extract ROI data
|
|
236
|
+
roi_data = trace.data[roi.start_index : roi.end_index]
|
|
237
|
+
|
|
238
|
+
# Create new trace for ROI with only standard metadata fields
|
|
239
|
+
roi_trace = WaveformTrace(
|
|
240
|
+
data=roi_data,
|
|
241
|
+
metadata=TraceMetadata(
|
|
242
|
+
sample_rate=trace.metadata.sample_rate,
|
|
243
|
+
vertical_scale=trace.metadata.vertical_scale,
|
|
244
|
+
vertical_offset=trace.metadata.vertical_offset,
|
|
245
|
+
acquisition_time=trace.metadata.acquisition_time,
|
|
246
|
+
trigger_info=trace.metadata.trigger_info,
|
|
247
|
+
source_file=trace.metadata.source_file,
|
|
248
|
+
channel_name=getattr(trace.metadata, "channel_name", None),
|
|
249
|
+
),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Apply analysis function
|
|
253
|
+
return analysis_func(roi_trace, **analysis_kwargs)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def progressive_analysis(
|
|
257
|
+
trace: WaveformTrace,
|
|
258
|
+
*,
|
|
259
|
+
analysis_func: Callable[[WaveformTrace], Any],
|
|
260
|
+
downsample_factor: int = 10,
|
|
261
|
+
roi_selector: Callable[[PreviewResult], ROISelection] | None = None,
|
|
262
|
+
**analysis_kwargs: Any,
|
|
263
|
+
) -> tuple[PreviewResult, Any]:
|
|
264
|
+
"""Perform progressive multi-pass analysis.
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
Workflow:
|
|
268
|
+
1. Create downsampled preview
|
|
269
|
+
2. User/algorithm selects ROI from preview
|
|
270
|
+
3. Perform high-resolution analysis on ROI only
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
trace: Input waveform trace.
|
|
274
|
+
analysis_func: Analysis function to apply.
|
|
275
|
+
downsample_factor: Downsampling factor for preview.
|
|
276
|
+
roi_selector: Function to select ROI from preview (if None, analyzes full trace).
|
|
277
|
+
**analysis_kwargs: Additional arguments for analysis function.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Tuple of (preview_result, analysis_result).
|
|
281
|
+
|
|
282
|
+
Example:
|
|
283
|
+
>>> def select_peak_region(preview):
|
|
284
|
+
... # Find region with highest amplitude
|
|
285
|
+
... peak_idx = np.argmax(np.abs(preview.downsampled_data))
|
|
286
|
+
... start_time = max(0, (peak_idx - 500) / preview.sample_rate)
|
|
287
|
+
... end_time = min(preview.preview_length / preview.sample_rate,
|
|
288
|
+
... (peak_idx + 500) / preview.sample_rate)
|
|
289
|
+
... return select_roi(trace, start_time, end_time)
|
|
290
|
+
>>>
|
|
291
|
+
>>> from oscura.analyzers.waveform.spectral import fft
|
|
292
|
+
>>> preview, result = progressive_analysis(
|
|
293
|
+
... trace,
|
|
294
|
+
... analysis_func=fft,
|
|
295
|
+
... downsample_factor=10,
|
|
296
|
+
... roi_selector=select_peak_region
|
|
297
|
+
... )
|
|
298
|
+
"""
|
|
299
|
+
# Pass 1: Create preview
|
|
300
|
+
preview = create_preview(trace, downsample_factor=downsample_factor)
|
|
301
|
+
|
|
302
|
+
# Pass 2: Select ROI
|
|
303
|
+
if roi_selector is not None:
|
|
304
|
+
roi = roi_selector(preview)
|
|
305
|
+
# Pass 3: Analyze ROI
|
|
306
|
+
result = analyze_roi(trace, roi, analysis_func=analysis_func, **analysis_kwargs)
|
|
307
|
+
else:
|
|
308
|
+
# No ROI selection, analyze full trace
|
|
309
|
+
result = analysis_func(trace, **analysis_kwargs)
|
|
310
|
+
|
|
311
|
+
return preview, result
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def estimate_optimal_preview_factor(
|
|
315
|
+
trace_length: int,
|
|
316
|
+
*,
|
|
317
|
+
target_memory: int = 100_000_000, # 100 MB
|
|
318
|
+
bytes_per_sample: int = 8,
|
|
319
|
+
) -> int:
|
|
320
|
+
"""Estimate optimal downsampling factor for preview.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
trace_length: Number of samples in original trace.
|
|
324
|
+
target_memory: Target memory for preview (bytes).
|
|
325
|
+
bytes_per_sample: Bytes per sample (8 for float64).
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Recommended downsampling factor.
|
|
329
|
+
|
|
330
|
+
Example:
|
|
331
|
+
>>> factor = estimate_optimal_preview_factor(1_000_000_000) # 1B samples
|
|
332
|
+
>>> print(f"Downsample by {factor}x for preview")
|
|
333
|
+
"""
|
|
334
|
+
# Calculate required factor to fit in target memory
|
|
335
|
+
current_memory = trace_length * bytes_per_sample
|
|
336
|
+
factor = max(1, int(np.ceil(current_memory / target_memory)))
|
|
337
|
+
|
|
338
|
+
# Round to power of 2
|
|
339
|
+
factor = 2 ** int(np.ceil(np.log2(factor)))
|
|
340
|
+
|
|
341
|
+
return factor # type: ignore[no-any-return]
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
__all__ = [
|
|
345
|
+
"PreviewResult",
|
|
346
|
+
"ROISelection",
|
|
347
|
+
"analyze_roi",
|
|
348
|
+
"create_preview",
|
|
349
|
+
"estimate_optimal_preview_factor",
|
|
350
|
+
"progressive_analysis",
|
|
351
|
+
"select_roi",
|
|
352
|
+
]
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""Window function support for spectral analysis.
|
|
2
|
+
|
|
3
|
+
This module provides standard window functions for FFT and spectral
|
|
4
|
+
analysis, implementing the requirements for windowed spectral estimation.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.utils.windowing import get_window, WINDOW_FUNCTIONS
|
|
9
|
+
>>> window = get_window("hann", 1024)
|
|
10
|
+
>>> print(f"Available windows: {list(WINDOW_FUNCTIONS.keys())}")
|
|
11
|
+
|
|
12
|
+
References:
|
|
13
|
+
Harris, F. J. (1978). "On the use of windows for harmonic analysis
|
|
14
|
+
with the discrete Fourier transform." Proceedings of the IEEE, 66(1).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from numpy.typing import NDArray
|
|
26
|
+
|
|
27
|
+
# Type alias for window function (using string annotation for TYPE_CHECKING compatibility)
|
|
28
|
+
WindowFunction = Callable[[int], "NDArray[np.float64]"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def rectangular(n: int) -> NDArray[np.float64]:
|
|
32
|
+
"""Rectangular (boxcar) window.
|
|
33
|
+
|
|
34
|
+
No tapering applied - all samples weighted equally.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
n: Window length in samples.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Window coefficients (all ones).
|
|
41
|
+
|
|
42
|
+
Example:
|
|
43
|
+
>>> w = rectangular(64)
|
|
44
|
+
>>> assert np.all(w == 1.0)
|
|
45
|
+
"""
|
|
46
|
+
return np.ones(n, dtype=np.float64)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def hann(n: int) -> NDArray[np.float64]:
|
|
50
|
+
"""Hann (raised cosine) window.
|
|
51
|
+
|
|
52
|
+
Also known as Hanning window. Provides good frequency resolution
|
|
53
|
+
with moderate sidelobe suppression.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
n: Window length in samples.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Window coefficients.
|
|
60
|
+
|
|
61
|
+
Example:
|
|
62
|
+
>>> w = hann(64)
|
|
63
|
+
>>> assert w[0] == w[-1] # Symmetric
|
|
64
|
+
|
|
65
|
+
References:
|
|
66
|
+
IEEE Std 1057-2017 Section 4.4.2
|
|
67
|
+
"""
|
|
68
|
+
return np.hanning(n).astype(np.float64)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def hamming(n: int) -> NDArray[np.float64]:
|
|
72
|
+
"""Hamming window.
|
|
73
|
+
|
|
74
|
+
Similar to Hann but with reduced first sidelobe at cost of
|
|
75
|
+
slower rolloff.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
n: Window length in samples.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Window coefficients.
|
|
82
|
+
|
|
83
|
+
Example:
|
|
84
|
+
>>> w = hamming(64)
|
|
85
|
+
>>> assert w[32] > w[0] # Peak in center
|
|
86
|
+
"""
|
|
87
|
+
return np.hamming(n).astype(np.float64)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def blackman(n: int) -> NDArray[np.float64]:
|
|
91
|
+
"""Blackman window.
|
|
92
|
+
|
|
93
|
+
Three-term cosine window with excellent sidelobe suppression
|
|
94
|
+
(-58 dB first sidelobe).
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
n: Window length in samples.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Window coefficients.
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
>>> w = blackman(64)
|
|
104
|
+
>>> assert w[32] > w[0]
|
|
105
|
+
"""
|
|
106
|
+
return np.blackman(n).astype(np.float64)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def kaiser(n: int, beta: float = 8.6) -> NDArray[np.float64]:
|
|
110
|
+
"""Kaiser window with configurable shape parameter.
|
|
111
|
+
|
|
112
|
+
Provides adjustable tradeoff between main lobe width and
|
|
113
|
+
sidelobe attenuation.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
n: Window length in samples.
|
|
117
|
+
beta: Shape parameter (default 8.6 for ~60 dB sidelobe attenuation).
|
|
118
|
+
- beta=0: Rectangular
|
|
119
|
+
- beta=5: ~30 dB sidelobe attenuation
|
|
120
|
+
- beta=8.6: ~60 dB sidelobe attenuation
|
|
121
|
+
- beta=14: ~90 dB sidelobe attenuation
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Window coefficients.
|
|
125
|
+
|
|
126
|
+
Example:
|
|
127
|
+
>>> w = kaiser(64, beta=10)
|
|
128
|
+
>>> assert 0 < w[0] < w[32]
|
|
129
|
+
"""
|
|
130
|
+
return np.kaiser(n, beta).astype(np.float64)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def flattop(n: int) -> NDArray[np.float64]:
|
|
134
|
+
"""Flat-top window for accurate amplitude measurements.
|
|
135
|
+
|
|
136
|
+
Provides minimal scalloping loss (<0.01 dB) at cost of
|
|
137
|
+
wider main lobe. Best for amplitude accuracy when frequency
|
|
138
|
+
resolution is not critical.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
n: Window length in samples.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Window coefficients.
|
|
145
|
+
|
|
146
|
+
Example:
|
|
147
|
+
>>> w = flattop(64)
|
|
148
|
+
>>> # Flat-top has characteristic near-zero values at edges
|
|
149
|
+
|
|
150
|
+
References:
|
|
151
|
+
D'Antona, G. & Ferrero, A. (2006). "Digital Signal Processing
|
|
152
|
+
for Measurement Systems."
|
|
153
|
+
"""
|
|
154
|
+
# Flat-top coefficients per HP/Agilent standard
|
|
155
|
+
a0 = 0.21557895
|
|
156
|
+
a1 = 0.41663158
|
|
157
|
+
a2 = 0.277263158
|
|
158
|
+
a3 = 0.083578947
|
|
159
|
+
a4 = 0.006947368
|
|
160
|
+
|
|
161
|
+
k = np.arange(n, dtype=np.float64)
|
|
162
|
+
w = (
|
|
163
|
+
a0
|
|
164
|
+
- a1 * np.cos(2 * np.pi * k / (n - 1))
|
|
165
|
+
+ a2 * np.cos(4 * np.pi * k / (n - 1))
|
|
166
|
+
- a3 * np.cos(6 * np.pi * k / (n - 1))
|
|
167
|
+
+ a4 * np.cos(8 * np.pi * k / (n - 1))
|
|
168
|
+
)
|
|
169
|
+
return np.asarray(w, dtype=np.float64)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def bartlett(n: int) -> NDArray[np.float64]:
|
|
173
|
+
"""Bartlett (triangular) window.
|
|
174
|
+
|
|
175
|
+
Linear taper from zero at edges to maximum at center.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
n: Window length in samples.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Window coefficients.
|
|
182
|
+
|
|
183
|
+
Example:
|
|
184
|
+
>>> w = bartlett(64)
|
|
185
|
+
>>> assert w[32] == 1.0 # Maximum at center
|
|
186
|
+
"""
|
|
187
|
+
return np.bartlett(n).astype(np.float64)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def blackman_harris(n: int) -> NDArray[np.float64]:
|
|
191
|
+
"""Blackman-Harris window (4-term).
|
|
192
|
+
|
|
193
|
+
Four-term cosine window with excellent sidelobe suppression
|
|
194
|
+
(-92 dB first sidelobe).
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
n: Window length in samples.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Window coefficients.
|
|
201
|
+
|
|
202
|
+
Example:
|
|
203
|
+
>>> w = blackman_harris(64)
|
|
204
|
+
>>> assert w[32] > w[0]
|
|
205
|
+
"""
|
|
206
|
+
a0 = 0.35875
|
|
207
|
+
a1 = 0.48829
|
|
208
|
+
a2 = 0.14128
|
|
209
|
+
a3 = 0.01168
|
|
210
|
+
|
|
211
|
+
k = np.arange(n, dtype=np.float64)
|
|
212
|
+
w = (
|
|
213
|
+
a0
|
|
214
|
+
- a1 * np.cos(2 * np.pi * k / (n - 1))
|
|
215
|
+
+ a2 * np.cos(4 * np.pi * k / (n - 1))
|
|
216
|
+
- a3 * np.cos(6 * np.pi * k / (n - 1))
|
|
217
|
+
)
|
|
218
|
+
return np.asarray(w, dtype=np.float64)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# Window function registry
|
|
222
|
+
WINDOW_FUNCTIONS: dict[str, WindowFunction] = {
|
|
223
|
+
"rectangular": rectangular,
|
|
224
|
+
"boxcar": rectangular,
|
|
225
|
+
"rect": rectangular,
|
|
226
|
+
"hann": hann,
|
|
227
|
+
"hanning": hann,
|
|
228
|
+
"hamming": hamming,
|
|
229
|
+
"blackman": blackman,
|
|
230
|
+
"kaiser": lambda n: kaiser(n, beta=8.6),
|
|
231
|
+
"flattop": flattop,
|
|
232
|
+
"flat_top": flattop,
|
|
233
|
+
"bartlett": bartlett,
|
|
234
|
+
"triangular": bartlett,
|
|
235
|
+
"blackman_harris": blackman_harris,
|
|
236
|
+
"blackmanharris": blackman_harris,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# Type for window names
|
|
241
|
+
WindowName = Literal[
|
|
242
|
+
"rectangular",
|
|
243
|
+
"boxcar",
|
|
244
|
+
"rect",
|
|
245
|
+
"hann",
|
|
246
|
+
"hanning",
|
|
247
|
+
"hamming",
|
|
248
|
+
"blackman",
|
|
249
|
+
"kaiser",
|
|
250
|
+
"flattop",
|
|
251
|
+
"flat_top",
|
|
252
|
+
"bartlett",
|
|
253
|
+
"triangular",
|
|
254
|
+
"blackman_harris",
|
|
255
|
+
"blackmanharris",
|
|
256
|
+
]
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def get_window(
|
|
260
|
+
window: str | WindowFunction | NDArray[np.floating[Any]],
|
|
261
|
+
n: int,
|
|
262
|
+
*,
|
|
263
|
+
beta: float | None = None,
|
|
264
|
+
) -> NDArray[np.float64]:
|
|
265
|
+
"""Get window coefficients by name or callable.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
window: Window specification. Can be:
|
|
269
|
+
- A string name from WINDOW_FUNCTIONS
|
|
270
|
+
- A callable that takes length and returns coefficients
|
|
271
|
+
- A pre-computed array of coefficients
|
|
272
|
+
n: Window length in samples.
|
|
273
|
+
beta: Optional beta parameter for Kaiser window.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Window coefficients array of length n.
|
|
277
|
+
|
|
278
|
+
Raises:
|
|
279
|
+
ValueError: If window name is unknown.
|
|
280
|
+
|
|
281
|
+
Example:
|
|
282
|
+
>>> w = get_window("hann", 1024)
|
|
283
|
+
>>> w = get_window("kaiser", 1024, beta=10)
|
|
284
|
+
>>> w = get_window(np.hamming, 1024)
|
|
285
|
+
"""
|
|
286
|
+
if isinstance(window, np.ndarray):
|
|
287
|
+
if len(window) != n:
|
|
288
|
+
raise ValueError(f"Window array length {len(window)} != requested {n}")
|
|
289
|
+
return window.astype(np.float64)
|
|
290
|
+
|
|
291
|
+
if callable(window) and not isinstance(window, str):
|
|
292
|
+
return np.asarray(window(n), dtype=np.float64)
|
|
293
|
+
|
|
294
|
+
window_name = window.lower()
|
|
295
|
+
|
|
296
|
+
if window_name == "kaiser" and beta is not None:
|
|
297
|
+
return kaiser(n, beta)
|
|
298
|
+
|
|
299
|
+
if window_name not in WINDOW_FUNCTIONS:
|
|
300
|
+
available = ", ".join(sorted(set(WINDOW_FUNCTIONS.keys())))
|
|
301
|
+
raise ValueError(f"Unknown window: {window}. Available: {available}")
|
|
302
|
+
|
|
303
|
+
return WINDOW_FUNCTIONS[window_name](n)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def window_properties(window: str | NDArray[np.floating[Any]], n: int = 1024) -> dict[str, Any]:
|
|
307
|
+
"""Compute window properties for analysis.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
window: Window name or coefficients.
|
|
311
|
+
n: Window length for named windows.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Dictionary with window properties:
|
|
315
|
+
- coherent_gain: Sum of window / length
|
|
316
|
+
- noise_bandwidth: Normalized equivalent noise bandwidth
|
|
317
|
+
- scalloping_loss: Peak amplitude error in dB
|
|
318
|
+
|
|
319
|
+
Example:
|
|
320
|
+
>>> props = window_properties("hann")
|
|
321
|
+
>>> print(f"ENBW: {props['noise_bandwidth']:.3f}")
|
|
322
|
+
"""
|
|
323
|
+
if isinstance(window, str):
|
|
324
|
+
w = get_window(window, n)
|
|
325
|
+
else:
|
|
326
|
+
w = np.asarray(window, dtype=np.float64)
|
|
327
|
+
n = len(w)
|
|
328
|
+
|
|
329
|
+
# Coherent gain (DC gain)
|
|
330
|
+
coherent_gain = np.sum(w) / n
|
|
331
|
+
|
|
332
|
+
# Noise equivalent bandwidth
|
|
333
|
+
# ENBW = N * sum(w^2) / (sum(w))^2
|
|
334
|
+
noise_bandwidth = n * np.sum(w**2) / np.sum(w) ** 2
|
|
335
|
+
|
|
336
|
+
# Scalloping loss (worst-case amplitude error at bin edge)
|
|
337
|
+
# Approximate by evaluating window at half-bin offset
|
|
338
|
+
k = np.arange(n)
|
|
339
|
+
w_shifted = w * np.exp(2j * np.pi * 0.5 * k / n)
|
|
340
|
+
scalloping_loss = 20 * np.log10(np.abs(np.sum(w_shifted)) / np.abs(np.sum(w)))
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
"coherent_gain": float(coherent_gain),
|
|
344
|
+
"noise_bandwidth": float(noise_bandwidth),
|
|
345
|
+
"scalloping_loss": float(scalloping_loss),
|
|
346
|
+
"length": n,
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
__all__ = [
|
|
351
|
+
"WINDOW_FUNCTIONS",
|
|
352
|
+
"bartlett",
|
|
353
|
+
"blackman",
|
|
354
|
+
"blackman_harris",
|
|
355
|
+
"flattop",
|
|
356
|
+
"get_window",
|
|
357
|
+
"hamming",
|
|
358
|
+
"hann",
|
|
359
|
+
"kaiser",
|
|
360
|
+
"rectangular",
|
|
361
|
+
"window_properties",
|
|
362
|
+
]
|