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/numpy_loader.py
CHANGED
|
@@ -78,90 +78,104 @@ def load_npz(
|
|
|
78
78
|
CSV, or HDF5.
|
|
79
79
|
"""
|
|
80
80
|
path = Path(path)
|
|
81
|
+
_validate_npz_file_exists(path)
|
|
81
82
|
|
|
83
|
+
npz = _load_npz_archive(path, mmap)
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
data_array = _extract_data_array(npz, channel, path)
|
|
87
|
+
data = _convert_to_float64(data_array, mmap, path)
|
|
88
|
+
metadata = _build_npz_metadata(npz, sample_rate, channel, path)
|
|
89
|
+
return WaveformTrace(data=data, metadata=metadata)
|
|
90
|
+
finally:
|
|
91
|
+
npz.close()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _validate_npz_file_exists(path: Path) -> None:
|
|
95
|
+
"""Validate that NPZ file exists."""
|
|
82
96
|
if not path.exists():
|
|
83
|
-
raise LoaderError(
|
|
84
|
-
|
|
97
|
+
raise LoaderError("File not found", file_path=str(path))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _load_npz_archive(path: Path, mmap: bool) -> Any:
|
|
101
|
+
"""Load NPZ archive with optional memory mapping."""
|
|
102
|
+
try:
|
|
103
|
+
return np.load(path, allow_pickle=True, mmap_mode="r" if mmap else None)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
raise LoaderError("Failed to load NPZ file", file_path=str(path), details=str(e)) from e
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _extract_data_array(npz: Any, channel: str | int | None, path: Path) -> Any:
|
|
109
|
+
"""Find and extract data array from NPZ file."""
|
|
110
|
+
data_array = _find_data_array(npz, channel)
|
|
111
|
+
|
|
112
|
+
if data_array is None:
|
|
113
|
+
available = list(npz.keys())
|
|
114
|
+
raise FormatError(
|
|
115
|
+
"No waveform data found in NPZ file",
|
|
85
116
|
file_path=str(path),
|
|
117
|
+
expected=f"Array named: {', '.join(DATA_ARRAY_NAMES)}",
|
|
118
|
+
got=f"Arrays: {', '.join(available)}",
|
|
86
119
|
)
|
|
87
120
|
|
|
121
|
+
return data_array
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _convert_to_float64(data_array: Any, mmap: bool, path: Path) -> NDArray[np.float64]:
|
|
125
|
+
"""Convert data array to float64, preserving memmap if enabled."""
|
|
126
|
+
if mmap and isinstance(data_array, np.memmap):
|
|
127
|
+
return _convert_memmap_to_float64(data_array, path)
|
|
128
|
+
|
|
129
|
+
return _convert_array_to_float64(data_array, path)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _convert_memmap_to_float64(data_array: np.memmap[Any, Any], path: Path) -> NDArray[np.float64]:
|
|
133
|
+
"""Convert memmap array to float64."""
|
|
134
|
+
if data_array.dtype == np.float64:
|
|
135
|
+
return data_array
|
|
136
|
+
|
|
88
137
|
try:
|
|
89
|
-
|
|
90
|
-
except
|
|
91
|
-
raise
|
|
92
|
-
"
|
|
138
|
+
return data_array.astype(np.float64)
|
|
139
|
+
except (ValueError, TypeError) as e:
|
|
140
|
+
raise FormatError(
|
|
141
|
+
"Data array is not numeric",
|
|
93
142
|
file_path=str(path),
|
|
94
|
-
|
|
143
|
+
expected="Numeric dtype (int, float)",
|
|
144
|
+
got=f"{data_array.dtype}",
|
|
95
145
|
) from e
|
|
96
146
|
|
|
147
|
+
|
|
148
|
+
def _convert_array_to_float64(data_array: Any, path: Path) -> NDArray[np.float64]:
|
|
149
|
+
"""Convert regular array to float64."""
|
|
97
150
|
try:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
got=f"Arrays: {', '.join(available)}",
|
|
108
|
-
)
|
|
151
|
+
converted: NDArray[np.float64] = np.asarray(data_array.astype(np.float64), dtype=np.float64)
|
|
152
|
+
return converted
|
|
153
|
+
except (ValueError, TypeError) as e:
|
|
154
|
+
raise FormatError(
|
|
155
|
+
"Data array is not numeric",
|
|
156
|
+
file_path=str(path),
|
|
157
|
+
expected="Numeric dtype (int, float)",
|
|
158
|
+
got=f"{data_array.dtype}",
|
|
159
|
+
) from e
|
|
109
160
|
|
|
110
|
-
# Convert to float64 (keep mmap if enabled)
|
|
111
|
-
if mmap and isinstance(data_array, np.memmap):
|
|
112
|
-
# Keep as memmap, just ensure float64 dtype
|
|
113
|
-
if data_array.dtype != np.float64:
|
|
114
|
-
# For memmap, we need to copy to convert dtype
|
|
115
|
-
try:
|
|
116
|
-
data = data_array.astype(np.float64)
|
|
117
|
-
except (ValueError, TypeError) as e:
|
|
118
|
-
raise FormatError(
|
|
119
|
-
"Data array is not numeric",
|
|
120
|
-
file_path=str(path),
|
|
121
|
-
expected="Numeric dtype (int, float)",
|
|
122
|
-
got=f"{data_array.dtype}",
|
|
123
|
-
) from e
|
|
124
|
-
else:
|
|
125
|
-
data = data_array
|
|
126
|
-
else:
|
|
127
|
-
try:
|
|
128
|
-
data = data_array.astype(np.float64)
|
|
129
|
-
except (ValueError, TypeError) as e:
|
|
130
|
-
raise FormatError(
|
|
131
|
-
"Data array is not numeric",
|
|
132
|
-
file_path=str(path),
|
|
133
|
-
expected="Numeric dtype (int, float)",
|
|
134
|
-
got=f"{data_array.dtype}",
|
|
135
|
-
) from e
|
|
136
|
-
|
|
137
|
-
# Extract metadata
|
|
138
|
-
detected_sample_rate = _find_metadata_value(npz, SAMPLE_RATE_KEYS)
|
|
139
|
-
detected_vertical_scale = _find_metadata_value(npz, VERTICAL_SCALE_KEYS)
|
|
140
|
-
detected_vertical_offset = _find_metadata_value(npz, VERTICAL_OFFSET_KEYS)
|
|
141
|
-
|
|
142
|
-
# Use provided sample_rate if specified
|
|
143
|
-
if sample_rate is not None:
|
|
144
|
-
detected_sample_rate = sample_rate
|
|
145
|
-
elif detected_sample_rate is None:
|
|
146
|
-
detected_sample_rate = 1e6 # Default to 1 MSa/s
|
|
147
|
-
|
|
148
|
-
# Build metadata
|
|
149
|
-
metadata = TraceMetadata(
|
|
150
|
-
sample_rate=float(detected_sample_rate),
|
|
151
|
-
vertical_scale=float(detected_vertical_scale)
|
|
152
|
-
if detected_vertical_scale is not None
|
|
153
|
-
else None,
|
|
154
|
-
vertical_offset=float(detected_vertical_offset)
|
|
155
|
-
if detected_vertical_offset is not None
|
|
156
|
-
else None,
|
|
157
|
-
source_file=str(path),
|
|
158
|
-
channel_name=_get_channel_name(npz, channel),
|
|
159
|
-
)
|
|
160
161
|
|
|
161
|
-
|
|
162
|
+
def _build_npz_metadata(
|
|
163
|
+
npz: Any, sample_rate: float | None, channel: str | int | None, path: Path
|
|
164
|
+
) -> TraceMetadata:
|
|
165
|
+
"""Build metadata from NPZ file."""
|
|
166
|
+
detected_sample_rate = _find_metadata_value(npz, SAMPLE_RATE_KEYS)
|
|
167
|
+
detected_vertical_scale = _find_metadata_value(npz, VERTICAL_SCALE_KEYS)
|
|
168
|
+
detected_vertical_offset = _find_metadata_value(npz, VERTICAL_OFFSET_KEYS)
|
|
162
169
|
|
|
163
|
-
|
|
164
|
-
|
|
170
|
+
final_sample_rate = sample_rate if sample_rate is not None else detected_sample_rate or 1e6
|
|
171
|
+
|
|
172
|
+
return TraceMetadata(
|
|
173
|
+
sample_rate=float(final_sample_rate),
|
|
174
|
+
vertical_scale=float(detected_vertical_scale) if detected_vertical_scale else None,
|
|
175
|
+
vertical_offset=float(detected_vertical_offset) if detected_vertical_offset else None,
|
|
176
|
+
source_file=str(path),
|
|
177
|
+
channel_name=_get_channel_name(npz, channel),
|
|
178
|
+
)
|
|
165
179
|
|
|
166
180
|
|
|
167
181
|
def _find_data_array(
|
|
@@ -181,28 +195,86 @@ def _find_data_array(
|
|
|
181
195
|
|
|
182
196
|
# If channel specified by name
|
|
183
197
|
if isinstance(channel, str):
|
|
184
|
-
|
|
185
|
-
return npz[channel]
|
|
186
|
-
# Try case-insensitive match
|
|
187
|
-
channel_lower = channel.lower()
|
|
188
|
-
for key in keys:
|
|
189
|
-
if key.lower() == channel_lower:
|
|
190
|
-
return npz[key]
|
|
191
|
-
return None
|
|
198
|
+
return _find_array_by_name(npz, keys, channel)
|
|
192
199
|
|
|
193
200
|
# If channel specified by index
|
|
194
201
|
if isinstance(channel, int):
|
|
195
|
-
|
|
196
|
-
channel_arrays = [k for k in keys if _is_channel_array(k)]
|
|
197
|
-
if channel_arrays and channel < len(channel_arrays):
|
|
198
|
-
return npz[sorted(channel_arrays)[channel]]
|
|
199
|
-
# Or use nth array
|
|
200
|
-
data_arrays = [k for k in keys if _is_data_array(k)]
|
|
201
|
-
if data_arrays and channel < len(data_arrays):
|
|
202
|
-
return npz[data_arrays[channel]]
|
|
203
|
-
return None
|
|
202
|
+
return _find_array_by_index(npz, keys, channel)
|
|
204
203
|
|
|
205
204
|
# Auto-detect: look for common data array names
|
|
205
|
+
return _find_array_auto_detect(npz, keys)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _find_array_by_name(
|
|
209
|
+
npz: np.lib.npyio.NpzFile,
|
|
210
|
+
keys: list[str],
|
|
211
|
+
channel: str,
|
|
212
|
+
) -> NDArray[np.float64] | None:
|
|
213
|
+
"""Find array by exact or case-insensitive channel name.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
npz: Loaded NPZ file.
|
|
217
|
+
keys: List of available keys.
|
|
218
|
+
channel: Channel name to find.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Array if found, None otherwise.
|
|
222
|
+
"""
|
|
223
|
+
# Exact match
|
|
224
|
+
if channel in keys:
|
|
225
|
+
return npz[channel]
|
|
226
|
+
|
|
227
|
+
# Case-insensitive match
|
|
228
|
+
channel_lower = channel.lower()
|
|
229
|
+
for key in keys:
|
|
230
|
+
if key.lower() == channel_lower:
|
|
231
|
+
return npz[key]
|
|
232
|
+
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _find_array_by_index(
|
|
237
|
+
npz: np.lib.npyio.NpzFile,
|
|
238
|
+
keys: list[str],
|
|
239
|
+
channel: int,
|
|
240
|
+
) -> NDArray[np.float64] | None:
|
|
241
|
+
"""Find array by channel index.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
npz: Loaded NPZ file.
|
|
245
|
+
keys: List of available keys.
|
|
246
|
+
channel: Channel index.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Array if found, None otherwise.
|
|
250
|
+
"""
|
|
251
|
+
# Try numeric-suffixed arrays (ch1, ch2, etc.)
|
|
252
|
+
channel_arrays = [k for k in keys if _is_channel_array(k)]
|
|
253
|
+
if channel_arrays and channel < len(channel_arrays):
|
|
254
|
+
return npz[sorted(channel_arrays)[channel]]
|
|
255
|
+
|
|
256
|
+
# Fall back to nth data array
|
|
257
|
+
data_arrays = [k for k in keys if _is_data_array(k)]
|
|
258
|
+
if data_arrays and channel < len(data_arrays):
|
|
259
|
+
return npz[data_arrays[channel]]
|
|
260
|
+
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _find_array_auto_detect(
|
|
265
|
+
npz: np.lib.npyio.NpzFile,
|
|
266
|
+
keys: list[str],
|
|
267
|
+
) -> NDArray[np.float64] | None:
|
|
268
|
+
"""Auto-detect waveform data array from common names.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
npz: Loaded NPZ file.
|
|
272
|
+
keys: List of available keys.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Array if found, None otherwise.
|
|
276
|
+
"""
|
|
277
|
+
# Look for common data array names
|
|
206
278
|
for name in DATA_ARRAY_NAMES:
|
|
207
279
|
if name in keys:
|
|
208
280
|
return npz[name]
|
|
@@ -212,13 +284,11 @@ def _find_data_array(
|
|
|
212
284
|
if key.lower() == name_lower:
|
|
213
285
|
return npz[key]
|
|
214
286
|
|
|
215
|
-
# Fall back to first 1D or 2D array
|
|
287
|
+
# Fall back to first 1D or 2D array (large enough to be data)
|
|
216
288
|
for key in keys:
|
|
217
289
|
arr = npz[key]
|
|
218
|
-
if isinstance(arr, np.ndarray) and arr.ndim in (1, 2):
|
|
219
|
-
|
|
220
|
-
if arr.size > 10: # Arbitrary threshold
|
|
221
|
-
return arr.ravel() if arr.ndim == 2 else arr
|
|
290
|
+
if isinstance(arr, np.ndarray) and arr.ndim in (1, 2) and arr.size > 10:
|
|
291
|
+
return arr.ravel() if arr.ndim == 2 else arr
|
|
222
292
|
|
|
223
293
|
return None
|
|
224
294
|
|
|
@@ -254,37 +324,104 @@ def _find_metadata_value(
|
|
|
254
324
|
"""
|
|
255
325
|
keys = list(npz.keys())
|
|
256
326
|
|
|
327
|
+
# Try exact and case-insensitive matches in keys
|
|
328
|
+
value = _find_value_in_keys(npz, keys, key_names)
|
|
329
|
+
if value is not None:
|
|
330
|
+
return value
|
|
331
|
+
|
|
332
|
+
# Check for metadata dict
|
|
333
|
+
return _find_value_in_metadata_dict(npz, keys, key_names)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _find_value_in_keys(
|
|
337
|
+
npz: np.lib.npyio.NpzFile,
|
|
338
|
+
keys: list[str],
|
|
339
|
+
key_names: list[str],
|
|
340
|
+
) -> float | None:
|
|
341
|
+
"""Find value by exact or case-insensitive match in keys.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
npz: Loaded NPZ file.
|
|
345
|
+
keys: List of available keys.
|
|
346
|
+
key_names: List of possible key names to try.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
Metadata value or None if not found.
|
|
350
|
+
"""
|
|
257
351
|
for name in key_names:
|
|
258
352
|
# Exact match
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return float(value) # type: ignore[arg-type]
|
|
263
|
-
elif isinstance(value, np.ndarray) and value.size == 1:
|
|
264
|
-
return float(value.item()) # type: ignore[arg-type]
|
|
353
|
+
value = _try_extract_float_from_key(npz, keys, name, name)
|
|
354
|
+
if value is not None:
|
|
355
|
+
return value
|
|
265
356
|
|
|
266
357
|
# Case-insensitive match
|
|
267
358
|
name_lower = name.lower()
|
|
268
359
|
for key in keys:
|
|
269
360
|
if key.lower() == name_lower:
|
|
270
|
-
value = npz
|
|
271
|
-
if
|
|
272
|
-
return
|
|
273
|
-
elif isinstance(value, np.ndarray) and value.size == 1:
|
|
274
|
-
return float(value.item()) # type: ignore[arg-type]
|
|
361
|
+
value = _try_extract_float_from_key(npz, keys, key, name_lower)
|
|
362
|
+
if value is not None:
|
|
363
|
+
return value
|
|
275
364
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
365
|
+
return None
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _try_extract_float_from_key(
|
|
369
|
+
npz: np.lib.npyio.NpzFile,
|
|
370
|
+
keys: list[str],
|
|
371
|
+
key: str,
|
|
372
|
+
_name: str,
|
|
373
|
+
) -> float | None:
|
|
374
|
+
"""Try to extract float value from NPZ key.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
npz: Loaded NPZ file.
|
|
378
|
+
keys: List of available keys.
|
|
379
|
+
key: Key to check.
|
|
380
|
+
_name: Original name (for matching, unused).
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
Float value or None.
|
|
384
|
+
"""
|
|
385
|
+
if key not in keys:
|
|
386
|
+
return None
|
|
387
|
+
|
|
388
|
+
value = npz[key]
|
|
389
|
+
if np.isscalar(value):
|
|
390
|
+
return float(value) # type: ignore[arg-type]
|
|
391
|
+
elif isinstance(value, np.ndarray) and value.size == 1:
|
|
392
|
+
return float(value.item())
|
|
393
|
+
|
|
394
|
+
return None
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _find_value_in_metadata_dict(
|
|
398
|
+
npz: np.lib.npyio.NpzFile,
|
|
399
|
+
keys: list[str],
|
|
400
|
+
key_names: list[str],
|
|
401
|
+
) -> float | None:
|
|
402
|
+
"""Find value in metadata dictionary.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
npz: Loaded NPZ file.
|
|
406
|
+
keys: List of available keys.
|
|
407
|
+
key_names: List of possible key names to try.
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Metadata value or None if not found.
|
|
411
|
+
"""
|
|
412
|
+
if "metadata" not in keys:
|
|
413
|
+
return None
|
|
414
|
+
|
|
415
|
+
metadata = npz["metadata"]
|
|
416
|
+
# NPZ files always return np.ndarray for valid keys
|
|
417
|
+
try:
|
|
418
|
+
meta_dict = metadata.item()
|
|
419
|
+
if isinstance(meta_dict, dict):
|
|
420
|
+
for name in key_names:
|
|
421
|
+
if name in meta_dict:
|
|
422
|
+
return float(meta_dict[name])
|
|
423
|
+
except (ValueError, TypeError):
|
|
424
|
+
pass
|
|
288
425
|
|
|
289
426
|
return None
|
|
290
427
|
|