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
oscura/loaders/sigrok.py
CHANGED
|
@@ -60,90 +60,23 @@ def load_sigrok(
|
|
|
60
60
|
sigrok session file format specification
|
|
61
61
|
"""
|
|
62
62
|
path = Path(path)
|
|
63
|
-
|
|
64
|
-
if not path.exists():
|
|
65
|
-
raise LoaderError(
|
|
66
|
-
"File not found",
|
|
67
|
-
file_path=str(path),
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
if not zipfile.is_zipfile(path):
|
|
71
|
-
raise FormatError(
|
|
72
|
-
"File is not a valid sigrok session (not a ZIP archive)",
|
|
73
|
-
file_path=str(path),
|
|
74
|
-
expected="ZIP archive",
|
|
75
|
-
)
|
|
63
|
+
_validate_sigrok_file(path)
|
|
76
64
|
|
|
77
65
|
try:
|
|
78
66
|
with zipfile.ZipFile(path, "r") as zf:
|
|
79
|
-
# Parse metadata
|
|
67
|
+
# Parse metadata and extract channel info
|
|
80
68
|
metadata_dict = _parse_metadata(zf, path)
|
|
69
|
+
sample_rate, channels, total_channels = _extract_channel_info(metadata_dict)
|
|
81
70
|
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
# Get channel information
|
|
86
|
-
channels = metadata_dict.get("channels", [])
|
|
87
|
-
total_channels = metadata_dict.get("total probes", len(channels))
|
|
88
|
-
|
|
89
|
-
# Find and read logic data files
|
|
90
|
-
logic_files = [name for name in zf.namelist() if name.startswith("logic-1")]
|
|
91
|
-
|
|
92
|
-
if not logic_files:
|
|
93
|
-
raise FormatError(
|
|
94
|
-
"No logic data found in sigrok session",
|
|
95
|
-
file_path=str(path),
|
|
96
|
-
expected="logic-1-* data files",
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
# Read and combine logic data
|
|
100
|
-
data = _read_logic_data(zf, logic_files, total_channels)
|
|
101
|
-
|
|
102
|
-
# Select specific channel if requested
|
|
103
|
-
if channel is not None:
|
|
104
|
-
if isinstance(channel, int):
|
|
105
|
-
if channel < 0 or channel >= data.shape[0]:
|
|
106
|
-
raise LoaderError(
|
|
107
|
-
f"Channel index {channel} out of range",
|
|
108
|
-
file_path=str(path),
|
|
109
|
-
details=f"Available channels: 0-{data.shape[0] - 1}",
|
|
110
|
-
)
|
|
111
|
-
channel_data = data[channel]
|
|
112
|
-
channel_name = channels[channel] if channel < len(channels) else f"D{channel}"
|
|
113
|
-
elif isinstance(channel, str):
|
|
114
|
-
if channel in channels:
|
|
115
|
-
idx = channels.index(channel)
|
|
116
|
-
channel_data = data[idx]
|
|
117
|
-
channel_name = channel
|
|
118
|
-
else:
|
|
119
|
-
raise LoaderError(
|
|
120
|
-
f"Channel '{channel}' not found",
|
|
121
|
-
file_path=str(path),
|
|
122
|
-
details=f"Available channels: {channels}",
|
|
123
|
-
)
|
|
124
|
-
else:
|
|
125
|
-
channel_data = data[0] # type: ignore[unreachable]
|
|
126
|
-
channel_name = channels[0] if channels else "D0"
|
|
127
|
-
else:
|
|
128
|
-
# Default to first channel
|
|
129
|
-
channel_data = data[0] if data.ndim > 1 else data
|
|
130
|
-
channel_name = channels[0] if channels else "D0"
|
|
131
|
-
|
|
132
|
-
# Compute edges
|
|
133
|
-
edges = _compute_edges(channel_data, sample_rate)
|
|
134
|
-
|
|
135
|
-
# Build metadata
|
|
136
|
-
trace_metadata = TraceMetadata(
|
|
137
|
-
sample_rate=float(sample_rate),
|
|
138
|
-
source_file=str(path),
|
|
139
|
-
channel_name=channel_name,
|
|
140
|
-
trigger_info=metadata_dict.get("trigger", None),
|
|
141
|
-
)
|
|
71
|
+
# Read logic data
|
|
72
|
+
data = _load_logic_data(zf, path, total_channels)
|
|
142
73
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
74
|
+
# Select channel and build trace
|
|
75
|
+
channel_data, channel_name = _select_channel_data(data, channel, channels, path)
|
|
76
|
+
|
|
77
|
+
# Compute edges and build trace
|
|
78
|
+
return _build_digital_trace(
|
|
79
|
+
channel_data, channel_name, sample_rate, metadata_dict, path
|
|
147
80
|
)
|
|
148
81
|
|
|
149
82
|
except zipfile.BadZipFile as e:
|
|
@@ -163,6 +96,196 @@ def load_sigrok(
|
|
|
163
96
|
) from e
|
|
164
97
|
|
|
165
98
|
|
|
99
|
+
def _validate_sigrok_file(path: Path) -> None:
|
|
100
|
+
"""Validate that file exists and is a zip archive.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
path: Path to sigrok file.
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
LoaderError: If file not found.
|
|
107
|
+
FormatError: If not a valid ZIP file.
|
|
108
|
+
"""
|
|
109
|
+
if not path.exists():
|
|
110
|
+
raise LoaderError("File not found", file_path=str(path))
|
|
111
|
+
|
|
112
|
+
if not zipfile.is_zipfile(path):
|
|
113
|
+
raise FormatError(
|
|
114
|
+
"File is not a valid sigrok session (not a ZIP archive)",
|
|
115
|
+
file_path=str(path),
|
|
116
|
+
expected="ZIP archive",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _extract_channel_info(metadata_dict: dict[str, Any]) -> tuple[float, list[str], int]:
|
|
121
|
+
"""Extract channel information from metadata.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
metadata_dict: Parsed metadata dictionary.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Tuple of (sample_rate, channels, total_channels).
|
|
128
|
+
"""
|
|
129
|
+
sample_rate = metadata_dict.get("samplerate", 1_000_000)
|
|
130
|
+
channels = metadata_dict.get("channels", [])
|
|
131
|
+
total_channels = metadata_dict.get("total probes", len(channels))
|
|
132
|
+
return float(sample_rate), channels, total_channels
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _load_logic_data(zf: zipfile.ZipFile, path: Path, total_channels: int) -> NDArray[np.bool_]:
|
|
136
|
+
"""Load logic data from sigrok session.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
zf: Open ZipFile object.
|
|
140
|
+
path: Path to session file (for error messages).
|
|
141
|
+
total_channels: Total number of channels.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Boolean array of shape (channels, samples).
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
FormatError: If no logic data found.
|
|
148
|
+
"""
|
|
149
|
+
logic_files = [name for name in zf.namelist() if name.startswith("logic-1")]
|
|
150
|
+
|
|
151
|
+
if not logic_files:
|
|
152
|
+
raise FormatError(
|
|
153
|
+
"No logic data found in sigrok session",
|
|
154
|
+
file_path=str(path),
|
|
155
|
+
expected="logic-1-* data files",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return _read_logic_data(zf, logic_files, total_channels)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _select_channel_data(
|
|
162
|
+
data: NDArray[np.bool_],
|
|
163
|
+
channel: str | int | None,
|
|
164
|
+
channels: list[str],
|
|
165
|
+
path: Path,
|
|
166
|
+
) -> tuple[NDArray[np.bool_], str]:
|
|
167
|
+
"""Select specific channel data or default to first channel.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
data: Multi-channel data array.
|
|
171
|
+
channel: Channel selector (name, index, or None).
|
|
172
|
+
channels: List of channel names.
|
|
173
|
+
path: Path to file (for error messages).
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Tuple of (channel_data, channel_name).
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
LoaderError: If channel not found or out of range.
|
|
180
|
+
"""
|
|
181
|
+
if channel is None:
|
|
182
|
+
channel_data = data[0] if data.ndim > 1 else data
|
|
183
|
+
channel_name = channels[0] if channels else "D0"
|
|
184
|
+
return channel_data, channel_name
|
|
185
|
+
|
|
186
|
+
if isinstance(channel, int):
|
|
187
|
+
return _select_channel_by_index(data, channel, channels, path)
|
|
188
|
+
# isinstance(channel, str) must be true here
|
|
189
|
+
return _select_channel_by_name(data, channel, channels, path)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _select_channel_by_index(
|
|
193
|
+
data: NDArray[np.bool_],
|
|
194
|
+
channel: int,
|
|
195
|
+
channels: list[str],
|
|
196
|
+
path: Path,
|
|
197
|
+
) -> tuple[NDArray[np.bool_], str]:
|
|
198
|
+
"""Select channel by numeric index.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
data: Multi-channel data array.
|
|
202
|
+
channel: Channel index.
|
|
203
|
+
channels: List of channel names.
|
|
204
|
+
path: Path to file (for error messages).
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Tuple of (channel_data, channel_name).
|
|
208
|
+
|
|
209
|
+
Raises:
|
|
210
|
+
LoaderError: If index out of range.
|
|
211
|
+
"""
|
|
212
|
+
if channel < 0 or channel >= data.shape[0]:
|
|
213
|
+
raise LoaderError(
|
|
214
|
+
f"Channel index {channel} out of range",
|
|
215
|
+
file_path=str(path),
|
|
216
|
+
details=f"Available channels: 0-{data.shape[0] - 1}",
|
|
217
|
+
)
|
|
218
|
+
channel_data = data[channel]
|
|
219
|
+
channel_name = channels[channel] if channel < len(channels) else f"D{channel}"
|
|
220
|
+
return channel_data, channel_name
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _select_channel_by_name(
|
|
224
|
+
data: NDArray[np.bool_],
|
|
225
|
+
channel: str,
|
|
226
|
+
channels: list[str],
|
|
227
|
+
path: Path,
|
|
228
|
+
) -> tuple[NDArray[np.bool_], str]:
|
|
229
|
+
"""Select channel by name.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
data: Multi-channel data array.
|
|
233
|
+
channel: Channel name.
|
|
234
|
+
channels: List of channel names.
|
|
235
|
+
path: Path to file (for error messages).
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Tuple of (channel_data, channel_name).
|
|
239
|
+
|
|
240
|
+
Raises:
|
|
241
|
+
LoaderError: If channel name not found.
|
|
242
|
+
"""
|
|
243
|
+
if channel in channels:
|
|
244
|
+
idx = channels.index(channel)
|
|
245
|
+
return data[idx], channel
|
|
246
|
+
else:
|
|
247
|
+
raise LoaderError(
|
|
248
|
+
f"Channel '{channel}' not found",
|
|
249
|
+
file_path=str(path),
|
|
250
|
+
details=f"Available channels: {channels}",
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _build_digital_trace(
|
|
255
|
+
channel_data: NDArray[np.bool_],
|
|
256
|
+
channel_name: str,
|
|
257
|
+
sample_rate: float,
|
|
258
|
+
metadata_dict: dict[str, Any],
|
|
259
|
+
path: Path,
|
|
260
|
+
) -> DigitalTrace:
|
|
261
|
+
"""Build DigitalTrace object from channel data.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
channel_data: Boolean array for selected channel.
|
|
265
|
+
channel_name: Name of the channel.
|
|
266
|
+
sample_rate: Sample rate in Hz.
|
|
267
|
+
metadata_dict: Metadata dictionary.
|
|
268
|
+
path: Path to source file.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
DigitalTrace object.
|
|
272
|
+
"""
|
|
273
|
+
edges = _compute_edges(channel_data, sample_rate)
|
|
274
|
+
|
|
275
|
+
trace_metadata = TraceMetadata(
|
|
276
|
+
sample_rate=sample_rate,
|
|
277
|
+
source_file=str(path),
|
|
278
|
+
channel_name=channel_name,
|
|
279
|
+
trigger_info=metadata_dict.get("trigger"),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
return DigitalTrace(
|
|
283
|
+
data=channel_data,
|
|
284
|
+
metadata=trace_metadata,
|
|
285
|
+
edges=edges,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
166
289
|
def _parse_metadata(zf: zipfile.ZipFile, path: Path) -> dict[str, Any]:
|
|
167
290
|
"""Parse sigrok session metadata.
|
|
168
291
|
|
oscura/loaders/tdms.py
CHANGED
|
@@ -16,6 +16,7 @@ from pathlib import Path
|
|
|
16
16
|
from typing import TYPE_CHECKING, Any
|
|
17
17
|
|
|
18
18
|
import numpy as np
|
|
19
|
+
from numpy.typing import NDArray
|
|
19
20
|
|
|
20
21
|
from oscura.core.exceptions import FormatError, LoaderError
|
|
21
22
|
from oscura.core.types import TraceMetadata, WaveformTrace
|
|
@@ -112,6 +113,17 @@ def _load_with_nptdms(
|
|
|
112
113
|
FormatError: If file is not valid TDMS format or has no data.
|
|
113
114
|
LoaderError: If channel or group not found.
|
|
114
115
|
"""
|
|
116
|
+
tdms_file = _parse_tdms_file(path)
|
|
117
|
+
target_group = _select_tdms_group(tdms_file, group, path)
|
|
118
|
+
target_channel = _select_tdms_channel(target_group, channel, path)
|
|
119
|
+
data = _extract_channel_data(target_channel, path)
|
|
120
|
+
metadata = _build_tdms_metadata(target_channel, target_group, tdms_file, path)
|
|
121
|
+
|
|
122
|
+
return WaveformTrace(data=data, metadata=metadata)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _parse_tdms_file(path: Path) -> Any:
|
|
126
|
+
"""Parse TDMS file and validate structure."""
|
|
115
127
|
try:
|
|
116
128
|
tdms_file = TdmsFile.read(str(path))
|
|
117
129
|
except Exception as e:
|
|
@@ -121,33 +133,37 @@ def _load_with_nptdms(
|
|
|
121
133
|
expected="Valid NI TDMS format",
|
|
122
134
|
) from e
|
|
123
135
|
|
|
124
|
-
# Get available groups
|
|
125
136
|
groups = list(tdms_file.groups())
|
|
126
|
-
|
|
127
137
|
if not groups:
|
|
128
138
|
raise FormatError(
|
|
129
139
|
"No groups found in TDMS file",
|
|
130
140
|
file_path=str(path),
|
|
131
141
|
)
|
|
132
142
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
143
|
+
return tdms_file
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _select_tdms_group(tdms_file: Any, group: str | None, path: Path) -> Any:
|
|
147
|
+
"""Select target group from TDMS file."""
|
|
148
|
+
groups = list(tdms_file.groups())
|
|
149
|
+
|
|
150
|
+
if group is None:
|
|
151
|
+
return groups[0]
|
|
152
|
+
|
|
153
|
+
for g in groups:
|
|
154
|
+
if g.name == group:
|
|
155
|
+
return g
|
|
156
|
+
|
|
157
|
+
available_groups = [g.name for g in groups]
|
|
158
|
+
raise LoaderError(
|
|
159
|
+
f"Group '{group}' not found",
|
|
160
|
+
file_path=str(path),
|
|
161
|
+
details=f"Available groups: {available_groups}",
|
|
162
|
+
)
|
|
163
|
+
|
|
149
164
|
|
|
150
|
-
|
|
165
|
+
def _select_tdms_channel(target_group: Any, channel: str | int | None, path: Path) -> Any:
|
|
166
|
+
"""Select target channel from TDMS group."""
|
|
151
167
|
channels = list(target_group.channels())
|
|
152
168
|
|
|
153
169
|
if not channels:
|
|
@@ -156,35 +172,44 @@ def _load_with_nptdms(
|
|
|
156
172
|
file_path=str(path),
|
|
157
173
|
)
|
|
158
174
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
details=f"Available channels: 0-{len(channels) - 1}",
|
|
167
|
-
)
|
|
168
|
-
target_channel = channels[channel]
|
|
169
|
-
elif isinstance(channel, str):
|
|
170
|
-
target_channel = None
|
|
171
|
-
for ch in channels:
|
|
172
|
-
if ch.name == channel:
|
|
173
|
-
target_channel = ch
|
|
174
|
-
break
|
|
175
|
-
if target_channel is None:
|
|
176
|
-
available_channels = [ch.name for ch in channels]
|
|
177
|
-
raise LoaderError(
|
|
178
|
-
f"Channel '{channel}' not found",
|
|
179
|
-
file_path=str(path),
|
|
180
|
-
details=f"Available channels: {available_channels}",
|
|
181
|
-
)
|
|
182
|
-
else:
|
|
183
|
-
target_channel = channels[0] # type: ignore[unreachable]
|
|
175
|
+
if channel is None:
|
|
176
|
+
return channels[0]
|
|
177
|
+
|
|
178
|
+
if isinstance(channel, int):
|
|
179
|
+
return _select_channel_by_index(channels, channel, path)
|
|
180
|
+
elif isinstance(channel, str):
|
|
181
|
+
return _select_channel_by_name(channels, channel, path)
|
|
184
182
|
else:
|
|
185
|
-
|
|
183
|
+
return channels[0] # type: ignore[unreachable]
|
|
184
|
+
|
|
186
185
|
|
|
187
|
-
|
|
186
|
+
def _select_channel_by_index(channels: list[Any], channel: int, path: Path) -> Any:
|
|
187
|
+
"""Select channel by index."""
|
|
188
|
+
if channel < 0 or channel >= len(channels):
|
|
189
|
+
raise LoaderError(
|
|
190
|
+
f"Channel index {channel} out of range",
|
|
191
|
+
file_path=str(path),
|
|
192
|
+
details=f"Available channels: 0-{len(channels) - 1}",
|
|
193
|
+
)
|
|
194
|
+
return channels[channel]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _select_channel_by_name(channels: list[Any], channel: str, path: Path) -> Any:
|
|
198
|
+
"""Select channel by name."""
|
|
199
|
+
for ch in channels:
|
|
200
|
+
if ch.name == channel:
|
|
201
|
+
return ch
|
|
202
|
+
|
|
203
|
+
available_channels = [ch.name for ch in channels]
|
|
204
|
+
raise LoaderError(
|
|
205
|
+
f"Channel '{channel}' not found",
|
|
206
|
+
file_path=str(path),
|
|
207
|
+
details=f"Available channels: {available_channels}",
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _extract_channel_data(target_channel: Any, path: Path) -> NDArray[np.float64]:
|
|
212
|
+
"""Extract and validate channel data."""
|
|
188
213
|
data = target_channel.data
|
|
189
214
|
if data is None or len(data) == 0:
|
|
190
215
|
raise FormatError(
|
|
@@ -192,21 +217,21 @@ def _load_with_nptdms(
|
|
|
192
217
|
file_path=str(path),
|
|
193
218
|
)
|
|
194
219
|
|
|
195
|
-
|
|
196
|
-
data = np.asarray(data, dtype=np.float64)
|
|
220
|
+
return np.asarray(data, dtype=np.float64)
|
|
197
221
|
|
|
198
|
-
# Extract sample rate from properties
|
|
199
|
-
sample_rate = _get_sample_rate(target_channel, target_group, tdms_file)
|
|
200
222
|
|
|
201
|
-
|
|
223
|
+
def _build_tdms_metadata(
|
|
224
|
+
target_channel: Any,
|
|
225
|
+
target_group: Any,
|
|
226
|
+
tdms_file: Any,
|
|
227
|
+
path: Path,
|
|
228
|
+
) -> TraceMetadata:
|
|
229
|
+
"""Build metadata from TDMS channel properties."""
|
|
230
|
+
sample_rate = _get_sample_rate(target_channel, target_group, tdms_file)
|
|
202
231
|
vertical_scale = target_channel.properties.get("NI_Scale[0]_Linear_Slope")
|
|
203
232
|
vertical_offset = target_channel.properties.get("NI_Scale[0]_Linear_Y_Intercept")
|
|
204
233
|
|
|
205
|
-
|
|
206
|
-
target_channel.properties.get("unit_string", None)
|
|
207
|
-
|
|
208
|
-
# Build metadata
|
|
209
|
-
metadata = TraceMetadata(
|
|
234
|
+
return TraceMetadata(
|
|
210
235
|
sample_rate=sample_rate,
|
|
211
236
|
vertical_scale=float(vertical_scale) if vertical_scale is not None else None,
|
|
212
237
|
vertical_offset=float(vertical_offset) if vertical_offset is not None else None,
|
|
@@ -215,8 +240,6 @@ def _load_with_nptdms(
|
|
|
215
240
|
trigger_info=_extract_tdms_properties(target_channel),
|
|
216
241
|
)
|
|
217
242
|
|
|
218
|
-
return WaveformTrace(data=data, metadata=metadata)
|
|
219
|
-
|
|
220
243
|
|
|
221
244
|
def _get_sample_rate(
|
|
222
245
|
channel: Any,
|