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/hdf5_loader.py
CHANGED
|
@@ -212,104 +212,26 @@ def load_hdf5(
|
|
|
212
212
|
>>> # Load as memory-mapped for large files
|
|
213
213
|
>>> trace = load_hdf5("huge_data.h5", mmap=True)
|
|
214
214
|
"""
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
details="h5py package is required for HDF5 loading",
|
|
219
|
-
fix_hint="Install h5py: pip install h5py",
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
path = Path(path)
|
|
223
|
-
|
|
224
|
-
if not path.exists():
|
|
225
|
-
raise LoaderError(
|
|
226
|
-
"File not found",
|
|
227
|
-
file_path=str(path),
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
# Use channel as dataset if dataset not specified
|
|
231
|
-
if dataset is None and channel is not None:
|
|
232
|
-
dataset = str(channel)
|
|
215
|
+
_validate_hdf5_availability()
|
|
216
|
+
file_path = _validate_file_path(path)
|
|
217
|
+
dataset_path = _resolve_dataset_name(dataset, channel)
|
|
233
218
|
|
|
234
219
|
try:
|
|
235
|
-
with h5py.File(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
else:
|
|
241
|
-
# Try to find by name
|
|
242
|
-
ds = _find_dataset_by_name(f, dataset)
|
|
243
|
-
if ds is None:
|
|
244
|
-
available = list_datasets(path)
|
|
245
|
-
raise FormatError(
|
|
246
|
-
f"Dataset not found: {dataset}",
|
|
247
|
-
file_path=str(path),
|
|
248
|
-
expected=dataset,
|
|
249
|
-
got=f"Available: {', '.join(available)}",
|
|
250
|
-
)
|
|
251
|
-
else:
|
|
252
|
-
# Auto-detect dataset
|
|
253
|
-
ds = _find_waveform_dataset(f)
|
|
254
|
-
if ds is None:
|
|
255
|
-
available = list_datasets(path)
|
|
256
|
-
raise FormatError(
|
|
257
|
-
"No waveform data found in HDF5 file",
|
|
258
|
-
file_path=str(path),
|
|
259
|
-
expected=f"Dataset named: {', '.join(DATASET_NAMES)}",
|
|
260
|
-
got=f"Datasets: {', '.join(available)}",
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
# Extract data
|
|
264
|
-
if not isinstance(ds, h5py.Dataset):
|
|
265
|
-
raise FormatError(
|
|
266
|
-
"Selected path is not a dataset",
|
|
267
|
-
file_path=str(path),
|
|
268
|
-
got=type(ds).__name__,
|
|
269
|
-
)
|
|
270
|
-
|
|
271
|
-
data = np.asarray(ds, dtype=np.float64)
|
|
272
|
-
if data.ndim > 1:
|
|
273
|
-
data = data.ravel()
|
|
274
|
-
|
|
275
|
-
# Extract metadata from attributes
|
|
276
|
-
detected_sample_rate = sample_rate
|
|
277
|
-
if detected_sample_rate is None:
|
|
278
|
-
detected_sample_rate = _find_sample_rate(f, ds)
|
|
279
|
-
|
|
280
|
-
if detected_sample_rate is None:
|
|
281
|
-
detected_sample_rate = 1e6 # Default
|
|
282
|
-
|
|
283
|
-
# Get other metadata
|
|
284
|
-
vertical_scale = _get_attr(ds, ["vertical_scale", "v_scale", "scale"])
|
|
285
|
-
vertical_offset = _get_attr(ds, ["vertical_offset", "v_offset", "offset"])
|
|
286
|
-
channel_name = _get_attr(ds, ["channel_name", "name", "channel"])
|
|
287
|
-
|
|
288
|
-
if channel_name is None:
|
|
289
|
-
channel_name = ds.name.split("/")[-1] if ds.name else "CH1"
|
|
290
|
-
|
|
291
|
-
metadata = TraceMetadata(
|
|
292
|
-
sample_rate=float(detected_sample_rate),
|
|
293
|
-
vertical_scale=float(vertical_scale) if vertical_scale else None,
|
|
294
|
-
vertical_offset=float(vertical_offset) if vertical_offset else None,
|
|
295
|
-
source_file=str(path),
|
|
296
|
-
channel_name=str(channel_name),
|
|
297
|
-
)
|
|
220
|
+
with h5py.File(file_path, "r") as f:
|
|
221
|
+
ds = _locate_dataset(f, dataset_path, file_path)
|
|
222
|
+
_validate_dataset(ds, file_path)
|
|
223
|
+
data = _extract_data_array(ds)
|
|
224
|
+
metadata = _build_metadata(f, ds, sample_rate, file_path)
|
|
298
225
|
|
|
299
|
-
# Return memory-mapped trace if requested
|
|
300
226
|
if mmap:
|
|
301
|
-
return
|
|
302
|
-
file_path=path,
|
|
303
|
-
dataset_path=ds.name,
|
|
304
|
-
metadata=metadata,
|
|
305
|
-
)
|
|
227
|
+
return _create_mmap_trace(file_path, ds.name, metadata)
|
|
306
228
|
|
|
307
229
|
return WaveformTrace(data=data, metadata=metadata)
|
|
308
230
|
|
|
309
231
|
except OSError as e:
|
|
310
232
|
raise LoaderError(
|
|
311
233
|
"Failed to read HDF5 file",
|
|
312
|
-
file_path=str(
|
|
234
|
+
file_path=str(file_path),
|
|
313
235
|
details=str(e),
|
|
314
236
|
) from e
|
|
315
237
|
except Exception as e:
|
|
@@ -317,16 +239,239 @@ def load_hdf5(
|
|
|
317
239
|
raise
|
|
318
240
|
raise LoaderError(
|
|
319
241
|
"Failed to load HDF5 file",
|
|
320
|
-
file_path=str(
|
|
242
|
+
file_path=str(file_path),
|
|
321
243
|
details=str(e),
|
|
322
244
|
) from e
|
|
323
245
|
|
|
324
246
|
|
|
247
|
+
def _validate_hdf5_availability() -> None:
|
|
248
|
+
"""Validate that h5py package is available.
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
LoaderError: If h5py is not installed.
|
|
252
|
+
"""
|
|
253
|
+
if not H5PY_AVAILABLE:
|
|
254
|
+
raise LoaderError(
|
|
255
|
+
"HDF5 support not available",
|
|
256
|
+
details="h5py package is required for HDF5 loading",
|
|
257
|
+
fix_hint="Install h5py: pip install h5py",
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _validate_file_path(path: str | PathLike[str]) -> Path:
|
|
262
|
+
"""Validate that the file path exists.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
path: File path to validate.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Validated Path object.
|
|
269
|
+
|
|
270
|
+
Raises:
|
|
271
|
+
LoaderError: If file does not exist.
|
|
272
|
+
"""
|
|
273
|
+
file_path = Path(path)
|
|
274
|
+
if not file_path.exists():
|
|
275
|
+
raise LoaderError("File not found", file_path=str(file_path))
|
|
276
|
+
return file_path
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _resolve_dataset_name(dataset: str | None, channel: str | int | None) -> str | None:
|
|
280
|
+
"""Resolve dataset name from dataset or channel parameter.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
dataset: Explicit dataset path.
|
|
284
|
+
channel: Channel name/number (alternative to dataset).
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
Resolved dataset name, or None for auto-detection.
|
|
288
|
+
|
|
289
|
+
Example:
|
|
290
|
+
>>> _resolve_dataset_name(None, 1)
|
|
291
|
+
'1'
|
|
292
|
+
>>> _resolve_dataset_name("/data", None)
|
|
293
|
+
'/data'
|
|
294
|
+
"""
|
|
295
|
+
if dataset is None and channel is not None:
|
|
296
|
+
return str(channel)
|
|
297
|
+
return dataset
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _locate_dataset(f: h5py.File, dataset_path: str | None, file_path: Path) -> h5py.Dataset:
|
|
301
|
+
"""Locate dataset in HDF5 file by path or auto-detection.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
f: Open HDF5 file handle.
|
|
305
|
+
dataset_path: Specific dataset path, or None for auto-detect.
|
|
306
|
+
file_path: Path to HDF5 file (for error messages).
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
Located HDF5 dataset.
|
|
310
|
+
|
|
311
|
+
Raises:
|
|
312
|
+
FormatError: If dataset not found.
|
|
313
|
+
|
|
314
|
+
Example:
|
|
315
|
+
>>> with h5py.File("data.h5") as f:
|
|
316
|
+
... ds = _locate_dataset(f, "/waveform", Path("data.h5"))
|
|
317
|
+
"""
|
|
318
|
+
if dataset_path is not None:
|
|
319
|
+
if dataset_path in f:
|
|
320
|
+
return f[dataset_path]
|
|
321
|
+
|
|
322
|
+
# Try fuzzy name matching
|
|
323
|
+
ds = _find_dataset_by_name(f, dataset_path)
|
|
324
|
+
if ds is None:
|
|
325
|
+
available = list_datasets(file_path)
|
|
326
|
+
raise FormatError(
|
|
327
|
+
f"Dataset not found: {dataset_path}",
|
|
328
|
+
file_path=str(file_path),
|
|
329
|
+
expected=dataset_path,
|
|
330
|
+
got=f"Available: {', '.join(available)}",
|
|
331
|
+
)
|
|
332
|
+
return ds
|
|
333
|
+
|
|
334
|
+
# Auto-detect waveform dataset
|
|
335
|
+
ds = _find_waveform_dataset(f)
|
|
336
|
+
if ds is None:
|
|
337
|
+
available = list_datasets(file_path)
|
|
338
|
+
raise FormatError(
|
|
339
|
+
"No waveform data found in HDF5 file",
|
|
340
|
+
file_path=str(file_path),
|
|
341
|
+
expected=f"Dataset named: {', '.join(DATASET_NAMES)}",
|
|
342
|
+
got=f"Datasets: {', '.join(available)}",
|
|
343
|
+
)
|
|
344
|
+
return ds
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _validate_dataset(ds: Any, file_path: Path) -> None:
|
|
348
|
+
"""Validate that object is a valid HDF5 dataset.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
ds: Object to validate.
|
|
352
|
+
file_path: Path to HDF5 file (for error messages).
|
|
353
|
+
|
|
354
|
+
Raises:
|
|
355
|
+
FormatError: If not a dataset.
|
|
356
|
+
"""
|
|
357
|
+
if not isinstance(ds, h5py.Dataset):
|
|
358
|
+
raise FormatError(
|
|
359
|
+
"Selected path is not a dataset",
|
|
360
|
+
file_path=str(file_path),
|
|
361
|
+
got=type(ds).__name__,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _extract_data_array(ds: h5py.Dataset) -> np.ndarray[Any, Any]:
|
|
366
|
+
"""Extract and flatten data array from dataset.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
ds: HDF5 dataset.
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
1D numpy array of float64 data.
|
|
373
|
+
|
|
374
|
+
Example:
|
|
375
|
+
>>> data = _extract_data_array(dataset)
|
|
376
|
+
>>> data.shape
|
|
377
|
+
(10000,)
|
|
378
|
+
"""
|
|
379
|
+
data = np.asarray(ds, dtype=np.float64)
|
|
380
|
+
if data.ndim > 1:
|
|
381
|
+
data = data.ravel()
|
|
382
|
+
return data
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _build_metadata(
|
|
386
|
+
f: h5py.File, ds: h5py.Dataset, sample_rate: float | None, file_path: Path
|
|
387
|
+
) -> TraceMetadata:
|
|
388
|
+
"""Build trace metadata from HDF5 attributes.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
f: Open HDF5 file handle.
|
|
392
|
+
ds: HDF5 dataset.
|
|
393
|
+
sample_rate: User-provided sample rate override.
|
|
394
|
+
file_path: Path to HDF5 file.
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
TraceMetadata with extracted attributes.
|
|
398
|
+
|
|
399
|
+
Example:
|
|
400
|
+
>>> metadata = _build_metadata(f, ds, None, Path("data.h5"))
|
|
401
|
+
>>> metadata.sample_rate
|
|
402
|
+
1000000.0
|
|
403
|
+
"""
|
|
404
|
+
detected_sample_rate = sample_rate if sample_rate is not None else _find_sample_rate(f, ds)
|
|
405
|
+
if detected_sample_rate is None:
|
|
406
|
+
detected_sample_rate = 1e6 # Default 1 MHz
|
|
407
|
+
|
|
408
|
+
vertical_scale = _get_attr(ds, ["vertical_scale", "v_scale", "scale"])
|
|
409
|
+
vertical_offset = _get_attr(ds, ["vertical_offset", "v_offset", "offset"])
|
|
410
|
+
channel_name = _get_channel_name(ds)
|
|
411
|
+
|
|
412
|
+
return TraceMetadata(
|
|
413
|
+
sample_rate=float(detected_sample_rate),
|
|
414
|
+
vertical_scale=float(vertical_scale) if vertical_scale else None,
|
|
415
|
+
vertical_offset=float(vertical_offset) if vertical_offset else None,
|
|
416
|
+
source_file=str(file_path),
|
|
417
|
+
channel_name=str(channel_name),
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _get_channel_name(ds: h5py.Dataset) -> str:
|
|
422
|
+
"""Get channel name from dataset attributes or path.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
ds: HDF5 dataset.
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
Channel name string.
|
|
429
|
+
|
|
430
|
+
Example:
|
|
431
|
+
>>> _get_channel_name(dataset)
|
|
432
|
+
'CH1'
|
|
433
|
+
"""
|
|
434
|
+
channel_name = _get_attr(ds, ["channel_name", "name", "channel"])
|
|
435
|
+
if channel_name is None:
|
|
436
|
+
channel_name = ds.name.split("/")[-1] if ds.name else "CH1"
|
|
437
|
+
return channel_name
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _create_mmap_trace(
|
|
441
|
+
file_path: Path, dataset_path: str, metadata: TraceMetadata
|
|
442
|
+
) -> HDF5MmapTrace:
|
|
443
|
+
"""Create memory-mapped HDF5 trace.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
file_path: Path to HDF5 file.
|
|
447
|
+
dataset_path: Path to dataset within file.
|
|
448
|
+
metadata: Trace metadata.
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
Memory-mapped trace object.
|
|
452
|
+
|
|
453
|
+
Example:
|
|
454
|
+
>>> trace = _create_mmap_trace(Path("data.h5"), "/waveform", metadata)
|
|
455
|
+
>>> trace[1000:2000] # Access subset without loading entire file
|
|
456
|
+
"""
|
|
457
|
+
return HDF5MmapTrace(
|
|
458
|
+
file_path=file_path,
|
|
459
|
+
dataset_path=dataset_path,
|
|
460
|
+
metadata=metadata,
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
|
|
325
464
|
def _find_waveform_dataset(f: h5py.File) -> h5py.Dataset | None:
|
|
326
465
|
"""Find a waveform dataset in the HDF5 file."""
|
|
327
466
|
result: h5py.Dataset | None = None
|
|
328
467
|
|
|
329
468
|
def visitor(name: str, obj: Any) -> None:
|
|
469
|
+
"""Visit HDF5 items to find waveform dataset.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
name: Dataset path within HDF5 file.
|
|
473
|
+
obj: HDF5 object (dataset or group).
|
|
474
|
+
"""
|
|
330
475
|
nonlocal result
|
|
331
476
|
if result is not None:
|
|
332
477
|
return
|
|
@@ -352,6 +497,12 @@ def _find_dataset_by_name(f: h5py.File, name: str) -> h5py.Dataset | None:
|
|
|
352
497
|
result: h5py.Dataset | None = None
|
|
353
498
|
|
|
354
499
|
def visitor(path: str, obj: Any) -> None:
|
|
500
|
+
"""Visit HDF5 items to find dataset by name.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
path: Dataset path within HDF5 file.
|
|
504
|
+
obj: HDF5 object (dataset or group).
|
|
505
|
+
"""
|
|
355
506
|
nonlocal result
|
|
356
507
|
if result is not None:
|
|
357
508
|
return
|
|
@@ -365,43 +516,45 @@ def _find_dataset_by_name(f: h5py.File, name: str) -> h5py.Dataset | None:
|
|
|
365
516
|
|
|
366
517
|
|
|
367
518
|
def _find_sample_rate(f: h5py.File, ds: h5py.Dataset) -> float | None:
|
|
368
|
-
"""Find sample rate from HDF5 attributes.
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
519
|
+
"""Find sample rate from HDF5 attributes.
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
f: HDF5 file handle.
|
|
523
|
+
ds: Dataset to search for sample rate.
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
Sample rate in Hz, or None if not found.
|
|
527
|
+
"""
|
|
528
|
+
# Check dataset, then parent, then root, then metadata group
|
|
529
|
+
search_locations = [ds, ds.parent, f, f.get("metadata")]
|
|
530
|
+
|
|
531
|
+
for location in search_locations:
|
|
532
|
+
if location is None:
|
|
533
|
+
continue
|
|
534
|
+
|
|
535
|
+
rate = _extract_sample_rate_from_attrs(location)
|
|
536
|
+
if rate is not None:
|
|
537
|
+
return rate
|
|
538
|
+
|
|
539
|
+
return None
|
|
540
|
+
|
|
376
541
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
for attr_name in SAMPLE_RATE_ATTRS:
|
|
380
|
-
if attr_name in ds.parent.attrs:
|
|
381
|
-
value = ds.parent.attrs[attr_name]
|
|
382
|
-
if attr_name in ("sample_interval", "dt") and value > 0:
|
|
383
|
-
return 1.0 / float(value)
|
|
384
|
-
return float(value)
|
|
542
|
+
def _extract_sample_rate_from_attrs(obj: h5py.Dataset | h5py.Group | h5py.File) -> float | None:
|
|
543
|
+
"""Extract sample rate from HDF5 object attributes.
|
|
385
544
|
|
|
386
|
-
|
|
545
|
+
Args:
|
|
546
|
+
obj: HDF5 object with attributes.
|
|
547
|
+
|
|
548
|
+
Returns:
|
|
549
|
+
Sample rate in Hz, or None if not found.
|
|
550
|
+
"""
|
|
387
551
|
for attr_name in SAMPLE_RATE_ATTRS:
|
|
388
|
-
if attr_name in
|
|
389
|
-
value =
|
|
552
|
+
if attr_name in obj.attrs:
|
|
553
|
+
value = obj.attrs[attr_name]
|
|
390
554
|
if attr_name in ("sample_interval", "dt") and value > 0:
|
|
391
555
|
return 1.0 / float(value)
|
|
392
556
|
return float(value)
|
|
393
557
|
|
|
394
|
-
# Check for metadata group
|
|
395
|
-
if "metadata" in f:
|
|
396
|
-
meta = f["metadata"]
|
|
397
|
-
if isinstance(meta, h5py.Group | h5py.Dataset):
|
|
398
|
-
for attr_name in SAMPLE_RATE_ATTRS:
|
|
399
|
-
if attr_name in meta.attrs:
|
|
400
|
-
value = meta.attrs[attr_name]
|
|
401
|
-
if attr_name in ("sample_interval", "dt") and value > 0:
|
|
402
|
-
return 1.0 / float(value)
|
|
403
|
-
return float(value)
|
|
404
|
-
|
|
405
558
|
return None
|
|
406
559
|
|
|
407
560
|
|
|
@@ -446,6 +599,12 @@ def list_datasets(path: str | PathLike[str]) -> list[str]:
|
|
|
446
599
|
datasets: list[str] = []
|
|
447
600
|
|
|
448
601
|
def visitor(name: str, obj: Any) -> None:
|
|
602
|
+
"""Visit HDF5 items to collect dataset paths.
|
|
603
|
+
|
|
604
|
+
Args:
|
|
605
|
+
name: Dataset path within HDF5 file.
|
|
606
|
+
obj: HDF5 object (dataset or group).
|
|
607
|
+
"""
|
|
449
608
|
if isinstance(obj, h5py.Dataset):
|
|
450
609
|
datasets.append("/" + name)
|
|
451
610
|
|
oscura/loaders/lazy.py
CHANGED
|
@@ -188,7 +188,7 @@ class LazyWaveformTrace:
|
|
|
188
188
|
|
|
189
189
|
metadata = TraceMetadata(
|
|
190
190
|
sample_rate=self._sample_rate,
|
|
191
|
-
**self._metadata,
|
|
191
|
+
**self._metadata,
|
|
192
192
|
)
|
|
193
193
|
return WaveformTrace(data=sliced_data, metadata=metadata) # type: ignore[return-value]
|
|
194
194
|
|
|
@@ -221,7 +221,7 @@ class LazyWaveformTrace:
|
|
|
221
221
|
|
|
222
222
|
metadata = TraceMetadata(
|
|
223
223
|
sample_rate=self._sample_rate,
|
|
224
|
-
**self._metadata,
|
|
224
|
+
**self._metadata,
|
|
225
225
|
)
|
|
226
226
|
return WaveformTrace(data=self.data, metadata=metadata)
|
|
227
227
|
|
|
@@ -299,72 +299,105 @@ def load_trace_lazy(
|
|
|
299
299
|
if not file_path.exists():
|
|
300
300
|
raise LoaderError(f"File not found: {file_path}")
|
|
301
301
|
|
|
302
|
-
|
|
302
|
+
if sample_rate is None:
|
|
303
|
+
raise LoaderError("sample_rate is required")
|
|
304
|
+
|
|
305
|
+
# Determine format and extract metadata
|
|
303
306
|
suffix = file_path.suffix.lower()
|
|
304
307
|
|
|
305
308
|
if suffix == ".npy":
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
309
|
+
length, dtype, offset = _extract_npy_metadata(file_path)
|
|
310
|
+
else:
|
|
311
|
+
length, dtype, offset = _extract_raw_metadata(file_path, kwargs)
|
|
312
|
+
|
|
313
|
+
# Return lazy or eager trace
|
|
314
|
+
if lazy:
|
|
315
|
+
return LazyWaveformTrace(
|
|
316
|
+
file_path=file_path,
|
|
317
|
+
sample_rate=sample_rate,
|
|
318
|
+
length=length,
|
|
319
|
+
dtype=dtype,
|
|
320
|
+
offset=offset,
|
|
321
|
+
)
|
|
322
|
+
else:
|
|
323
|
+
return _load_eager_trace(file_path, sample_rate, length, dtype, offset, suffix == ".npy")
|
|
310
324
|
|
|
311
|
-
npf.read_magic(f) # type: ignore[no-untyped-call]
|
|
312
|
-
shape, _fortran_order, dtype = npf.read_array_header_1_0(f) # type: ignore[no-untyped-call]
|
|
313
|
-
offset = f.tell()
|
|
314
325
|
|
|
315
|
-
|
|
316
|
-
|
|
326
|
+
def _extract_npy_metadata(file_path: Path) -> tuple[int, DTypeLike, int]:
|
|
327
|
+
"""Extract metadata from NumPy file without loading data.
|
|
317
328
|
|
|
318
|
-
|
|
329
|
+
Args:
|
|
330
|
+
file_path: Path to .npy file.
|
|
319
331
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
raise LoaderError("sample_rate is required for .npy files")
|
|
332
|
+
Returns:
|
|
333
|
+
Tuple of (length, dtype, offset).
|
|
323
334
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
dtype=dtype,
|
|
330
|
-
offset=offset,
|
|
331
|
-
)
|
|
332
|
-
else:
|
|
333
|
-
# Load eagerly
|
|
334
|
-
from oscura.core.types import TraceMetadata, WaveformTrace
|
|
335
|
+
Raises:
|
|
336
|
+
LoaderError: If file format is invalid.
|
|
337
|
+
"""
|
|
338
|
+
with open(file_path, "rb") as f:
|
|
339
|
+
import numpy.lib.format as npf
|
|
335
340
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
341
|
+
npf.read_magic(f) # type: ignore[no-untyped-call]
|
|
342
|
+
shape, _fortran_order, dtype = npf.read_array_header_1_0(f) # type: ignore[no-untyped-call]
|
|
343
|
+
offset = f.tell()
|
|
339
344
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if sample_rate is None:
|
|
343
|
-
raise LoaderError("sample_rate is required for raw binary files")
|
|
345
|
+
if not isinstance(shape, tuple) or len(shape) != 1:
|
|
346
|
+
raise LoaderError(f"Expected 1D array, got shape {shape}")
|
|
344
347
|
|
|
345
|
-
|
|
346
|
-
offset = kwargs.get("offset", 0)
|
|
348
|
+
return shape[0], dtype, offset
|
|
347
349
|
|
|
348
|
-
# Compute length from file size
|
|
349
|
-
file_size = file_path.stat().st_size - offset
|
|
350
|
-
dtype_size = np.dtype(dtype).itemsize
|
|
351
|
-
length = file_size // dtype_size
|
|
352
350
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
351
|
+
def _extract_raw_metadata(file_path: Path, kwargs: dict[str, Any]) -> tuple[int, DTypeLike, int]:
|
|
352
|
+
"""Extract metadata from raw binary file.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
file_path: Path to raw binary file.
|
|
356
|
+
kwargs: Additional arguments (dtype, offset).
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Tuple of (length, dtype, offset).
|
|
360
|
+
"""
|
|
361
|
+
dtype = kwargs.get("dtype", np.float64)
|
|
362
|
+
offset = kwargs.get("offset", 0)
|
|
363
|
+
|
|
364
|
+
file_size = file_path.stat().st_size - offset
|
|
365
|
+
dtype_size = np.dtype(dtype).itemsize
|
|
366
|
+
length = file_size // dtype_size
|
|
367
|
+
|
|
368
|
+
return length, dtype, offset
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _load_eager_trace(
|
|
372
|
+
file_path: Path,
|
|
373
|
+
sample_rate: float,
|
|
374
|
+
length: int,
|
|
375
|
+
dtype: DTypeLike,
|
|
376
|
+
offset: int,
|
|
377
|
+
is_npy: bool,
|
|
378
|
+
) -> WaveformTrace:
|
|
379
|
+
"""Load trace data eagerly into memory.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
file_path: Path to trace file.
|
|
383
|
+
sample_rate: Sample rate in Hz.
|
|
384
|
+
length: Number of samples.
|
|
385
|
+
dtype: Data type.
|
|
386
|
+
offset: Byte offset in file.
|
|
387
|
+
is_npy: True if .npy format, False if raw binary.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
WaveformTrace with data loaded.
|
|
391
|
+
"""
|
|
392
|
+
from oscura.core.types import TraceMetadata, WaveformTrace
|
|
393
|
+
|
|
394
|
+
if is_npy:
|
|
395
|
+
data = np.load(file_path).astype(np.float64)
|
|
396
|
+
else:
|
|
397
|
+
data = np.fromfile(file_path, dtype=dtype, count=length, offset=offset).astype(np.float64)
|
|
364
398
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
return WaveformTrace(data=data.astype(np.float64), metadata=metadata)
|
|
399
|
+
metadata = TraceMetadata(sample_rate=sample_rate)
|
|
400
|
+
return WaveformTrace(data=data, metadata=metadata)
|
|
368
401
|
|
|
369
402
|
|
|
370
403
|
__all__ = ["LazyWaveformTrace", "load_trace_lazy"]
|
oscura/loaders/mmap_loader.py
CHANGED
|
@@ -309,7 +309,7 @@ class MmapWaveformTrace:
|
|
|
309
309
|
metadata = TraceMetadata(
|
|
310
310
|
sample_rate=self._sample_rate,
|
|
311
311
|
source_file=str(self._file_path),
|
|
312
|
-
**self._metadata,
|
|
312
|
+
**self._metadata,
|
|
313
313
|
)
|
|
314
314
|
|
|
315
315
|
return WaveformTrace(data=data, metadata=metadata)
|