oscura 0.5.0__py3-none-any.whl → 0.6.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/__init__.py +0 -48
- oscura/analyzers/digital/edges.py +325 -65
- oscura/analyzers/digital/extraction.py +0 -195
- 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/__init__.py +1 -22
- 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 +2763 -0
- 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/core/schemas/bus_configuration.json +322 -0
- oscura/core/schemas/device_mapping.json +182 -0
- oscura/core/schemas/packet_format.json +418 -0
- oscura/core/schemas/protocol_definition.json +363 -0
- 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 -20
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- oscura/export/wireshark/README.md +15 -15
- 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/README.md +7 -7
- 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 +171 -63
- 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/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -7
- 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/reporting/templates/index.md +13 -13
- 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/autodetect.py +1 -5
- 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 +11 -3
- 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.6.0.dist-info/METADATA +643 -0
- oscura-0.6.0.dist-info/RECORD +590 -0
- oscura/analyzers/digital/ic_database.py +0 -498
- oscura/analyzers/digital/timing_paths.py +0 -339
- oscura/analyzers/digital/vintage.py +0 -377
- oscura/analyzers/digital/vintage_result.py +0 -148
- oscura/analyzers/protocols/parallel_bus.py +0 -449
- 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/export/wavedrom.py +0 -430
- 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 -338
- 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/exporters/vintage_logic_csv.py +0 -247
- oscura/reporting/vintage_logic_report.py +0 -523
- 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/visualization/digital_advanced.py +0 -718
- oscura/visualization/figure_manager.py +0 -156
- oscura/workflow/__init__.py +0 -13
- oscura-0.5.0.dist-info/METADATA +0 -407
- oscura-0.5.0.dist-info/RECORD +0 -486
- /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/{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.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -25,12 +25,68 @@ from typing import TYPE_CHECKING, Any, Literal, cast
|
|
|
25
25
|
|
|
26
26
|
import numpy as np
|
|
27
27
|
|
|
28
|
+
from oscura.core.numba_backend import njit, prange
|
|
28
29
|
from oscura.core.types import WaveformTrace
|
|
29
30
|
|
|
30
31
|
if TYPE_CHECKING:
|
|
31
32
|
from numpy.typing import NDArray
|
|
32
33
|
|
|
33
34
|
|
|
35
|
+
@njit(parallel=True, cache=True) # type: ignore[untyped-decorator] # Numba JIT decorator
|
|
36
|
+
def _autocorr_direct_numba(
|
|
37
|
+
data: np.ndarray, # type: ignore[type-arg]
|
|
38
|
+
max_lag: int,
|
|
39
|
+
) -> np.ndarray: # type: ignore[type-arg]
|
|
40
|
+
"""Compute autocorrelation using direct method with Numba JIT compilation.
|
|
41
|
+
|
|
42
|
+
Alternative implementation using Numba for autocorrelation computation.
|
|
43
|
+
Uses parallel execution across lags for potential speedup on multi-core systems.
|
|
44
|
+
|
|
45
|
+
**Note**: In practice, numpy.correlate is faster for most cases due to highly
|
|
46
|
+
optimized BLAS/LAPACK routines. This function is provided for educational
|
|
47
|
+
purposes and specific use cases where custom computation logic is needed.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
data: Mean-centered input signal data (1D array).
|
|
51
|
+
max_lag: Maximum lag to compute (inclusive).
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Autocorrelation values from lag 0 to max_lag (unnormalized).
|
|
55
|
+
|
|
56
|
+
Performance characteristics:
|
|
57
|
+
- First call: ~100-200ms compilation overhead (cached for subsequent calls)
|
|
58
|
+
- Typical performance: ~2ms for n=100, ~10ms for n=256 (compiled)
|
|
59
|
+
- NumPy's correlate: ~0.02ms for n=100 (100x faster due to BLAS)
|
|
60
|
+
- Parallel execution: Benefits from multi-core for large max_lag
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
>>> import numpy as np
|
|
64
|
+
>>> data = np.random.randn(128) - np.mean(np.random.randn(128))
|
|
65
|
+
>>> acf = _autocorr_direct_numba(data, max_lag=64)
|
|
66
|
+
>>> print(acf.shape) # (65,)
|
|
67
|
+
|
|
68
|
+
Notes:
|
|
69
|
+
- Input data should be mean-centered before calling this function
|
|
70
|
+
- Result is NOT normalized; caller must normalize if needed
|
|
71
|
+
- NumPy's correlate is recommended for production use (faster)
|
|
72
|
+
- Thread safety: Numba releases GIL, safe for parallel execution
|
|
73
|
+
|
|
74
|
+
References:
|
|
75
|
+
Box, G. E. P. & Jenkins, G. M. (1976). Time Series Analysis
|
|
76
|
+
"""
|
|
77
|
+
n = len(data)
|
|
78
|
+
acf = np.zeros(max_lag + 1, dtype=np.float64)
|
|
79
|
+
|
|
80
|
+
# Compute autocorrelation for each lag in parallel
|
|
81
|
+
for lag in prange(max_lag + 1):
|
|
82
|
+
sum_val = 0.0
|
|
83
|
+
for i in range(n - lag):
|
|
84
|
+
sum_val += data[i] * data[i + lag]
|
|
85
|
+
acf[lag] = sum_val
|
|
86
|
+
|
|
87
|
+
return acf
|
|
88
|
+
|
|
89
|
+
|
|
34
90
|
@dataclass
|
|
35
91
|
class CrossCorrelationResult:
|
|
36
92
|
"""Result of cross-correlation analysis.
|
|
@@ -66,6 +122,10 @@ def autocorrelation(
|
|
|
66
122
|
Measures self-similarity of a signal at different time lags.
|
|
67
123
|
Useful for detecting periodicities and characteristic time scales.
|
|
68
124
|
|
|
125
|
+
This function automatically selects the optimal computation method:
|
|
126
|
+
- Small signals (n < 256): Direct method using numpy.correlate (optimized BLAS)
|
|
127
|
+
- Large signals (n >= 256): FFT-based method (O(n log n) complexity)
|
|
128
|
+
|
|
69
129
|
Args:
|
|
70
130
|
trace: Input trace or numpy array.
|
|
71
131
|
max_lag: Maximum lag to compute (samples). If None, uses n // 2.
|
|
@@ -80,6 +140,11 @@ def autocorrelation(
|
|
|
80
140
|
Raises:
|
|
81
141
|
ValueError: If sample_rate is not provided when trace is array.
|
|
82
142
|
|
|
143
|
+
Performance:
|
|
144
|
+
- Small signals (n<256): ~0.02-0.05ms using numpy.correlate
|
|
145
|
+
- Large signals (n>=256): ~0.1-1ms using FFT method
|
|
146
|
+
- Both methods use highly optimized numerical libraries
|
|
147
|
+
|
|
83
148
|
Example:
|
|
84
149
|
>>> lag_times, acf = autocorrelation(trace, max_lag=1000)
|
|
85
150
|
>>> # Find first zero crossing for decorrelation time
|
|
@@ -108,15 +173,19 @@ def autocorrelation(
|
|
|
108
173
|
# Remove mean for proper correlation
|
|
109
174
|
data_centered = data - np.mean(data)
|
|
110
175
|
|
|
111
|
-
# Compute autocorrelation
|
|
112
|
-
|
|
176
|
+
# Compute autocorrelation using optimal method based on signal size:
|
|
177
|
+
# - n >= 256: FFT-based method (O(n log n) complexity, fastest for large signals)
|
|
178
|
+
# - n < 256: Direct method using numpy.correlate (highly optimized BLAS)
|
|
179
|
+
# Note: Numba implementation available (_autocorr_direct_numba) but numpy.correlate
|
|
180
|
+
# is faster due to optimized BLAS/LAPACK routines
|
|
181
|
+
if n >= 256:
|
|
113
182
|
# Zero-pad for full correlation
|
|
114
183
|
nfft = int(2 ** np.ceil(np.log2(2 * n)))
|
|
115
184
|
fft_data = np.fft.rfft(data_centered, n=nfft)
|
|
116
185
|
acf_full = np.fft.irfft(fft_data * np.conj(fft_data), n=nfft)
|
|
117
186
|
acf = acf_full[: max_lag + 1]
|
|
118
187
|
else:
|
|
119
|
-
# Direct computation for small
|
|
188
|
+
# Direct computation for small signals (numpy.correlate uses optimized BLAS)
|
|
120
189
|
acf = np.correlate(data_centered, data_centered, mode="full")
|
|
121
190
|
acf = acf[n - 1 : n + max_lag]
|
|
122
191
|
|
|
@@ -131,6 +200,71 @@ def autocorrelation(
|
|
|
131
200
|
return lag_times, acf.astype(np.float64)
|
|
132
201
|
|
|
133
202
|
|
|
203
|
+
def _extract_correlation_data(
|
|
204
|
+
trace1: WaveformTrace | NDArray[np.floating[Any]],
|
|
205
|
+
trace2: WaveformTrace | NDArray[np.floating[Any]],
|
|
206
|
+
sample_rate: float | None,
|
|
207
|
+
) -> tuple[NDArray[np.floating[Any]], NDArray[np.floating[Any]], float]:
|
|
208
|
+
"""Extract data arrays and sample rate from traces.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
trace1: First input trace or array
|
|
212
|
+
trace2: Second input trace or array
|
|
213
|
+
sample_rate: Sample rate if traces are arrays
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Tuple of (data1, data2, sample_rate)
|
|
217
|
+
|
|
218
|
+
Raises:
|
|
219
|
+
ValueError: If sample_rate needed but not provided
|
|
220
|
+
"""
|
|
221
|
+
if isinstance(trace1, WaveformTrace):
|
|
222
|
+
data1 = trace1.data
|
|
223
|
+
fs = trace1.metadata.sample_rate
|
|
224
|
+
else:
|
|
225
|
+
data1 = trace1
|
|
226
|
+
if sample_rate is None:
|
|
227
|
+
raise ValueError("sample_rate required when traces are arrays")
|
|
228
|
+
fs = sample_rate
|
|
229
|
+
|
|
230
|
+
if isinstance(trace2, WaveformTrace):
|
|
231
|
+
data2 = trace2.data
|
|
232
|
+
if not isinstance(trace1, WaveformTrace):
|
|
233
|
+
fs = trace2.metadata.sample_rate
|
|
234
|
+
else:
|
|
235
|
+
data2 = trace2
|
|
236
|
+
|
|
237
|
+
return data1, data2, fs
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _compute_normalized_xcorr(
|
|
241
|
+
data1_centered: NDArray[np.floating[Any]],
|
|
242
|
+
data2_centered: NDArray[np.floating[Any]],
|
|
243
|
+
xcorr: NDArray[np.floating[Any]],
|
|
244
|
+
) -> NDArray[np.floating[Any]]:
|
|
245
|
+
"""Normalize cross-correlation to [-1, 1].
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
data1_centered: First centered data array
|
|
249
|
+
data2_centered: Second centered data array
|
|
250
|
+
xcorr: Raw cross-correlation
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Normalized cross-correlation
|
|
254
|
+
"""
|
|
255
|
+
norm1 = np.sqrt(np.sum(data1_centered**2))
|
|
256
|
+
norm2 = np.sqrt(np.sum(data2_centered**2))
|
|
257
|
+
if norm1 > 0 and norm2 > 0:
|
|
258
|
+
# Division returns proper NDArray type
|
|
259
|
+
if TYPE_CHECKING:
|
|
260
|
+
from typing import cast
|
|
261
|
+
|
|
262
|
+
return cast("NDArray[np.floating[Any]]", xcorr / (norm1 * norm2))
|
|
263
|
+
else:
|
|
264
|
+
return xcorr / (norm1 * norm2) # type: ignore[return-value]
|
|
265
|
+
return xcorr
|
|
266
|
+
|
|
267
|
+
|
|
134
268
|
def cross_correlation(
|
|
135
269
|
trace1: WaveformTrace | NDArray[np.floating[Any]],
|
|
136
270
|
trace2: WaveformTrace | NDArray[np.floating[Any]],
|
|
@@ -165,71 +299,37 @@ def cross_correlation(
|
|
|
165
299
|
References:
|
|
166
300
|
Oppenheim, A. V. & Schafer, R. W. (2009). Discrete-Time Signal Processing
|
|
167
301
|
"""
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
else:
|
|
172
|
-
data1 = trace1
|
|
173
|
-
if sample_rate is None:
|
|
174
|
-
raise ValueError("sample_rate required when traces are arrays")
|
|
175
|
-
fs = sample_rate
|
|
302
|
+
data1, data2, fs = _extract_correlation_data(trace1, trace2, sample_rate)
|
|
303
|
+
n1 = len(data1)
|
|
304
|
+
max_lag = min(len(data1), len(data2)) // 2 if max_lag is None else max_lag
|
|
176
305
|
|
|
177
|
-
|
|
178
|
-
data2 = trace2.data
|
|
179
|
-
# Use trace2 sample rate if available and trace1 wasn't a WaveformTrace
|
|
180
|
-
if not isinstance(trace1, WaveformTrace):
|
|
181
|
-
fs = trace2.metadata.sample_rate
|
|
182
|
-
else:
|
|
183
|
-
data2 = trace2
|
|
184
|
-
|
|
185
|
-
n1, n2 = len(data1), len(data2)
|
|
186
|
-
|
|
187
|
-
if max_lag is None:
|
|
188
|
-
max_lag = min(n1, n2) // 2
|
|
189
|
-
|
|
190
|
-
# Center the data
|
|
306
|
+
# Center and compute correlation
|
|
191
307
|
data1_centered = data1 - np.mean(data1)
|
|
192
308
|
data2_centered = data2 - np.mean(data2)
|
|
193
|
-
|
|
194
|
-
# Full cross-correlation
|
|
195
|
-
# Note: np.correlate(a, b) computes sum(a[n+k] * conj(b[k]))
|
|
196
|
-
# For cross-correlation where we want to detect b delayed relative to a,
|
|
197
|
-
# we need correlate(b, a) so positive lag means b is delayed
|
|
198
309
|
xcorr_full = np.correlate(data2_centered, data1_centered, mode="full")
|
|
199
310
|
|
|
200
|
-
# Extract relevant portion
|
|
201
|
-
# Full correlation has length n1 + n2 - 1, with zero lag at index n1 - 1
|
|
202
|
-
# (since we swapped the order above)
|
|
311
|
+
# Extract relevant portion
|
|
203
312
|
zero_lag_idx = n1 - 1
|
|
204
313
|
start_idx = max(0, zero_lag_idx - max_lag)
|
|
205
314
|
end_idx = min(len(xcorr_full), zero_lag_idx + max_lag + 1)
|
|
206
315
|
xcorr = xcorr_full[start_idx:end_idx]
|
|
207
|
-
|
|
208
|
-
# Create lag array
|
|
209
316
|
lags = np.arange(start_idx - zero_lag_idx, end_idx - zero_lag_idx)
|
|
210
317
|
|
|
211
|
-
# Normalize
|
|
318
|
+
# Normalize if requested
|
|
212
319
|
if normalized:
|
|
213
|
-
|
|
214
|
-
norm2 = np.sqrt(np.sum(data2_centered**2))
|
|
215
|
-
if norm1 > 0 and norm2 > 0:
|
|
216
|
-
xcorr = xcorr / (norm1 * norm2)
|
|
320
|
+
xcorr = _compute_normalized_xcorr(data1_centered, data2_centered, xcorr)
|
|
217
321
|
|
|
218
322
|
# Find peak
|
|
219
323
|
peak_local_idx = np.argmax(np.abs(xcorr))
|
|
220
324
|
peak_lag = int(lags[peak_local_idx])
|
|
221
325
|
peak_coefficient = float(xcorr[peak_local_idx])
|
|
222
326
|
|
|
223
|
-
# Time values
|
|
224
|
-
lag_times = lags / fs
|
|
225
|
-
peak_lag_time = peak_lag / fs
|
|
226
|
-
|
|
227
327
|
return CrossCorrelationResult(
|
|
228
328
|
correlation=xcorr.astype(np.float64),
|
|
229
329
|
lags=lags,
|
|
230
|
-
lag_times=
|
|
330
|
+
lag_times=(lags / fs).astype(np.float64),
|
|
231
331
|
peak_lag=peak_lag,
|
|
232
|
-
peak_lag_time=
|
|
332
|
+
peak_lag_time=peak_lag / fs,
|
|
233
333
|
peak_coefficient=peak_coefficient,
|
|
234
334
|
sample_rate=fs,
|
|
235
335
|
)
|
|
@@ -267,6 +367,92 @@ def correlation_coefficient(
|
|
|
267
367
|
return float(np.corrcoef(data1, data2)[0, 1])
|
|
268
368
|
|
|
269
369
|
|
|
370
|
+
def _extract_periodicity_data(
|
|
371
|
+
trace: WaveformTrace | NDArray[np.floating[Any]], sample_rate: float | None
|
|
372
|
+
) -> tuple[NDArray[np.floating[Any]], float]:
|
|
373
|
+
"""Extract data and sample rate from trace.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
trace: Input trace or array.
|
|
377
|
+
sample_rate: Sample rate if array.
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
Tuple of (data, sample_rate).
|
|
381
|
+
|
|
382
|
+
Raises:
|
|
383
|
+
ValueError: If sample_rate not provided for array.
|
|
384
|
+
"""
|
|
385
|
+
if isinstance(trace, WaveformTrace):
|
|
386
|
+
return trace.data, trace.metadata.sample_rate
|
|
387
|
+
else:
|
|
388
|
+
if sample_rate is None:
|
|
389
|
+
raise ValueError("sample_rate required when trace is array")
|
|
390
|
+
return trace, sample_rate
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _find_primary_peak(acf: NDArray[np.float64], min_period_samples: int) -> tuple[int, float]:
|
|
394
|
+
"""Find primary peak in autocorrelation function.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
acf: Autocorrelation function.
|
|
398
|
+
min_period_samples: Minimum period to consider.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Tuple of (period_samples, strength).
|
|
402
|
+
"""
|
|
403
|
+
acf_search = acf[min_period_samples:]
|
|
404
|
+
|
|
405
|
+
if len(acf_search) < 3:
|
|
406
|
+
return -1, np.nan
|
|
407
|
+
|
|
408
|
+
local_max = (acf_search[1:-1] > acf_search[:-2]) & (acf_search[1:-1] > acf_search[2:])
|
|
409
|
+
max_indices = np.where(local_max)[0] + 1
|
|
410
|
+
|
|
411
|
+
if len(max_indices) == 0:
|
|
412
|
+
primary_idx = int(np.argmax(acf_search)) + min_period_samples
|
|
413
|
+
else:
|
|
414
|
+
peak_values = acf_search[max_indices]
|
|
415
|
+
best_peak_idx = int(np.argmax(peak_values))
|
|
416
|
+
primary_idx = int(max_indices[best_peak_idx]) + min_period_samples
|
|
417
|
+
|
|
418
|
+
return primary_idx, float(acf[primary_idx])
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _find_harmonics(acf: NDArray[np.float64], period_samples: int) -> list[dict[str, int | float]]:
|
|
422
|
+
"""Find harmonic peaks at multiples of primary period.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
acf: Autocorrelation function.
|
|
426
|
+
period_samples: Primary period in samples.
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
List of harmonic dictionaries.
|
|
430
|
+
"""
|
|
431
|
+
harmonics: list[dict[str, int | float]] = []
|
|
432
|
+
|
|
433
|
+
for h in range(2, 6):
|
|
434
|
+
harmonic_lag = h * period_samples
|
|
435
|
+
if harmonic_lag >= len(acf):
|
|
436
|
+
break
|
|
437
|
+
|
|
438
|
+
search_range = max(1, period_samples // 4)
|
|
439
|
+
start = int(max(0, harmonic_lag - search_range))
|
|
440
|
+
end = int(min(len(acf), harmonic_lag + search_range))
|
|
441
|
+
local_max_idx = int(start + int(np.argmax(acf[start:end])))
|
|
442
|
+
harmonic_strength = float(acf[local_max_idx])
|
|
443
|
+
|
|
444
|
+
if harmonic_strength > 0.3:
|
|
445
|
+
harmonics.append(
|
|
446
|
+
{
|
|
447
|
+
"harmonic": h,
|
|
448
|
+
"lag_samples": local_max_idx,
|
|
449
|
+
"strength": harmonic_strength,
|
|
450
|
+
}
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
return harmonics
|
|
454
|
+
|
|
455
|
+
|
|
270
456
|
def find_periodicity(
|
|
271
457
|
trace: WaveformTrace | NDArray[np.floating[Any]],
|
|
272
458
|
*,
|
|
@@ -301,32 +487,19 @@ def find_periodicity(
|
|
|
301
487
|
>>> print(f"Period: {result['period_time']*1e6:.2f} us")
|
|
302
488
|
>>> print(f"Frequency: {result['frequency']/1e3:.1f} kHz")
|
|
303
489
|
"""
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
fs = trace.metadata.sample_rate
|
|
307
|
-
else:
|
|
308
|
-
data = trace
|
|
309
|
-
if sample_rate is None:
|
|
310
|
-
raise ValueError("sample_rate required when trace is array")
|
|
311
|
-
fs = sample_rate
|
|
312
|
-
|
|
490
|
+
# Setup: extract data and compute autocorrelation
|
|
491
|
+
data, fs = _extract_periodicity_data(trace, sample_rate)
|
|
313
492
|
n = len(data)
|
|
493
|
+
max_period_samples = max_period_samples if max_period_samples is not None else n // 2
|
|
314
494
|
|
|
315
|
-
if max_period_samples is None:
|
|
316
|
-
max_period_samples = n // 2
|
|
317
|
-
|
|
318
|
-
# Compute autocorrelation
|
|
319
495
|
_lag_times, acf = autocorrelation(
|
|
320
|
-
trace,
|
|
321
|
-
max_lag=max_period_samples,
|
|
322
|
-
sample_rate=sample_rate if sample_rate else fs,
|
|
496
|
+
trace, max_lag=max_period_samples, sample_rate=sample_rate if sample_rate else fs
|
|
323
497
|
)
|
|
324
498
|
|
|
325
|
-
#
|
|
326
|
-
|
|
327
|
-
acf_search = acf[min_period_samples:]
|
|
499
|
+
# Processing: find primary peak and harmonics
|
|
500
|
+
period_samples, strength = _find_primary_peak(acf, min_period_samples)
|
|
328
501
|
|
|
329
|
-
if
|
|
502
|
+
if period_samples < 0:
|
|
330
503
|
return {
|
|
331
504
|
"period_samples": np.nan,
|
|
332
505
|
"period_time": np.nan,
|
|
@@ -335,46 +508,11 @@ def find_periodicity(
|
|
|
335
508
|
"harmonics": [],
|
|
336
509
|
}
|
|
337
510
|
|
|
338
|
-
# Find local maxima
|
|
339
|
-
local_max = (acf_search[1:-1] > acf_search[:-2]) & (acf_search[1:-1] > acf_search[2:])
|
|
340
|
-
max_indices = np.where(local_max)[0] + 1 # +1 for offset from [1:-1]
|
|
341
|
-
|
|
342
|
-
if len(max_indices) == 0:
|
|
343
|
-
# No local maxima found, use global max
|
|
344
|
-
primary_idx = int(np.argmax(acf_search)) + min_period_samples
|
|
345
|
-
strength = float(acf[primary_idx])
|
|
346
|
-
else:
|
|
347
|
-
# Find strongest peak
|
|
348
|
-
peak_values = acf_search[max_indices]
|
|
349
|
-
best_peak_idx = int(np.argmax(peak_values))
|
|
350
|
-
primary_idx = int(max_indices[best_peak_idx]) + min_period_samples
|
|
351
|
-
strength = float(acf[primary_idx])
|
|
352
|
-
|
|
353
|
-
period_samples = int(primary_idx)
|
|
354
511
|
period_time = period_samples / fs
|
|
355
512
|
frequency = 1.0 / period_time if period_time > 0 else np.nan
|
|
513
|
+
harmonics = _find_harmonics(acf, period_samples)
|
|
356
514
|
|
|
357
|
-
#
|
|
358
|
-
harmonics: list[dict[str, int | float]] = []
|
|
359
|
-
for h in range(2, 6): # Check up to 5th harmonic
|
|
360
|
-
harmonic_lag = h * period_samples
|
|
361
|
-
if harmonic_lag < len(acf):
|
|
362
|
-
# Look for peak near expected harmonic
|
|
363
|
-
search_range = max(1, period_samples // 4)
|
|
364
|
-
start = int(max(0, harmonic_lag - search_range))
|
|
365
|
-
end = int(min(len(acf), harmonic_lag + search_range))
|
|
366
|
-
local_max_idx = int(start + int(np.argmax(acf[start:end])))
|
|
367
|
-
harmonic_strength = float(acf[local_max_idx])
|
|
368
|
-
|
|
369
|
-
if harmonic_strength > 0.3: # Threshold for significant harmonic
|
|
370
|
-
harmonics.append(
|
|
371
|
-
{
|
|
372
|
-
"harmonic": h,
|
|
373
|
-
"lag_samples": local_max_idx,
|
|
374
|
-
"strength": harmonic_strength,
|
|
375
|
-
}
|
|
376
|
-
)
|
|
377
|
-
|
|
515
|
+
# Result building: construct result dictionary
|
|
378
516
|
return {
|
|
379
517
|
"period_samples": period_samples,
|
|
380
518
|
"period_time": float(period_time),
|
|
@@ -483,140 +621,198 @@ def correlate_chunked(
|
|
|
483
621
|
MEM-008: Chunked Correlation
|
|
484
622
|
Oppenheim & Schafer (2009): Discrete-Time Signal Processing, Ch 8
|
|
485
623
|
"""
|
|
624
|
+
_validate_chunked_inputs(signal1, signal2, mode)
|
|
625
|
+
|
|
626
|
+
n1, n2 = len(signal1), len(signal2)
|
|
627
|
+
chunk_size_final = _determine_chunk_size(chunk_size, n1, n2)
|
|
628
|
+
|
|
629
|
+
# Use direct correlation for small signals
|
|
630
|
+
if _should_use_direct_method(n1, n2, chunk_size_final):
|
|
631
|
+
return _direct_correlate(signal1, signal2, mode)
|
|
632
|
+
|
|
633
|
+
# Setup overlap-save parameters
|
|
634
|
+
params = _setup_overlap_save_params(chunk_size_final, n2)
|
|
635
|
+
if params is None:
|
|
636
|
+
return _direct_correlate(signal1, signal2, mode)
|
|
637
|
+
|
|
638
|
+
# Prepare kernel FFT and output buffer
|
|
639
|
+
signal2_flipped = signal2[::-1].copy()
|
|
640
|
+
kernel_fft = np.fft.fft(signal2_flipped, n=params.nfft)
|
|
641
|
+
output = np.zeros(_get_output_length(n1, n2, mode), dtype=np.float64)
|
|
642
|
+
|
|
643
|
+
# Process signal in chunks
|
|
644
|
+
_process_chunks_overlap_save(signal1, kernel_fft, output, params, mode)
|
|
645
|
+
|
|
646
|
+
return output
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _validate_chunked_inputs(
|
|
650
|
+
signal1: NDArray[np.floating[Any]], signal2: NDArray[np.floating[Any]], mode: str
|
|
651
|
+
) -> None:
|
|
652
|
+
"""Validate inputs for chunked correlation."""
|
|
486
653
|
if len(signal1) == 0 or len(signal2) == 0:
|
|
487
654
|
raise ValueError("Input signals cannot be empty")
|
|
488
|
-
|
|
489
655
|
if mode not in ("same", "valid", "full"):
|
|
490
656
|
raise ValueError(f"Invalid mode: {mode}. Must be 'same', 'valid', or 'full'")
|
|
491
657
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
658
|
+
|
|
659
|
+
def _determine_chunk_size(chunk_size: int | None, n1: int, n2: int) -> int:
|
|
660
|
+
"""Determine optimal chunk size for processing."""
|
|
661
|
+
if chunk_size is not None:
|
|
662
|
+
return chunk_size
|
|
663
|
+
|
|
664
|
+
# Auto-select: aim for ~100MB chunks (float64 = 8 bytes)
|
|
665
|
+
target_bytes = 100 * 1024 * 1024
|
|
666
|
+
auto_chunk = min(target_bytes // 8, n1)
|
|
667
|
+
log2_val: np.floating[Any] = np.log2(auto_chunk)
|
|
668
|
+
chunk_power_of_2: int = int(2 ** int(log2_val)) # Round to power of 2
|
|
669
|
+
return chunk_power_of_2
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _should_use_direct_method(n1: int, n2: int, chunk_size: int) -> bool:
|
|
673
|
+
"""Check if direct correlation should be used instead of chunked."""
|
|
506
674
|
min_chunk_size = max(2 * n2, 64)
|
|
675
|
+
return n1 <= min_chunk_size or n2 >= n1 or chunk_size < min_chunk_size
|
|
507
676
|
|
|
508
|
-
# For small signals or when chunk_size is too small, use direct method
|
|
509
|
-
if n1 <= min_chunk_size or n2 >= n1 or chunk_size < min_chunk_size:
|
|
510
|
-
mode_literal = cast("Literal['same', 'valid', 'full']", mode)
|
|
511
|
-
result = np.correlate(signal1, signal2, mode=mode_literal)
|
|
512
|
-
return result.astype(np.float64)
|
|
513
677
|
|
|
514
|
-
|
|
515
|
-
|
|
678
|
+
def _direct_correlate(
|
|
679
|
+
signal1: NDArray[np.floating[Any]], signal2: NDArray[np.floating[Any]], mode: str
|
|
680
|
+
) -> NDArray[np.float64]:
|
|
681
|
+
"""Perform direct correlation using numpy."""
|
|
682
|
+
mode_literal = cast("Literal['same', 'valid', 'full']", mode)
|
|
683
|
+
result = np.correlate(signal1, signal2, mode=mode_literal)
|
|
684
|
+
return result.astype(np.float64)
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
@dataclass
|
|
688
|
+
class _OverlapSaveParams:
|
|
689
|
+
"""Parameters for overlap-save algorithm."""
|
|
516
690
|
|
|
517
|
-
|
|
518
|
-
|
|
691
|
+
chunk_size: int
|
|
692
|
+
filter_len: int
|
|
693
|
+
overlap: int
|
|
694
|
+
step_size: int
|
|
695
|
+
nfft: int
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def _setup_overlap_save_params(chunk_size: int, n2: int) -> _OverlapSaveParams | None:
|
|
699
|
+
"""Setup overlap-save algorithm parameters."""
|
|
700
|
+
min_chunk_size = max(2 * n2, 64)
|
|
519
701
|
L = max(chunk_size, min_chunk_size)
|
|
520
702
|
M = n2
|
|
521
703
|
overlap = M - 1
|
|
522
|
-
|
|
523
|
-
# Ensure step size is positive (L must be > overlap)
|
|
524
704
|
step_size = L - overlap
|
|
705
|
+
|
|
525
706
|
if step_size <= 0:
|
|
526
|
-
|
|
527
|
-
mode_literal = cast("Literal['same', 'valid', 'full']", mode)
|
|
528
|
-
result = np.correlate(signal1, signal2, mode=mode_literal)
|
|
529
|
-
return result.astype(np.float64)
|
|
707
|
+
return None
|
|
530
708
|
|
|
531
|
-
# FFT size (power of 2, >= L + M - 1)
|
|
532
709
|
nfft = int(2 ** np.ceil(np.log2(L + M - 1)))
|
|
710
|
+
return _OverlapSaveParams(L, M, overlap, step_size, nfft)
|
|
533
711
|
|
|
534
|
-
# Pre-compute FFT of flipped signal2 (kernel)
|
|
535
|
-
kernel_fft = np.fft.fft(signal2_flipped, n=nfft)
|
|
536
712
|
|
|
537
|
-
|
|
713
|
+
def _get_output_length(n1: int, n2: int, mode: str) -> int:
|
|
714
|
+
"""Calculate output length based on correlation mode."""
|
|
538
715
|
if mode == "full":
|
|
539
|
-
|
|
716
|
+
return n1 + n2 - 1
|
|
540
717
|
elif mode == "same":
|
|
541
|
-
|
|
718
|
+
return n1
|
|
542
719
|
else: # valid
|
|
543
|
-
|
|
720
|
+
return max(0, n1 - n2 + 1)
|
|
544
721
|
|
|
545
|
-
# Initialize output
|
|
546
|
-
output = np.zeros(output_len, dtype=np.float64)
|
|
547
722
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
723
|
+
def _process_chunks_overlap_save(
|
|
724
|
+
signal1: NDArray[np.floating[Any]],
|
|
725
|
+
kernel_fft: NDArray[np.complexfloating[Any, Any]],
|
|
726
|
+
output: NDArray[np.float64],
|
|
727
|
+
params: _OverlapSaveParams,
|
|
728
|
+
mode: str,
|
|
729
|
+
) -> None:
|
|
730
|
+
"""Process signal in chunks using overlap-save method."""
|
|
731
|
+
n1 = len(signal1)
|
|
732
|
+
pos = 0
|
|
733
|
+
max_iterations = (n1 // params.step_size) + 2
|
|
734
|
+
|
|
735
|
+
for _iteration in range(max_iterations):
|
|
736
|
+
if pos >= n1:
|
|
737
|
+
break
|
|
738
|
+
|
|
739
|
+
# Extract and process chunk
|
|
740
|
+
chunk = _extract_chunk(signal1, pos, params, n1)
|
|
741
|
+
conv_result = _convolve_chunk_fft(chunk, kernel_fft, params.nfft)
|
|
742
|
+
valid_output = _extract_valid_portion(conv_result, pos, params)
|
|
743
|
+
|
|
744
|
+
# Write to output buffer
|
|
745
|
+
_write_chunk_output(output, valid_output, pos, params, mode)
|
|
746
|
+
|
|
747
|
+
pos += params.step_size
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def _extract_chunk(
|
|
751
|
+
signal1: NDArray[np.floating[Any]], pos: int, params: _OverlapSaveParams, n1: int
|
|
752
|
+
) -> NDArray[np.floating[Any]]:
|
|
753
|
+
"""Extract chunk with appropriate overlap."""
|
|
754
|
+
if pos == 0:
|
|
755
|
+
return signal1[0 : min(params.chunk_size, n1)]
|
|
756
|
+
|
|
757
|
+
chunk_start = max(0, pos - params.overlap)
|
|
758
|
+
chunk_end = min(chunk_start + params.chunk_size, n1)
|
|
759
|
+
return signal1[chunk_start:chunk_end]
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def _convolve_chunk_fft(
|
|
763
|
+
chunk: NDArray[np.floating[Any]],
|
|
764
|
+
kernel_fft: NDArray[np.complexfloating[Any, Any]],
|
|
765
|
+
nfft: int,
|
|
766
|
+
) -> NDArray[np.floating[Any]]:
|
|
767
|
+
"""Perform FFT-based convolution on chunk."""
|
|
768
|
+
chunk_padded = np.zeros(nfft, dtype=np.float64)
|
|
769
|
+
chunk_padded[: len(chunk)] = chunk
|
|
770
|
+
chunk_fft = np.fft.fft(chunk_padded)
|
|
771
|
+
conv_fft = chunk_fft * kernel_fft
|
|
772
|
+
return np.fft.ifft(conv_fft).real
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def _extract_valid_portion(
|
|
776
|
+
conv_result: NDArray[np.floating[Any]], pos: int, params: _OverlapSaveParams
|
|
777
|
+
) -> NDArray[np.floating[Any]]:
|
|
778
|
+
"""Extract valid portion from convolution result."""
|
|
779
|
+
if pos == 0:
|
|
780
|
+
valid_start = 0
|
|
781
|
+
valid_end = min(params.chunk_size, len(conv_result))
|
|
782
|
+
else:
|
|
783
|
+
valid_start = params.overlap
|
|
784
|
+
valid_end = min(params.chunk_size, len(conv_result))
|
|
552
785
|
|
|
553
|
-
|
|
554
|
-
iteration += 1
|
|
786
|
+
return conv_result[valid_start:valid_end]
|
|
555
787
|
|
|
556
|
-
# Extract chunk with overlap from previous chunk
|
|
557
|
-
if pos == 0:
|
|
558
|
-
# First chunk: no overlap needed
|
|
559
|
-
chunk_start = 0
|
|
560
|
-
chunk = signal1[0 : min(L, n1)]
|
|
561
|
-
else:
|
|
562
|
-
# Subsequent chunks: include overlap
|
|
563
|
-
chunk_start = max(0, pos - overlap)
|
|
564
|
-
chunk_end = min(chunk_start + L, n1)
|
|
565
|
-
chunk = signal1[chunk_start:chunk_end]
|
|
566
|
-
|
|
567
|
-
# Zero-pad chunk to FFT size
|
|
568
|
-
chunk_padded = np.zeros(nfft, dtype=np.float64)
|
|
569
|
-
chunk_padded[: len(chunk)] = chunk
|
|
570
|
-
|
|
571
|
-
# Perform FFT-based convolution
|
|
572
|
-
chunk_fft = np.fft.fft(chunk_padded)
|
|
573
|
-
conv_fft = chunk_fft * kernel_fft
|
|
574
|
-
conv_result = np.fft.ifft(conv_fft).real
|
|
575
|
-
|
|
576
|
-
# Extract valid portion (discard transient at start)
|
|
577
|
-
if pos == 0:
|
|
578
|
-
# First chunk
|
|
579
|
-
valid_start = 0
|
|
580
|
-
valid_end = min(L, len(conv_result))
|
|
581
|
-
else:
|
|
582
|
-
# Subsequent chunks: discard overlap region
|
|
583
|
-
valid_start = overlap
|
|
584
|
-
valid_end = min(len(chunk), len(conv_result))
|
|
585
|
-
|
|
586
|
-
valid_output = conv_result[valid_start:valid_end]
|
|
587
|
-
|
|
588
|
-
# Determine output range based on mode
|
|
589
|
-
if mode == "full":
|
|
590
|
-
# Full convolution includes all overlap
|
|
591
|
-
out_start = pos
|
|
592
|
-
out_end = min(out_start + len(valid_output), output_len)
|
|
593
|
-
elif mode == "same":
|
|
594
|
-
# Same mode: center-aligned
|
|
595
|
-
offset = (M - 1) // 2
|
|
596
|
-
out_start = max(0, pos - offset)
|
|
597
|
-
out_end = min(out_start + len(valid_output), output_len)
|
|
598
|
-
# Adjust valid_output if we're at boundaries
|
|
599
|
-
if pos == 0 and offset > 0:
|
|
600
|
-
valid_output = valid_output[offset:]
|
|
601
|
-
else: # valid
|
|
602
|
-
# Valid mode: only where signals fully overlap
|
|
603
|
-
offset = M - 1
|
|
604
|
-
if pos < offset:
|
|
605
|
-
# Skip this chunk, not in valid region yet
|
|
606
|
-
pos += step_size
|
|
607
|
-
continue
|
|
608
|
-
out_start = pos - offset
|
|
609
|
-
out_end = min(out_start + len(valid_output), output_len)
|
|
610
|
-
|
|
611
|
-
# Copy to output
|
|
612
|
-
copy_len = min(len(valid_output), out_end - out_start)
|
|
613
|
-
if copy_len > 0:
|
|
614
|
-
output[out_start : out_start + copy_len] = valid_output[:copy_len]
|
|
615
|
-
|
|
616
|
-
# Move to next chunk with guaranteed progress
|
|
617
|
-
pos += step_size
|
|
618
788
|
|
|
619
|
-
|
|
789
|
+
def _write_chunk_output(
|
|
790
|
+
output: NDArray[np.float64],
|
|
791
|
+
valid_output: NDArray[np.floating[Any]],
|
|
792
|
+
pos: int,
|
|
793
|
+
params: _OverlapSaveParams,
|
|
794
|
+
mode: str,
|
|
795
|
+
) -> None:
|
|
796
|
+
"""Write chunk output to final buffer based on mode."""
|
|
797
|
+
output_len = len(output)
|
|
798
|
+
offset = (params.filter_len - 1) // 2 if mode == "same" else params.filter_len - 1
|
|
799
|
+
|
|
800
|
+
if mode == "full":
|
|
801
|
+
out_start = pos
|
|
802
|
+
elif mode == "same":
|
|
803
|
+
out_start = max(0, pos - offset)
|
|
804
|
+
if pos == 0 and offset > 0:
|
|
805
|
+
valid_output = valid_output[offset:]
|
|
806
|
+
else: # valid
|
|
807
|
+
if pos < offset:
|
|
808
|
+
return
|
|
809
|
+
out_start = pos - offset
|
|
810
|
+
|
|
811
|
+
out_end = min(out_start + len(valid_output), output_len)
|
|
812
|
+
copy_len = min(len(valid_output), out_end - out_start)
|
|
813
|
+
|
|
814
|
+
if copy_len > 0:
|
|
815
|
+
output[out_start : out_start + copy_len] = valid_output[:copy_len]
|
|
620
816
|
|
|
621
817
|
|
|
622
818
|
__all__ = [
|