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
|
@@ -273,98 +273,122 @@ def detect_glitches(
|
|
|
273
273
|
References:
|
|
274
274
|
Application note AN-905: Understanding Glitch Detection
|
|
275
275
|
"""
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
sample_rate = trace.metadata.sample_rate
|
|
280
|
-
threshold_used = 0.5 # Not used for amplitude calc on digital
|
|
281
|
-
data = trace.data.astype(np.float64)
|
|
282
|
-
else:
|
|
283
|
-
# Analog trace - need to threshold
|
|
284
|
-
data = trace.data
|
|
285
|
-
sample_rate = trace.metadata.sample_rate
|
|
276
|
+
digital, data, sample_rate, threshold_used = _prepare_glitch_detection_data(trace, threshold)
|
|
277
|
+
if digital is None or len(digital) < 3:
|
|
278
|
+
return []
|
|
286
279
|
|
|
287
|
-
|
|
288
|
-
|
|
280
|
+
sample_period = 1.0 / sample_rate
|
|
281
|
+
rising_edges, falling_edges = _find_pulse_edges(digital)
|
|
282
|
+
glitches = []
|
|
283
|
+
glitches.extend(
|
|
284
|
+
_detect_positive_glitches(
|
|
285
|
+
rising_edges, falling_edges, data, sample_period, min_width, threshold_used, trace
|
|
286
|
+
)
|
|
287
|
+
)
|
|
288
|
+
glitches.extend(
|
|
289
|
+
_detect_negative_glitches(
|
|
290
|
+
falling_edges, rising_edges, data, sample_period, min_width, threshold_used, trace
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
glitches.sort(key=lambda g: g.timestamp)
|
|
294
|
+
return glitches
|
|
289
295
|
|
|
290
|
-
# Find threshold
|
|
291
|
-
low, high = _find_logic_levels(data)
|
|
292
|
-
threshold_used = (low + high) / 2 if threshold is None else threshold
|
|
293
296
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
+
def _prepare_glitch_detection_data(
|
|
298
|
+
trace: WaveformTrace | DigitalTrace, threshold: float | None
|
|
299
|
+
) -> tuple[NDArray[np.bool_] | None, NDArray[np.float64], float, float]:
|
|
300
|
+
"""Prepare data for glitch detection."""
|
|
301
|
+
if isinstance(trace, DigitalTrace):
|
|
302
|
+
return trace.data, trace.data.astype(np.float64), trace.metadata.sample_rate, 0.5
|
|
297
303
|
|
|
298
|
-
|
|
299
|
-
|
|
304
|
+
data = trace.data
|
|
305
|
+
sample_rate = trace.metadata.sample_rate
|
|
306
|
+
if len(data) < 3:
|
|
307
|
+
return None, data, sample_rate, 0.0
|
|
300
308
|
|
|
301
|
-
|
|
302
|
-
|
|
309
|
+
low, high = _find_logic_levels(data)
|
|
310
|
+
threshold_used = (low + high) / 2 if threshold is None else threshold
|
|
311
|
+
if high - low <= 0:
|
|
312
|
+
return None, data, sample_rate, threshold_used
|
|
303
313
|
|
|
304
|
-
|
|
314
|
+
return data >= threshold_used, data, sample_rate, threshold_used
|
|
305
315
|
|
|
306
|
-
glitches: list[Glitch] = []
|
|
307
316
|
|
|
308
|
-
|
|
317
|
+
def _find_pulse_edges(digital: NDArray[np.bool_]) -> tuple[NDArray[np.intp], NDArray[np.intp]]:
|
|
318
|
+
"""Find rising and falling edges in digital signal."""
|
|
309
319
|
transitions = np.diff(digital.astype(np.int8))
|
|
310
320
|
rising_edges = np.where(transitions == 1)[0]
|
|
311
321
|
falling_edges = np.where(transitions == -1)[0]
|
|
322
|
+
return rising_edges, falling_edges
|
|
323
|
+
|
|
312
324
|
|
|
313
|
-
|
|
325
|
+
def _detect_positive_glitches(
|
|
326
|
+
rising_edges: NDArray[np.intp],
|
|
327
|
+
falling_edges: NDArray[np.intp],
|
|
328
|
+
data: NDArray[np.float64],
|
|
329
|
+
sample_period: float,
|
|
330
|
+
min_width: float,
|
|
331
|
+
threshold_used: float,
|
|
332
|
+
trace: WaveformTrace | DigitalTrace,
|
|
333
|
+
) -> list[Glitch]:
|
|
334
|
+
"""Detect positive (high) glitches."""
|
|
335
|
+
glitches = []
|
|
314
336
|
for rising_idx in rising_edges:
|
|
315
|
-
# Find next falling edge
|
|
316
337
|
subsequent_falling = falling_edges[falling_edges > rising_idx]
|
|
317
|
-
if len(subsequent_falling)
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
polarity="positive",
|
|
335
|
-
amplitude=pulse_amplitude,
|
|
336
|
-
)
|
|
338
|
+
if len(subsequent_falling) == 0:
|
|
339
|
+
continue
|
|
340
|
+
falling_idx = subsequent_falling[0]
|
|
341
|
+
width = (falling_idx - rising_idx) * sample_period
|
|
342
|
+
if width < min_width:
|
|
343
|
+
pulse_data = data[rising_idx : falling_idx + 1]
|
|
344
|
+
amplitude = (
|
|
345
|
+
1.0
|
|
346
|
+
if isinstance(trace, DigitalTrace)
|
|
347
|
+
else float(np.max(pulse_data) - threshold_used)
|
|
348
|
+
)
|
|
349
|
+
glitches.append(
|
|
350
|
+
Glitch(
|
|
351
|
+
timestamp=rising_idx * sample_period,
|
|
352
|
+
width=width,
|
|
353
|
+
polarity="positive",
|
|
354
|
+
amplitude=amplitude,
|
|
337
355
|
)
|
|
356
|
+
)
|
|
357
|
+
return glitches
|
|
358
|
+
|
|
338
359
|
|
|
339
|
-
|
|
360
|
+
def _detect_negative_glitches(
|
|
361
|
+
falling_edges: NDArray[np.intp],
|
|
362
|
+
rising_edges: NDArray[np.intp],
|
|
363
|
+
data: NDArray[np.float64],
|
|
364
|
+
sample_period: float,
|
|
365
|
+
min_width: float,
|
|
366
|
+
threshold_used: float,
|
|
367
|
+
trace: WaveformTrace | DigitalTrace,
|
|
368
|
+
) -> list[Glitch]:
|
|
369
|
+
"""Detect negative (low) glitches."""
|
|
370
|
+
glitches = []
|
|
340
371
|
for falling_idx in falling_edges:
|
|
341
|
-
# Find next rising edge
|
|
342
372
|
subsequent_rising = rising_edges[rising_edges > falling_idx]
|
|
343
|
-
if len(subsequent_rising)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
polarity="negative",
|
|
361
|
-
amplitude=pulse_amplitude,
|
|
362
|
-
)
|
|
373
|
+
if len(subsequent_rising) == 0:
|
|
374
|
+
continue
|
|
375
|
+
rising_idx = subsequent_rising[0]
|
|
376
|
+
width = (rising_idx - falling_idx) * sample_period
|
|
377
|
+
if width < min_width:
|
|
378
|
+
pulse_data = data[falling_idx : rising_idx + 1]
|
|
379
|
+
amplitude = (
|
|
380
|
+
1.0
|
|
381
|
+
if isinstance(trace, DigitalTrace)
|
|
382
|
+
else float(threshold_used - np.min(pulse_data))
|
|
383
|
+
)
|
|
384
|
+
glitches.append(
|
|
385
|
+
Glitch(
|
|
386
|
+
timestamp=falling_idx * sample_period,
|
|
387
|
+
width=width,
|
|
388
|
+
polarity="negative",
|
|
389
|
+
amplitude=amplitude,
|
|
363
390
|
)
|
|
364
|
-
|
|
365
|
-
# Sort by timestamp
|
|
366
|
-
glitches.sort(key=lambda g: g.timestamp)
|
|
367
|
-
|
|
391
|
+
)
|
|
368
392
|
return glitches
|
|
369
393
|
|
|
370
394
|
|
|
@@ -604,22 +628,74 @@ def mask_test(
|
|
|
604
628
|
# Convert UI to sample indices
|
|
605
629
|
samples_per_ui = bit_period * sample_rate
|
|
606
630
|
time_samples = time_ui * samples_per_ui
|
|
607
|
-
|
|
608
|
-
# For simplicity, test over one or two bit periods
|
|
609
|
-
# Align signal to start of bit period
|
|
610
631
|
n_ui = int(np.max(time_ui)) # 1 or 2 UI mask
|
|
632
|
+
n_periods = n_samples // int(samples_per_ui * n_ui)
|
|
633
|
+
|
|
634
|
+
# Detect violations
|
|
635
|
+
violations, hit_count = _detect_mask_violations(
|
|
636
|
+
data,
|
|
637
|
+
n_samples,
|
|
638
|
+
sample_rate,
|
|
639
|
+
n_periods,
|
|
640
|
+
samples_per_ui,
|
|
641
|
+
n_ui,
|
|
642
|
+
time_ui,
|
|
643
|
+
time_samples,
|
|
644
|
+
v_top,
|
|
645
|
+
v_bottom,
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
# Calculate margins
|
|
649
|
+
margin_top, margin_bottom = _calculate_mask_margins(
|
|
650
|
+
data, n_samples, n_periods, samples_per_ui, n_ui, time_ui, time_samples, v_top, v_bottom
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
return MaskTestResult(
|
|
654
|
+
pass_fail=(hit_count == 0),
|
|
655
|
+
hit_count=hit_count,
|
|
656
|
+
total_samples=n_periods * len(time_ui),
|
|
657
|
+
margin_top=margin_top if margin_top != np.inf else 0.0,
|
|
658
|
+
margin_bottom=margin_bottom if margin_bottom != np.inf else 0.0,
|
|
659
|
+
violations=violations,
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def _detect_mask_violations(
|
|
664
|
+
data: NDArray[np.float64],
|
|
665
|
+
n_samples: int,
|
|
666
|
+
sample_rate: float,
|
|
667
|
+
n_periods: int,
|
|
668
|
+
samples_per_ui: float,
|
|
669
|
+
n_ui: int,
|
|
670
|
+
time_ui: NDArray[np.float64],
|
|
671
|
+
time_samples: NDArray[np.float64],
|
|
672
|
+
v_top: NDArray[np.float64],
|
|
673
|
+
v_bottom: NDArray[np.float64],
|
|
674
|
+
) -> tuple[list[tuple[float, float]], int]:
|
|
675
|
+
"""Detect mask violations across all bit periods.
|
|
611
676
|
|
|
677
|
+
Args:
|
|
678
|
+
data: Signal voltage data.
|
|
679
|
+
n_samples: Total number of samples.
|
|
680
|
+
sample_rate: Sample rate in Hz.
|
|
681
|
+
n_periods: Number of complete bit periods to test.
|
|
682
|
+
samples_per_ui: Samples per unit interval.
|
|
683
|
+
n_ui: Number of UI in mask template.
|
|
684
|
+
time_ui: Mask time coordinates in UI.
|
|
685
|
+
time_samples: Mask time coordinates in samples.
|
|
686
|
+
v_top: Upper voltage boundary.
|
|
687
|
+
v_bottom: Lower voltage boundary.
|
|
688
|
+
|
|
689
|
+
Returns:
|
|
690
|
+
Tuple of (violations list, hit count).
|
|
691
|
+
"""
|
|
612
692
|
violations: list[tuple[float, float]] = []
|
|
613
693
|
hit_count = 0
|
|
614
694
|
|
|
615
|
-
# Test all complete bit periods in the signal
|
|
616
|
-
n_periods = n_samples // int(samples_per_ui * n_ui)
|
|
617
|
-
|
|
618
695
|
for period_idx in range(n_periods):
|
|
619
696
|
period_start_sample = int(period_idx * samples_per_ui * n_ui)
|
|
620
697
|
|
|
621
|
-
|
|
622
|
-
for i, _t_ui in enumerate(time_ui):
|
|
698
|
+
for i in range(len(time_ui)):
|
|
623
699
|
sample_idx = period_start_sample + int(time_samples[i])
|
|
624
700
|
|
|
625
701
|
if sample_idx >= n_samples:
|
|
@@ -633,38 +709,53 @@ def mask_test(
|
|
|
633
709
|
violations.append((timestamp, voltage))
|
|
634
710
|
hit_count += 1
|
|
635
711
|
|
|
636
|
-
|
|
712
|
+
return violations, hit_count
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def _calculate_mask_margins(
|
|
716
|
+
data: NDArray[np.float64],
|
|
717
|
+
n_samples: int,
|
|
718
|
+
n_periods: int,
|
|
719
|
+
samples_per_ui: float,
|
|
720
|
+
n_ui: int,
|
|
721
|
+
time_ui: NDArray[np.float64],
|
|
722
|
+
time_samples: NDArray[np.float64],
|
|
723
|
+
v_top: NDArray[np.float64],
|
|
724
|
+
v_bottom: NDArray[np.float64],
|
|
725
|
+
) -> tuple[float, float]:
|
|
726
|
+
"""Calculate minimum margins to mask boundaries.
|
|
727
|
+
|
|
728
|
+
Args:
|
|
729
|
+
data: Signal voltage data.
|
|
730
|
+
n_samples: Total number of samples.
|
|
731
|
+
n_periods: Number of complete bit periods to test.
|
|
732
|
+
samples_per_ui: Samples per unit interval.
|
|
733
|
+
n_ui: Number of UI in mask template.
|
|
734
|
+
time_ui: Mask time coordinates in UI.
|
|
735
|
+
time_samples: Mask time coordinates in samples.
|
|
736
|
+
v_top: Upper voltage boundary.
|
|
737
|
+
v_bottom: Lower voltage boundary.
|
|
738
|
+
|
|
739
|
+
Returns:
|
|
740
|
+
Tuple of (margin_top, margin_bottom).
|
|
741
|
+
"""
|
|
637
742
|
margin_top = float(np.inf)
|
|
638
743
|
margin_bottom = float(np.inf)
|
|
639
744
|
|
|
640
745
|
for period_idx in range(n_periods):
|
|
641
746
|
period_start_sample = int(period_idx * samples_per_ui * n_ui)
|
|
642
747
|
|
|
643
|
-
for i
|
|
748
|
+
for i in range(len(time_ui)):
|
|
644
749
|
sample_idx = period_start_sample + int(time_samples[i])
|
|
645
750
|
|
|
646
751
|
if sample_idx >= n_samples:
|
|
647
752
|
break
|
|
648
753
|
|
|
649
754
|
voltage = data[sample_idx]
|
|
650
|
-
|
|
651
|
-
# Margin to top
|
|
652
755
|
margin_top = min(margin_top, v_top[i] - voltage)
|
|
653
|
-
|
|
654
|
-
# Margin to bottom
|
|
655
756
|
margin_bottom = min(margin_bottom, voltage - v_bottom[i])
|
|
656
757
|
|
|
657
|
-
|
|
658
|
-
pass_fail = hit_count == 0
|
|
659
|
-
|
|
660
|
-
return MaskTestResult(
|
|
661
|
-
pass_fail=pass_fail,
|
|
662
|
-
hit_count=hit_count,
|
|
663
|
-
total_samples=n_periods * len(time_ui),
|
|
664
|
-
margin_top=margin_top if margin_top != np.inf else 0.0,
|
|
665
|
-
margin_bottom=margin_bottom if margin_bottom != np.inf else 0.0,
|
|
666
|
-
violations=violations,
|
|
667
|
-
)
|
|
758
|
+
return margin_top, margin_bottom
|
|
668
759
|
|
|
669
760
|
|
|
670
761
|
def _get_predefined_mask(mask_name: str) -> dict[str, NDArray[np.float64]]:
|
|
@@ -750,8 +841,37 @@ def pll_clock_recovery(
|
|
|
750
841
|
References:
|
|
751
842
|
Gardner, F. M. (2005). Phaselock Techniques, 3rd ed.
|
|
752
843
|
"""
|
|
753
|
-
data
|
|
844
|
+
data, sample_rate, n_samples = _prepare_pll_data(trace)
|
|
845
|
+
dt = 1.0 / sample_rate
|
|
846
|
+
|
|
847
|
+
K1, K2, K_vco = _calculate_pll_coefficients(loop_bandwidth, damping, vco_gain)
|
|
848
|
+
edges = _find_edges_for_phase_detection(data)
|
|
849
|
+
nominal_phase_inc = 2 * np.pi * nominal_frequency * dt
|
|
850
|
+
|
|
851
|
+
phase, vco_control = _run_pll_loop(
|
|
852
|
+
n_samples, edges, nominal_phase_inc, K1, K2, K_vco, nominal_frequency, dt
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
lock_status, lock_time = _analyze_lock_status(vco_control, n_samples, dt)
|
|
856
|
+
recovered_frequency, frequency_error = _calculate_recovered_frequency(
|
|
857
|
+
vco_control, nominal_frequency, K_vco, n_samples
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
return PLLRecoveryResult(
|
|
861
|
+
recovered_frequency=float(recovered_frequency),
|
|
862
|
+
recovered_phase=phase,
|
|
863
|
+
vco_control=vco_control,
|
|
864
|
+
lock_status=lock_status,
|
|
865
|
+
lock_time=lock_time,
|
|
866
|
+
frequency_error=float(frequency_error),
|
|
867
|
+
)
|
|
754
868
|
|
|
869
|
+
|
|
870
|
+
def _prepare_pll_data(
|
|
871
|
+
trace: WaveformTrace | DigitalTrace,
|
|
872
|
+
) -> tuple[NDArray[np.float64], float, int]:
|
|
873
|
+
"""Prepare data for PLL processing."""
|
|
874
|
+
data = trace.data.astype(np.float64) if isinstance(trace, DigitalTrace) else trace.data
|
|
755
875
|
sample_rate = trace.metadata.sample_rate
|
|
756
876
|
n_samples = len(data)
|
|
757
877
|
|
|
@@ -763,104 +883,111 @@ def pll_clock_recovery(
|
|
|
763
883
|
analysis_type="pll_clock_recovery",
|
|
764
884
|
)
|
|
765
885
|
|
|
766
|
-
|
|
886
|
+
return data, sample_rate, n_samples
|
|
767
887
|
|
|
768
|
-
# PLL parameters
|
|
769
|
-
omega_n = 2 * np.pi * loop_bandwidth # Natural frequency
|
|
770
|
-
K_vco = 2 * np.pi * vco_gain # VCO gain in rad/s/V
|
|
771
888
|
|
|
772
|
-
|
|
773
|
-
|
|
889
|
+
def _calculate_pll_coefficients(
|
|
890
|
+
loop_bandwidth: float, damping: float, vco_gain: float
|
|
891
|
+
) -> tuple[float, float, float]:
|
|
892
|
+
"""Calculate PLL loop filter coefficients."""
|
|
893
|
+
omega_n = 2 * np.pi * loop_bandwidth
|
|
894
|
+
K_vco = 2 * np.pi * vco_gain
|
|
774
895
|
K1 = (2 * damping * omega_n) / K_vco
|
|
775
896
|
K2 = (omega_n**2) / K_vco
|
|
897
|
+
return K1, K2, K_vco
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
def _find_edges_for_phase_detection(data: NDArray[np.float64]) -> NDArray[np.intp]:
|
|
901
|
+
"""Find edges in data for phase detection."""
|
|
902
|
+
threshold = (np.max(data) + np.min(data)) / 2
|
|
903
|
+
return np.where(np.abs(np.diff(np.sign(data - threshold))) > 0)[0]
|
|
904
|
+
|
|
776
905
|
|
|
777
|
-
|
|
906
|
+
def _run_pll_loop(
|
|
907
|
+
n_samples: int,
|
|
908
|
+
edges: NDArray[np.intp],
|
|
909
|
+
nominal_phase_inc: float,
|
|
910
|
+
K1: float,
|
|
911
|
+
K2: float,
|
|
912
|
+
K_vco: float,
|
|
913
|
+
nominal_frequency: float,
|
|
914
|
+
dt: float,
|
|
915
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
916
|
+
"""Run PLL loop simulation."""
|
|
778
917
|
phase = np.zeros(n_samples)
|
|
779
918
|
vco_control = np.zeros(n_samples)
|
|
780
919
|
integrator = 0.0
|
|
781
920
|
theta = 0.0
|
|
782
|
-
|
|
783
|
-
# Nominal phase increment per sample
|
|
784
|
-
nominal_phase_inc = 2 * np.pi * nominal_frequency * dt
|
|
785
|
-
|
|
786
|
-
# Find edges for phase detection (simplified)
|
|
787
|
-
threshold = (np.max(data) + np.min(data)) / 2
|
|
788
|
-
edges = np.where(np.abs(np.diff(np.sign(data - threshold))) > 0)[0]
|
|
789
|
-
|
|
790
921
|
edge_idx = 0
|
|
791
922
|
|
|
792
|
-
# Run PLL loop
|
|
793
923
|
for i in range(n_samples):
|
|
794
|
-
|
|
924
|
+
phase_error = _compute_phase_error(i, edge_idx, edges, nominal_phase_inc, theta)
|
|
795
925
|
if edge_idx < len(edges) and i == edges[edge_idx]:
|
|
796
|
-
# Edge detected - compute phase error
|
|
797
|
-
# Phase error = expected phase - actual VCO phase
|
|
798
|
-
expected_phase = (edges[edge_idx] * nominal_phase_inc) % (2 * np.pi)
|
|
799
|
-
phase_error = expected_phase - (theta % (2 * np.pi))
|
|
800
|
-
|
|
801
|
-
# Wrap to [-pi, pi]
|
|
802
|
-
phase_error = (phase_error + np.pi) % (2 * np.pi) - np.pi
|
|
803
|
-
|
|
804
926
|
edge_idx += 1
|
|
805
|
-
else:
|
|
806
|
-
phase_error = 0.0
|
|
807
927
|
|
|
808
|
-
# Loop filter (proportional + integral)
|
|
809
928
|
integrator += K2 * phase_error * dt
|
|
810
929
|
vco_input = K1 * phase_error + integrator
|
|
811
930
|
|
|
812
|
-
# VCO: frequency = nominal + K_vco * control voltage
|
|
813
931
|
vco_freq = nominal_frequency + K_vco * vco_input / (2 * np.pi)
|
|
814
932
|
phase_increment = 2 * np.pi * vco_freq * dt
|
|
815
|
-
|
|
816
|
-
# Update phase
|
|
817
933
|
theta += phase_increment
|
|
818
934
|
|
|
819
|
-
# Store results
|
|
820
935
|
phase[i] = theta
|
|
821
936
|
vco_control[i] = vco_input
|
|
822
937
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
938
|
+
return phase, vco_control
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
def _compute_phase_error(
|
|
942
|
+
i: int, edge_idx: int, edges: NDArray[np.intp], nominal_phase_inc: float, theta: float
|
|
943
|
+
) -> float:
|
|
944
|
+
"""Compute phase error at sample i."""
|
|
945
|
+
if edge_idx < len(edges) and i == edges[edge_idx]:
|
|
946
|
+
expected_phase = (edges[edge_idx] * nominal_phase_inc) % (2 * np.pi)
|
|
947
|
+
phase_error = expected_phase - (theta % (2 * np.pi))
|
|
948
|
+
wrapped_error: float = float((phase_error + np.pi) % (2 * np.pi) - np.pi)
|
|
949
|
+
return wrapped_error
|
|
950
|
+
return 0.0
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
def _analyze_lock_status(
|
|
954
|
+
vco_control: NDArray[np.float64], n_samples: int, dt: float
|
|
955
|
+
) -> tuple[bool, float | None]:
|
|
956
|
+
"""Analyze PLL lock status and find lock time."""
|
|
957
|
+
lock_threshold = 0.1
|
|
826
958
|
last_20_percent = vco_control[int(0.8 * n_samples) :]
|
|
827
959
|
|
|
828
|
-
if len(last_20_percent)
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
window_std = np.std(vco_control[i : i + window])
|
|
839
|
-
if window_std < lock_threshold:
|
|
840
|
-
lock_time = i * dt
|
|
841
|
-
break
|
|
842
|
-
else:
|
|
843
|
-
lock_time = None
|
|
844
|
-
else:
|
|
845
|
-
lock_time = None
|
|
846
|
-
else:
|
|
847
|
-
lock_status = False
|
|
848
|
-
lock_time = None
|
|
960
|
+
if len(last_20_percent) == 0:
|
|
961
|
+
return False, None
|
|
962
|
+
|
|
963
|
+
vco_std = np.std(last_20_percent)
|
|
964
|
+
vco_mean = np.abs(np.mean(last_20_percent))
|
|
965
|
+
lock_status = vco_std < lock_threshold * max(vco_mean, 1.0)
|
|
966
|
+
|
|
967
|
+
lock_time = _find_lock_time(vco_control, n_samples, dt, lock_threshold) if lock_status else None
|
|
968
|
+
return lock_status, lock_time
|
|
969
|
+
|
|
849
970
|
|
|
850
|
-
|
|
971
|
+
def _find_lock_time(
|
|
972
|
+
vco_control: NDArray[np.float64], n_samples: int, dt: float, lock_threshold: float
|
|
973
|
+
) -> float | None:
|
|
974
|
+
"""Find time when PLL achieved lock."""
|
|
975
|
+
window = int(0.1 * n_samples)
|
|
976
|
+
for i in range(window, n_samples - window):
|
|
977
|
+
window_std = np.std(vco_control[i : i + window])
|
|
978
|
+
if window_std < lock_threshold:
|
|
979
|
+
return i * dt
|
|
980
|
+
return None
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
def _calculate_recovered_frequency(
|
|
984
|
+
vco_control: NDArray[np.float64], nominal_frequency: float, K_vco: float, n_samples: int
|
|
985
|
+
) -> tuple[float, float]:
|
|
986
|
+
"""Calculate recovered frequency and frequency error."""
|
|
851
987
|
final_vco = np.mean(vco_control[-int(0.1 * n_samples) :])
|
|
852
988
|
recovered_frequency = nominal_frequency + K_vco * final_vco / (2 * np.pi)
|
|
853
|
-
|
|
854
989
|
frequency_error = recovered_frequency - nominal_frequency
|
|
855
|
-
|
|
856
|
-
return PLLRecoveryResult(
|
|
857
|
-
recovered_frequency=float(recovered_frequency),
|
|
858
|
-
recovered_phase=phase,
|
|
859
|
-
vco_control=vco_control,
|
|
860
|
-
lock_status=lock_status,
|
|
861
|
-
lock_time=lock_time,
|
|
862
|
-
frequency_error=float(frequency_error),
|
|
863
|
-
)
|
|
990
|
+
return recovered_frequency, frequency_error
|
|
864
991
|
|
|
865
992
|
|
|
866
993
|
__all__ = [
|