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,368 @@
|
|
|
1
|
+
"""Idle and padding detection and removal.
|
|
2
|
+
|
|
3
|
+
This module provides functions to detect and optionally remove idle regions,
|
|
4
|
+
padding, and non-data samples from loaded binary captures.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.loaders.preprocessing import detect_idle_regions, trim_idle
|
|
9
|
+
>>> regions = detect_idle_regions(trace, pattern='zeros', min_duration=100)
|
|
10
|
+
>>> print(f"Found {len(regions)} idle regions")
|
|
11
|
+
>>> trimmed_trace = trim_idle(trace, trim_start=True, trim_end=True)
|
|
12
|
+
>>> print(f"Trimmed {len(trace.data) - len(trimmed_trace.data)} samples")
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
from oscura.core.types import DigitalTrace, TraceMetadata
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from numpy.typing import NDArray
|
|
27
|
+
|
|
28
|
+
# Logger for debug output
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class IdleRegion:
|
|
34
|
+
"""Idle region in a trace.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
start: Start sample index.
|
|
40
|
+
end: End sample index (exclusive).
|
|
41
|
+
pattern: Detected idle pattern.
|
|
42
|
+
duration_samples: Duration in samples.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
start: int
|
|
46
|
+
end: int
|
|
47
|
+
pattern: str
|
|
48
|
+
duration_samples: int
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def length(self) -> int:
|
|
52
|
+
"""Get region length in samples.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Number of samples in region.
|
|
56
|
+
"""
|
|
57
|
+
return self.end - self.start
|
|
58
|
+
|
|
59
|
+
def get_duration_seconds(self, sample_rate: float) -> float:
|
|
60
|
+
"""Get region duration in seconds.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
sample_rate: Sample rate in Hz.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Duration in seconds.
|
|
67
|
+
"""
|
|
68
|
+
return self.length / sample_rate
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class IdleStatistics:
|
|
73
|
+
"""Statistics about idle regions in a trace.
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
Attributes:
|
|
78
|
+
total_samples: Total number of samples in trace.
|
|
79
|
+
idle_samples: Total number of idle samples.
|
|
80
|
+
active_samples: Total number of active samples.
|
|
81
|
+
idle_regions: List of idle regions.
|
|
82
|
+
dominant_pattern: Most common idle pattern.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
total_samples: int
|
|
86
|
+
idle_samples: int
|
|
87
|
+
active_samples: int
|
|
88
|
+
idle_regions: list[IdleRegion]
|
|
89
|
+
dominant_pattern: str
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def idle_fraction(self) -> float:
|
|
93
|
+
"""Fraction of trace that is idle.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Idle fraction (0.0 to 1.0).
|
|
97
|
+
"""
|
|
98
|
+
if self.total_samples == 0:
|
|
99
|
+
return 0.0
|
|
100
|
+
return self.idle_samples / self.total_samples
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def active_fraction(self) -> float:
|
|
104
|
+
"""Fraction of trace that is active.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Active fraction (0.0 to 1.0).
|
|
108
|
+
"""
|
|
109
|
+
return 1.0 - self.idle_fraction
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def detect_idle_regions(
|
|
113
|
+
trace: DigitalTrace,
|
|
114
|
+
pattern: str = "auto",
|
|
115
|
+
min_duration: int = 100,
|
|
116
|
+
) -> list[IdleRegion]:
|
|
117
|
+
"""Detect idle regions in a digital trace.
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
Identifies regions where the signal is idle (constant pattern) for
|
|
122
|
+
a minimum duration. Supports auto-detection and explicit patterns.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
trace: Digital trace to analyze.
|
|
126
|
+
pattern: Idle pattern to detect ("auto", "zeros", "ones", or byte value).
|
|
127
|
+
min_duration: Minimum duration in samples to consider as idle.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
List of detected idle regions.
|
|
131
|
+
|
|
132
|
+
Example:
|
|
133
|
+
>>> regions = detect_idle_regions(trace, pattern='zeros', min_duration=100)
|
|
134
|
+
>>> for region in regions:
|
|
135
|
+
... print(f"Idle from {region.start} to {region.end}")
|
|
136
|
+
"""
|
|
137
|
+
data = trace.data
|
|
138
|
+
|
|
139
|
+
if len(data) < min_duration:
|
|
140
|
+
return []
|
|
141
|
+
|
|
142
|
+
idle_regions: list[IdleRegion] = []
|
|
143
|
+
|
|
144
|
+
if pattern == "auto":
|
|
145
|
+
# Auto-detect pattern from start/end of trace
|
|
146
|
+
pattern = _auto_detect_pattern(data)
|
|
147
|
+
logger.debug("Auto-detected idle pattern: %s", pattern)
|
|
148
|
+
|
|
149
|
+
# Detect idle runs
|
|
150
|
+
if pattern == "zeros":
|
|
151
|
+
idle_mask = ~data # Invert: True where data is False (zero)
|
|
152
|
+
elif pattern == "ones":
|
|
153
|
+
idle_mask = data # True where data is True (one)
|
|
154
|
+
else:
|
|
155
|
+
# For specific byte values, would need multi-bit comparison
|
|
156
|
+
# For now, default to zeros
|
|
157
|
+
logger.warning("Pattern '%s' not fully supported, using zeros", pattern)
|
|
158
|
+
idle_mask = ~data
|
|
159
|
+
|
|
160
|
+
# Find runs of idle samples
|
|
161
|
+
# Pad mask to detect transitions at boundaries
|
|
162
|
+
padded = np.concatenate(([False], idle_mask, [False]))
|
|
163
|
+
transitions = np.diff(padded.astype(np.int8))
|
|
164
|
+
|
|
165
|
+
# Rising edges (start of idle region)
|
|
166
|
+
starts = np.where(transitions == 1)[0]
|
|
167
|
+
# Falling edges (end of idle region)
|
|
168
|
+
ends = np.where(transitions == -1)[0]
|
|
169
|
+
|
|
170
|
+
# Filter by minimum duration
|
|
171
|
+
for start, end in zip(starts, ends, strict=False):
|
|
172
|
+
duration = end - start
|
|
173
|
+
if duration >= min_duration:
|
|
174
|
+
idle_regions.append(
|
|
175
|
+
IdleRegion(
|
|
176
|
+
start=int(start),
|
|
177
|
+
end=int(end),
|
|
178
|
+
pattern=pattern,
|
|
179
|
+
duration_samples=int(duration),
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
logger.info(
|
|
184
|
+
"Detected %d idle regions (pattern: %s, min_duration: %d)",
|
|
185
|
+
len(idle_regions),
|
|
186
|
+
pattern,
|
|
187
|
+
min_duration,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return idle_regions
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _auto_detect_pattern(data: NDArray[np.bool_]) -> str:
|
|
194
|
+
"""Auto-detect idle pattern from trace data.
|
|
195
|
+
|
|
196
|
+
Looks at the start and end of the trace to determine the
|
|
197
|
+
most likely idle pattern.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
data: Boolean trace data.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Detected pattern ("zeros", "ones", or "unknown").
|
|
204
|
+
"""
|
|
205
|
+
if len(data) == 0:
|
|
206
|
+
return "zeros"
|
|
207
|
+
|
|
208
|
+
# Check first and last 100 samples (or 10% of trace, whichever is smaller)
|
|
209
|
+
check_len = min(100, len(data) // 10, len(data))
|
|
210
|
+
|
|
211
|
+
if check_len == 0:
|
|
212
|
+
return "zeros"
|
|
213
|
+
|
|
214
|
+
start_samples = data[:check_len]
|
|
215
|
+
end_samples = data[-check_len:]
|
|
216
|
+
|
|
217
|
+
# Count zeros in start/end regions
|
|
218
|
+
start_zeros = np.sum(~start_samples)
|
|
219
|
+
end_zeros = np.sum(~end_samples)
|
|
220
|
+
|
|
221
|
+
# If majority are zeros, pattern is zeros
|
|
222
|
+
if start_zeros > check_len // 2 or end_zeros > check_len // 2:
|
|
223
|
+
return "zeros"
|
|
224
|
+
|
|
225
|
+
# If majority are ones, pattern is ones
|
|
226
|
+
if start_zeros < check_len // 4 and end_zeros < check_len // 4:
|
|
227
|
+
return "ones"
|
|
228
|
+
|
|
229
|
+
# Default to zeros
|
|
230
|
+
return "zeros"
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def trim_idle(
|
|
234
|
+
trace: DigitalTrace,
|
|
235
|
+
trim_start: bool = True,
|
|
236
|
+
trim_end: bool = True,
|
|
237
|
+
pattern: str = "auto",
|
|
238
|
+
min_duration: int = 100,
|
|
239
|
+
) -> DigitalTrace:
|
|
240
|
+
"""Trim idle regions from trace.
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
Removes idle regions from the start and/or end of a trace.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
trace: Digital trace to trim.
|
|
248
|
+
trim_start: Remove idle from start of trace.
|
|
249
|
+
trim_end: Remove idle from end of trace.
|
|
250
|
+
pattern: Idle pattern to detect ("auto", "zeros", "ones").
|
|
251
|
+
min_duration: Minimum idle duration to trim.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
New DigitalTrace with idle regions removed.
|
|
255
|
+
|
|
256
|
+
Example:
|
|
257
|
+
>>> trimmed = trim_idle(trace, trim_start=True, trim_end=True)
|
|
258
|
+
>>> print(f"Removed {len(trace.data) - len(trimmed.data)} idle samples")
|
|
259
|
+
"""
|
|
260
|
+
if len(trace.data) == 0:
|
|
261
|
+
return trace
|
|
262
|
+
|
|
263
|
+
# Detect idle regions
|
|
264
|
+
idle_regions = detect_idle_regions(trace, pattern=pattern, min_duration=min_duration)
|
|
265
|
+
|
|
266
|
+
if not idle_regions:
|
|
267
|
+
return trace
|
|
268
|
+
|
|
269
|
+
# Find start and end trim points
|
|
270
|
+
start_idx = 0
|
|
271
|
+
end_idx = len(trace.data)
|
|
272
|
+
|
|
273
|
+
if trim_start and idle_regions:
|
|
274
|
+
# Check if first region starts at beginning
|
|
275
|
+
first_region = idle_regions[0]
|
|
276
|
+
if first_region.start == 0:
|
|
277
|
+
start_idx = first_region.end
|
|
278
|
+
logger.info("Trimming %d idle samples from start", first_region.length)
|
|
279
|
+
|
|
280
|
+
if trim_end and idle_regions:
|
|
281
|
+
# Check if last region ends at end
|
|
282
|
+
last_region = idle_regions[-1]
|
|
283
|
+
if last_region.end == len(trace.data):
|
|
284
|
+
end_idx = last_region.start
|
|
285
|
+
logger.info("Trimming %d idle samples from end", last_region.length)
|
|
286
|
+
|
|
287
|
+
# Create trimmed trace
|
|
288
|
+
if start_idx > 0 or end_idx < len(trace.data):
|
|
289
|
+
trimmed_data = trace.data[start_idx:end_idx]
|
|
290
|
+
|
|
291
|
+
# Preserve metadata
|
|
292
|
+
new_metadata = TraceMetadata(
|
|
293
|
+
sample_rate=trace.metadata.sample_rate,
|
|
294
|
+
vertical_scale=trace.metadata.vertical_scale,
|
|
295
|
+
vertical_offset=trace.metadata.vertical_offset,
|
|
296
|
+
acquisition_time=trace.metadata.acquisition_time,
|
|
297
|
+
trigger_info=trace.metadata.trigger_info,
|
|
298
|
+
source_file=trace.metadata.source_file,
|
|
299
|
+
channel_name=trace.metadata.channel_name,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
return DigitalTrace(data=trimmed_data, metadata=new_metadata, edges=None)
|
|
303
|
+
|
|
304
|
+
return trace
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def get_idle_statistics(
|
|
308
|
+
trace: DigitalTrace,
|
|
309
|
+
pattern: str = "auto",
|
|
310
|
+
min_duration: int = 100,
|
|
311
|
+
) -> IdleStatistics:
|
|
312
|
+
"""Get statistics about idle regions in trace.
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
Computes comprehensive statistics about idle vs. active samples.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
trace: Digital trace to analyze.
|
|
320
|
+
pattern: Idle pattern to detect ("auto", "zeros", "ones").
|
|
321
|
+
min_duration: Minimum idle duration to count.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
IdleStatistics with analysis results.
|
|
325
|
+
|
|
326
|
+
Example:
|
|
327
|
+
>>> stats = get_idle_statistics(trace)
|
|
328
|
+
>>> print(f"Idle fraction: {stats.idle_fraction:.1%}")
|
|
329
|
+
>>> print(f"Found {len(stats.idle_regions)} idle regions")
|
|
330
|
+
"""
|
|
331
|
+
idle_regions = detect_idle_regions(trace, pattern=pattern, min_duration=min_duration)
|
|
332
|
+
|
|
333
|
+
total_samples = len(trace.data)
|
|
334
|
+
idle_samples = sum(region.length for region in idle_regions)
|
|
335
|
+
active_samples = total_samples - idle_samples
|
|
336
|
+
|
|
337
|
+
# Determine dominant pattern
|
|
338
|
+
if idle_regions:
|
|
339
|
+
# Count pattern occurrences
|
|
340
|
+
pattern_counts: dict[str, int] = {}
|
|
341
|
+
for region in idle_regions:
|
|
342
|
+
pattern_counts[region.pattern] = pattern_counts.get(region.pattern, 0) + region.length
|
|
343
|
+
|
|
344
|
+
dominant_pattern = max(pattern_counts, key=pattern_counts.get) # type: ignore[arg-type]
|
|
345
|
+
else:
|
|
346
|
+
dominant_pattern = "none"
|
|
347
|
+
|
|
348
|
+
return IdleStatistics(
|
|
349
|
+
total_samples=total_samples,
|
|
350
|
+
idle_samples=idle_samples,
|
|
351
|
+
active_samples=active_samples,
|
|
352
|
+
idle_regions=idle_regions,
|
|
353
|
+
dominant_pattern=dominant_pattern,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# Type alias for backward compatibility
|
|
358
|
+
IdleStats = IdleStatistics
|
|
359
|
+
"""Type alias for IdleStatistics."""
|
|
360
|
+
|
|
361
|
+
__all__ = [
|
|
362
|
+
"IdleRegion",
|
|
363
|
+
"IdleStatistics",
|
|
364
|
+
"IdleStats",
|
|
365
|
+
"detect_idle_regions",
|
|
366
|
+
"get_idle_statistics",
|
|
367
|
+
"trim_idle",
|
|
368
|
+
]
|
oscura/loaders/rigol.py
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Rigol WFM file loader.
|
|
2
|
+
|
|
3
|
+
This module provides loading of Rigol oscilloscope .wfm files
|
|
4
|
+
using the RigolWFM library when available.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.loaders.rigol import load_rigol_wfm
|
|
9
|
+
>>> trace = load_rigol_wfm("DS1054Z_001.wfm")
|
|
10
|
+
>>> print(f"Sample rate: {trace.metadata.sample_rate} Hz")
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
from oscura.core.exceptions import FormatError, LoaderError
|
|
21
|
+
from oscura.core.types import TraceMetadata, WaveformTrace
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from os import PathLike
|
|
25
|
+
|
|
26
|
+
# Try to import RigolWFM for full Rigol support
|
|
27
|
+
try:
|
|
28
|
+
import RigolWFM.wfm as rigol_wfm # type: ignore[import-not-found, import-untyped]
|
|
29
|
+
|
|
30
|
+
RIGOL_WFM_AVAILABLE = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
RIGOL_WFM_AVAILABLE = False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def load_rigol_wfm(
|
|
36
|
+
path: str | PathLike[str],
|
|
37
|
+
*,
|
|
38
|
+
channel: int = 0,
|
|
39
|
+
) -> WaveformTrace:
|
|
40
|
+
"""Load a Rigol oscilloscope WFM file.
|
|
41
|
+
|
|
42
|
+
Extracts waveform data and metadata from Rigol .wfm files.
|
|
43
|
+
Uses the RigolWFM library when available for full support.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
path: Path to the Rigol .wfm file.
|
|
47
|
+
channel: Channel index for multi-channel files (default: 0).
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
WaveformTrace containing the waveform data and metadata.
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
LoaderError: If the file cannot be loaded or does not exist.
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
>>> trace = load_rigol_wfm("DS1054Z_001.wfm")
|
|
57
|
+
>>> print(f"Sample rate: {trace.metadata.sample_rate} Hz")
|
|
58
|
+
>>> print(f"Vertical scale: {trace.metadata.vertical_scale} V/div")
|
|
59
|
+
"""
|
|
60
|
+
path = Path(path)
|
|
61
|
+
|
|
62
|
+
if not path.exists():
|
|
63
|
+
raise LoaderError(
|
|
64
|
+
"File not found",
|
|
65
|
+
file_path=str(path),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Try RigolWFM first for full metadata, fall back to basic loader
|
|
69
|
+
if RIGOL_WFM_AVAILABLE:
|
|
70
|
+
try:
|
|
71
|
+
return _load_with_rigolwfm(path, channel=channel)
|
|
72
|
+
except Exception:
|
|
73
|
+
# RigolWFM failed (likely synthetic/malformed file)
|
|
74
|
+
# Force garbage collection to close any leaked file handles
|
|
75
|
+
import gc
|
|
76
|
+
|
|
77
|
+
gc.collect()
|
|
78
|
+
# Try basic loader as fallback
|
|
79
|
+
return _load_basic(path, channel=channel)
|
|
80
|
+
else:
|
|
81
|
+
return _load_basic(path, channel=channel)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _load_with_rigolwfm(
|
|
85
|
+
path: Path,
|
|
86
|
+
*,
|
|
87
|
+
channel: int = 0,
|
|
88
|
+
) -> WaveformTrace:
|
|
89
|
+
"""Load Rigol WFM using RigolWFM library.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
path: Path to the WFM file.
|
|
93
|
+
channel: Channel index.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
WaveformTrace with full metadata.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
FormatError: If no waveform data is found in the file.
|
|
100
|
+
LoaderError: If the file cannot be loaded.
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
# Try to auto-detect model from filename (e.g., DS1054Z)
|
|
104
|
+
model = None
|
|
105
|
+
filename_upper = path.name.upper()
|
|
106
|
+
if "DS1" in filename_upper or "MSO1" in filename_upper or "DHO" in filename_upper:
|
|
107
|
+
if "Z" in filename_upper or "MSO" in filename_upper or "DHO" in filename_upper:
|
|
108
|
+
model = "Z"
|
|
109
|
+
elif "E" in filename_upper:
|
|
110
|
+
model = "E"
|
|
111
|
+
|
|
112
|
+
# Try model detection, fallback to trying both models
|
|
113
|
+
last_error = None
|
|
114
|
+
for try_model in [model] if model else ["Z", "E"]:
|
|
115
|
+
try:
|
|
116
|
+
wfm = rigol_wfm.Wfm.from_file(str(path), model=try_model)
|
|
117
|
+
break
|
|
118
|
+
except Exception as e:
|
|
119
|
+
last_error = e
|
|
120
|
+
continue
|
|
121
|
+
else:
|
|
122
|
+
# None of the models worked
|
|
123
|
+
raise last_error if last_error else RuntimeError("Failed to load WFM file")
|
|
124
|
+
|
|
125
|
+
# Get channel data
|
|
126
|
+
if hasattr(wfm, "channels") and len(wfm.channels) > channel:
|
|
127
|
+
ch = wfm.channels[channel]
|
|
128
|
+
data = np.array(ch.volts, dtype=np.float64)
|
|
129
|
+
sample_rate = wfm.sample_rate if hasattr(wfm, "sample_rate") else 1e6
|
|
130
|
+
vertical_scale = ch.volts_per_div if hasattr(ch, "volts_per_div") else None
|
|
131
|
+
vertical_offset = ch.volt_offset if hasattr(ch, "volt_offset") else None
|
|
132
|
+
channel_name = f"CH{channel + 1}"
|
|
133
|
+
elif hasattr(wfm, "volts"):
|
|
134
|
+
# Single channel format
|
|
135
|
+
data = np.array(wfm.volts, dtype=np.float64)
|
|
136
|
+
sample_rate = wfm.sample_rate if hasattr(wfm, "sample_rate") else 1e6
|
|
137
|
+
vertical_scale = wfm.volts_per_div if hasattr(wfm, "volts_per_div") else None
|
|
138
|
+
vertical_offset = wfm.volt_offset if hasattr(wfm, "volt_offset") else None
|
|
139
|
+
channel_name = "CH1"
|
|
140
|
+
else:
|
|
141
|
+
raise FormatError(
|
|
142
|
+
"No waveform data found in Rigol file",
|
|
143
|
+
file_path=str(path),
|
|
144
|
+
expected="Rigol channel data",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Build metadata
|
|
148
|
+
metadata = TraceMetadata(
|
|
149
|
+
sample_rate=sample_rate,
|
|
150
|
+
vertical_scale=vertical_scale,
|
|
151
|
+
vertical_offset=vertical_offset,
|
|
152
|
+
source_file=str(path),
|
|
153
|
+
channel_name=channel_name,
|
|
154
|
+
trigger_info=_extract_trigger_info(wfm),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return WaveformTrace(data=data, metadata=metadata)
|
|
158
|
+
|
|
159
|
+
except Exception as e:
|
|
160
|
+
# Re-raise FormatError as-is for tests that expect it
|
|
161
|
+
# All other exceptions (including kaitaistruct errors) get wrapped
|
|
162
|
+
if isinstance(e, FormatError):
|
|
163
|
+
raise
|
|
164
|
+
# Wrap other exceptions in LoaderError
|
|
165
|
+
# The outer load_rigol_wfm() will catch LoaderError and fall back to basic loader
|
|
166
|
+
raise LoaderError(
|
|
167
|
+
"Failed to load Rigol WFM file with RigolWFM library",
|
|
168
|
+
file_path=str(path),
|
|
169
|
+
details=str(e),
|
|
170
|
+
fix_hint="File may be malformed or incompatible with RigolWFM library.",
|
|
171
|
+
) from e
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _load_basic(
|
|
175
|
+
path: Path,
|
|
176
|
+
*,
|
|
177
|
+
channel: int = 0,
|
|
178
|
+
) -> WaveformTrace:
|
|
179
|
+
"""Basic Rigol WFM loader without RigolWFM library.
|
|
180
|
+
|
|
181
|
+
This is a simplified loader that reads basic waveform data
|
|
182
|
+
from Rigol WFM files. For full feature support, install RigolWFM.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
path: Path to the WFM file.
|
|
186
|
+
channel: Channel index (ignored in basic mode).
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
WaveformTrace with basic metadata.
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
FormatError: If the file is too small or has no waveform data.
|
|
193
|
+
LoaderError: If the file cannot be read or parsed.
|
|
194
|
+
"""
|
|
195
|
+
try:
|
|
196
|
+
with open(path, "rb") as f:
|
|
197
|
+
# Read header
|
|
198
|
+
header = f.read(256)
|
|
199
|
+
|
|
200
|
+
# Basic validation
|
|
201
|
+
if len(header) < 256:
|
|
202
|
+
raise FormatError(
|
|
203
|
+
"File too small to be a valid Rigol WFM",
|
|
204
|
+
file_path=str(path),
|
|
205
|
+
expected="At least 256 bytes header",
|
|
206
|
+
got=f"{len(header)} bytes",
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Default values
|
|
210
|
+
sample_rate = 1e6 # Default 1 MSa/s
|
|
211
|
+
vertical_scale = None
|
|
212
|
+
vertical_offset = None
|
|
213
|
+
|
|
214
|
+
# Read waveform data
|
|
215
|
+
f.seek(0, 2)
|
|
216
|
+
file_size = f.tell()
|
|
217
|
+
data_size = file_size - 256
|
|
218
|
+
|
|
219
|
+
if data_size <= 0:
|
|
220
|
+
raise FormatError(
|
|
221
|
+
"No waveform data in file",
|
|
222
|
+
file_path=str(path),
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
f.seek(256)
|
|
226
|
+
raw_data = f.read(data_size)
|
|
227
|
+
|
|
228
|
+
# Rigol typically uses int16 or int8 for samples
|
|
229
|
+
try:
|
|
230
|
+
# Try int16 first (common in Rigol files)
|
|
231
|
+
data = np.frombuffer(raw_data, dtype=np.int16).astype(np.float64)
|
|
232
|
+
data = data / 32768.0 # Normalize to -1 to 1
|
|
233
|
+
except ValueError:
|
|
234
|
+
# Fall back to int8
|
|
235
|
+
data = np.frombuffer(raw_data, dtype=np.int8).astype(np.float64)
|
|
236
|
+
data = data / 128.0 # Normalize to -1 to 1
|
|
237
|
+
|
|
238
|
+
# Build metadata
|
|
239
|
+
metadata = TraceMetadata(
|
|
240
|
+
sample_rate=sample_rate,
|
|
241
|
+
vertical_scale=vertical_scale,
|
|
242
|
+
vertical_offset=vertical_offset,
|
|
243
|
+
source_file=str(path),
|
|
244
|
+
channel_name=f"CH{channel + 1}",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
return WaveformTrace(data=data, metadata=metadata)
|
|
248
|
+
|
|
249
|
+
except OSError as e:
|
|
250
|
+
raise LoaderError(
|
|
251
|
+
"Failed to read Rigol WFM file",
|
|
252
|
+
file_path=str(path),
|
|
253
|
+
details=str(e),
|
|
254
|
+
) from e
|
|
255
|
+
except Exception as e:
|
|
256
|
+
if isinstance(e, LoaderError | FormatError):
|
|
257
|
+
raise
|
|
258
|
+
raise LoaderError(
|
|
259
|
+
"Failed to parse Rigol WFM file",
|
|
260
|
+
file_path=str(path),
|
|
261
|
+
details=str(e),
|
|
262
|
+
fix_hint="Install RigolWFM for full Rigol support: pip install RigolWFM",
|
|
263
|
+
) from e
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _extract_trigger_info(wfm: Any) -> dict[str, Any] | None:
|
|
267
|
+
"""Extract trigger information from Rigol waveform object.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
wfm: Rigol waveform object from RigolWFM.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Dictionary of trigger settings or None.
|
|
274
|
+
"""
|
|
275
|
+
trigger_info: dict[str, Any] = {}
|
|
276
|
+
|
|
277
|
+
if hasattr(wfm, "trigger_level"):
|
|
278
|
+
trigger_info["level"] = wfm.trigger_level
|
|
279
|
+
if hasattr(wfm, "trigger_mode"):
|
|
280
|
+
trigger_info["mode"] = wfm.trigger_mode
|
|
281
|
+
if hasattr(wfm, "trigger_source"):
|
|
282
|
+
trigger_info["source"] = wfm.trigger_source
|
|
283
|
+
|
|
284
|
+
return trigger_info if trigger_info else None
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
__all__ = ["load_rigol_wfm"]
|