oscura 0.5.0__py3-none-any.whl → 0.6.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 +169 -167
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/classification.py +659 -0
- oscura/analyzers/digital/__init__.py +0 -48
- oscura/analyzers/digital/edges.py +325 -65
- oscura/analyzers/digital/extraction.py +0 -195
- oscura/analyzers/digital/quality.py +293 -166
- oscura/analyzers/digital/timing.py +260 -115
- oscura/analyzers/digital/timing_numba.py +334 -0
- oscura/analyzers/entropy.py +605 -0
- oscura/analyzers/eye/diagram.py +176 -109
- oscura/analyzers/eye/metrics.py +5 -5
- oscura/analyzers/jitter/__init__.py +6 -4
- oscura/analyzers/jitter/ber.py +52 -52
- oscura/analyzers/jitter/classification.py +156 -0
- oscura/analyzers/jitter/decomposition.py +163 -113
- oscura/analyzers/jitter/spectrum.py +80 -64
- oscura/analyzers/ml/__init__.py +39 -0
- oscura/analyzers/ml/features.py +600 -0
- oscura/analyzers/ml/signal_classifier.py +604 -0
- oscura/analyzers/packet/daq.py +246 -158
- oscura/analyzers/packet/parser.py +12 -1
- oscura/analyzers/packet/payload.py +50 -2110
- oscura/analyzers/packet/payload_analysis.py +361 -181
- oscura/analyzers/packet/payload_patterns.py +133 -70
- oscura/analyzers/packet/stream.py +84 -23
- oscura/analyzers/patterns/__init__.py +26 -5
- oscura/analyzers/patterns/anomaly_detection.py +908 -0
- oscura/analyzers/patterns/clustering.py +169 -108
- oscura/analyzers/patterns/clustering_optimized.py +227 -0
- oscura/analyzers/patterns/discovery.py +1 -1
- oscura/analyzers/patterns/matching.py +581 -197
- oscura/analyzers/patterns/pattern_mining.py +778 -0
- oscura/analyzers/patterns/periodic.py +121 -38
- oscura/analyzers/patterns/sequences.py +175 -78
- oscura/analyzers/power/conduction.py +1 -1
- oscura/analyzers/power/soa.py +6 -6
- oscura/analyzers/power/switching.py +250 -110
- oscura/analyzers/protocol/__init__.py +17 -1
- oscura/analyzers/protocols/__init__.py +1 -22
- oscura/analyzers/protocols/base.py +6 -6
- oscura/analyzers/protocols/ble/__init__.py +38 -0
- oscura/analyzers/protocols/ble/analyzer.py +809 -0
- oscura/analyzers/protocols/ble/uuids.py +288 -0
- oscura/analyzers/protocols/can.py +257 -127
- oscura/analyzers/protocols/can_fd.py +107 -80
- oscura/analyzers/protocols/flexray.py +139 -80
- oscura/analyzers/protocols/hdlc.py +93 -58
- oscura/analyzers/protocols/i2c.py +247 -106
- oscura/analyzers/protocols/i2s.py +138 -86
- oscura/analyzers/protocols/industrial/__init__.py +40 -0
- oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
- oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
- oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
- oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
- oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
- oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
- oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
- oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
- oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
- oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
- oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
- oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
- oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
- oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
- oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
- oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
- oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
- oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
- oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
- oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
- oscura/analyzers/protocols/jtag.py +180 -98
- oscura/analyzers/protocols/lin.py +219 -114
- oscura/analyzers/protocols/manchester.py +4 -4
- oscura/analyzers/protocols/onewire.py +253 -149
- oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
- oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
- oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
- oscura/analyzers/protocols/spi.py +192 -95
- oscura/analyzers/protocols/swd.py +321 -167
- oscura/analyzers/protocols/uart.py +267 -125
- oscura/analyzers/protocols/usb.py +235 -131
- oscura/analyzers/side_channel/power.py +17 -12
- oscura/analyzers/signal/__init__.py +15 -0
- oscura/analyzers/signal/timing_analysis.py +1086 -0
- oscura/analyzers/signal_integrity/__init__.py +4 -1
- oscura/analyzers/signal_integrity/sparams.py +2 -19
- oscura/analyzers/spectral/chunked.py +129 -60
- oscura/analyzers/spectral/chunked_fft.py +300 -94
- oscura/analyzers/spectral/chunked_wavelet.py +100 -80
- oscura/analyzers/statistical/checksum.py +376 -217
- oscura/analyzers/statistical/classification.py +229 -107
- oscura/analyzers/statistical/entropy.py +78 -53
- oscura/analyzers/statistics/correlation.py +407 -211
- oscura/analyzers/statistics/outliers.py +2 -2
- oscura/analyzers/statistics/streaming.py +30 -5
- oscura/analyzers/validation.py +216 -101
- oscura/analyzers/waveform/measurements.py +9 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
- oscura/analyzers/waveform/spectral.py +500 -228
- oscura/api/__init__.py +31 -5
- oscura/api/dsl/__init__.py +582 -0
- oscura/{dsl → api/dsl}/commands.py +43 -76
- oscura/{dsl → api/dsl}/interpreter.py +26 -51
- oscura/{dsl → api/dsl}/parser.py +107 -77
- oscura/{dsl → api/dsl}/repl.py +2 -2
- oscura/api/dsl.py +1 -1
- oscura/{integrations → api/integrations}/__init__.py +1 -1
- oscura/{integrations → api/integrations}/llm.py +201 -102
- oscura/api/operators.py +3 -3
- oscura/api/optimization.py +144 -30
- oscura/api/rest_server.py +921 -0
- oscura/api/server/__init__.py +17 -0
- oscura/api/server/dashboard.py +850 -0
- oscura/api/server/static/README.md +34 -0
- oscura/api/server/templates/base.html +181 -0
- oscura/api/server/templates/export.html +120 -0
- oscura/api/server/templates/home.html +284 -0
- oscura/api/server/templates/protocols.html +58 -0
- oscura/api/server/templates/reports.html +43 -0
- oscura/api/server/templates/session_detail.html +89 -0
- oscura/api/server/templates/sessions.html +83 -0
- oscura/api/server/templates/waveforms.html +73 -0
- oscura/automotive/__init__.py +8 -1
- oscura/automotive/can/__init__.py +10 -0
- oscura/automotive/can/checksum.py +3 -1
- oscura/automotive/can/dbc_generator.py +590 -0
- oscura/automotive/can/message_wrapper.py +121 -74
- oscura/automotive/can/patterns.py +98 -21
- oscura/automotive/can/session.py +292 -56
- oscura/automotive/can/state_machine.py +6 -3
- oscura/automotive/can/stimulus_response.py +97 -75
- oscura/automotive/dbc/__init__.py +10 -2
- oscura/automotive/dbc/generator.py +84 -56
- oscura/automotive/dbc/parser.py +6 -6
- oscura/automotive/dtc/data.json +2763 -0
- oscura/automotive/dtc/database.py +2 -2
- oscura/automotive/flexray/__init__.py +31 -0
- oscura/automotive/flexray/analyzer.py +504 -0
- oscura/automotive/flexray/crc.py +185 -0
- oscura/automotive/flexray/fibex.py +449 -0
- oscura/automotive/j1939/__init__.py +45 -8
- oscura/automotive/j1939/analyzer.py +605 -0
- oscura/automotive/j1939/spns.py +326 -0
- oscura/automotive/j1939/transport.py +306 -0
- oscura/automotive/lin/__init__.py +47 -0
- oscura/automotive/lin/analyzer.py +612 -0
- oscura/automotive/loaders/blf.py +13 -2
- oscura/automotive/loaders/csv_can.py +143 -72
- oscura/automotive/loaders/dispatcher.py +50 -2
- oscura/automotive/loaders/mdf.py +86 -45
- oscura/automotive/loaders/pcap.py +111 -61
- oscura/automotive/uds/__init__.py +4 -0
- oscura/automotive/uds/analyzer.py +725 -0
- oscura/automotive/uds/decoder.py +140 -58
- oscura/automotive/uds/models.py +7 -1
- oscura/automotive/visualization.py +1 -1
- oscura/cli/analyze.py +348 -0
- oscura/cli/batch.py +142 -122
- oscura/cli/benchmark.py +275 -0
- oscura/cli/characterize.py +137 -82
- oscura/cli/compare.py +224 -131
- oscura/cli/completion.py +250 -0
- oscura/cli/config_cmd.py +361 -0
- oscura/cli/decode.py +164 -87
- oscura/cli/export.py +286 -0
- oscura/cli/main.py +115 -31
- oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
- oscura/{onboarding → cli/onboarding}/help.py +80 -58
- oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
- oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
- oscura/cli/progress.py +147 -0
- oscura/cli/shell.py +157 -135
- oscura/cli/validate_cmd.py +204 -0
- oscura/cli/visualize.py +158 -0
- oscura/convenience.py +125 -79
- oscura/core/__init__.py +4 -2
- oscura/core/backend_selector.py +3 -3
- oscura/core/cache.py +126 -15
- oscura/core/cancellation.py +1 -1
- oscura/{config → core/config}/__init__.py +20 -11
- oscura/{config → core/config}/defaults.py +1 -1
- oscura/{config → core/config}/loader.py +7 -5
- oscura/{config → core/config}/memory.py +5 -5
- oscura/{config → core/config}/migration.py +1 -1
- oscura/{config → core/config}/pipeline.py +99 -23
- oscura/{config → core/config}/preferences.py +1 -1
- oscura/{config → core/config}/protocol.py +3 -3
- oscura/{config → core/config}/schema.py +426 -272
- oscura/{config → core/config}/settings.py +1 -1
- oscura/{config → core/config}/thresholds.py +195 -153
- oscura/core/correlation.py +5 -6
- oscura/core/cross_domain.py +0 -2
- oscura/core/debug.py +9 -5
- oscura/{extensibility → core/extensibility}/docs.py +158 -70
- oscura/{extensibility → core/extensibility}/extensions.py +160 -76
- oscura/{extensibility → core/extensibility}/logging.py +1 -1
- oscura/{extensibility → core/extensibility}/measurements.py +1 -1
- oscura/{extensibility → core/extensibility}/plugins.py +1 -1
- oscura/{extensibility → core/extensibility}/templates.py +73 -3
- oscura/{extensibility → core/extensibility}/validation.py +1 -1
- oscura/core/gpu_backend.py +11 -7
- oscura/core/log_query.py +101 -11
- oscura/core/logging.py +126 -54
- oscura/core/logging_advanced.py +5 -5
- oscura/core/memory_limits.py +108 -70
- oscura/core/memory_monitor.py +2 -2
- oscura/core/memory_progress.py +7 -7
- oscura/core/memory_warnings.py +1 -1
- oscura/core/numba_backend.py +13 -13
- oscura/{plugins → core/plugins}/__init__.py +9 -9
- oscura/{plugins → core/plugins}/base.py +7 -7
- oscura/{plugins → core/plugins}/cli.py +3 -3
- oscura/{plugins → core/plugins}/discovery.py +186 -106
- oscura/{plugins → core/plugins}/lifecycle.py +1 -1
- oscura/{plugins → core/plugins}/manager.py +7 -7
- oscura/{plugins → core/plugins}/registry.py +3 -3
- oscura/{plugins → core/plugins}/versioning.py +1 -1
- oscura/core/progress.py +16 -1
- oscura/core/provenance.py +8 -2
- oscura/{schemas → core/schemas}/__init__.py +2 -2
- oscura/core/schemas/bus_configuration.json +322 -0
- oscura/core/schemas/device_mapping.json +182 -0
- oscura/core/schemas/packet_format.json +418 -0
- oscura/core/schemas/protocol_definition.json +363 -0
- oscura/core/types.py +4 -0
- oscura/core/uncertainty.py +3 -3
- oscura/correlation/__init__.py +52 -0
- oscura/correlation/multi_protocol.py +811 -0
- oscura/discovery/auto_decoder.py +117 -35
- oscura/discovery/comparison.py +191 -86
- oscura/discovery/quality_validator.py +155 -68
- oscura/discovery/signal_detector.py +196 -79
- oscura/export/__init__.py +18 -20
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- oscura/export/wireshark/README.md +15 -15
- oscura/export/wireshark/generator.py +1 -1
- oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
- oscura/export/wireshark_dissector.py +746 -0
- oscura/guidance/wizard.py +207 -111
- oscura/hardware/__init__.py +19 -0
- oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
- oscura/{acquisition → hardware/acquisition}/file.py +2 -2
- oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
- oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
- oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
- oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
- oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
- oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
- oscura/hardware/firmware/__init__.py +29 -0
- oscura/hardware/firmware/pattern_recognition.py +874 -0
- oscura/hardware/hal_detector.py +736 -0
- oscura/hardware/security/__init__.py +37 -0
- oscura/hardware/security/side_channel_detector.py +1126 -0
- oscura/inference/__init__.py +4 -0
- oscura/inference/active_learning/README.md +7 -7
- oscura/inference/active_learning/observation_table.py +4 -1
- oscura/inference/alignment.py +216 -123
- oscura/inference/bayesian.py +113 -33
- oscura/inference/crc_reverse.py +101 -55
- oscura/inference/logic.py +6 -2
- oscura/inference/message_format.py +342 -183
- oscura/inference/protocol.py +95 -44
- oscura/inference/protocol_dsl.py +180 -82
- oscura/inference/signal_intelligence.py +1439 -706
- oscura/inference/spectral.py +99 -57
- oscura/inference/state_machine.py +810 -158
- oscura/inference/stream.py +270 -110
- oscura/iot/__init__.py +34 -0
- oscura/iot/coap/__init__.py +32 -0
- oscura/iot/coap/analyzer.py +668 -0
- oscura/iot/coap/options.py +212 -0
- oscura/iot/lorawan/__init__.py +21 -0
- oscura/iot/lorawan/crypto.py +206 -0
- oscura/iot/lorawan/decoder.py +801 -0
- oscura/iot/lorawan/mac_commands.py +341 -0
- oscura/iot/mqtt/__init__.py +27 -0
- oscura/iot/mqtt/analyzer.py +999 -0
- oscura/iot/mqtt/properties.py +315 -0
- oscura/iot/zigbee/__init__.py +31 -0
- oscura/iot/zigbee/analyzer.py +615 -0
- oscura/iot/zigbee/security.py +153 -0
- oscura/iot/zigbee/zcl.py +349 -0
- oscura/jupyter/display.py +125 -45
- oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
- oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
- oscura/jupyter/exploratory/fuzzy.py +746 -0
- oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
- oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
- oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
- oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
- oscura/jupyter/exploratory/sync.py +612 -0
- oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
- oscura/jupyter/magic.py +4 -4
- oscura/{ui → jupyter/ui}/__init__.py +2 -2
- oscura/{ui → jupyter/ui}/formatters.py +3 -3
- oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
- oscura/loaders/__init__.py +171 -63
- oscura/loaders/binary.py +88 -1
- oscura/loaders/chipwhisperer.py +153 -137
- oscura/loaders/configurable.py +208 -86
- oscura/loaders/csv_loader.py +458 -215
- oscura/loaders/hdf5_loader.py +278 -119
- oscura/loaders/lazy.py +87 -54
- oscura/loaders/mmap_loader.py +1 -1
- oscura/loaders/numpy_loader.py +253 -116
- oscura/loaders/pcap.py +226 -151
- oscura/loaders/rigol.py +110 -49
- oscura/loaders/sigrok.py +201 -78
- oscura/loaders/tdms.py +81 -58
- oscura/loaders/tektronix.py +291 -174
- oscura/loaders/touchstone.py +182 -87
- oscura/loaders/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -7
- oscura/reporting/analyze.py +352 -146
- oscura/reporting/argument_preparer.py +69 -14
- oscura/reporting/auto_report.py +97 -61
- oscura/reporting/batch.py +131 -58
- oscura/reporting/chart_selection.py +57 -45
- oscura/reporting/comparison.py +63 -17
- oscura/reporting/content/executive.py +76 -24
- oscura/reporting/core_formats/multi_format.py +11 -8
- oscura/reporting/engine.py +312 -158
- oscura/reporting/enhanced_reports.py +949 -0
- oscura/reporting/export.py +86 -43
- oscura/reporting/formatting/numbers.py +69 -42
- oscura/reporting/html.py +139 -58
- oscura/reporting/index.py +137 -65
- oscura/reporting/output.py +158 -67
- oscura/reporting/pdf.py +67 -102
- oscura/reporting/plots.py +191 -112
- oscura/reporting/sections.py +88 -47
- oscura/reporting/standards.py +104 -61
- oscura/reporting/summary_generator.py +75 -55
- oscura/reporting/tables.py +138 -54
- oscura/reporting/templates/enhanced/protocol_re.html +525 -0
- oscura/reporting/templates/index.md +13 -13
- oscura/sessions/__init__.py +14 -23
- oscura/sessions/base.py +3 -3
- oscura/sessions/blackbox.py +106 -10
- oscura/sessions/generic.py +2 -2
- oscura/sessions/legacy.py +783 -0
- oscura/side_channel/__init__.py +63 -0
- oscura/side_channel/dpa.py +1025 -0
- oscura/utils/__init__.py +15 -1
- oscura/utils/autodetect.py +1 -5
- oscura/utils/bitwise.py +118 -0
- oscura/{builders → utils/builders}/__init__.py +1 -1
- oscura/{comparison → utils/comparison}/__init__.py +6 -6
- oscura/{comparison → utils/comparison}/compare.py +202 -101
- oscura/{comparison → utils/comparison}/golden.py +83 -63
- oscura/{comparison → utils/comparison}/limits.py +313 -89
- oscura/{comparison → utils/comparison}/mask.py +151 -45
- oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
- oscura/{comparison → utils/comparison}/visualization.py +147 -89
- oscura/{component → utils/component}/__init__.py +3 -3
- oscura/{component → utils/component}/impedance.py +122 -58
- oscura/{component → utils/component}/reactive.py +165 -168
- oscura/{component → utils/component}/transmission_line.py +3 -3
- oscura/{filtering → utils/filtering}/__init__.py +6 -6
- oscura/{filtering → utils/filtering}/base.py +1 -1
- oscura/{filtering → utils/filtering}/convenience.py +2 -2
- oscura/{filtering → utils/filtering}/design.py +169 -93
- oscura/{filtering → utils/filtering}/filters.py +2 -2
- oscura/{filtering → utils/filtering}/introspection.py +2 -2
- oscura/utils/geometry.py +31 -0
- oscura/utils/imports.py +184 -0
- oscura/utils/lazy.py +1 -1
- oscura/{math → utils/math}/__init__.py +2 -2
- oscura/{math → utils/math}/arithmetic.py +114 -48
- oscura/{math → utils/math}/interpolation.py +139 -106
- oscura/utils/memory.py +129 -66
- oscura/utils/memory_advanced.py +92 -9
- oscura/utils/memory_extensions.py +10 -8
- oscura/{optimization → utils/optimization}/__init__.py +1 -1
- oscura/{optimization → utils/optimization}/search.py +2 -2
- oscura/utils/performance/__init__.py +58 -0
- oscura/utils/performance/caching.py +889 -0
- oscura/utils/performance/lsh_clustering.py +333 -0
- oscura/utils/performance/memory_optimizer.py +699 -0
- oscura/utils/performance/optimizations.py +675 -0
- oscura/utils/performance/parallel.py +654 -0
- oscura/utils/performance/profiling.py +661 -0
- oscura/{pipeline → utils/pipeline}/base.py +1 -1
- oscura/{pipeline → utils/pipeline}/composition.py +11 -3
- oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
- oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
- oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
- oscura/{search → utils/search}/__init__.py +3 -3
- oscura/{search → utils/search}/anomaly.py +188 -58
- oscura/utils/search/context.py +294 -0
- oscura/{search → utils/search}/pattern.py +138 -10
- oscura/utils/serial.py +51 -0
- oscura/utils/storage/__init__.py +61 -0
- oscura/utils/storage/database.py +1166 -0
- oscura/{streaming → utils/streaming}/chunked.py +302 -143
- oscura/{streaming → utils/streaming}/progressive.py +1 -1
- oscura/{streaming → utils/streaming}/realtime.py +3 -2
- oscura/{triggering → utils/triggering}/__init__.py +6 -6
- oscura/{triggering → utils/triggering}/base.py +6 -6
- oscura/{triggering → utils/triggering}/edge.py +2 -2
- oscura/{triggering → utils/triggering}/pattern.py +2 -2
- oscura/{triggering → utils/triggering}/pulse.py +115 -74
- oscura/{triggering → utils/triggering}/window.py +2 -2
- oscura/utils/validation.py +32 -0
- oscura/validation/__init__.py +121 -0
- oscura/{compliance → validation/compliance}/__init__.py +5 -5
- oscura/{compliance → validation/compliance}/advanced.py +5 -5
- oscura/{compliance → validation/compliance}/masks.py +1 -1
- oscura/{compliance → validation/compliance}/reporting.py +127 -53
- oscura/{compliance → validation/compliance}/testing.py +114 -52
- oscura/validation/compliance_tests.py +915 -0
- oscura/validation/fuzzer.py +990 -0
- oscura/validation/grammar_tests.py +596 -0
- oscura/validation/grammar_validator.py +904 -0
- oscura/validation/hil_testing.py +977 -0
- oscura/{quality → validation/quality}/__init__.py +4 -4
- oscura/{quality → validation/quality}/ensemble.py +251 -171
- oscura/{quality → validation/quality}/explainer.py +3 -3
- oscura/{quality → validation/quality}/scoring.py +1 -1
- oscura/{quality → validation/quality}/warnings.py +4 -4
- oscura/validation/regression_suite.py +808 -0
- oscura/validation/replay.py +788 -0
- oscura/{testing → validation/testing}/__init__.py +2 -2
- oscura/{testing → validation/testing}/synthetic.py +5 -5
- oscura/visualization/__init__.py +9 -0
- oscura/visualization/accessibility.py +1 -1
- oscura/visualization/annotations.py +64 -67
- oscura/visualization/colors.py +7 -7
- oscura/visualization/digital.py +180 -81
- oscura/visualization/eye.py +236 -85
- oscura/visualization/interactive.py +320 -143
- oscura/visualization/jitter.py +587 -247
- oscura/visualization/layout.py +169 -134
- oscura/visualization/optimization.py +103 -52
- oscura/visualization/palettes.py +1 -1
- oscura/visualization/power.py +427 -211
- oscura/visualization/power_extended.py +626 -297
- oscura/visualization/presets.py +2 -0
- oscura/visualization/protocols.py +495 -181
- oscura/visualization/render.py +79 -63
- oscura/visualization/reverse_engineering.py +171 -124
- oscura/visualization/signal_integrity.py +460 -279
- oscura/visualization/specialized.py +190 -100
- oscura/visualization/spectral.py +670 -255
- oscura/visualization/thumbnails.py +166 -137
- oscura/visualization/waveform.py +150 -63
- oscura/workflows/__init__.py +3 -0
- oscura/{batch → workflows/batch}/__init__.py +5 -5
- oscura/{batch → workflows/batch}/advanced.py +150 -75
- oscura/workflows/batch/aggregate.py +531 -0
- oscura/workflows/batch/analyze.py +236 -0
- oscura/{batch → workflows/batch}/logging.py +2 -2
- oscura/{batch → workflows/batch}/metrics.py +1 -1
- oscura/workflows/complete_re.py +1144 -0
- oscura/workflows/compliance.py +44 -54
- oscura/workflows/digital.py +197 -51
- oscura/workflows/legacy/__init__.py +12 -0
- oscura/{workflow → workflows/legacy}/dag.py +4 -1
- oscura/workflows/multi_trace.py +9 -9
- oscura/workflows/power.py +42 -62
- oscura/workflows/protocol.py +82 -49
- oscura/workflows/reverse_engineering.py +351 -150
- oscura/workflows/signal_integrity.py +157 -82
- oscura-0.6.0.dist-info/METADATA +643 -0
- oscura-0.6.0.dist-info/RECORD +590 -0
- oscura/analyzers/digital/ic_database.py +0 -498
- oscura/analyzers/digital/timing_paths.py +0 -339
- oscura/analyzers/digital/vintage.py +0 -377
- oscura/analyzers/digital/vintage_result.py +0 -148
- oscura/analyzers/protocols/parallel_bus.py +0 -449
- oscura/batch/aggregate.py +0 -300
- oscura/batch/analyze.py +0 -139
- oscura/dsl/__init__.py +0 -73
- oscura/exceptions.py +0 -59
- oscura/exploratory/fuzzy.py +0 -513
- oscura/exploratory/sync.py +0 -384
- oscura/export/wavedrom.py +0 -430
- oscura/exporters/__init__.py +0 -94
- oscura/exporters/csv.py +0 -303
- oscura/exporters/exporters.py +0 -44
- oscura/exporters/hdf5.py +0 -217
- oscura/exporters/html_export.py +0 -701
- oscura/exporters/json_export.py +0 -338
- oscura/exporters/markdown_export.py +0 -367
- oscura/exporters/matlab_export.py +0 -354
- oscura/exporters/npz_export.py +0 -219
- oscura/exporters/spice_export.py +0 -210
- oscura/exporters/vintage_logic_csv.py +0 -247
- oscura/reporting/vintage_logic_report.py +0 -523
- oscura/search/context.py +0 -149
- oscura/session/__init__.py +0 -34
- oscura/session/annotations.py +0 -289
- oscura/session/history.py +0 -313
- oscura/session/session.py +0 -520
- oscura/visualization/digital_advanced.py +0 -718
- oscura/visualization/figure_manager.py +0 -156
- oscura/workflow/__init__.py +0 -13
- oscura-0.5.0.dist-info/METADATA +0 -407
- oscura-0.5.0.dist-info/RECORD +0 -486
- /oscura/core/{config.py → config/legacy.py} +0 -0
- /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
- /oscura/{extensibility → core/extensibility}/registry.py +0 -0
- /oscura/{plugins → core/plugins}/isolation.py +0 -0
- /oscura/{builders → utils/builders}/signal_builder.py +0 -0
- /oscura/{optimization → utils/optimization}/parallel.py +0 -0
- /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
- /oscura/{streaming → utils/streaming}/__init__.py +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
oscura/loaders/tektronix.py
CHANGED
|
@@ -39,7 +39,7 @@ logger = logging.getLogger(__name__)
|
|
|
39
39
|
|
|
40
40
|
# Try to import tm_data_types for full Tektronix support
|
|
41
41
|
try:
|
|
42
|
-
import tm_data_types # type: ignore[import-untyped
|
|
42
|
+
import tm_data_types # type: ignore[import-untyped] # Optional third-party library
|
|
43
43
|
|
|
44
44
|
TM_DATA_TYPES_AVAILABLE = True
|
|
45
45
|
except ImportError:
|
|
@@ -150,105 +150,8 @@ def _load_with_tm_data_types(
|
|
|
150
150
|
if hasattr(wfm, "digital_waveforms"):
|
|
151
151
|
logger.debug("Digital waveforms found: %d", len(wfm.digital_waveforms))
|
|
152
152
|
|
|
153
|
-
#
|
|
154
|
-
|
|
155
|
-
if hasattr(wfm, "analog_waveforms") and len(wfm.analog_waveforms) > channel:
|
|
156
|
-
logger.debug("Loading from analog_waveforms[%d]", channel)
|
|
157
|
-
waveform = wfm.analog_waveforms[channel]
|
|
158
|
-
data = np.array(waveform.y_data, dtype=np.float64)
|
|
159
|
-
sample_rate = 1.0 / waveform.x_increment if waveform.x_increment > 0 else 1e6
|
|
160
|
-
vertical_scale = getattr(waveform, "y_scale", None)
|
|
161
|
-
vertical_offset = getattr(waveform, "y_offset", None)
|
|
162
|
-
channel_name = getattr(waveform, "name", f"CH{channel + 1}")
|
|
163
|
-
|
|
164
|
-
return _build_waveform_trace(
|
|
165
|
-
data=data,
|
|
166
|
-
sample_rate=sample_rate,
|
|
167
|
-
vertical_scale=vertical_scale,
|
|
168
|
-
vertical_offset=vertical_offset,
|
|
169
|
-
channel_name=channel_name,
|
|
170
|
-
path=path,
|
|
171
|
-
wfm=wfm,
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
# Path 2: Direct AnalogWaveform format (tm_data_types 0.3.0+)
|
|
175
|
-
elif hasattr(wfm, "y_axis_values") and wfm_type == "AnalogWaveform":
|
|
176
|
-
logger.debug("Loading direct AnalogWaveform with y_axis_values")
|
|
177
|
-
# Extract raw integer values
|
|
178
|
-
y_raw = np.array(wfm.y_axis_values, dtype=np.float64)
|
|
179
|
-
# Reconstruct voltage values using offset and spacing
|
|
180
|
-
y_spacing = float(wfm.y_axis_spacing) if wfm.y_axis_spacing else 1.0
|
|
181
|
-
y_offset = float(wfm.y_axis_offset) if wfm.y_axis_offset else 0.0
|
|
182
|
-
data = y_raw * y_spacing + y_offset
|
|
183
|
-
|
|
184
|
-
x_spacing = float(wfm.x_axis_spacing) if wfm.x_axis_spacing else 1e-6
|
|
185
|
-
sample_rate = 1.0 / x_spacing if x_spacing > 0 else 1e6
|
|
186
|
-
vertical_offset = y_offset
|
|
187
|
-
channel_name = (
|
|
188
|
-
wfm.source_name
|
|
189
|
-
if hasattr(wfm, "source_name") and wfm.source_name
|
|
190
|
-
else f"CH{channel + 1}"
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
return _build_waveform_trace(
|
|
194
|
-
data=data,
|
|
195
|
-
sample_rate=sample_rate,
|
|
196
|
-
vertical_scale=None,
|
|
197
|
-
vertical_offset=vertical_offset,
|
|
198
|
-
channel_name=channel_name,
|
|
199
|
-
path=path,
|
|
200
|
-
wfm=wfm,
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
# Path 3: DigitalWaveform format
|
|
204
|
-
elif wfm_type == "DigitalWaveform" or hasattr(wfm, "y_axis_byte_values"):
|
|
205
|
-
logger.debug("Loading DigitalWaveform with y_axis_byte_values")
|
|
206
|
-
return _load_digital_waveform(wfm, path, channel)
|
|
207
|
-
|
|
208
|
-
# Path 4: Legacy single channel format with y_data
|
|
209
|
-
elif hasattr(wfm, "y_data"):
|
|
210
|
-
logger.debug("Loading legacy format with y_data")
|
|
211
|
-
data = np.array(wfm.y_data, dtype=np.float64)
|
|
212
|
-
x_increment = getattr(wfm, "x_increment", 1e-6)
|
|
213
|
-
sample_rate = 1.0 / x_increment if x_increment > 0 else 1e6
|
|
214
|
-
vertical_scale = getattr(wfm, "y_scale", None)
|
|
215
|
-
vertical_offset = getattr(wfm, "y_offset", None)
|
|
216
|
-
channel_name = getattr(wfm, "name", "CH1")
|
|
217
|
-
|
|
218
|
-
return _build_waveform_trace(
|
|
219
|
-
data=data,
|
|
220
|
-
sample_rate=sample_rate,
|
|
221
|
-
vertical_scale=vertical_scale,
|
|
222
|
-
vertical_offset=vertical_offset,
|
|
223
|
-
channel_name=channel_name,
|
|
224
|
-
path=path,
|
|
225
|
-
wfm=wfm,
|
|
226
|
-
)
|
|
227
|
-
|
|
228
|
-
# Path 5: Check for wrapped digital waveforms
|
|
229
|
-
elif hasattr(wfm, "digital_waveforms") and len(wfm.digital_waveforms) > channel:
|
|
230
|
-
logger.debug("Loading from digital_waveforms[%d]", channel)
|
|
231
|
-
digital_wfm = wfm.digital_waveforms[channel]
|
|
232
|
-
return _load_digital_waveform(digital_wfm, path, channel)
|
|
233
|
-
|
|
234
|
-
# Path 6: IQWaveform format (I/Q data)
|
|
235
|
-
elif wfm_type == "IQWaveform" or (
|
|
236
|
-
hasattr(wfm, "i_axis_values") and hasattr(wfm, "q_axis_values")
|
|
237
|
-
):
|
|
238
|
-
logger.debug("Loading IQWaveform with i_axis_values and q_axis_values")
|
|
239
|
-
return _load_iq_waveform(wfm, path)
|
|
240
|
-
|
|
241
|
-
# No recognized format - provide detailed error
|
|
242
|
-
raise FormatError(
|
|
243
|
-
f"No waveform data found. Object type: {wfm_type}. "
|
|
244
|
-
f"Available attributes: {', '.join(available_attrs[:15])}",
|
|
245
|
-
file_path=str(path),
|
|
246
|
-
expected="Tektronix analog or digital waveform data",
|
|
247
|
-
fix_hint=(
|
|
248
|
-
"This file may use an unsupported Tektronix format variant. "
|
|
249
|
-
"Check that tm_data_types is up to date: pip install -U tm_data_types"
|
|
250
|
-
),
|
|
251
|
-
)
|
|
153
|
+
# Dispatch to appropriate loader based on waveform format
|
|
154
|
+
return _dispatch_waveform_loader(wfm, wfm_type, available_attrs, path, channel)
|
|
252
155
|
|
|
253
156
|
except Exception as e:
|
|
254
157
|
if isinstance(e, LoaderError | FormatError):
|
|
@@ -261,6 +164,176 @@ def _load_with_tm_data_types(
|
|
|
261
164
|
) from e
|
|
262
165
|
|
|
263
166
|
|
|
167
|
+
def _dispatch_waveform_loader(
|
|
168
|
+
wfm: Any,
|
|
169
|
+
wfm_type: str,
|
|
170
|
+
available_attrs: list[str],
|
|
171
|
+
path: Path,
|
|
172
|
+
channel: int,
|
|
173
|
+
) -> TektronixTrace:
|
|
174
|
+
"""Dispatch to appropriate waveform loader based on format.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
wfm: Waveform object from tm_data_types.
|
|
178
|
+
wfm_type: Type name of waveform object.
|
|
179
|
+
available_attrs: List of available attributes on waveform object.
|
|
180
|
+
path: Path to WFM file.
|
|
181
|
+
channel: Channel index.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Loaded trace (WaveformTrace, DigitalTrace, or IQTrace).
|
|
185
|
+
|
|
186
|
+
Raises:
|
|
187
|
+
FormatError: If no recognized waveform format found.
|
|
188
|
+
"""
|
|
189
|
+
# Path 1: Multi-channel container format (wrapped analog)
|
|
190
|
+
if hasattr(wfm, "analog_waveforms") and len(wfm.analog_waveforms) > channel:
|
|
191
|
+
logger.debug("Loading from analog_waveforms[%d]", channel)
|
|
192
|
+
return _load_analog_waveforms_container(wfm.analog_waveforms[channel], path, channel)
|
|
193
|
+
|
|
194
|
+
# Path 2: Direct AnalogWaveform format (tm_data_types 0.3.0+)
|
|
195
|
+
if hasattr(wfm, "y_axis_values") and wfm_type == "AnalogWaveform":
|
|
196
|
+
logger.debug("Loading direct AnalogWaveform with y_axis_values")
|
|
197
|
+
return _load_analog_waveform_direct(wfm, path, channel)
|
|
198
|
+
|
|
199
|
+
# Path 3: DigitalWaveform format
|
|
200
|
+
if wfm_type == "DigitalWaveform" or hasattr(wfm, "y_axis_byte_values"):
|
|
201
|
+
logger.debug("Loading DigitalWaveform with y_axis_byte_values")
|
|
202
|
+
return _load_digital_waveform(wfm, path, channel)
|
|
203
|
+
|
|
204
|
+
# Path 4: Legacy single channel format with y_data
|
|
205
|
+
if hasattr(wfm, "y_data"):
|
|
206
|
+
logger.debug("Loading legacy format with y_data")
|
|
207
|
+
return _load_legacy_y_data(wfm, path)
|
|
208
|
+
|
|
209
|
+
# Path 5: Check for wrapped digital waveforms
|
|
210
|
+
if hasattr(wfm, "digital_waveforms") and len(wfm.digital_waveforms) > channel:
|
|
211
|
+
logger.debug("Loading from digital_waveforms[%d]", channel)
|
|
212
|
+
return _load_digital_waveform(wfm.digital_waveforms[channel], path, channel)
|
|
213
|
+
|
|
214
|
+
# Path 6: IQWaveform format (I/Q data)
|
|
215
|
+
if wfm_type == "IQWaveform" or (
|
|
216
|
+
hasattr(wfm, "i_axis_values") and hasattr(wfm, "q_axis_values")
|
|
217
|
+
):
|
|
218
|
+
logger.debug("Loading IQWaveform with i_axis_values and q_axis_values")
|
|
219
|
+
return _load_iq_waveform(wfm, path)
|
|
220
|
+
|
|
221
|
+
# No recognized format - provide detailed error
|
|
222
|
+
raise FormatError(
|
|
223
|
+
f"No waveform data found. Object type: {wfm_type}. "
|
|
224
|
+
f"Available attributes: {', '.join(available_attrs[:15])}",
|
|
225
|
+
file_path=str(path),
|
|
226
|
+
expected="Tektronix analog or digital waveform data",
|
|
227
|
+
fix_hint=(
|
|
228
|
+
"This file may use an unsupported Tektronix format variant. "
|
|
229
|
+
"Check that tm_data_types is up to date: pip install -U tm_data_types"
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _load_analog_waveforms_container(
|
|
235
|
+
waveform: Any,
|
|
236
|
+
path: Path,
|
|
237
|
+
channel: int,
|
|
238
|
+
) -> WaveformTrace:
|
|
239
|
+
"""Load analog waveform from multi-channel container format.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
waveform: Analog waveform object from container.
|
|
243
|
+
path: Path to WFM file.
|
|
244
|
+
channel: Channel index.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
WaveformTrace with extracted data.
|
|
248
|
+
"""
|
|
249
|
+
data = np.array(waveform.y_data, dtype=np.float64)
|
|
250
|
+
sample_rate = 1.0 / waveform.x_increment if waveform.x_increment > 0 else 1e6
|
|
251
|
+
vertical_scale = getattr(waveform, "y_scale", None)
|
|
252
|
+
vertical_offset = getattr(waveform, "y_offset", None)
|
|
253
|
+
channel_name = getattr(waveform, "name", f"CH{channel + 1}")
|
|
254
|
+
|
|
255
|
+
# Use original wfm for trigger info (need to get it from parent)
|
|
256
|
+
return _build_waveform_trace(
|
|
257
|
+
data=data,
|
|
258
|
+
sample_rate=sample_rate,
|
|
259
|
+
vertical_scale=vertical_scale,
|
|
260
|
+
vertical_offset=vertical_offset,
|
|
261
|
+
channel_name=channel_name,
|
|
262
|
+
path=path,
|
|
263
|
+
wfm=waveform,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _load_analog_waveform_direct(
|
|
268
|
+
wfm: Any,
|
|
269
|
+
path: Path,
|
|
270
|
+
channel: int,
|
|
271
|
+
) -> WaveformTrace:
|
|
272
|
+
"""Load direct AnalogWaveform format (tm_data_types 0.3.0+).
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
wfm: AnalogWaveform object.
|
|
276
|
+
path: Path to WFM file.
|
|
277
|
+
channel: Channel index.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
WaveformTrace with extracted data.
|
|
281
|
+
"""
|
|
282
|
+
# Extract raw integer values and reconstruct voltage values
|
|
283
|
+
y_raw = np.array(wfm.y_axis_values, dtype=np.float64)
|
|
284
|
+
y_spacing = float(wfm.y_axis_spacing) if wfm.y_axis_spacing else 1.0
|
|
285
|
+
y_offset = float(wfm.y_axis_offset) if wfm.y_axis_offset else 0.0
|
|
286
|
+
data = y_raw * y_spacing + y_offset
|
|
287
|
+
|
|
288
|
+
x_spacing = float(wfm.x_axis_spacing) if wfm.x_axis_spacing else 1e-6
|
|
289
|
+
sample_rate = 1.0 / x_spacing if x_spacing > 0 else 1e6
|
|
290
|
+
vertical_offset = y_offset
|
|
291
|
+
channel_name = (
|
|
292
|
+
wfm.source_name if hasattr(wfm, "source_name") and wfm.source_name else f"CH{channel + 1}"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
return _build_waveform_trace(
|
|
296
|
+
data=data,
|
|
297
|
+
sample_rate=sample_rate,
|
|
298
|
+
vertical_scale=None,
|
|
299
|
+
vertical_offset=vertical_offset,
|
|
300
|
+
channel_name=channel_name,
|
|
301
|
+
path=path,
|
|
302
|
+
wfm=wfm,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _load_legacy_y_data(
|
|
307
|
+
wfm: Any,
|
|
308
|
+
path: Path,
|
|
309
|
+
) -> WaveformTrace:
|
|
310
|
+
"""Load legacy single channel format with y_data.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
wfm: Legacy waveform object with y_data.
|
|
314
|
+
path: Path to WFM file.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
WaveformTrace with extracted data.
|
|
318
|
+
"""
|
|
319
|
+
data = np.array(wfm.y_data, dtype=np.float64)
|
|
320
|
+
x_increment = getattr(wfm, "x_increment", 1e-6)
|
|
321
|
+
sample_rate = 1.0 / x_increment if x_increment > 0 else 1e6
|
|
322
|
+
vertical_scale = getattr(wfm, "y_scale", None)
|
|
323
|
+
vertical_offset = getattr(wfm, "y_offset", None)
|
|
324
|
+
channel_name = getattr(wfm, "name", "CH1")
|
|
325
|
+
|
|
326
|
+
return _build_waveform_trace(
|
|
327
|
+
data=data,
|
|
328
|
+
sample_rate=sample_rate,
|
|
329
|
+
vertical_scale=vertical_scale,
|
|
330
|
+
vertical_offset=vertical_offset,
|
|
331
|
+
channel_name=channel_name,
|
|
332
|
+
path=path,
|
|
333
|
+
wfm=wfm,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
264
337
|
def _build_waveform_trace(
|
|
265
338
|
data: NDArray[np.float64],
|
|
266
339
|
sample_rate: float,
|
|
@@ -328,64 +401,95 @@ def _load_digital_waveform(
|
|
|
328
401
|
logger.debug("Extracting digital waveform data")
|
|
329
402
|
|
|
330
403
|
# Extract digital sample data
|
|
404
|
+
data = _extract_digital_samples(wfm, path)
|
|
405
|
+
|
|
406
|
+
# Extract timing information
|
|
407
|
+
sample_rate = _extract_sample_rate(wfm)
|
|
408
|
+
|
|
409
|
+
# Extract channel name
|
|
410
|
+
channel_name = _extract_channel_name(wfm, channel)
|
|
411
|
+
|
|
412
|
+
# Build metadata
|
|
413
|
+
metadata = TraceMetadata(
|
|
414
|
+
sample_rate=sample_rate,
|
|
415
|
+
source_file=str(path),
|
|
416
|
+
channel_name=channel_name,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
# Extract edge information if available
|
|
420
|
+
edges = _extract_edges(wfm)
|
|
421
|
+
|
|
422
|
+
return DigitalTrace(data=data, metadata=metadata, edges=edges)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _extract_digital_samples(wfm: Any, path: Path) -> NDArray[np.bool_]:
|
|
426
|
+
"""Extract digital sample data from waveform object."""
|
|
427
|
+
# Try y_axis_byte_values (most common)
|
|
331
428
|
if hasattr(wfm, "y_axis_byte_values"):
|
|
332
|
-
# y_axis_byte_values contains byte-level digital data
|
|
333
429
|
raw_bytes = wfm.y_axis_byte_values
|
|
334
|
-
# Convert bytes to numpy array and interpret as boolean
|
|
335
|
-
# Each byte typically represents a logic state (0 = low, non-zero = high)
|
|
336
430
|
byte_array = np.frombuffer(bytes(raw_bytes), dtype=np.uint8)
|
|
337
431
|
data = byte_array.astype(np.bool_)
|
|
338
432
|
logger.debug("Loaded %d digital samples from y_axis_byte_values", len(data))
|
|
339
|
-
|
|
340
|
-
|
|
433
|
+
return data
|
|
434
|
+
|
|
435
|
+
# Try samples attribute
|
|
436
|
+
if hasattr(wfm, "samples"):
|
|
341
437
|
data = np.array(wfm.samples, dtype=np.bool_)
|
|
342
438
|
logger.debug("Loaded %d digital samples from samples", len(data))
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
439
|
+
return data
|
|
440
|
+
|
|
441
|
+
# Try alternative data attributes
|
|
442
|
+
for attr in ["data", "digital_data", "logic_data"]:
|
|
443
|
+
if hasattr(wfm, attr):
|
|
444
|
+
data = np.array(getattr(wfm, attr), dtype=np.bool_)
|
|
445
|
+
logger.debug("Loaded %d digital samples from %s", len(data), attr)
|
|
446
|
+
return data
|
|
447
|
+
|
|
448
|
+
# No recognized attribute found
|
|
449
|
+
raise FormatError(
|
|
450
|
+
"DigitalWaveform has no recognized data attribute",
|
|
451
|
+
file_path=str(path),
|
|
452
|
+
expected="y_axis_byte_values, samples, or data attribute",
|
|
453
|
+
)
|
|
356
454
|
|
|
357
|
-
|
|
455
|
+
|
|
456
|
+
def _extract_sample_rate(wfm: Any) -> float:
|
|
457
|
+
"""Extract sample rate from waveform timing attributes."""
|
|
358
458
|
x_spacing = 1e-6 # Default 1 microsecond per sample
|
|
459
|
+
|
|
359
460
|
if hasattr(wfm, "x_axis_spacing") and wfm.x_axis_spacing:
|
|
360
461
|
x_spacing = float(wfm.x_axis_spacing)
|
|
361
462
|
elif hasattr(wfm, "horizontal_spacing") and wfm.horizontal_spacing:
|
|
362
463
|
x_spacing = float(wfm.horizontal_spacing)
|
|
363
464
|
|
|
364
|
-
|
|
465
|
+
return 1.0 / x_spacing if x_spacing > 0 else 1e6
|
|
365
466
|
|
|
366
|
-
|
|
367
|
-
|
|
467
|
+
|
|
468
|
+
def _extract_channel_name(wfm: Any, channel: int) -> str:
|
|
469
|
+
"""Extract channel name from waveform object."""
|
|
470
|
+
# Try source_name first
|
|
368
471
|
if hasattr(wfm, "source_name") and wfm.source_name:
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
channel_name = wfm.name
|
|
472
|
+
name: str = str(wfm.source_name)
|
|
473
|
+
return name
|
|
372
474
|
|
|
373
|
-
#
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
channel_name=channel_name,
|
|
378
|
-
)
|
|
475
|
+
# Try name attribute
|
|
476
|
+
if hasattr(wfm, "name") and wfm.name:
|
|
477
|
+
name_str: str = str(wfm.name)
|
|
478
|
+
return name_str
|
|
379
479
|
|
|
380
|
-
#
|
|
381
|
-
|
|
382
|
-
if hasattr(wfm, "edges"):
|
|
383
|
-
try:
|
|
384
|
-
edges = [(float(ts), bool(is_rising)) for ts, is_rising in wfm.edges]
|
|
385
|
-
except (TypeError, ValueError):
|
|
386
|
-
pass
|
|
480
|
+
# Default: digital channels labeled D1, D2, etc.
|
|
481
|
+
return f"D{channel + 1}"
|
|
387
482
|
|
|
388
|
-
|
|
483
|
+
|
|
484
|
+
def _extract_edges(wfm: Any) -> list[tuple[float, bool]] | None:
|
|
485
|
+
"""Extract edge timing information if available."""
|
|
486
|
+
if not hasattr(wfm, "edges"):
|
|
487
|
+
return None
|
|
488
|
+
|
|
489
|
+
try:
|
|
490
|
+
return [(float(ts), bool(is_rising)) for ts, is_rising in wfm.edges]
|
|
491
|
+
except (TypeError, ValueError):
|
|
492
|
+
return None
|
|
389
493
|
|
|
390
494
|
|
|
391
495
|
def _load_iq_waveform(
|
|
@@ -528,9 +632,30 @@ def _parse_wfm003(
|
|
|
528
632
|
Raises:
|
|
529
633
|
FormatError: If the file signature is invalid or no waveform data found.
|
|
530
634
|
"""
|
|
531
|
-
import struct
|
|
532
635
|
|
|
533
|
-
|
|
636
|
+
_validate_wfm003_signature(file_data, path)
|
|
637
|
+
header_size = 838
|
|
638
|
+
waveform_bytes = _extract_waveform_data(file_data, header_size, path)
|
|
639
|
+
data = np.frombuffer(waveform_bytes, dtype=np.int16).astype(np.float64)
|
|
640
|
+
|
|
641
|
+
# Extract metadata from header
|
|
642
|
+
sample_rate = _extract_sample_interval(file_data, header_size)
|
|
643
|
+
vertical_scale, vertical_offset = _extract_vertical_params(file_data, header_size)
|
|
644
|
+
channel_name = f"CH{channel + 1}"
|
|
645
|
+
|
|
646
|
+
metadata = TraceMetadata(
|
|
647
|
+
sample_rate=sample_rate,
|
|
648
|
+
vertical_scale=vertical_scale,
|
|
649
|
+
vertical_offset=vertical_offset,
|
|
650
|
+
source_file=str(path),
|
|
651
|
+
channel_name=channel_name,
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
return WaveformTrace(data=data, metadata=metadata)
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _validate_wfm003_signature(file_data: bytes, path: Path) -> None:
|
|
658
|
+
"""Validate WFM#003 file signature."""
|
|
534
659
|
signature = file_data[2:10]
|
|
535
660
|
if signature != b":WFM#003":
|
|
536
661
|
raise FormatError(
|
|
@@ -540,20 +665,14 @@ def _parse_wfm003(
|
|
|
540
665
|
got=signature.decode("latin-1", errors="replace"),
|
|
541
666
|
)
|
|
542
667
|
|
|
543
|
-
# WFM#003 files have a fixed header size of 838 bytes
|
|
544
|
-
# This is consistent across all WFM#003 files
|
|
545
|
-
header_size = 838
|
|
546
668
|
|
|
547
|
-
|
|
548
|
-
|
|
669
|
+
def _extract_waveform_data(file_data: bytes, header_size: int, path: Path) -> bytes:
|
|
670
|
+
"""Extract waveform data region from file."""
|
|
549
671
|
footer_start = len(file_data)
|
|
550
672
|
if b"tekmeta!" in file_data:
|
|
551
673
|
footer_start = file_data.find(b"tekmeta!")
|
|
552
674
|
|
|
553
|
-
|
|
554
|
-
data_start = header_size
|
|
555
|
-
data_end = footer_start
|
|
556
|
-
waveform_bytes = file_data[data_start:data_end]
|
|
675
|
+
waveform_bytes = file_data[header_size:footer_start]
|
|
557
676
|
|
|
558
677
|
if len(waveform_bytes) < 2:
|
|
559
678
|
raise FormatError(
|
|
@@ -561,23 +680,20 @@ def _parse_wfm003(
|
|
|
561
680
|
file_path=str(path),
|
|
562
681
|
)
|
|
563
682
|
|
|
564
|
-
#
|
|
565
|
-
# Ensure we have an even number of bytes
|
|
683
|
+
# Ensure even number of bytes for int16
|
|
566
684
|
if len(waveform_bytes) % 2 != 0:
|
|
567
685
|
waveform_bytes = waveform_bytes[:-1]
|
|
568
686
|
|
|
569
|
-
|
|
570
|
-
data = np.frombuffer(waveform_bytes, dtype=np.int16).astype(np.float64)
|
|
687
|
+
return waveform_bytes
|
|
571
688
|
|
|
572
|
-
# Try to extract metadata from header
|
|
573
|
-
sample_rate = 1e6 # Default 1 MSa/s
|
|
574
|
-
vertical_scale = None
|
|
575
|
-
vertical_offset = None
|
|
576
|
-
channel_name = f"CH{channel + 1}"
|
|
577
689
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
690
|
+
def _extract_sample_interval(file_data: bytes, header_size: int) -> float:
|
|
691
|
+
"""Extract sample rate from header doubles."""
|
|
692
|
+
import struct
|
|
693
|
+
|
|
694
|
+
# Default 1 MSa/s
|
|
695
|
+
sample_rate = 1e6
|
|
696
|
+
|
|
581
697
|
try:
|
|
582
698
|
# Search for reasonable sample interval values (doubles in header)
|
|
583
699
|
for offset in range(16, min(header_size - 8, 200), 8):
|
|
@@ -589,8 +705,18 @@ def _parse_wfm003(
|
|
|
589
705
|
except (struct.error, ZeroDivisionError):
|
|
590
706
|
pass
|
|
591
707
|
|
|
592
|
-
|
|
593
|
-
|
|
708
|
+
return sample_rate
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def _extract_vertical_params(
|
|
712
|
+
file_data: bytes, header_size: int
|
|
713
|
+
) -> tuple[float | None, float | None]:
|
|
714
|
+
"""Extract vertical scale and offset from header."""
|
|
715
|
+
import struct
|
|
716
|
+
|
|
717
|
+
vertical_scale = None
|
|
718
|
+
vertical_offset = None
|
|
719
|
+
|
|
594
720
|
try:
|
|
595
721
|
# Vertical scale is often in a specific range
|
|
596
722
|
for offset in range(16, min(header_size - 8, 400), 8):
|
|
@@ -606,16 +732,7 @@ def _parse_wfm003(
|
|
|
606
732
|
except struct.error:
|
|
607
733
|
pass
|
|
608
734
|
|
|
609
|
-
|
|
610
|
-
metadata = TraceMetadata(
|
|
611
|
-
sample_rate=sample_rate,
|
|
612
|
-
vertical_scale=vertical_scale,
|
|
613
|
-
vertical_offset=vertical_offset,
|
|
614
|
-
source_file=str(path),
|
|
615
|
-
channel_name=channel_name,
|
|
616
|
-
)
|
|
617
|
-
|
|
618
|
-
return WaveformTrace(data=data, metadata=metadata)
|
|
735
|
+
return vertical_scale, vertical_offset
|
|
619
736
|
|
|
620
737
|
|
|
621
738
|
def _parse_wfm_legacy(
|