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
|
@@ -8,12 +8,12 @@ accumulator pattern for rolling statistics.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import TYPE_CHECKING, Any
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
12
|
|
|
13
13
|
import numpy as np
|
|
14
14
|
from scipy import signal
|
|
15
15
|
|
|
16
|
-
from
|
|
16
|
+
from oscura.core.types import WaveformTrace
|
|
17
17
|
|
|
18
18
|
if TYPE_CHECKING:
|
|
19
19
|
from collections.abc import Callable, Generator
|
|
@@ -69,57 +69,99 @@ def load_trace_chunks(
|
|
|
69
69
|
References:
|
|
70
70
|
API-003: Streaming/Generator API for Large Files
|
|
71
71
|
"""
|
|
72
|
-
|
|
72
|
+
full_trace = _load_full_trace(file_path, loader)
|
|
73
|
+
chunk_samples, num_chunks = _compute_chunk_parameters(len(full_trace.data), chunk_size, overlap)
|
|
74
|
+
yield from _generate_chunks(full_trace, chunk_samples, overlap, num_chunks, progress_callback)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _load_full_trace(
|
|
78
|
+
file_path: str | Path, loader: Callable[[str | Path], WaveformTrace] | None
|
|
79
|
+
) -> WaveformTrace:
|
|
80
|
+
"""Load full trace metadata.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
file_path: Path to trace file.
|
|
84
|
+
loader: Optional custom loader.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
WaveformTrace object.
|
|
73
88
|
|
|
74
|
-
|
|
75
|
-
|
|
89
|
+
Raises:
|
|
90
|
+
ValueError: If failed to load trace metadata.
|
|
91
|
+
TypeError: If loaded object is not WaveformTrace.
|
|
92
|
+
"""
|
|
93
|
+
from oscura.loaders import load
|
|
76
94
|
|
|
77
|
-
|
|
95
|
+
file_path = Path(file_path)
|
|
78
96
|
load_func = loader if loader is not None else load
|
|
79
97
|
|
|
80
|
-
# Load full trace metadata to get total size
|
|
81
|
-
# For memory-mapped files, this doesn't load data
|
|
82
98
|
try:
|
|
83
|
-
|
|
99
|
+
loaded_trace = load_func(file_path)
|
|
84
100
|
except Exception as e:
|
|
85
101
|
raise ValueError(f"Failed to load trace metadata: {e}") from e
|
|
86
102
|
|
|
87
|
-
|
|
88
|
-
|
|
103
|
+
if not isinstance(loaded_trace, WaveformTrace):
|
|
104
|
+
raise TypeError(f"Expected WaveformTrace, got {type(loaded_trace)}")
|
|
105
|
+
|
|
106
|
+
return loaded_trace
|
|
107
|
+
|
|
89
108
|
|
|
90
|
-
|
|
109
|
+
def _compute_chunk_parameters(
|
|
110
|
+
total_samples: int, chunk_size: int | float, overlap: int
|
|
111
|
+
) -> tuple[int, int]:
|
|
112
|
+
"""Compute chunk size and number of chunks.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
total_samples: Total number of samples.
|
|
116
|
+
chunk_size: Size specification.
|
|
117
|
+
overlap: Overlap samples.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Tuple of (chunk_samples, num_chunks).
|
|
121
|
+
"""
|
|
122
|
+
chunk_samples = int(chunk_size) if chunk_size < 1e6 else int(chunk_size / 8)
|
|
91
123
|
num_chunks = (total_samples - overlap) // (chunk_samples - overlap)
|
|
92
124
|
if (total_samples - overlap) % (chunk_samples - overlap) != 0:
|
|
93
125
|
num_chunks += 1
|
|
126
|
+
return chunk_samples, num_chunks
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _generate_chunks(
|
|
130
|
+
full_trace: WaveformTrace,
|
|
131
|
+
chunk_samples: int,
|
|
132
|
+
overlap: int,
|
|
133
|
+
num_chunks: int,
|
|
134
|
+
progress_callback: Callable[[int, int], None] | None,
|
|
135
|
+
) -> Generator[WaveformTrace, None, None]:
|
|
136
|
+
"""Generate trace chunks.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
full_trace: Full trace object.
|
|
140
|
+
chunk_samples: Samples per chunk.
|
|
141
|
+
overlap: Overlap samples.
|
|
142
|
+
num_chunks: Total number of chunks.
|
|
143
|
+
progress_callback: Optional progress callback.
|
|
94
144
|
|
|
95
|
-
|
|
145
|
+
Yields:
|
|
146
|
+
WaveformTrace chunks.
|
|
147
|
+
"""
|
|
148
|
+
total_samples = len(full_trace.data)
|
|
96
149
|
chunk_num = 0
|
|
97
150
|
start_idx = 0
|
|
98
151
|
|
|
99
152
|
while start_idx < total_samples:
|
|
100
153
|
end_idx = min(start_idx + chunk_samples, total_samples)
|
|
154
|
+
chunk_data = full_trace.data[start_idx:end_idx]
|
|
155
|
+
chunk_trace = WaveformTrace(data=chunk_data, metadata=full_trace.metadata)
|
|
101
156
|
|
|
102
|
-
# Extract chunk
|
|
103
|
-
chunk_data = full_trace.data[start_idx:end_idx] # type: ignore[union-attr]
|
|
104
|
-
|
|
105
|
-
# Create chunk trace with same metadata
|
|
106
|
-
# Cast needed for mypy: slicing a floating array returns a floating array
|
|
107
|
-
chunk_trace = WaveformTrace(
|
|
108
|
-
data=cast("NDArray[np.floating[Any]]", chunk_data),
|
|
109
|
-
metadata=full_trace.metadata,
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
# Call progress callback if provided
|
|
113
157
|
if progress_callback is not None:
|
|
114
158
|
progress_callback(chunk_num, num_chunks)
|
|
115
159
|
|
|
116
160
|
yield chunk_trace
|
|
117
161
|
|
|
118
|
-
# Move to next chunk, accounting for overlap
|
|
119
162
|
start_idx = end_idx - overlap
|
|
120
163
|
chunk_num += 1
|
|
121
164
|
|
|
122
|
-
# Break if we've reached the end
|
|
123
165
|
if end_idx >= total_samples:
|
|
124
166
|
break
|
|
125
167
|
|
|
@@ -347,6 +389,121 @@ class StreamingAnalyzer:
|
|
|
347
389
|
self._hist_edges = None
|
|
348
390
|
|
|
349
391
|
|
|
392
|
+
def _prepare_spectrogram_params(
|
|
393
|
+
n: int, nperseg: int, noverlap: int | None, overlap: int, chunk_size: int
|
|
394
|
+
) -> tuple[int, int]:
|
|
395
|
+
"""Prepare spectrogram parameters and validate inputs.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
n: Signal length.
|
|
399
|
+
nperseg: Segment length for STFT.
|
|
400
|
+
noverlap: Overlap between STFT segments.
|
|
401
|
+
overlap: Overlap between chunks.
|
|
402
|
+
chunk_size: Chunk size.
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
Tuple of (noverlap, overlap).
|
|
406
|
+
"""
|
|
407
|
+
if noverlap is None:
|
|
408
|
+
noverlap = nperseg // 2
|
|
409
|
+
|
|
410
|
+
# Auto-adjust overlap if not specified to ensure continuity
|
|
411
|
+
if overlap == 0:
|
|
412
|
+
overlap = 2 * nperseg
|
|
413
|
+
|
|
414
|
+
return noverlap, overlap
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _compute_single_chunk_spectrogram(
|
|
418
|
+
data: NDArray[np.float64],
|
|
419
|
+
sample_rate: float,
|
|
420
|
+
window: str,
|
|
421
|
+
nperseg: int,
|
|
422
|
+
noverlap: int,
|
|
423
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]:
|
|
424
|
+
"""Compute spectrogram for single chunk that fits in memory.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
data: Input signal.
|
|
428
|
+
sample_rate: Sample rate in Hz.
|
|
429
|
+
window: Window function name.
|
|
430
|
+
nperseg: Segment length for STFT.
|
|
431
|
+
noverlap: Overlap between STFT segments.
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
(times, frequencies, Sxx_db) tuple.
|
|
435
|
+
"""
|
|
436
|
+
freq, times, Sxx = signal.spectrogram(
|
|
437
|
+
data,
|
|
438
|
+
fs=sample_rate,
|
|
439
|
+
window=window,
|
|
440
|
+
nperseg=nperseg,
|
|
441
|
+
noverlap=noverlap,
|
|
442
|
+
scaling="spectrum",
|
|
443
|
+
)
|
|
444
|
+
Sxx = np.maximum(Sxx, 1e-20)
|
|
445
|
+
Sxx_db = 10 * np.log10(Sxx)
|
|
446
|
+
return times, freq, Sxx_db
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _process_spectrogram_chunk(
|
|
450
|
+
data: NDArray[np.float64],
|
|
451
|
+
chunk_start: int,
|
|
452
|
+
chunk_end: int,
|
|
453
|
+
overlap: int,
|
|
454
|
+
n: int,
|
|
455
|
+
sample_rate: float,
|
|
456
|
+
window: str,
|
|
457
|
+
nperseg: int,
|
|
458
|
+
noverlap: int,
|
|
459
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]:
|
|
460
|
+
"""Process single chunk of spectrogram.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
data: Full signal array.
|
|
464
|
+
chunk_start: Chunk start index.
|
|
465
|
+
chunk_end: Chunk end index.
|
|
466
|
+
overlap: Overlap samples.
|
|
467
|
+
n: Total signal length.
|
|
468
|
+
sample_rate: Sample rate in Hz.
|
|
469
|
+
window: Window function.
|
|
470
|
+
nperseg: Segment length.
|
|
471
|
+
noverlap: Overlap between segments.
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
(freq, times_adjusted, Sxx_chunk) for this chunk.
|
|
475
|
+
"""
|
|
476
|
+
extended_start = max(0, chunk_start - overlap)
|
|
477
|
+
extended_end = min(n, chunk_end + overlap)
|
|
478
|
+
|
|
479
|
+
chunk_data = data[extended_start:extended_end]
|
|
480
|
+
|
|
481
|
+
freq, times_chunk, Sxx_chunk = signal.spectrogram(
|
|
482
|
+
chunk_data,
|
|
483
|
+
fs=sample_rate,
|
|
484
|
+
window=window,
|
|
485
|
+
nperseg=nperseg,
|
|
486
|
+
noverlap=noverlap,
|
|
487
|
+
scaling="spectrum",
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
# Adjust time axis and trim overlap
|
|
491
|
+
time_offset = extended_start / sample_rate
|
|
492
|
+
times_chunk_adjusted = times_chunk + time_offset
|
|
493
|
+
|
|
494
|
+
valid_time_start = chunk_start / sample_rate
|
|
495
|
+
valid_time_end = chunk_end / sample_rate
|
|
496
|
+
|
|
497
|
+
valid_mask = (times_chunk_adjusted >= valid_time_start) & (
|
|
498
|
+
times_chunk_adjusted < valid_time_end
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
Sxx_chunk = Sxx_chunk[:, valid_mask]
|
|
502
|
+
times_chunk_adjusted = times_chunk_adjusted[valid_mask]
|
|
503
|
+
|
|
504
|
+
return freq, times_chunk_adjusted, Sxx_chunk
|
|
505
|
+
|
|
506
|
+
|
|
350
507
|
def chunked_spectrogram(
|
|
351
508
|
data: NDArray[np.float64],
|
|
352
509
|
sample_rate: float,
|
|
@@ -394,31 +551,14 @@ def chunked_spectrogram(
|
|
|
394
551
|
"""
|
|
395
552
|
n = len(data)
|
|
396
553
|
|
|
397
|
-
# Handle empty input
|
|
398
554
|
if n == 0:
|
|
399
555
|
return np.array([]), np.array([]), np.array([]).reshape(0, 0)
|
|
400
556
|
|
|
401
|
-
|
|
402
|
-
noverlap = nperseg // 2
|
|
403
|
-
|
|
404
|
-
# Auto-adjust overlap if not specified to ensure continuity
|
|
405
|
-
if overlap == 0:
|
|
406
|
-
overlap = 2 * nperseg
|
|
557
|
+
noverlap, overlap = _prepare_spectrogram_params(n, nperseg, noverlap, overlap, chunk_size)
|
|
407
558
|
|
|
408
|
-
#
|
|
559
|
+
# Single chunk optimization
|
|
409
560
|
if n <= chunk_size:
|
|
410
|
-
|
|
411
|
-
data,
|
|
412
|
-
fs=sample_rate,
|
|
413
|
-
window=window,
|
|
414
|
-
nperseg=nperseg,
|
|
415
|
-
noverlap=noverlap,
|
|
416
|
-
scaling="spectrum",
|
|
417
|
-
)
|
|
418
|
-
# Convert to dB
|
|
419
|
-
Sxx = np.maximum(Sxx, 1e-20)
|
|
420
|
-
Sxx_db = 10 * np.log10(Sxx)
|
|
421
|
-
return times, freq, Sxx_db
|
|
561
|
+
return _compute_single_chunk_spectrogram(data, sample_rate, window, nperseg, noverlap)
|
|
422
562
|
|
|
423
563
|
# Process chunks
|
|
424
564
|
chunks_stft = []
|
|
@@ -426,61 +566,133 @@ def chunked_spectrogram(
|
|
|
426
566
|
chunk_start = 0
|
|
427
567
|
|
|
428
568
|
while chunk_start < n:
|
|
429
|
-
# Determine chunk boundaries with overlap
|
|
430
569
|
chunk_end = min(chunk_start + chunk_size, n)
|
|
431
570
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
extended_end = min(n, chunk_end + overlap)
|
|
435
|
-
|
|
436
|
-
chunk_data = data[extended_start:extended_end]
|
|
437
|
-
|
|
438
|
-
# Compute spectrogram for chunk
|
|
439
|
-
freq, times_chunk, Sxx_chunk = signal.spectrogram(
|
|
440
|
-
chunk_data,
|
|
441
|
-
fs=sample_rate,
|
|
442
|
-
window=window,
|
|
443
|
-
nperseg=nperseg,
|
|
444
|
-
noverlap=noverlap,
|
|
445
|
-
scaling="spectrum",
|
|
446
|
-
)
|
|
447
|
-
|
|
448
|
-
# Adjust time axis for chunk position
|
|
449
|
-
time_offset = extended_start / sample_rate
|
|
450
|
-
times_chunk_adjusted = times_chunk + time_offset
|
|
451
|
-
|
|
452
|
-
# Trim overlap regions to avoid duplication
|
|
453
|
-
valid_time_start = chunk_start / sample_rate
|
|
454
|
-
valid_time_end = chunk_end / sample_rate
|
|
455
|
-
|
|
456
|
-
valid_mask = (times_chunk_adjusted >= valid_time_start) & (
|
|
457
|
-
times_chunk_adjusted < valid_time_end
|
|
571
|
+
freq, times_adjusted, Sxx_chunk = _process_spectrogram_chunk(
|
|
572
|
+
data, chunk_start, chunk_end, overlap, n, sample_rate, window, nperseg, noverlap
|
|
458
573
|
)
|
|
459
574
|
|
|
460
|
-
if
|
|
461
|
-
Sxx_chunk = Sxx_chunk[:, valid_mask]
|
|
462
|
-
times_chunk_adjusted = times_chunk_adjusted[valid_mask]
|
|
463
|
-
|
|
575
|
+
if Sxx_chunk.shape[1] > 0:
|
|
464
576
|
chunks_stft.append(Sxx_chunk)
|
|
465
|
-
chunks_times.append(
|
|
577
|
+
chunks_times.append(times_adjusted)
|
|
466
578
|
|
|
467
|
-
# Move to next chunk
|
|
468
579
|
chunk_start = chunk_end
|
|
469
580
|
|
|
470
|
-
# Concatenate all chunks
|
|
471
581
|
if len(chunks_stft) == 0:
|
|
472
582
|
raise ValueError("No valid chunks produced")
|
|
473
583
|
|
|
474
584
|
Sxx = np.concatenate(chunks_stft, axis=1)
|
|
475
585
|
times = np.concatenate(chunks_times)
|
|
476
586
|
|
|
477
|
-
# Convert to dB
|
|
478
587
|
Sxx = np.maximum(Sxx, 1e-20)
|
|
479
|
-
Sxx_db = 10 * np.log10(Sxx)
|
|
588
|
+
Sxx_db: NDArray[np.float64] = np.asarray(10 * np.log10(Sxx), dtype=np.float64)
|
|
480
589
|
|
|
481
590
|
return times, freq, Sxx_db
|
|
482
591
|
|
|
483
592
|
|
|
593
|
+
def _compute_single_chunk_fft(
|
|
594
|
+
data: NDArray[np.float64],
|
|
595
|
+
sample_rate: float,
|
|
596
|
+
window: str,
|
|
597
|
+
nfft: int | None,
|
|
598
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
599
|
+
"""Compute FFT for signal that fits in single chunk.
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
data: Input signal.
|
|
603
|
+
sample_rate: Sample rate in Hz.
|
|
604
|
+
window: Window function name.
|
|
605
|
+
nfft: FFT length.
|
|
606
|
+
|
|
607
|
+
Returns:
|
|
608
|
+
Tuple of (frequencies, magnitude_db).
|
|
609
|
+
"""
|
|
610
|
+
from oscura.utils.windowing import get_window
|
|
611
|
+
|
|
612
|
+
n = len(data)
|
|
613
|
+
nfft = nfft if nfft is not None else int(2 ** np.ceil(np.log2(n)))
|
|
614
|
+
|
|
615
|
+
w = get_window(window, n)
|
|
616
|
+
data_windowed = data * w
|
|
617
|
+
spectrum = np.fft.rfft(data_windowed, n=nfft)
|
|
618
|
+
freq = np.fft.rfftfreq(nfft, d=1.0 / sample_rate)
|
|
619
|
+
|
|
620
|
+
window_gain = np.sum(w) / n
|
|
621
|
+
magnitude = np.abs(spectrum) / (n * window_gain)
|
|
622
|
+
magnitude = np.maximum(magnitude, 1e-20)
|
|
623
|
+
magnitude_db = 20 * np.log10(magnitude)
|
|
624
|
+
|
|
625
|
+
return freq, magnitude_db
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _setup_chunked_fft_params(
|
|
629
|
+
chunk_size: int, overlap: float, nfft_in: int | None, window: str, sample_rate: float
|
|
630
|
+
) -> tuple[int, int, NDArray[np.float64], float, NDArray[np.float64]]:
|
|
631
|
+
"""Setup parameters for chunked FFT processing.
|
|
632
|
+
|
|
633
|
+
Args:
|
|
634
|
+
chunk_size: Chunk size.
|
|
635
|
+
overlap: Overlap percentage.
|
|
636
|
+
nfft: FFT length.
|
|
637
|
+
window: Window function name.
|
|
638
|
+
sample_rate: Sample rate in Hz.
|
|
639
|
+
|
|
640
|
+
Returns:
|
|
641
|
+
Tuple of (hop, nfft, w, window_gain, freq).
|
|
642
|
+
"""
|
|
643
|
+
from oscura.utils.windowing import get_window
|
|
644
|
+
|
|
645
|
+
overlap_samples = int(chunk_size * overlap / 100.0)
|
|
646
|
+
hop = chunk_size - overlap_samples
|
|
647
|
+
nfft = nfft_in if nfft_in is not None else int(2 ** np.ceil(np.log2(chunk_size)))
|
|
648
|
+
|
|
649
|
+
w = get_window(window, chunk_size)
|
|
650
|
+
window_gain_raw = np.sum(w) / chunk_size
|
|
651
|
+
window_gain: float = float(window_gain_raw)
|
|
652
|
+
freq: NDArray[np.float64] = np.asarray(
|
|
653
|
+
np.fft.rfftfreq(nfft, d=1.0 / sample_rate), dtype=np.float64
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
return hop, nfft, w, window_gain, freq
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _process_fft_segment(
|
|
660
|
+
data: NDArray[np.float64],
|
|
661
|
+
start: int,
|
|
662
|
+
chunk_size: int,
|
|
663
|
+
n: int,
|
|
664
|
+
w: NDArray[np.float64],
|
|
665
|
+
window_gain: float,
|
|
666
|
+
nfft: int,
|
|
667
|
+
) -> NDArray[np.float64]:
|
|
668
|
+
"""Process single FFT segment.
|
|
669
|
+
|
|
670
|
+
Args:
|
|
671
|
+
data: Input signal.
|
|
672
|
+
start: Segment start index.
|
|
673
|
+
chunk_size: Chunk size.
|
|
674
|
+
n: Total signal length.
|
|
675
|
+
w: Window function.
|
|
676
|
+
window_gain: Window gain.
|
|
677
|
+
nfft: FFT length.
|
|
678
|
+
|
|
679
|
+
Returns:
|
|
680
|
+
Magnitude spectrum for segment.
|
|
681
|
+
"""
|
|
682
|
+
end = min(start + chunk_size, n)
|
|
683
|
+
|
|
684
|
+
if end - start < chunk_size:
|
|
685
|
+
segment = np.zeros(chunk_size)
|
|
686
|
+
segment[: end - start] = data[start:end]
|
|
687
|
+
else:
|
|
688
|
+
segment = data[start:end]
|
|
689
|
+
|
|
690
|
+
segment = segment - np.mean(segment)
|
|
691
|
+
segment_windowed = segment * w
|
|
692
|
+
spectrum = np.fft.rfft(segment_windowed, n=nfft)
|
|
693
|
+
return np.abs(spectrum) / (chunk_size * window_gain)
|
|
694
|
+
|
|
695
|
+
|
|
484
696
|
def chunked_fft(
|
|
485
697
|
data: NDArray[np.float64],
|
|
486
698
|
sample_rate: float,
|
|
@@ -519,84 +731,31 @@ def chunked_fft(
|
|
|
519
731
|
MEM-006: Chunked FFT requirement
|
|
520
732
|
Welch's method for spectral estimation
|
|
521
733
|
"""
|
|
522
|
-
from ..utils.windowing import get_window
|
|
523
|
-
|
|
524
734
|
n = len(data)
|
|
525
735
|
|
|
526
|
-
#
|
|
736
|
+
# Setup: handle empty or single-chunk signals
|
|
527
737
|
if n == 0:
|
|
528
738
|
return np.array([]), np.array([])
|
|
529
739
|
|
|
530
|
-
# If data fits in one chunk, compute single FFT
|
|
531
740
|
if n <= chunk_size:
|
|
532
|
-
|
|
533
|
-
nfft = int(2 ** np.ceil(np.log2(n)))
|
|
534
|
-
|
|
535
|
-
# Apply window
|
|
536
|
-
w = get_window(window, n)
|
|
537
|
-
data_windowed = data * w
|
|
538
|
-
|
|
539
|
-
# Compute FFT
|
|
540
|
-
spectrum = np.fft.rfft(data_windowed, n=nfft)
|
|
541
|
-
|
|
542
|
-
# Frequency axis
|
|
543
|
-
freq = np.fft.rfftfreq(nfft, d=1.0 / sample_rate)
|
|
544
|
-
|
|
545
|
-
# Magnitude in dB (normalized by window gain)
|
|
546
|
-
window_gain = np.sum(w) / n
|
|
547
|
-
magnitude = np.abs(spectrum) / (n * window_gain)
|
|
548
|
-
magnitude = np.maximum(magnitude, 1e-20)
|
|
549
|
-
magnitude_db = 20 * np.log10(magnitude)
|
|
741
|
+
return _compute_single_chunk_fft(data, sample_rate, window, nfft)
|
|
550
742
|
|
|
551
|
-
|
|
743
|
+
# Processing: setup parameters and process segments
|
|
744
|
+
hop, nfft, w, window_gain, freq = _setup_chunked_fft_params(
|
|
745
|
+
chunk_size, overlap, nfft, window, sample_rate
|
|
746
|
+
)
|
|
552
747
|
|
|
553
|
-
# Calculate overlap
|
|
554
748
|
overlap_samples = int(chunk_size * overlap / 100.0)
|
|
555
|
-
hop = chunk_size - overlap_samples
|
|
556
|
-
|
|
557
|
-
# Determine number of segments
|
|
558
749
|
num_segments = max(1, (n - overlap_samples) // hop)
|
|
559
|
-
|
|
560
|
-
if nfft is None:
|
|
561
|
-
nfft = int(2 ** np.ceil(np.log2(chunk_size)))
|
|
562
|
-
|
|
563
|
-
# Prepare window
|
|
564
|
-
w = get_window(window, chunk_size)
|
|
565
|
-
window_gain = np.sum(w) / chunk_size
|
|
566
|
-
|
|
567
|
-
# Accumulate magnitude spectra
|
|
568
|
-
freq = np.fft.rfftfreq(nfft, d=1.0 / sample_rate)
|
|
569
750
|
magnitude_sum = np.zeros(len(freq))
|
|
570
751
|
|
|
571
752
|
for i in range(num_segments):
|
|
572
753
|
start = i * hop
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
# Extract segment
|
|
576
|
-
if end - start < chunk_size:
|
|
577
|
-
# Last segment: pad with zeros
|
|
578
|
-
segment = np.zeros(chunk_size)
|
|
579
|
-
segment[: end - start] = data[start:end]
|
|
580
|
-
else:
|
|
581
|
-
segment = data[start:end]
|
|
582
|
-
|
|
583
|
-
# Detrend (remove mean)
|
|
584
|
-
segment = segment - np.mean(segment)
|
|
585
|
-
|
|
586
|
-
# Window
|
|
587
|
-
segment_windowed = segment * w
|
|
588
|
-
|
|
589
|
-
# FFT
|
|
590
|
-
spectrum = np.fft.rfft(segment_windowed, n=nfft)
|
|
591
|
-
|
|
592
|
-
# Accumulate magnitude
|
|
593
|
-
magnitude = np.abs(spectrum) / (chunk_size * window_gain)
|
|
754
|
+
magnitude = _process_fft_segment(data, start, chunk_size, n, w, window_gain, nfft)
|
|
594
755
|
magnitude_sum += magnitude
|
|
595
756
|
|
|
596
|
-
#
|
|
757
|
+
# Result building: average and convert to dB
|
|
597
758
|
magnitude_avg = magnitude_sum / num_segments
|
|
598
|
-
|
|
599
|
-
# Convert to dB
|
|
600
759
|
magnitude_avg = np.maximum(magnitude_avg, 1e-20)
|
|
601
760
|
magnitude_db = 20 * np.log10(magnitude_avg)
|
|
602
761
|
|
|
@@ -16,7 +16,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
16
16
|
|
|
17
17
|
import numpy as np
|
|
18
18
|
|
|
19
|
-
from
|
|
19
|
+
from oscura.core.types import TraceMetadata, WaveformTrace
|
|
20
20
|
|
|
21
21
|
if TYPE_CHECKING:
|
|
22
22
|
from collections.abc import Generator
|
|
@@ -335,7 +335,8 @@ class SimulatedSource(RealtimeSource):
|
|
|
335
335
|
) % (2 * np.pi)
|
|
336
336
|
|
|
337
337
|
# Cast to the expected return type
|
|
338
|
-
|
|
338
|
+
result: NDArray[np.floating[Any]] = signal.astype(np.float64)
|
|
339
|
+
return result
|
|
339
340
|
|
|
340
341
|
def start(self) -> None:
|
|
341
342
|
"""Start acquisition."""
|
|
@@ -5,35 +5,35 @@ triggering, pattern triggering, pulse width triggering, glitch detection,
|
|
|
5
5
|
runt pulse detection, and window/zone triggering.
|
|
6
6
|
|
|
7
7
|
Example:
|
|
8
|
-
>>> from oscura.triggering import EdgeTrigger, find_triggers
|
|
8
|
+
>>> from oscura.utils.triggering import EdgeTrigger, find_triggers
|
|
9
9
|
>>> trigger = EdgeTrigger(level=1.5, edge="rising")
|
|
10
10
|
>>> events = trigger.find_events(trace)
|
|
11
11
|
>>> # Or use convenience function
|
|
12
12
|
>>> events = find_triggers(trace, "edge", level=1.5, edge="rising")
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
-
from oscura.triggering.base import (
|
|
15
|
+
from oscura.utils.triggering.base import (
|
|
16
16
|
Trigger,
|
|
17
17
|
TriggerEvent,
|
|
18
18
|
find_triggers,
|
|
19
19
|
)
|
|
20
|
-
from oscura.triggering.edge import (
|
|
20
|
+
from oscura.utils.triggering.edge import (
|
|
21
21
|
EdgeTrigger,
|
|
22
22
|
find_all_edges,
|
|
23
23
|
find_falling_edges,
|
|
24
24
|
find_rising_edges,
|
|
25
25
|
)
|
|
26
|
-
from oscura.triggering.pattern import (
|
|
26
|
+
from oscura.utils.triggering.pattern import (
|
|
27
27
|
PatternTrigger,
|
|
28
28
|
find_pattern,
|
|
29
29
|
)
|
|
30
|
-
from oscura.triggering.pulse import (
|
|
30
|
+
from oscura.utils.triggering.pulse import (
|
|
31
31
|
PulseWidthTrigger,
|
|
32
32
|
find_glitches,
|
|
33
33
|
find_pulses,
|
|
34
34
|
find_runt_pulses,
|
|
35
35
|
)
|
|
36
|
-
from oscura.triggering.window import (
|
|
36
|
+
from oscura.utils.triggering.window import (
|
|
37
37
|
WindowTrigger,
|
|
38
38
|
ZoneTrigger,
|
|
39
39
|
check_limits,
|
|
@@ -134,9 +134,9 @@ def find_triggers(
|
|
|
134
134
|
>>> events = find_triggers(trace, "pulse_width", min_width=1e-6, max_width=2e-6)
|
|
135
135
|
>>> events = find_triggers(trace, "glitch", max_width=50e-9)
|
|
136
136
|
"""
|
|
137
|
-
from oscura.triggering.edge import EdgeTrigger
|
|
138
|
-
from oscura.triggering.pulse import PulseWidthTrigger
|
|
139
|
-
from oscura.triggering.window import WindowTrigger
|
|
137
|
+
from oscura.utils.triggering.edge import EdgeTrigger
|
|
138
|
+
from oscura.utils.triggering.pulse import PulseWidthTrigger
|
|
139
|
+
from oscura.utils.triggering.window import WindowTrigger
|
|
140
140
|
|
|
141
141
|
if trigger_type == "edge":
|
|
142
142
|
trigger = EdgeTrigger(
|
|
@@ -145,7 +145,7 @@ def find_triggers(
|
|
|
145
145
|
hysteresis=kwargs.get("hysteresis", 0.0),
|
|
146
146
|
)
|
|
147
147
|
elif trigger_type == "pattern":
|
|
148
|
-
from oscura.triggering.pattern import PatternTrigger
|
|
148
|
+
from oscura.utils.triggering.pattern import PatternTrigger
|
|
149
149
|
|
|
150
150
|
trigger = PatternTrigger( # type: ignore[assignment]
|
|
151
151
|
pattern=kwargs.get("pattern", []),
|
|
@@ -159,7 +159,7 @@ def find_triggers(
|
|
|
159
159
|
max_width=kwargs.get("max_width"),
|
|
160
160
|
)
|
|
161
161
|
elif trigger_type == "glitch":
|
|
162
|
-
from oscura.triggering.pulse import GlitchTrigger
|
|
162
|
+
from oscura.utils.triggering.pulse import GlitchTrigger
|
|
163
163
|
|
|
164
164
|
trigger = GlitchTrigger( # type: ignore[assignment]
|
|
165
165
|
level=kwargs.get("level", 0.0),
|
|
@@ -167,7 +167,7 @@ def find_triggers(
|
|
|
167
167
|
polarity=kwargs.get("polarity", "either"),
|
|
168
168
|
)
|
|
169
169
|
elif trigger_type == "runt":
|
|
170
|
-
from oscura.triggering.pulse import RuntTrigger
|
|
170
|
+
from oscura.utils.triggering.pulse import RuntTrigger
|
|
171
171
|
|
|
172
172
|
trigger = RuntTrigger( # type: ignore[assignment]
|
|
173
173
|
low_threshold=kwargs.get("low_threshold", 0.0),
|
|
@@ -4,7 +4,7 @@ Provides edge detection with configurable thresholds, hysteresis,
|
|
|
4
4
|
and edge polarity (rising, falling, or both).
|
|
5
5
|
|
|
6
6
|
Example:
|
|
7
|
-
>>> from oscura.triggering.edge import EdgeTrigger, find_rising_edges
|
|
7
|
+
>>> from oscura.utils.triggering.edge import EdgeTrigger, find_rising_edges
|
|
8
8
|
>>> # Object-oriented approach
|
|
9
9
|
>>> trigger = EdgeTrigger(level=1.5, edge="rising", hysteresis=0.1)
|
|
10
10
|
>>> events = trigger.find_events(trace)
|
|
@@ -19,7 +19,7 @@ from typing import TYPE_CHECKING, Any, Literal
|
|
|
19
19
|
import numpy as np
|
|
20
20
|
|
|
21
21
|
from oscura.core.types import DigitalTrace, WaveformTrace
|
|
22
|
-
from oscura.triggering.base import (
|
|
22
|
+
from oscura.utils.triggering.base import (
|
|
23
23
|
Trigger,
|
|
24
24
|
TriggerEvent,
|
|
25
25
|
TriggerType,
|