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
|
@@ -18,7 +18,7 @@ References:
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
20
|
from dataclasses import dataclass
|
|
21
|
-
from typing import TYPE_CHECKING, Literal
|
|
21
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
22
22
|
|
|
23
23
|
import numpy as np
|
|
24
24
|
|
|
@@ -325,11 +325,12 @@ def slew_rate(
|
|
|
325
325
|
Args:
|
|
326
326
|
trace: Input waveform trace.
|
|
327
327
|
ref_levels: Reference levels as fractions (default 20%-80%).
|
|
328
|
-
edge_type: Type of edges to measure.
|
|
328
|
+
edge_type: Type of edges to measure ("rising", "falling", or "both").
|
|
329
329
|
return_all: If True, return array of all slew rates. If False, return mean.
|
|
330
330
|
|
|
331
331
|
Returns:
|
|
332
332
|
Slew rate in V/s (positive for rising, negative for falling).
|
|
333
|
+
Returns NaN if no transitions found or amplitude is zero.
|
|
333
334
|
|
|
334
335
|
Example:
|
|
335
336
|
>>> sr = slew_rate(trace)
|
|
@@ -339,69 +340,107 @@ def slew_rate(
|
|
|
339
340
|
IEEE 181-2011 Section 5.2
|
|
340
341
|
"""
|
|
341
342
|
if len(trace.data) < 3:
|
|
342
|
-
if return_all
|
|
343
|
-
return np.array([], dtype=np.float64)
|
|
344
|
-
return np.nan
|
|
343
|
+
return np.array([], dtype=np.float64) if return_all else np.nan
|
|
345
344
|
|
|
346
345
|
data = trace.data
|
|
347
346
|
sample_period = trace.metadata.time_base
|
|
348
347
|
|
|
349
|
-
# Find signal levels
|
|
348
|
+
# Find signal levels and validate
|
|
350
349
|
low, high = _find_levels(data)
|
|
351
350
|
amplitude = high - low
|
|
352
351
|
|
|
353
352
|
if amplitude <= 0:
|
|
354
|
-
if return_all
|
|
355
|
-
return np.array([], dtype=np.float64)
|
|
356
|
-
return np.nan
|
|
353
|
+
return np.array([], dtype=np.float64) if return_all else np.nan
|
|
357
354
|
|
|
358
355
|
# Calculate reference voltages
|
|
359
356
|
v_low = low + ref_levels[0] * amplitude
|
|
360
357
|
v_high = low + ref_levels[1] * amplitude
|
|
361
358
|
dv = v_high - v_low
|
|
362
359
|
|
|
360
|
+
# Measure slew rates for requested edge types
|
|
363
361
|
slew_rates: list[float] = []
|
|
364
362
|
|
|
365
363
|
if edge_type in ("rising", "both"):
|
|
366
|
-
|
|
367
|
-
rising_start = np.where((data[:-1] < v_low) & (data[1:] >= v_low))[0]
|
|
364
|
+
slew_rates.extend(_measure_rising_slew_rates(data, v_low, v_high, dv, sample_period))
|
|
368
365
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
remaining = data[start_idx:]
|
|
372
|
-
above_high = remaining >= v_high
|
|
366
|
+
if edge_type in ("falling", "both"):
|
|
367
|
+
slew_rates.extend(_measure_falling_slew_rates(data, v_low, v_high, dv, sample_period))
|
|
373
368
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
dt = end_offset * sample_period
|
|
377
|
-
if dt > 0:
|
|
378
|
-
slew_rates.append(float(dv / dt))
|
|
369
|
+
if len(slew_rates) == 0:
|
|
370
|
+
return np.array([], dtype=np.float64) if return_all else np.nan
|
|
379
371
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
falling_start = np.where((data[:-1] > v_high) & (data[1:] <= v_high))[0]
|
|
372
|
+
result = np.array(slew_rates, dtype=np.float64)
|
|
373
|
+
return result if return_all else float(np.mean(result))
|
|
383
374
|
|
|
384
|
-
for start_idx in falling_start:
|
|
385
|
-
# Find where signal reaches v_low
|
|
386
|
-
remaining = data[start_idx:]
|
|
387
|
-
below_low = remaining <= v_low
|
|
388
375
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
376
|
+
def _measure_rising_slew_rates(
|
|
377
|
+
data: NDArray[np.float64],
|
|
378
|
+
v_low: float,
|
|
379
|
+
v_high: float,
|
|
380
|
+
dv: float,
|
|
381
|
+
sample_period: float,
|
|
382
|
+
) -> list[float]:
|
|
383
|
+
"""Measure slew rates for rising edges.
|
|
394
384
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
385
|
+
Args:
|
|
386
|
+
data: Signal data.
|
|
387
|
+
v_low: Low reference voltage.
|
|
388
|
+
v_high: High reference voltage.
|
|
389
|
+
dv: Voltage difference between reference levels.
|
|
390
|
+
sample_period: Time between samples.
|
|
399
391
|
|
|
400
|
-
|
|
392
|
+
Returns:
|
|
393
|
+
List of rising slew rates (V/s).
|
|
394
|
+
"""
|
|
395
|
+
slew_rates: list[float] = []
|
|
396
|
+
rising_start = np.where((data[:-1] < v_low) & (data[1:] >= v_low))[0]
|
|
401
397
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
398
|
+
for start_idx in rising_start:
|
|
399
|
+
remaining = data[start_idx:]
|
|
400
|
+
above_high = remaining >= v_high
|
|
401
|
+
|
|
402
|
+
if np.any(above_high):
|
|
403
|
+
end_offset = np.argmax(above_high)
|
|
404
|
+
dt = end_offset * sample_period
|
|
405
|
+
if dt > 0:
|
|
406
|
+
slew_rates.append(float(dv / dt))
|
|
407
|
+
|
|
408
|
+
return slew_rates
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _measure_falling_slew_rates(
|
|
412
|
+
data: NDArray[np.float64],
|
|
413
|
+
v_low: float,
|
|
414
|
+
v_high: float,
|
|
415
|
+
dv: float,
|
|
416
|
+
sample_period: float,
|
|
417
|
+
) -> list[float]:
|
|
418
|
+
"""Measure slew rates for falling edges.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
data: Signal data.
|
|
422
|
+
v_low: Low reference voltage.
|
|
423
|
+
v_high: High reference voltage.
|
|
424
|
+
dv: Voltage difference between reference levels.
|
|
425
|
+
sample_period: Time between samples.
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
List of falling slew rates (negative V/s).
|
|
429
|
+
"""
|
|
430
|
+
slew_rates: list[float] = []
|
|
431
|
+
falling_start = np.where((data[:-1] > v_high) & (data[1:] <= v_high))[0]
|
|
432
|
+
|
|
433
|
+
for start_idx in falling_start:
|
|
434
|
+
remaining = data[start_idx:]
|
|
435
|
+
below_low = remaining <= v_low
|
|
436
|
+
|
|
437
|
+
if np.any(below_low):
|
|
438
|
+
end_offset = np.argmax(below_low)
|
|
439
|
+
dt = end_offset * sample_period
|
|
440
|
+
if dt > 0:
|
|
441
|
+
slew_rates.append(float(-dv / dt)) # Negative for falling
|
|
442
|
+
|
|
443
|
+
return slew_rates
|
|
405
444
|
|
|
406
445
|
|
|
407
446
|
def phase(
|
|
@@ -456,13 +495,13 @@ def _phase_edge(
|
|
|
456
495
|
edges2 = _get_edge_timestamps(trace2, "rising", 0.5)
|
|
457
496
|
|
|
458
497
|
if len(edges1) < 2 or len(edges2) < 2:
|
|
459
|
-
return np.nan
|
|
498
|
+
return np.nan
|
|
460
499
|
|
|
461
500
|
# Calculate period from first signal
|
|
462
501
|
period1 = np.mean(np.diff(edges1))
|
|
463
502
|
|
|
464
503
|
if period1 <= 0:
|
|
465
|
-
return np.nan
|
|
504
|
+
return np.nan
|
|
466
505
|
|
|
467
506
|
# Calculate phase from edge differences
|
|
468
507
|
phase_times: list[float] = []
|
|
@@ -475,7 +514,7 @@ def _phase_edge(
|
|
|
475
514
|
phase_times.append(diffs[idx])
|
|
476
515
|
|
|
477
516
|
if len(phase_times) == 0:
|
|
478
|
-
return np.nan
|
|
517
|
+
return np.nan
|
|
479
518
|
|
|
480
519
|
mean_phase_time = np.mean(phase_times)
|
|
481
520
|
|
|
@@ -505,7 +544,7 @@ def _phase_fft(
|
|
|
505
544
|
data2 = data2[:n]
|
|
506
545
|
|
|
507
546
|
if n < 16:
|
|
508
|
-
return np.nan
|
|
547
|
+
return np.nan
|
|
509
548
|
|
|
510
549
|
# Compute FFTs
|
|
511
550
|
fft1 = np.fft.rfft(data1)
|
|
@@ -543,12 +582,7 @@ def skew(
|
|
|
543
582
|
edge_type: Type of edges to use for comparison.
|
|
544
583
|
|
|
545
584
|
Returns:
|
|
546
|
-
Dictionary with skew statistics
|
|
547
|
-
- skew_values: Array of skew for each non-reference trace
|
|
548
|
-
- min: Minimum skew
|
|
549
|
-
- max: Maximum skew
|
|
550
|
-
- mean: Mean skew
|
|
551
|
-
- range: Max - min (total skew spread)
|
|
585
|
+
Dictionary with skew statistics (skew_values, min, max, mean, range).
|
|
552
586
|
|
|
553
587
|
Raises:
|
|
554
588
|
ValueError: If fewer than 2 traces or reference_idx out of range.
|
|
@@ -562,52 +596,103 @@ def skew(
|
|
|
562
596
|
"""
|
|
563
597
|
if len(traces) < 2:
|
|
564
598
|
raise ValueError("Need at least 2 traces for skew measurement")
|
|
565
|
-
|
|
566
599
|
if reference_idx >= len(traces):
|
|
567
600
|
raise ValueError(f"reference_idx {reference_idx} out of range")
|
|
568
601
|
|
|
569
|
-
|
|
570
|
-
ref_trace = traces[reference_idx]
|
|
571
|
-
ref_edges = _get_edge_timestamps(ref_trace, edge_type, 0.5)
|
|
602
|
+
ref_edges = _get_edge_timestamps(traces[reference_idx], edge_type, 0.5)
|
|
572
603
|
|
|
573
604
|
if len(ref_edges) == 0:
|
|
574
|
-
return
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
605
|
+
return _empty_skew_result()
|
|
606
|
+
|
|
607
|
+
all_skews, skew_values = _compute_all_skews(traces, reference_idx, ref_edges, edge_type)
|
|
608
|
+
|
|
609
|
+
return _build_skew_result(skew_values, all_skews)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def _empty_skew_result() -> dict[str, float | NDArray[np.float64]]:
|
|
613
|
+
"""Return empty skew result dictionary.
|
|
614
|
+
|
|
615
|
+
Returns:
|
|
616
|
+
Dictionary with empty/NaN skew values.
|
|
617
|
+
"""
|
|
618
|
+
return {
|
|
619
|
+
"skew_values": np.array([], dtype=np.float64),
|
|
620
|
+
"min": float(np.nan),
|
|
621
|
+
"max": float(np.nan),
|
|
622
|
+
"mean": float(np.nan),
|
|
623
|
+
"range": float(np.nan),
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def _compute_all_skews(
|
|
628
|
+
traces: list[WaveformTrace | DigitalTrace],
|
|
629
|
+
reference_idx: int,
|
|
630
|
+
ref_edges: NDArray[np.float64],
|
|
631
|
+
edge_type: Literal["rising", "falling"],
|
|
632
|
+
) -> tuple[list[float], list[float]]:
|
|
633
|
+
"""Compute skew values for all traces.
|
|
634
|
+
|
|
635
|
+
Args:
|
|
636
|
+
traces: List of traces to analyze.
|
|
637
|
+
reference_idx: Index of reference trace.
|
|
638
|
+
ref_edges: Reference edge timestamps.
|
|
639
|
+
edge_type: Edge type to analyze.
|
|
581
640
|
|
|
582
|
-
|
|
641
|
+
Returns:
|
|
642
|
+
Tuple of (all_skews including reference, skew_values excluding reference).
|
|
643
|
+
"""
|
|
583
644
|
all_skews: list[float] = []
|
|
584
645
|
skew_values: list[float] = []
|
|
585
646
|
|
|
586
647
|
for i, trace in enumerate(traces):
|
|
587
648
|
if i == reference_idx:
|
|
588
|
-
# Reference has zero skew by definition
|
|
589
649
|
all_skews.append(0.0)
|
|
590
650
|
continue
|
|
591
651
|
|
|
592
652
|
trace_edges = _get_edge_timestamps(trace, edge_type, 0.5)
|
|
593
|
-
|
|
594
|
-
if len(trace_edges) == 0:
|
|
595
|
-
skew_val = np.nan
|
|
596
|
-
else:
|
|
597
|
-
# Match edges and compute skew
|
|
598
|
-
edge_skews = []
|
|
599
|
-
for ref_edge in ref_edges:
|
|
600
|
-
# Find nearest edge in this trace
|
|
601
|
-
diffs = np.abs(trace_edges - ref_edge)
|
|
602
|
-
nearest_idx = np.argmin(diffs)
|
|
603
|
-
skew_val_edge = trace_edges[nearest_idx] - ref_edge
|
|
604
|
-
edge_skews.append(skew_val_edge)
|
|
605
|
-
|
|
606
|
-
skew_val = float(np.mean(edge_skews)) if len(edge_skews) > 0 else np.nan
|
|
653
|
+
skew_val = _compute_trace_skew(trace_edges, ref_edges)
|
|
607
654
|
|
|
608
655
|
skew_values.append(skew_val)
|
|
609
656
|
all_skews.append(skew_val)
|
|
610
657
|
|
|
658
|
+
return all_skews, skew_values
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def _compute_trace_skew(trace_edges: NDArray[np.float64], ref_edges: NDArray[np.float64]) -> float:
|
|
662
|
+
"""Compute skew for a single trace relative to reference.
|
|
663
|
+
|
|
664
|
+
Args:
|
|
665
|
+
trace_edges: Edge timestamps for trace.
|
|
666
|
+
ref_edges: Reference edge timestamps.
|
|
667
|
+
|
|
668
|
+
Returns:
|
|
669
|
+
Mean skew value or NaN if no edges.
|
|
670
|
+
"""
|
|
671
|
+
if len(trace_edges) == 0:
|
|
672
|
+
return float(np.nan)
|
|
673
|
+
|
|
674
|
+
edge_skews = []
|
|
675
|
+
for ref_edge in ref_edges:
|
|
676
|
+
diffs = np.abs(trace_edges - ref_edge)
|
|
677
|
+
nearest_idx = np.argmin(diffs)
|
|
678
|
+
skew_val_edge = trace_edges[nearest_idx] - ref_edge
|
|
679
|
+
edge_skews.append(skew_val_edge)
|
|
680
|
+
|
|
681
|
+
return float(np.mean(edge_skews)) if len(edge_skews) > 0 else float(np.nan)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def _build_skew_result(
|
|
685
|
+
skew_values: list[float], all_skews: list[float]
|
|
686
|
+
) -> dict[str, float | NDArray[np.float64]]:
|
|
687
|
+
"""Build final skew result dictionary.
|
|
688
|
+
|
|
689
|
+
Args:
|
|
690
|
+
skew_values: Skew values excluding reference.
|
|
691
|
+
all_skews: Skew values including reference.
|
|
692
|
+
|
|
693
|
+
Returns:
|
|
694
|
+
Dictionary with skew statistics.
|
|
695
|
+
"""
|
|
611
696
|
skew_arr = np.array(skew_values, dtype=np.float64)
|
|
612
697
|
all_skews_arr = np.array(all_skews, dtype=np.float64)
|
|
613
698
|
valid_all_skews = all_skews_arr[~np.isnan(all_skews_arr)]
|
|
@@ -615,13 +700,12 @@ def skew(
|
|
|
615
700
|
if len(valid_all_skews) == 0:
|
|
616
701
|
return {
|
|
617
702
|
"skew_values": skew_arr,
|
|
618
|
-
"min": np.nan,
|
|
619
|
-
"max": np.nan,
|
|
620
|
-
"mean": np.nan,
|
|
621
|
-
"range": np.nan,
|
|
703
|
+
"min": float(np.nan),
|
|
704
|
+
"max": float(np.nan),
|
|
705
|
+
"mean": float(np.nan),
|
|
706
|
+
"range": float(np.nan),
|
|
622
707
|
}
|
|
623
708
|
|
|
624
|
-
# Compute statistics across ALL traces (including reference)
|
|
625
709
|
return {
|
|
626
710
|
"skew_values": skew_arr,
|
|
627
711
|
"min": float(np.min(valid_all_skews)),
|
|
@@ -673,73 +757,141 @@ def recover_clock_fft(
|
|
|
673
757
|
References:
|
|
674
758
|
IEEE 1241-2010 Section 4.1
|
|
675
759
|
"""
|
|
760
|
+
# Prepare data and validate
|
|
676
761
|
data = trace.data.astype(np.float64) if isinstance(trace, DigitalTrace) else trace.data
|
|
677
|
-
|
|
678
|
-
n = len(data)
|
|
679
762
|
sample_rate = trace.metadata.sample_rate
|
|
763
|
+
_validate_fft_requirements(len(data))
|
|
764
|
+
|
|
765
|
+
# Set frequency range
|
|
766
|
+
min_freq_val, max_freq_val = _determine_frequency_range(min_freq, max_freq, sample_rate)
|
|
767
|
+
|
|
768
|
+
# Compute FFT spectrum
|
|
769
|
+
freq, magnitude = _compute_fft_spectrum(data, sample_rate)
|
|
770
|
+
|
|
771
|
+
# Find peak frequency
|
|
772
|
+
peak_freq, peak_mag, valid_indices = _find_peak_frequency(
|
|
773
|
+
freq, magnitude, min_freq_val, max_freq_val
|
|
774
|
+
)
|
|
680
775
|
|
|
681
|
-
#
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
776
|
+
# Calculate confidence
|
|
777
|
+
confidence = _calculate_fft_confidence(magnitude, peak_mag, valid_indices)
|
|
778
|
+
|
|
779
|
+
# Refine frequency with interpolation
|
|
780
|
+
peak_freq_refined = _refine_peak_frequency(peak_freq, magnitude, freq, sample_rate, len(data))
|
|
781
|
+
|
|
782
|
+
# Warn if low confidence
|
|
783
|
+
_check_confidence_and_warn(confidence, peak_freq_refined)
|
|
784
|
+
|
|
785
|
+
period = 1.0 / peak_freq_refined if peak_freq_refined > 0 else np.nan
|
|
786
|
+
|
|
787
|
+
return ClockRecoveryResult(
|
|
788
|
+
frequency=float(peak_freq_refined),
|
|
789
|
+
period=float(period),
|
|
790
|
+
method="fft",
|
|
791
|
+
confidence=float(confidence),
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def _validate_fft_requirements(n_samples: int) -> None:
|
|
796
|
+
"""Validate trace has enough samples for FFT."""
|
|
797
|
+
min_samples = 64
|
|
798
|
+
if n_samples < min_samples:
|
|
686
799
|
raise InsufficientDataError(
|
|
687
800
|
f"FFT clock recovery requires at least {min_samples} samples for reliable frequency detection",
|
|
688
801
|
required=min_samples,
|
|
689
|
-
available=
|
|
802
|
+
available=n_samples,
|
|
690
803
|
analysis_type="clock_recovery_fft",
|
|
691
804
|
fix_hint="Use edge-based clock recovery for short signals or acquire more data",
|
|
692
805
|
)
|
|
693
806
|
|
|
694
|
-
# Set frequency range defaults
|
|
695
|
-
if min_freq is None:
|
|
696
|
-
min_freq = sample_rate / 1000
|
|
697
|
-
if max_freq is None:
|
|
698
|
-
max_freq = sample_rate / 2
|
|
699
807
|
|
|
700
|
-
|
|
808
|
+
def _determine_frequency_range(
|
|
809
|
+
min_freq: float | None,
|
|
810
|
+
max_freq: float | None,
|
|
811
|
+
sample_rate: float,
|
|
812
|
+
) -> tuple[float, float]:
|
|
813
|
+
"""Determine frequency range for FFT analysis."""
|
|
814
|
+
min_freq_val = min_freq if min_freq is not None else sample_rate / 1000
|
|
815
|
+
max_freq_val = max_freq if max_freq is not None else sample_rate / 2
|
|
816
|
+
return min_freq_val, max_freq_val
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def _compute_fft_spectrum(
|
|
820
|
+
data: NDArray[Any],
|
|
821
|
+
sample_rate: float,
|
|
822
|
+
) -> tuple[NDArray[Any], NDArray[Any]]:
|
|
823
|
+
"""Compute FFT spectrum of signal."""
|
|
824
|
+
n = len(data)
|
|
701
825
|
data_centered = data - np.mean(data)
|
|
702
826
|
nfft = int(2 ** np.ceil(np.log2(n)))
|
|
703
827
|
spectrum = np.fft.rfft(data_centered, n=nfft)
|
|
704
828
|
freq = np.fft.rfftfreq(nfft, d=1.0 / sample_rate)
|
|
705
829
|
magnitude = np.abs(spectrum)
|
|
830
|
+
return freq, magnitude
|
|
831
|
+
|
|
706
832
|
|
|
707
|
-
|
|
833
|
+
def _find_peak_frequency(
|
|
834
|
+
freq: NDArray[Any],
|
|
835
|
+
magnitude: NDArray[Any],
|
|
836
|
+
min_freq: float,
|
|
837
|
+
max_freq: float,
|
|
838
|
+
) -> tuple[float, float, NDArray[Any]]:
|
|
839
|
+
"""Find peak frequency in specified range."""
|
|
708
840
|
mask = (freq >= min_freq) & (freq <= max_freq)
|
|
709
841
|
valid_indices = np.where(mask)[0]
|
|
710
842
|
|
|
711
843
|
if len(valid_indices) == 0:
|
|
712
|
-
# No valid frequencies in range - signal may be DC or out of range
|
|
713
844
|
raise ValueError(
|
|
714
845
|
f"No frequency components found in range [{min_freq:.0f} Hz, {max_freq:.0f} Hz]. "
|
|
715
846
|
f"Signal may be constant (DC) or frequency is outside specified range. "
|
|
716
847
|
f"Adjust min_freq/max_freq or check signal integrity."
|
|
717
848
|
)
|
|
718
849
|
|
|
719
|
-
# Find peak in valid range
|
|
720
850
|
local_peak_idx = np.argmax(magnitude[valid_indices])
|
|
721
851
|
peak_idx = valid_indices[local_peak_idx]
|
|
722
852
|
peak_freq = freq[peak_idx]
|
|
723
853
|
peak_mag = magnitude[peak_idx]
|
|
724
854
|
|
|
725
|
-
|
|
855
|
+
return peak_freq, peak_mag, valid_indices
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def _calculate_fft_confidence(
|
|
859
|
+
magnitude: NDArray[Any],
|
|
860
|
+
peak_mag: float,
|
|
861
|
+
valid_indices: NDArray[Any],
|
|
862
|
+
) -> float:
|
|
863
|
+
"""Calculate confidence score for FFT peak."""
|
|
726
864
|
rms_mag = np.sqrt(np.mean(magnitude[valid_indices] ** 2))
|
|
727
|
-
|
|
865
|
+
return min(1.0, (peak_mag / rms_mag - 1) / 10) if rms_mag > 0 else 0.0
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def _refine_peak_frequency(
|
|
869
|
+
peak_freq: float,
|
|
870
|
+
magnitude: NDArray[Any],
|
|
871
|
+
freq: NDArray[Any],
|
|
872
|
+
sample_rate: float,
|
|
873
|
+
n_data: int,
|
|
874
|
+
) -> float:
|
|
875
|
+
"""Refine peak frequency using parabolic interpolation."""
|
|
876
|
+
peak_idx = np.argmin(np.abs(freq - peak_freq))
|
|
728
877
|
|
|
729
|
-
# Parabolic interpolation for more accurate frequency
|
|
730
878
|
if 0 < peak_idx < len(magnitude) - 1:
|
|
731
879
|
alpha = magnitude[peak_idx - 1]
|
|
732
880
|
beta = magnitude[peak_idx]
|
|
733
881
|
gamma = magnitude[peak_idx + 1]
|
|
734
882
|
|
|
735
883
|
if beta > alpha and beta > gamma:
|
|
884
|
+
nfft = int(2 ** np.ceil(np.log2(n_data)))
|
|
736
885
|
freq_resolution = sample_rate / nfft
|
|
737
886
|
delta = 0.5 * (alpha - gamma) / (alpha - 2 * beta + gamma + 1e-12)
|
|
738
|
-
|
|
887
|
+
refined: float = float(peak_freq + delta * freq_resolution)
|
|
888
|
+
return refined
|
|
739
889
|
|
|
740
|
-
|
|
890
|
+
return peak_freq
|
|
741
891
|
|
|
742
|
-
|
|
892
|
+
|
|
893
|
+
def _check_confidence_and_warn(confidence: float, peak_freq: float) -> None:
|
|
894
|
+
"""Warn if confidence is low."""
|
|
743
895
|
if confidence < 0.5:
|
|
744
896
|
import warnings
|
|
745
897
|
|
|
@@ -748,16 +900,9 @@ def recover_clock_fft(
|
|
|
748
900
|
f"Detected frequency: {peak_freq / 1e6:.3f} MHz. "
|
|
749
901
|
f"Consider using longer signal, edge-based recovery, or verifying signal periodicity.",
|
|
750
902
|
UserWarning,
|
|
751
|
-
stacklevel=
|
|
903
|
+
stacklevel=3,
|
|
752
904
|
)
|
|
753
905
|
|
|
754
|
-
return ClockRecoveryResult(
|
|
755
|
-
frequency=float(peak_freq),
|
|
756
|
-
period=float(period),
|
|
757
|
-
method="fft",
|
|
758
|
-
confidence=float(confidence),
|
|
759
|
-
)
|
|
760
|
-
|
|
761
906
|
|
|
762
907
|
def recover_clock_edge(
|
|
763
908
|
trace: WaveformTrace | DigitalTrace,
|
|
@@ -1038,13 +1183,13 @@ def peak_to_peak_jitter(
|
|
|
1038
1183
|
edges = _get_edge_timestamps(trace, edge_type, threshold)
|
|
1039
1184
|
|
|
1040
1185
|
if len(edges) < 3:
|
|
1041
|
-
return np.nan
|
|
1186
|
+
return np.nan
|
|
1042
1187
|
|
|
1043
1188
|
# Calculate periods
|
|
1044
1189
|
periods = np.diff(edges)
|
|
1045
1190
|
|
|
1046
1191
|
if len(periods) < 2:
|
|
1047
|
-
return np.nan
|
|
1192
|
+
return np.nan
|
|
1048
1193
|
|
|
1049
1194
|
# Pk-Pk jitter is the range of period variations
|
|
1050
1195
|
jitter_pp = float(np.max(periods) - np.min(periods))
|