oscura 0.5.1__py3-none-any.whl → 0.7.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/edges.py +325 -65
- 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/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 +17 -102
- 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/{schemas → core/schemas}/device_mapping.json +2 -8
- oscura/{schemas → core/schemas}/packet_format.json +4 -24
- oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
- 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 -8
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- 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/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 +183 -67
- 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/tss.py +456 -0
- oscura/loaders/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -0
- 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/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/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 +1 -1
- 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.7.0.dist-info/METADATA +661 -0
- oscura-0.7.0.dist-info/RECORD +591 -0
- 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/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 -291
- 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/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/workflow/__init__.py +0 -13
- oscura-0.5.1.dist-info/METADATA +0 -583
- oscura-0.5.1.dist-info/RECORD +0 -481
- /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/{schemas → core/schemas}/bus_configuration.json +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.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,7 +5,7 @@ results first with options to drill down into details on demand.
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
Example:
|
|
8
|
-
>>> from oscura.ui import ProgressiveDisplay
|
|
8
|
+
>>> from oscura.jupyter.ui import ProgressiveDisplay
|
|
9
9
|
>>> display = ProgressiveDisplay()
|
|
10
10
|
>>> output = display.render(result)
|
|
11
11
|
>>> print(output.summary()) # Level 1: Summary
|
|
@@ -215,7 +215,27 @@ class ProgressiveDisplay:
|
|
|
215
215
|
>>> output = display.render(characterization_result)
|
|
216
216
|
>>> print(output.summary())
|
|
217
217
|
"""
|
|
218
|
-
|
|
218
|
+
level1_content = self._build_level1_summary(result)
|
|
219
|
+
level2_sections = self._build_level2_sections(result)
|
|
220
|
+
level3_data = self._build_level3_expert_data(result)
|
|
221
|
+
current_level = self._determine_current_level()
|
|
222
|
+
|
|
223
|
+
return ProgressiveOutput(
|
|
224
|
+
level1_content=level1_content,
|
|
225
|
+
level2_sections=level2_sections,
|
|
226
|
+
level3_data=level3_data,
|
|
227
|
+
current_level=current_level,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def _build_level1_summary(self, result: Any) -> str:
|
|
231
|
+
"""Build Level 1 summary content.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
result: Analysis result.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Summary string with key items.
|
|
238
|
+
"""
|
|
219
239
|
level1_items = []
|
|
220
240
|
|
|
221
241
|
if hasattr(result, "signal_type"):
|
|
@@ -231,85 +251,136 @@ class ProgressiveDisplay:
|
|
|
231
251
|
if hasattr(result, "status"):
|
|
232
252
|
level1_items.append(f"Status: {result.status}")
|
|
233
253
|
|
|
234
|
-
# Limit to max_summary_items
|
|
235
254
|
level1_items = level1_items[: self.max_summary_items]
|
|
236
255
|
|
|
237
|
-
level1_content = "\n".join(level1_items)
|
|
238
|
-
|
|
239
256
|
if not level1_items:
|
|
240
|
-
|
|
257
|
+
return "Analysis complete. Expand for details."
|
|
258
|
+
|
|
259
|
+
return "\n".join(level1_items)
|
|
260
|
+
|
|
261
|
+
def _build_level2_sections(self, result: Any) -> list[Section]:
|
|
262
|
+
"""Build Level 2 detailed sections.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
result: Analysis result.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
List of Section objects.
|
|
269
|
+
"""
|
|
270
|
+
sections = []
|
|
271
|
+
|
|
272
|
+
params_section = self._build_parameters_section(result)
|
|
273
|
+
if params_section:
|
|
274
|
+
sections.append(params_section)
|
|
241
275
|
|
|
242
|
-
|
|
243
|
-
|
|
276
|
+
quality_section = self._build_quality_metrics_section(result)
|
|
277
|
+
if quality_section:
|
|
278
|
+
sections.append(quality_section)
|
|
244
279
|
|
|
245
|
-
|
|
246
|
-
if
|
|
247
|
-
|
|
248
|
-
|
|
280
|
+
findings_section = self._build_findings_section(result)
|
|
281
|
+
if findings_section:
|
|
282
|
+
sections.append(findings_section)
|
|
283
|
+
|
|
284
|
+
return sections
|
|
285
|
+
|
|
286
|
+
def _build_parameters_section(self, result: Any) -> Section | None:
|
|
287
|
+
"""Build parameters section.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
result: Analysis result.
|
|
249
291
|
|
|
250
|
-
|
|
251
|
-
|
|
292
|
+
Returns:
|
|
293
|
+
Section or None if no parameters.
|
|
294
|
+
"""
|
|
295
|
+
if not (hasattr(result, "parameters") and result.parameters):
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
params = result.parameters
|
|
299
|
+
summary = f"{len(params)} parameters detected"
|
|
300
|
+
|
|
301
|
+
content = "Parameters:\n"
|
|
302
|
+
for key, value in params.items():
|
|
303
|
+
content += f" {key}: {value}\n"
|
|
304
|
+
|
|
305
|
+
return Section(
|
|
306
|
+
title="Parameters",
|
|
307
|
+
summary=summary,
|
|
308
|
+
content=content,
|
|
309
|
+
is_collapsed=self.enable_collapsible_sections,
|
|
310
|
+
detail_level=2,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
def _build_quality_metrics_section(self, result: Any) -> Section | None:
|
|
314
|
+
"""Build quality metrics section.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
result: Analysis result.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Section or None if no quality data.
|
|
321
|
+
"""
|
|
322
|
+
if not (hasattr(result, "quality") or hasattr(result, "metrics")):
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
metrics = getattr(result, "metrics", {})
|
|
326
|
+
summary = "Quality assessment available"
|
|
327
|
+
content = "Quality Metrics:\n"
|
|
328
|
+
|
|
329
|
+
if hasattr(result, "quality"):
|
|
330
|
+
content += f" Overall: {result.quality}\n"
|
|
331
|
+
|
|
332
|
+
if metrics:
|
|
333
|
+
for key, value in metrics.items():
|
|
252
334
|
content += f" {key}: {value}\n"
|
|
253
335
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
level2_sections.append(
|
|
303
|
-
Section(
|
|
304
|
-
title="Findings",
|
|
305
|
-
summary=summary,
|
|
306
|
-
content=content,
|
|
307
|
-
is_collapsed=self.enable_collapsible_sections,
|
|
308
|
-
detail_level=2,
|
|
309
|
-
)
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
# Level 3: Build expert data
|
|
336
|
+
return Section(
|
|
337
|
+
title="Quality Metrics",
|
|
338
|
+
summary=summary,
|
|
339
|
+
content=content,
|
|
340
|
+
is_collapsed=self.enable_collapsible_sections,
|
|
341
|
+
detail_level=2,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
def _build_findings_section(self, result: Any) -> Section | None:
|
|
345
|
+
"""Build findings section.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
result: Analysis result.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Section or None if no findings.
|
|
352
|
+
"""
|
|
353
|
+
if not (hasattr(result, "findings") and result.findings):
|
|
354
|
+
return None
|
|
355
|
+
|
|
356
|
+
findings = result.findings
|
|
357
|
+
summary = f"{len(findings)} findings"
|
|
358
|
+
content = "Findings:\n"
|
|
359
|
+
|
|
360
|
+
for i, finding in enumerate(findings, 1):
|
|
361
|
+
if hasattr(finding, "title") and hasattr(finding, "description"):
|
|
362
|
+
content += f"\n{i}. {finding.title}\n"
|
|
363
|
+
content += f" {finding.description}\n"
|
|
364
|
+
else:
|
|
365
|
+
content += f"\n{i}. {finding}\n"
|
|
366
|
+
|
|
367
|
+
return Section(
|
|
368
|
+
title="Findings",
|
|
369
|
+
summary=summary,
|
|
370
|
+
content=content,
|
|
371
|
+
is_collapsed=self.enable_collapsible_sections,
|
|
372
|
+
detail_level=2,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
def _build_level3_expert_data(self, result: Any) -> dict[str, Any]:
|
|
376
|
+
"""Build Level 3 expert data.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
result: Analysis result.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
Dict of expert-level data.
|
|
383
|
+
"""
|
|
313
384
|
level3_data = {}
|
|
314
385
|
|
|
315
386
|
if hasattr(result, "raw_data"):
|
|
@@ -321,16 +392,16 @@ class ProgressiveDisplay:
|
|
|
321
392
|
if hasattr(result, "debug_trace"):
|
|
322
393
|
level3_data["debug_trace"] = result.debug_trace
|
|
323
394
|
|
|
324
|
-
|
|
325
|
-
level_map = {"summary": 1, "intermediate": 2, "expert": 3}
|
|
326
|
-
current_level = level_map.get(self.default_level, 1)
|
|
395
|
+
return level3_data
|
|
327
396
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
397
|
+
def _determine_current_level(self) -> int:
|
|
398
|
+
"""Determine current detail level from default.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Current level (1-3).
|
|
402
|
+
"""
|
|
403
|
+
level_map = {"summary": 1, "intermediate": 2, "expert": 3}
|
|
404
|
+
return level_map.get(self.default_level, 1)
|
|
334
405
|
|
|
335
406
|
|
|
336
407
|
__all__ = [
|
oscura/loaders/__init__.py
CHANGED
|
@@ -29,6 +29,7 @@ from oscura.core.types import DigitalTrace, IQTrace, WaveformTrace
|
|
|
29
29
|
_LOADER_REGISTRY: dict[str, tuple[str, str]] = {
|
|
30
30
|
"tektronix": ("oscura.loaders.tektronix", "load_tektronix_wfm"),
|
|
31
31
|
"tek": ("oscura.loaders.tektronix", "load_tektronix_wfm"),
|
|
32
|
+
"tss": ("oscura.loaders.tss", "load_tss"),
|
|
32
33
|
"rigol": ("oscura.loaders.rigol", "load_rigol_wfm"),
|
|
33
34
|
"numpy": ("oscura.loaders.numpy_loader", "load_npz"),
|
|
34
35
|
"csv": ("oscura.loaders.csv_loader", "load_csv"),
|
|
@@ -95,6 +96,7 @@ from oscura.loaders import (
|
|
|
95
96
|
csv,
|
|
96
97
|
hdf5,
|
|
97
98
|
)
|
|
99
|
+
from oscura.loaders.binary import load_binary
|
|
98
100
|
|
|
99
101
|
# Import configurable binary loading functionality
|
|
100
102
|
from oscura.loaders.configurable import (
|
|
@@ -122,6 +124,44 @@ from oscura.loaders.preprocessing import (
|
|
|
122
124
|
get_idle_statistics,
|
|
123
125
|
trim_idle,
|
|
124
126
|
)
|
|
127
|
+
|
|
128
|
+
# LAZY IMPORT: load_touchstone is loaded on first use to avoid 14.5s import penalty
|
|
129
|
+
# The touchstone loader imports signal_integrity which pulls in the entire analyzer chain
|
|
130
|
+
# See .claude/PERFORMANCE_PROFILE_2026-01-25.md for performance analysis
|
|
131
|
+
_load_touchstone_impl: Any = None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def load_touchstone(path: str | Path) -> Any:
|
|
135
|
+
"""Load S-parameter data from Touchstone file (lazy import).
|
|
136
|
+
|
|
137
|
+
This function uses lazy imports to avoid a 14.5 second import penalty
|
|
138
|
+
from the touchstone -> signal_integrity -> analyzer dependency chain.
|
|
139
|
+
|
|
140
|
+
Supports .s1p through .s8p formats and both Touchstone 1.0
|
|
141
|
+
and 2.0 file formats.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
path: Path to Touchstone file.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
SParameterData with loaded S-parameters.
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
LoaderError: If file cannot be read.
|
|
151
|
+
FormatError: If file format is invalid.
|
|
152
|
+
|
|
153
|
+
Example:
|
|
154
|
+
>>> s_params = load_touchstone("cable.s2p")
|
|
155
|
+
>>> print(f"Loaded {s_params.n_ports}-port, {len(s_params.frequencies)} points")
|
|
156
|
+
"""
|
|
157
|
+
global _load_touchstone_impl
|
|
158
|
+
if _load_touchstone_impl is None:
|
|
159
|
+
from oscura.loaders.touchstone import load_touchstone as _impl
|
|
160
|
+
|
|
161
|
+
_load_touchstone_impl = _impl
|
|
162
|
+
return _load_touchstone_impl(path)
|
|
163
|
+
|
|
164
|
+
|
|
125
165
|
from oscura.loaders.validation import (
|
|
126
166
|
PacketValidator,
|
|
127
167
|
SequenceGap,
|
|
@@ -141,6 +181,7 @@ logger = logging.getLogger(__name__)
|
|
|
141
181
|
# Supported format extensions mapped to loader names
|
|
142
182
|
SUPPORTED_FORMATS: dict[str, str] = {
|
|
143
183
|
".wfm": "auto_wfm", # Auto-detect Tektronix vs Rigol
|
|
184
|
+
".tss": "tss", # Tektronix session files
|
|
144
185
|
".npz": "numpy",
|
|
145
186
|
".csv": "csv",
|
|
146
187
|
".h5": "hdf5",
|
|
@@ -302,7 +343,7 @@ def load_all_channels(
|
|
|
302
343
|
path: str | PathLike[str],
|
|
303
344
|
*,
|
|
304
345
|
format: str | None = None,
|
|
305
|
-
) -> dict[str, WaveformTrace | DigitalTrace]:
|
|
346
|
+
) -> dict[str, WaveformTrace | DigitalTrace | IQTrace]:
|
|
306
347
|
"""Load all channels from a multi-channel waveform file.
|
|
307
348
|
|
|
308
349
|
Reads the file once and extracts all available channels (both analog
|
|
@@ -353,23 +394,23 @@ def load_all_channels(
|
|
|
353
394
|
)
|
|
354
395
|
loader_name = SUPPORTED_FORMATS[ext]
|
|
355
396
|
|
|
356
|
-
# Currently only supports Tektronix WFM for multi-channel loading
|
|
357
|
-
if loader_name in ("auto_wfm", "tektronix", "tek"):
|
|
397
|
+
# Currently only supports Tektronix WFM and TSS for multi-channel loading
|
|
398
|
+
if loader_name in ("auto_wfm", "tektronix", "tek", "tss"):
|
|
358
399
|
return _load_all_channels_tektronix(path)
|
|
359
400
|
else:
|
|
360
401
|
# For other formats, try loading as single channel
|
|
361
402
|
trace = load(path, format=format)
|
|
362
403
|
channel_name = getattr(trace.metadata, "channel_name", None) or "ch1"
|
|
363
|
-
return {channel_name: trace}
|
|
404
|
+
return {channel_name: trace}
|
|
364
405
|
|
|
365
406
|
|
|
366
407
|
def _load_all_channels_tektronix(
|
|
367
408
|
path: Path,
|
|
368
|
-
) -> dict[str, WaveformTrace | DigitalTrace]:
|
|
369
|
-
"""Load all channels from a Tektronix WFM file.
|
|
409
|
+
) -> dict[str, WaveformTrace | DigitalTrace | IQTrace]:
|
|
410
|
+
"""Load all channels from a Tektronix WFM or TSS file.
|
|
370
411
|
|
|
371
412
|
Args:
|
|
372
|
-
path: Path to the Tektronix .wfm file.
|
|
413
|
+
path: Path to the Tektronix .wfm or .tss file.
|
|
373
414
|
|
|
374
415
|
Returns:
|
|
375
416
|
Dictionary mapping channel names to traces.
|
|
@@ -377,16 +418,58 @@ def _load_all_channels_tektronix(
|
|
|
377
418
|
Raises:
|
|
378
419
|
LoaderError: If the file cannot be read or parsed.
|
|
379
420
|
"""
|
|
421
|
+
# Check if this is a .tss session file
|
|
422
|
+
if path.suffix.lower() == ".tss":
|
|
423
|
+
from oscura.loaders.tss import load_all_channels_tss
|
|
424
|
+
|
|
425
|
+
return load_all_channels_tss(path)
|
|
426
|
+
|
|
427
|
+
wfm = _read_tektronix_file(path)
|
|
428
|
+
channels: dict[str, WaveformTrace | DigitalTrace | IQTrace] = {}
|
|
429
|
+
|
|
430
|
+
# Extract analog waveforms
|
|
431
|
+
_extract_analog_waveforms(wfm, path, channels)
|
|
432
|
+
|
|
433
|
+
# Extract digital waveforms
|
|
434
|
+
_extract_digital_waveforms(wfm, path, channels)
|
|
435
|
+
|
|
436
|
+
# Handle direct waveform formats (single file = single channel)
|
|
437
|
+
if not channels:
|
|
438
|
+
_extract_direct_waveform(wfm, path, channels)
|
|
439
|
+
|
|
440
|
+
if not channels:
|
|
441
|
+
raise LoaderError(
|
|
442
|
+
"No channels found in file",
|
|
443
|
+
file_path=str(path),
|
|
444
|
+
fix_hint="File may be empty or use an unsupported format variant.",
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
return channels
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _read_tektronix_file(path: Path) -> Any:
|
|
451
|
+
"""Read Tektronix WFM file, falling back to single channel if tm_data_types unavailable.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
path: Path to file.
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
WFM file object.
|
|
458
|
+
|
|
459
|
+
Raises:
|
|
460
|
+
LoaderError: If file cannot be read.
|
|
461
|
+
"""
|
|
380
462
|
try:
|
|
381
|
-
import tm_data_types # type: ignore[import-
|
|
463
|
+
import tm_data_types # type: ignore[import-untyped]
|
|
382
464
|
except ImportError:
|
|
383
465
|
# Fall back to single channel loading
|
|
384
466
|
trace = load(path, format="tektronix")
|
|
385
467
|
channel_name = getattr(trace.metadata, "channel_name", None) or "ch1"
|
|
386
|
-
|
|
468
|
+
# Return a dict-like object to maintain compatibility
|
|
469
|
+
return {"__fallback__": {channel_name: trace}}
|
|
387
470
|
|
|
388
471
|
try:
|
|
389
|
-
|
|
472
|
+
return tm_data_types.read_file(str(path))
|
|
390
473
|
except Exception as e:
|
|
391
474
|
raise LoaderError(
|
|
392
475
|
"Failed to read Tektronix WFM file",
|
|
@@ -394,72 +477,103 @@ def _load_all_channels_tektronix(
|
|
|
394
477
|
details=str(e),
|
|
395
478
|
) from e
|
|
396
479
|
|
|
397
|
-
channels: dict[str, WaveformTrace | DigitalTrace] = {}
|
|
398
480
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
from oscura.loaders.tektronix import _build_waveform_trace
|
|
404
|
-
|
|
405
|
-
for i, awfm in enumerate(wfm.analog_waveforms):
|
|
406
|
-
try:
|
|
407
|
-
data = np.array(awfm.y_data, dtype=np.float64)
|
|
408
|
-
x_increment = getattr(awfm, "x_increment", 1e-6)
|
|
409
|
-
sample_rate = 1.0 / x_increment if x_increment > 0 else 1e6
|
|
410
|
-
vertical_scale = getattr(awfm, "y_scale", None)
|
|
411
|
-
vertical_offset = getattr(awfm, "y_offset", None)
|
|
412
|
-
channel_name = getattr(awfm, "name", f"CH{i + 1}")
|
|
413
|
-
|
|
414
|
-
trace = _build_waveform_trace(
|
|
415
|
-
data=data,
|
|
416
|
-
sample_rate=sample_rate,
|
|
417
|
-
vertical_scale=vertical_scale,
|
|
418
|
-
vertical_offset=vertical_offset,
|
|
419
|
-
channel_name=channel_name,
|
|
420
|
-
path=path,
|
|
421
|
-
wfm=awfm,
|
|
422
|
-
)
|
|
423
|
-
channels[f"ch{i + 1}"] = trace
|
|
424
|
-
except Exception as e:
|
|
425
|
-
logger.warning("Failed to extract analog channel %d: %s", i + 1, e)
|
|
481
|
+
def _extract_analog_waveforms(
|
|
482
|
+
wfm: Any, path: Path, channels: dict[str, WaveformTrace | DigitalTrace | IQTrace]
|
|
483
|
+
) -> None:
|
|
484
|
+
"""Extract analog waveforms from WFM file.
|
|
426
485
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
486
|
+
Args:
|
|
487
|
+
wfm: WFM file object.
|
|
488
|
+
path: Path to file.
|
|
489
|
+
channels: Dictionary to populate with channels.
|
|
490
|
+
"""
|
|
491
|
+
if not hasattr(wfm, "analog_waveforms") or not wfm.analog_waveforms:
|
|
492
|
+
return
|
|
493
|
+
|
|
494
|
+
import numpy as np
|
|
495
|
+
|
|
496
|
+
from oscura.loaders.tektronix import _build_waveform_trace
|
|
497
|
+
|
|
498
|
+
for i, awfm in enumerate(wfm.analog_waveforms):
|
|
499
|
+
try:
|
|
500
|
+
data = np.array(awfm.y_data, dtype=np.float64)
|
|
501
|
+
x_increment = getattr(awfm, "x_increment", 1e-6)
|
|
502
|
+
sample_rate = 1.0 / x_increment if x_increment > 0 else 1e6
|
|
503
|
+
vertical_scale = getattr(awfm, "y_scale", None)
|
|
504
|
+
vertical_offset = getattr(awfm, "y_offset", None)
|
|
505
|
+
channel_name = getattr(awfm, "name", f"CH{i + 1}")
|
|
506
|
+
|
|
507
|
+
trace = _build_waveform_trace(
|
|
508
|
+
data=data,
|
|
509
|
+
sample_rate=sample_rate,
|
|
510
|
+
vertical_scale=vertical_scale,
|
|
511
|
+
vertical_offset=vertical_offset,
|
|
512
|
+
channel_name=channel_name,
|
|
513
|
+
path=path,
|
|
514
|
+
wfm=awfm,
|
|
515
|
+
)
|
|
516
|
+
channels[f"ch{i + 1}"] = trace
|
|
517
|
+
except Exception as e:
|
|
518
|
+
logger.warning("Failed to extract analog channel %d: %s", i + 1, e)
|
|
430
519
|
|
|
431
|
-
for i, dwfm in enumerate(wfm.digital_waveforms):
|
|
432
|
-
try:
|
|
433
|
-
trace = _load_digital_waveform(dwfm, path, i)
|
|
434
|
-
channels[f"d{i + 1}"] = trace
|
|
435
|
-
except Exception as e:
|
|
436
|
-
logger.warning("Failed to extract digital channel %d: %s", i + 1, e)
|
|
437
520
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
521
|
+
def _extract_digital_waveforms(
|
|
522
|
+
wfm: Any, path: Path, channels: dict[str, WaveformTrace | DigitalTrace | IQTrace]
|
|
523
|
+
) -> None:
|
|
524
|
+
"""Extract digital waveforms from WFM file.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
wfm: WFM file object.
|
|
528
|
+
path: Path to file.
|
|
529
|
+
channels: Dictionary to populate with channels.
|
|
530
|
+
"""
|
|
531
|
+
if not hasattr(wfm, "digital_waveforms") or not wfm.digital_waveforms:
|
|
532
|
+
return
|
|
441
533
|
|
|
442
|
-
|
|
443
|
-
from oscura.loaders.tektronix import _load_digital_waveform
|
|
534
|
+
from oscura.loaders.tektronix import _load_digital_waveform
|
|
444
535
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
536
|
+
for i, dwfm in enumerate(wfm.digital_waveforms):
|
|
537
|
+
try:
|
|
538
|
+
trace = _load_digital_waveform(dwfm, path, i)
|
|
539
|
+
channels[f"d{i + 1}"] = trace
|
|
540
|
+
except Exception as e:
|
|
541
|
+
logger.warning("Failed to extract digital channel %d: %s", i + 1, e)
|
|
448
542
|
|
|
449
|
-
elif hasattr(wfm, "y_axis_values") or hasattr(wfm, "y_data"):
|
|
450
|
-
# Direct analog waveform
|
|
451
|
-
trace = load(path, format="tektronix")
|
|
452
|
-
channel_name = trace.metadata.channel_name or "ch1"
|
|
453
|
-
channels[channel_name.lower()] = trace # type: ignore[assignment]
|
|
454
543
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
fix_hint="File may be empty or use an unsupported format variant.",
|
|
460
|
-
)
|
|
544
|
+
def _extract_direct_waveform(
|
|
545
|
+
wfm: Any, path: Path, channels: dict[str, WaveformTrace | DigitalTrace | IQTrace]
|
|
546
|
+
) -> None:
|
|
547
|
+
"""Extract single channel from direct waveform format.
|
|
461
548
|
|
|
462
|
-
|
|
549
|
+
Args:
|
|
550
|
+
wfm: WFM file object.
|
|
551
|
+
path: Path to file.
|
|
552
|
+
channels: Dictionary to populate with channels.
|
|
553
|
+
"""
|
|
554
|
+
wfm_type = type(wfm).__name__
|
|
555
|
+
|
|
556
|
+
if wfm_type == "DigitalWaveform" or hasattr(wfm, "y_axis_byte_values"):
|
|
557
|
+
from oscura.loaders.tektronix import _load_digital_waveform
|
|
558
|
+
|
|
559
|
+
trace = _load_digital_waveform(wfm, path, 0)
|
|
560
|
+
channel_name = trace.metadata.channel_name or "d1"
|
|
561
|
+
channels[channel_name.lower()] = trace
|
|
562
|
+
|
|
563
|
+
elif wfm_type == "IQWaveform" or (
|
|
564
|
+
hasattr(wfm, "i_axis_values") and hasattr(wfm, "q_axis_values")
|
|
565
|
+
):
|
|
566
|
+
# IQWaveform format (RF/SDR data)
|
|
567
|
+
loaded_trace = load(path, format="tektronix")
|
|
568
|
+
channel_name = loaded_trace.metadata.channel_name or "iq1"
|
|
569
|
+
channels[channel_name.lower()] = loaded_trace
|
|
570
|
+
|
|
571
|
+
elif hasattr(wfm, "y_axis_values") or hasattr(wfm, "y_data"):
|
|
572
|
+
# Direct analog waveform
|
|
573
|
+
loaded_trace = load(path, format="tektronix")
|
|
574
|
+
# Add both analog and digital traces to channels
|
|
575
|
+
channel_name = loaded_trace.metadata.channel_name or "ch1"
|
|
576
|
+
channels[channel_name.lower()] = loaded_trace
|
|
463
577
|
|
|
464
578
|
|
|
465
579
|
def get_supported_formats() -> list[str]:
|
|
@@ -535,9 +649,11 @@ __all__ = [
|
|
|
535
649
|
"hdf5",
|
|
536
650
|
"load",
|
|
537
651
|
"load_all_channels",
|
|
652
|
+
"load_binary",
|
|
538
653
|
"load_binary_packets",
|
|
539
654
|
"load_lazy",
|
|
540
655
|
"load_packets_streaming",
|
|
656
|
+
"load_touchstone",
|
|
541
657
|
"load_trace_lazy",
|
|
542
658
|
"trim_idle",
|
|
543
659
|
]
|