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
|
@@ -5,7 +5,7 @@ mixed logic families and multi-voltage domains.
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
Example:
|
|
8
|
-
>>> from oscura.exploratory.legacy import detect_logic_families_multi_channel
|
|
8
|
+
>>> from oscura.jupyter.exploratory.legacy import detect_logic_families_multi_channel
|
|
9
9
|
>>> families = detect_logic_families_multi_channel(channels)
|
|
10
10
|
>>> for ch, result in families.items():
|
|
11
11
|
... print(f"Channel {ch}: {result['family']} (confidence={result['confidence']:.2f})")
|
|
@@ -24,7 +24,7 @@ if TYPE_CHECKING:
|
|
|
24
24
|
from oscura.core.types import WaveformTrace
|
|
25
25
|
|
|
26
26
|
# Logic family specifications per IEEE/JEDEC standards
|
|
27
|
-
LOGIC_FAMILY_SPECS = {
|
|
27
|
+
LOGIC_FAMILY_SPECS: dict[str, dict[str, float | None]] = {
|
|
28
28
|
"TTL": {
|
|
29
29
|
"vil_max": 0.8,
|
|
30
30
|
"vih_min": 2.0,
|
|
@@ -152,90 +152,138 @@ def detect_logic_families_multi_channel(
|
|
|
152
152
|
confidence_thresholds = {"high": 0.9, "medium": 0.7}
|
|
153
153
|
|
|
154
154
|
# Convert list to dict if needed
|
|
155
|
-
if isinstance(channels, list)
|
|
156
|
-
channels = dict(enumerate(channels))
|
|
155
|
+
channel_dict = dict(enumerate(channels)) if isinstance(channels, list) else channels
|
|
157
156
|
|
|
158
157
|
results = {}
|
|
158
|
+
for ch_id, trace in channel_dict.items():
|
|
159
|
+
voltage_levels = _extract_voltage_levels(trace.data)
|
|
160
|
+
candidates = _score_all_logic_families(voltage_levels, voltage_tolerance)
|
|
161
|
+
result = _build_logic_family_result(
|
|
162
|
+
voltage_levels, candidates, min_edges_for_detection, warn_on_degradation
|
|
163
|
+
)
|
|
164
|
+
results[ch_id] = result
|
|
159
165
|
|
|
160
|
-
|
|
161
|
-
data = trace.data
|
|
166
|
+
return results
|
|
162
167
|
|
|
163
|
-
# Extract voltage percentiles
|
|
164
|
-
p10 = np.percentile(data, 10)
|
|
165
|
-
np.percentile(data, 50)
|
|
166
|
-
p90 = np.percentile(data, 90)
|
|
167
168
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
v_high = p90
|
|
171
|
-
v_high - v_low
|
|
169
|
+
def _extract_voltage_levels(data: NDArray[np.float64]) -> dict[str, float]:
|
|
170
|
+
"""Extract voltage levels from channel data.
|
|
172
171
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
edges = np.sum(np.abs(np.diff(data > threshold)))
|
|
172
|
+
Args:
|
|
173
|
+
data: Channel voltage data.
|
|
176
174
|
|
|
177
|
-
|
|
178
|
-
|
|
175
|
+
Returns:
|
|
176
|
+
Dict with v_low, v_high, threshold, edges.
|
|
177
|
+
"""
|
|
178
|
+
v_low = float(np.percentile(data, 10))
|
|
179
|
+
v_high = float(np.percentile(data, 90))
|
|
180
|
+
threshold = (v_low + v_high) / 2
|
|
181
|
+
edges = int(np.sum(np.abs(np.diff(data > threshold))))
|
|
179
182
|
|
|
180
|
-
|
|
181
|
-
score = _score_logic_family(v_low, v_high, specs, voltage_tolerance) # type: ignore[arg-type]
|
|
182
|
-
if score > 0:
|
|
183
|
-
candidates.append((family_name, score))
|
|
183
|
+
return {"v_low": v_low, "v_high": v_high, "threshold": threshold, "edges": edges}
|
|
184
184
|
|
|
185
|
-
# Sort by score descending
|
|
186
|
-
candidates.sort(key=lambda x: x[1], reverse=True)
|
|
187
185
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
confidence=0.0,
|
|
193
|
-
v_low=v_low,
|
|
194
|
-
v_high=v_high,
|
|
195
|
-
alternatives=[],
|
|
196
|
-
degradation_warning="No matching logic family found",
|
|
197
|
-
)
|
|
198
|
-
else:
|
|
199
|
-
best_family, best_score = candidates[0]
|
|
200
|
-
confidence = min(1.0, best_score)
|
|
201
|
-
|
|
202
|
-
# Reduce confidence if insufficient edges
|
|
203
|
-
if edges < min_edges_for_detection:
|
|
204
|
-
confidence *= 0.5
|
|
205
|
-
|
|
206
|
-
# Check for ambiguity (multiple families close in score)
|
|
207
|
-
alternatives = [
|
|
208
|
-
(name, score) for name, score in candidates[1:4] if best_score - score < 0.2
|
|
209
|
-
]
|
|
210
|
-
|
|
211
|
-
# Check for degradation
|
|
212
|
-
degradation_warning = None
|
|
213
|
-
deviation_pct = 0.0
|
|
214
|
-
|
|
215
|
-
if warn_on_degradation:
|
|
216
|
-
specs = LOGIC_FAMILY_SPECS[best_family]
|
|
217
|
-
if specs["voh_min"] is not None: # type: ignore[index]
|
|
218
|
-
expected_voh = specs["voh_min"] # type: ignore[index]
|
|
219
|
-
if v_high < expected_voh:
|
|
220
|
-
deviation_pct = 100 * (expected_voh - v_high) / expected_voh
|
|
221
|
-
if deviation_pct > 10:
|
|
222
|
-
degradation_warning = (
|
|
223
|
-
f"V_high below spec (expected >= {expected_voh:.3f}V)"
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
result = LogicFamilyResult(
|
|
227
|
-
family=best_family,
|
|
228
|
-
confidence=confidence,
|
|
229
|
-
v_low=v_low,
|
|
230
|
-
v_high=v_high,
|
|
231
|
-
alternatives=alternatives,
|
|
232
|
-
degradation_warning=degradation_warning,
|
|
233
|
-
deviation_pct=deviation_pct,
|
|
234
|
-
)
|
|
186
|
+
def _score_all_logic_families(
|
|
187
|
+
voltage_levels: dict[str, float], tolerance: float
|
|
188
|
+
) -> list[tuple[str, float]]:
|
|
189
|
+
"""Score all logic families against measured voltage levels.
|
|
235
190
|
|
|
236
|
-
|
|
191
|
+
Args:
|
|
192
|
+
voltage_levels: Dict with v_low and v_high.
|
|
193
|
+
tolerance: Voltage tolerance for matching.
|
|
237
194
|
|
|
238
|
-
|
|
195
|
+
Returns:
|
|
196
|
+
List of (family_name, score) tuples sorted by score.
|
|
197
|
+
"""
|
|
198
|
+
candidates = []
|
|
199
|
+
for family_name, specs in LOGIC_FAMILY_SPECS.items():
|
|
200
|
+
score = _score_logic_family(
|
|
201
|
+
voltage_levels["v_low"], voltage_levels["v_high"], specs, tolerance
|
|
202
|
+
)
|
|
203
|
+
if score > 0:
|
|
204
|
+
candidates.append((family_name, score))
|
|
205
|
+
|
|
206
|
+
candidates.sort(key=lambda x: x[1], reverse=True)
|
|
207
|
+
return candidates
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _build_logic_family_result(
|
|
211
|
+
voltage_levels: dict[str, float],
|
|
212
|
+
candidates: list[tuple[str, float]],
|
|
213
|
+
min_edges: int,
|
|
214
|
+
warn_on_degradation: bool,
|
|
215
|
+
) -> LogicFamilyResult:
|
|
216
|
+
"""Build LogicFamilyResult from voltage data and candidates.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
voltage_levels: Dict with v_low, v_high, edges.
|
|
220
|
+
candidates: Sorted list of (family, score) tuples.
|
|
221
|
+
min_edges: Minimum edges for reliable detection.
|
|
222
|
+
warn_on_degradation: Check for signal degradation.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
LogicFamilyResult with classification.
|
|
226
|
+
"""
|
|
227
|
+
v_low, v_high, edges = (
|
|
228
|
+
voltage_levels["v_low"],
|
|
229
|
+
voltage_levels["v_high"],
|
|
230
|
+
voltage_levels["edges"],
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
if not candidates:
|
|
234
|
+
return LogicFamilyResult(
|
|
235
|
+
family="UNKNOWN",
|
|
236
|
+
confidence=0.0,
|
|
237
|
+
v_low=v_low,
|
|
238
|
+
v_high=v_high,
|
|
239
|
+
alternatives=[],
|
|
240
|
+
degradation_warning="No matching logic family found",
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
best_family, best_score = candidates[0]
|
|
244
|
+
confidence = min(1.0, best_score)
|
|
245
|
+
if edges < min_edges:
|
|
246
|
+
confidence *= 0.5
|
|
247
|
+
|
|
248
|
+
alternatives = [(n, s) for n, s in candidates[1:4] if best_score - s < 0.2]
|
|
249
|
+
degradation_warning, deviation_pct = _check_degradation(
|
|
250
|
+
best_family, v_high, warn_on_degradation
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return LogicFamilyResult(
|
|
254
|
+
family=best_family,
|
|
255
|
+
confidence=confidence,
|
|
256
|
+
v_low=v_low,
|
|
257
|
+
v_high=v_high,
|
|
258
|
+
alternatives=alternatives,
|
|
259
|
+
degradation_warning=degradation_warning,
|
|
260
|
+
deviation_pct=deviation_pct,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _check_degradation(family: str, v_high: float, check: bool) -> tuple[str | None, float]:
|
|
265
|
+
"""Check for signal degradation against spec.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
family: Logic family name.
|
|
269
|
+
v_high: Measured high voltage.
|
|
270
|
+
check: Whether to perform check.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Tuple of (warning_message, deviation_percent).
|
|
274
|
+
"""
|
|
275
|
+
if not check:
|
|
276
|
+
return None, 0.0
|
|
277
|
+
|
|
278
|
+
specs = LOGIC_FAMILY_SPECS[family]
|
|
279
|
+
expected_voh = specs["voh_min"]
|
|
280
|
+
|
|
281
|
+
if expected_voh is not None and v_high < expected_voh:
|
|
282
|
+
deviation_pct = 100 * (expected_voh - v_high) / expected_voh
|
|
283
|
+
if deviation_pct > 10:
|
|
284
|
+
return f"V_high below spec (expected >= {expected_voh:.3f}V)", deviation_pct
|
|
285
|
+
|
|
286
|
+
return None, 0.0
|
|
239
287
|
|
|
240
288
|
|
|
241
289
|
def _score_logic_family(
|
|
@@ -309,6 +357,104 @@ class CrossCorrelationResult:
|
|
|
309
357
|
normalized_signal2: NDArray[np.float64] | None = None
|
|
310
358
|
|
|
311
359
|
|
|
360
|
+
def _prepare_signals_for_correlation(
|
|
361
|
+
data1: NDArray[np.float64], data2: NDArray[np.float64]
|
|
362
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64], int]:
|
|
363
|
+
"""Prepare signals for correlation by normalizing and aligning.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
data1: First signal data.
|
|
367
|
+
data2: Second signal data.
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
Tuple of (norm1, norm2_corrected, min_len).
|
|
371
|
+
"""
|
|
372
|
+
norm1 = _normalize_to_logic_levels(data1)
|
|
373
|
+
norm2 = _normalize_to_logic_levels(data2)
|
|
374
|
+
dc_offset = np.mean(norm1) - np.mean(norm2)
|
|
375
|
+
norm2_corrected = norm2 + dc_offset
|
|
376
|
+
min_len = min(len(norm1), len(norm2_corrected))
|
|
377
|
+
return norm1[:min_len], norm2_corrected[:min_len], min_len
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _compute_correlation_and_lag(
|
|
381
|
+
norm1: NDArray[np.float64], norm2: NDArray[np.float64]
|
|
382
|
+
) -> tuple[float, int]:
|
|
383
|
+
"""Compute correlation coefficient and lag.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
norm1: Normalized first signal.
|
|
387
|
+
norm2: Normalized second signal.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
Tuple of (correlation, lag_samples).
|
|
391
|
+
"""
|
|
392
|
+
correlation = np.corrcoef(norm1, norm2)[0, 1]
|
|
393
|
+
xcorr = np.correlate(norm1 - np.mean(norm1), norm2 - np.mean(norm2), mode="full")
|
|
394
|
+
lag_samples = xcorr.argmax() - (len(norm1) - 1)
|
|
395
|
+
return float(correlation), int(lag_samples)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _compute_reference_offset(
|
|
399
|
+
data1: NDArray[np.float64], data2: NDArray[np.float64], correlation: float
|
|
400
|
+
) -> tuple[float, float]:
|
|
401
|
+
"""Compute reference voltage offset and confidence.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
data1: First signal data.
|
|
405
|
+
data2: Second signal data.
|
|
406
|
+
correlation: Correlation coefficient.
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
Tuple of (ref_offset_mv, confidence).
|
|
410
|
+
"""
|
|
411
|
+
v1_min: float = float(np.min(data1))
|
|
412
|
+
v2_min: float = float(np.min(data2))
|
|
413
|
+
ref_offset_mv: float = (v2_min - v1_min) * 1000
|
|
414
|
+
confidence: float = abs(correlation) * (1 - min(abs(ref_offset_mv) / 1000, 1.0))
|
|
415
|
+
return float(ref_offset_mv), float(confidence)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _detect_reference_drift(
|
|
419
|
+
data1: NDArray[np.float64],
|
|
420
|
+
data2: NDArray[np.float64],
|
|
421
|
+
sample_rate: float,
|
|
422
|
+
drift_window_ms: float,
|
|
423
|
+
min_len: int,
|
|
424
|
+
) -> tuple[bool, float | None]:
|
|
425
|
+
"""Detect time-varying reference drift.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
data1: First signal data.
|
|
429
|
+
data2: Second signal data.
|
|
430
|
+
sample_rate: Sample rate in Hz.
|
|
431
|
+
drift_window_ms: Window size in ms.
|
|
432
|
+
min_len: Minimum length of signals.
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
Tuple of (drift_detected, drift_rate).
|
|
436
|
+
"""
|
|
437
|
+
window_samples = int(drift_window_ms * 1e-3 * sample_rate)
|
|
438
|
+
n_windows = min_len // window_samples
|
|
439
|
+
|
|
440
|
+
if n_windows < 2:
|
|
441
|
+
return False, None
|
|
442
|
+
|
|
443
|
+
offsets = [
|
|
444
|
+
np.mean(data1[i * window_samples : (i + 1) * window_samples])
|
|
445
|
+
- np.mean(data2[i * window_samples : (i + 1) * window_samples])
|
|
446
|
+
for i in range(n_windows)
|
|
447
|
+
]
|
|
448
|
+
|
|
449
|
+
offset_change = abs(offsets[-1] - offsets[0])
|
|
450
|
+
drift_rate_val = offset_change / (n_windows * drift_window_ms)
|
|
451
|
+
|
|
452
|
+
if drift_rate_val > 0.1:
|
|
453
|
+
return True, float(drift_rate_val)
|
|
454
|
+
|
|
455
|
+
return False, None
|
|
456
|
+
|
|
457
|
+
|
|
312
458
|
def cross_correlate_multi_reference(
|
|
313
459
|
signal1: WaveformTrace,
|
|
314
460
|
signal2: WaveformTrace,
|
|
@@ -341,82 +487,33 @@ def cross_correlate_multi_reference(
|
|
|
341
487
|
References:
|
|
342
488
|
LEGACY-002: Multi-Reference Voltage Signal Correlation
|
|
343
489
|
"""
|
|
344
|
-
|
|
345
|
-
|
|
490
|
+
# Setup: normalize and align signals
|
|
491
|
+
norm1, norm2_corrected, min_len = _prepare_signals_for_correlation(signal1.data, signal2.data)
|
|
346
492
|
|
|
347
|
-
#
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
# Estimate DC offset between normalized signals
|
|
352
|
-
dc_offset = np.mean(norm1) - np.mean(norm2)
|
|
493
|
+
# Processing: compute correlation
|
|
494
|
+
correlation, lag_samples = _compute_correlation_and_lag(norm1, norm2_corrected)
|
|
495
|
+
ref_offset_mv, confidence = _compute_reference_offset(signal1.data, signal2.data, correlation)
|
|
353
496
|
|
|
354
|
-
#
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
correlation = np.corrcoef(norm1, norm2_corrected)[0, 1]
|
|
363
|
-
|
|
364
|
-
# Find lag using cross-correlation
|
|
365
|
-
xcorr = np.correlate(
|
|
366
|
-
norm1 - np.mean(norm1), norm2_corrected - np.mean(norm2_corrected), mode="full"
|
|
497
|
+
# Formatting: detect drift if requested
|
|
498
|
+
drift_result = (
|
|
499
|
+
_detect_reference_drift(
|
|
500
|
+
signal1.data, signal2.data, signal1.metadata.sample_rate, drift_window_ms, min_len
|
|
501
|
+
)
|
|
502
|
+
if detect_drift
|
|
503
|
+
else (False, None)
|
|
367
504
|
)
|
|
368
|
-
lag_samples = xcorr.argmax() - (len(norm1) - 1)
|
|
369
|
-
|
|
370
|
-
# Convert lag to nanoseconds
|
|
371
|
-
sample_rate = signal1.metadata.sample_rate
|
|
372
|
-
lag_ns = lag_samples / sample_rate * 1e9
|
|
373
|
-
|
|
374
|
-
# Estimate reference voltage offset
|
|
375
|
-
# Reference offset is how much signal2's ground differs from signal1's ground
|
|
376
|
-
v1_min = np.min(data1)
|
|
377
|
-
v2_min = np.min(data2)
|
|
378
|
-
|
|
379
|
-
# Reference offset is difference in ground levels (signal2 relative to signal1)
|
|
380
|
-
ref_offset_mv = (v2_min - v1_min) * 1000
|
|
381
505
|
|
|
382
|
-
|
|
383
|
-
offset_uncertainty_mv = abs(ref_offset_mv) * 0.1 # 10% uncertainty
|
|
384
|
-
confidence = abs(correlation) * (1 - min(abs(ref_offset_mv) / 1000, 1.0))
|
|
385
|
-
|
|
386
|
-
# Drift detection
|
|
387
|
-
drift_detected = False
|
|
388
|
-
drift_rate = None
|
|
389
|
-
|
|
390
|
-
if detect_drift:
|
|
391
|
-
# Calculate offset in windows
|
|
392
|
-
window_samples = int(drift_window_ms * 1e-3 * sample_rate)
|
|
393
|
-
n_windows = min_len // window_samples
|
|
394
|
-
|
|
395
|
-
if n_windows >= 2:
|
|
396
|
-
offsets = []
|
|
397
|
-
for i in range(n_windows):
|
|
398
|
-
start = i * window_samples
|
|
399
|
-
end = start + window_samples
|
|
400
|
-
win_offset = np.mean(data1[start:end]) - np.mean(data2[start:end])
|
|
401
|
-
offsets.append(win_offset)
|
|
402
|
-
|
|
403
|
-
# Check for drift
|
|
404
|
-
offset_change = abs(offsets[-1] - offsets[0])
|
|
405
|
-
drift_rate_val = offset_change / (n_windows * drift_window_ms)
|
|
406
|
-
|
|
407
|
-
if drift_rate_val > 0.1: # V/ms threshold
|
|
408
|
-
drift_detected = True
|
|
409
|
-
drift_rate = drift_rate_val
|
|
506
|
+
lag_ns = lag_samples / signal1.metadata.sample_rate * 1e9
|
|
410
507
|
|
|
411
508
|
return CrossCorrelationResult(
|
|
412
509
|
correlation=float(correlation),
|
|
413
510
|
confidence=float(confidence),
|
|
414
511
|
ref_offset_mv=float(ref_offset_mv),
|
|
415
|
-
offset_uncertainty_mv=float(
|
|
512
|
+
offset_uncertainty_mv=float(abs(ref_offset_mv) * 0.1),
|
|
416
513
|
lag_samples=int(lag_samples),
|
|
417
514
|
lag_ns=float(lag_ns),
|
|
418
|
-
drift_detected=
|
|
419
|
-
drift_rate=
|
|
515
|
+
drift_detected=drift_result[0],
|
|
516
|
+
drift_rate=drift_result[1],
|
|
420
517
|
normalized_signal1=norm1,
|
|
421
518
|
normalized_signal2=norm2_corrected,
|
|
422
519
|
)
|
|
@@ -511,36 +608,95 @@ def assess_signal_quality(
|
|
|
511
608
|
if logic_family not in LOGIC_FAMILY_SPECS:
|
|
512
609
|
logic_family = "TTL" # Default fallback
|
|
513
610
|
|
|
514
|
-
|
|
611
|
+
specs_dict: dict[str, float | None] = dict(LOGIC_FAMILY_SPECS[logic_family])
|
|
515
612
|
data = signal.data
|
|
516
613
|
sample_rate = signal.metadata.sample_rate
|
|
517
|
-
n_samples = len(data)
|
|
518
|
-
|
|
519
|
-
# Threshold for high/low classification
|
|
520
|
-
threshold = (specs["vil_max"] + specs["vih_min"]) / 2 # type: ignore[index]
|
|
521
614
|
|
|
522
|
-
# Classify samples
|
|
615
|
+
# Classify samples and check violations
|
|
616
|
+
vil_max = specs_dict["vil_max"]
|
|
617
|
+
vih_min = specs_dict["vih_min"]
|
|
618
|
+
assert vil_max is not None and vih_min is not None
|
|
619
|
+
threshold = (vil_max + vih_min) / 2
|
|
523
620
|
is_high = data > threshold
|
|
524
|
-
|
|
621
|
+
violations, voh_violations, vol_violations = _check_voltage_violations(
|
|
622
|
+
data, is_high, specs_dict, sample_rate
|
|
623
|
+
)
|
|
525
624
|
|
|
526
|
-
#
|
|
625
|
+
# Calculate margins and status
|
|
527
626
|
high_samples = data[is_high]
|
|
528
|
-
low_samples = data[
|
|
627
|
+
low_samples = data[~is_high]
|
|
628
|
+
min_margin_mv = _calculate_voltage_margins(high_samples, low_samples, specs_dict)
|
|
629
|
+
status = _determine_quality_status(min_margin_mv)
|
|
630
|
+
|
|
631
|
+
# Calculate violation rates
|
|
632
|
+
n_high = len(high_samples)
|
|
633
|
+
n_low = len(low_samples)
|
|
634
|
+
voh_rate = voh_violations / n_high if n_high > 0 else 0.0
|
|
635
|
+
vol_rate = vol_violations / n_low if n_low > 0 else 0.0
|
|
636
|
+
|
|
637
|
+
# Aging analysis
|
|
638
|
+
aging_result = None
|
|
639
|
+
if check_aging and len(data) > 1000:
|
|
640
|
+
aging_result = _analyze_aging(
|
|
641
|
+
data,
|
|
642
|
+
high_samples,
|
|
643
|
+
specs_dict,
|
|
644
|
+
sample_rate,
|
|
645
|
+
time_window_s,
|
|
646
|
+
voh_violations,
|
|
647
|
+
vol_violations,
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
return SignalQualityResult(
|
|
651
|
+
status=status,
|
|
652
|
+
violation_count=voh_violations + vol_violations,
|
|
653
|
+
total_samples=len(data),
|
|
654
|
+
min_margin_mv=min_margin_mv,
|
|
655
|
+
violations=violations,
|
|
656
|
+
voh_violations=voh_violations,
|
|
657
|
+
vol_violations=vol_violations,
|
|
658
|
+
voh_rate=voh_rate,
|
|
659
|
+
vol_rate=vol_rate,
|
|
660
|
+
failure_diagnosis=aging_result.get("diagnosis") if aging_result else None,
|
|
661
|
+
time_to_failure_s=aging_result.get("time_to_failure") if aging_result else None,
|
|
662
|
+
drift_rate_mv_per_s=aging_result.get("drift_rate") if aging_result else None,
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def _check_voltage_violations(
|
|
667
|
+
data: NDArray[np.float64],
|
|
668
|
+
is_high: NDArray[np.bool_],
|
|
669
|
+
specs: dict[str, float | None],
|
|
670
|
+
sample_rate: float,
|
|
671
|
+
) -> tuple[list[dict[str, Any]], int, int]:
|
|
672
|
+
"""Check for VOH and VOL violations.
|
|
529
673
|
|
|
530
|
-
|
|
531
|
-
|
|
674
|
+
Args:
|
|
675
|
+
data: Signal data.
|
|
676
|
+
is_high: Boolean mask for high samples.
|
|
677
|
+
specs: Logic family specifications.
|
|
678
|
+
sample_rate: Sample rate in Hz.
|
|
532
679
|
|
|
680
|
+
Returns:
|
|
681
|
+
Tuple of (violations list, voh_violation_count, vol_violation_count).
|
|
682
|
+
"""
|
|
683
|
+
high_samples = data[is_high]
|
|
684
|
+
low_samples = data[~is_high]
|
|
685
|
+
|
|
686
|
+
voh_min = specs["voh_min"]
|
|
687
|
+
vol_max = specs["vol_max"]
|
|
688
|
+
|
|
689
|
+
violations = []
|
|
533
690
|
voh_violations = 0
|
|
534
691
|
vol_violations = 0
|
|
535
|
-
violations = []
|
|
536
692
|
|
|
537
|
-
# Check VOH violations
|
|
693
|
+
# Check VOH violations
|
|
538
694
|
if voh_min is not None and len(high_samples) > 0:
|
|
539
695
|
voh_mask = high_samples < voh_min
|
|
540
|
-
voh_violations = np.sum(voh_mask)
|
|
696
|
+
voh_violations = int(np.sum(voh_mask))
|
|
541
697
|
if voh_violations > 0:
|
|
542
698
|
violation_indices = np.where(is_high)[0][voh_mask]
|
|
543
|
-
for idx in violation_indices[:10]:
|
|
699
|
+
for idx in violation_indices[:10]:
|
|
544
700
|
violations.append(
|
|
545
701
|
{
|
|
546
702
|
"timestamp_us": idx / sample_rate * 1e6,
|
|
@@ -550,12 +706,12 @@ def assess_signal_quality(
|
|
|
550
706
|
}
|
|
551
707
|
)
|
|
552
708
|
|
|
553
|
-
# Check VOL violations
|
|
709
|
+
# Check VOL violations
|
|
554
710
|
if vol_max is not None and len(low_samples) > 0:
|
|
555
711
|
vol_mask = low_samples > vol_max
|
|
556
|
-
vol_violations = np.sum(vol_mask)
|
|
712
|
+
vol_violations = int(np.sum(vol_mask))
|
|
557
713
|
if vol_violations > 0:
|
|
558
|
-
violation_indices = np.where(
|
|
714
|
+
violation_indices = np.where(~is_high)[0][vol_mask]
|
|
559
715
|
for idx in violation_indices[:10]:
|
|
560
716
|
violations.append(
|
|
561
717
|
{
|
|
@@ -566,79 +722,113 @@ def assess_signal_quality(
|
|
|
566
722
|
}
|
|
567
723
|
)
|
|
568
724
|
|
|
569
|
-
|
|
725
|
+
return violations, voh_violations, vol_violations
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def _calculate_voltage_margins(
|
|
729
|
+
high_samples: NDArray[np.float64],
|
|
730
|
+
low_samples: NDArray[np.float64],
|
|
731
|
+
specs: dict[str, float | None],
|
|
732
|
+
) -> float:
|
|
733
|
+
"""Calculate minimum voltage margin to spec.
|
|
734
|
+
|
|
735
|
+
Args:
|
|
736
|
+
high_samples: High-level samples.
|
|
737
|
+
low_samples: Low-level samples.
|
|
738
|
+
specs: Logic family specifications.
|
|
570
739
|
|
|
571
|
-
|
|
572
|
-
|
|
740
|
+
Returns:
|
|
741
|
+
Minimum margin in mV.
|
|
742
|
+
"""
|
|
743
|
+
voh_min = specs["voh_min"]
|
|
744
|
+
vol_max = specs["vol_max"]
|
|
745
|
+
|
|
746
|
+
margins: list[float] = []
|
|
573
747
|
if len(high_samples) > 0 and voh_min is not None:
|
|
574
|
-
margins.extend((high_samples - voh_min) * 1000)
|
|
748
|
+
margins.extend((high_samples - voh_min) * 1000)
|
|
575
749
|
if len(low_samples) > 0 and vol_max is not None:
|
|
576
750
|
margins.extend((vol_max - low_samples) * 1000)
|
|
577
751
|
|
|
578
|
-
|
|
752
|
+
return min(margins) if margins else 0.0
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def _determine_quality_status(min_margin_mv: float) -> Literal["OK", "WARNING", "CRITICAL"]:
|
|
756
|
+
"""Determine quality status from voltage margin.
|
|
579
757
|
|
|
580
|
-
|
|
758
|
+
Args:
|
|
759
|
+
min_margin_mv: Minimum voltage margin in mV.
|
|
760
|
+
|
|
761
|
+
Returns:
|
|
762
|
+
Quality status.
|
|
763
|
+
"""
|
|
581
764
|
if min_margin_mv < 100:
|
|
582
|
-
|
|
765
|
+
return "CRITICAL"
|
|
583
766
|
elif min_margin_mv < 200:
|
|
584
|
-
|
|
767
|
+
return "WARNING"
|
|
585
768
|
else:
|
|
586
|
-
|
|
769
|
+
return "OK"
|
|
587
770
|
|
|
588
|
-
# Calculate rates
|
|
589
|
-
n_high = len(high_samples)
|
|
590
|
-
n_low = len(low_samples)
|
|
591
|
-
voh_rate = voh_violations / n_high if n_high > 0 else 0.0
|
|
592
|
-
vol_rate = vol_violations / n_low if n_low > 0 else 0.0
|
|
593
771
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
if n_windows >= 2:
|
|
605
|
-
window_means = [
|
|
606
|
-
np.mean(data[i * window_samples : (i + 1) * window_samples])
|
|
607
|
-
for i in range(n_windows)
|
|
608
|
-
]
|
|
609
|
-
|
|
610
|
-
drift = window_means[-1] - window_means[0]
|
|
611
|
-
drift_rate_mv_per_s = drift * 1000 / (n_windows * time_window_s)
|
|
612
|
-
|
|
613
|
-
if abs(drift_rate_mv_per_s) > 0.1: # Significant drift
|
|
614
|
-
# Estimate time to failure
|
|
615
|
-
if voh_min is not None and drift_rate_mv_per_s < 0:
|
|
616
|
-
current_margin = np.mean(high_samples) - voh_min
|
|
617
|
-
if current_margin > 0:
|
|
618
|
-
time_to_failure_s = current_margin * 1000 / abs(drift_rate_mv_per_s)
|
|
619
|
-
|
|
620
|
-
# Diagnose failure mode
|
|
621
|
-
if voh_violations > vol_violations:
|
|
622
|
-
failure_diagnosis = "Degraded output driver (weak high)"
|
|
623
|
-
elif vol_violations > voh_violations:
|
|
624
|
-
failure_diagnosis = "Degraded output driver (weak low)"
|
|
625
|
-
else:
|
|
626
|
-
failure_diagnosis = "General signal degradation"
|
|
772
|
+
def _analyze_aging(
|
|
773
|
+
data: NDArray[np.float64],
|
|
774
|
+
high_samples: NDArray[np.float64],
|
|
775
|
+
specs: dict[str, float | None],
|
|
776
|
+
sample_rate: float,
|
|
777
|
+
time_window_s: float,
|
|
778
|
+
voh_violations: int,
|
|
779
|
+
vol_violations: int,
|
|
780
|
+
) -> dict[str, Any] | None:
|
|
781
|
+
"""Analyze signal for aging and degradation.
|
|
627
782
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
voh_violations
|
|
635
|
-
vol_violations
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
)
|
|
783
|
+
Args:
|
|
784
|
+
data: Full signal data.
|
|
785
|
+
high_samples: High-level samples.
|
|
786
|
+
specs: Logic family specifications.
|
|
787
|
+
sample_rate: Sample rate in Hz.
|
|
788
|
+
time_window_s: Window size for drift analysis.
|
|
789
|
+
voh_violations: Count of VOH violations.
|
|
790
|
+
vol_violations: Count of VOL violations.
|
|
791
|
+
|
|
792
|
+
Returns:
|
|
793
|
+
Dict with diagnosis, time_to_failure, and drift_rate, or None.
|
|
794
|
+
"""
|
|
795
|
+
window_samples = int(time_window_s * sample_rate)
|
|
796
|
+
n_windows = len(data) // window_samples
|
|
797
|
+
|
|
798
|
+
if n_windows < 2:
|
|
799
|
+
return None
|
|
800
|
+
|
|
801
|
+
window_means = [
|
|
802
|
+
np.mean(data[i * window_samples : (i + 1) * window_samples]) for i in range(n_windows)
|
|
803
|
+
]
|
|
804
|
+
|
|
805
|
+
drift = window_means[-1] - window_means[0]
|
|
806
|
+
drift_rate_mv_per_s = drift * 1000 / (n_windows * time_window_s)
|
|
807
|
+
|
|
808
|
+
if abs(drift_rate_mv_per_s) <= 0.1:
|
|
809
|
+
return None
|
|
810
|
+
|
|
811
|
+
# Estimate time to failure
|
|
812
|
+
time_to_failure = None
|
|
813
|
+
voh_min = specs["voh_min"]
|
|
814
|
+
if voh_min is not None and drift_rate_mv_per_s < 0:
|
|
815
|
+
current_margin = np.mean(high_samples) - voh_min
|
|
816
|
+
if current_margin > 0:
|
|
817
|
+
time_to_failure = current_margin * 1000 / abs(drift_rate_mv_per_s)
|
|
818
|
+
|
|
819
|
+
# Diagnose failure mode
|
|
820
|
+
if voh_violations > vol_violations:
|
|
821
|
+
diagnosis = "Degraded output driver (weak high)"
|
|
822
|
+
elif vol_violations > voh_violations:
|
|
823
|
+
diagnosis = "Degraded output driver (weak low)"
|
|
824
|
+
else:
|
|
825
|
+
diagnosis = "General signal degradation"
|
|
826
|
+
|
|
827
|
+
return {
|
|
828
|
+
"diagnosis": diagnosis,
|
|
829
|
+
"time_to_failure": time_to_failure,
|
|
830
|
+
"drift_rate": drift_rate_mv_per_s,
|
|
831
|
+
}
|
|
642
832
|
|
|
643
833
|
|
|
644
834
|
@dataclass
|
|
@@ -773,49 +963,81 @@ def _is_bimodal(data: NDArray[np.float64], bins: int = 50) -> bool:
|
|
|
773
963
|
hist, bin_edges = np.histogram(data, bins=bins)
|
|
774
964
|
centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
775
965
|
|
|
776
|
-
# Find peaks
|
|
966
|
+
# Find peaks in histogram
|
|
967
|
+
peaks = _find_histogram_peaks(hist, centers)
|
|
968
|
+
|
|
969
|
+
# Too many peaks suggests analog signal
|
|
970
|
+
if len(peaks) >= 4:
|
|
971
|
+
return False
|
|
972
|
+
|
|
973
|
+
# Check if distribution is bimodal
|
|
974
|
+
if len(peaks) == 2 or len(peaks) == 3:
|
|
975
|
+
return _is_bimodal_distribution(data, peaks)
|
|
976
|
+
|
|
977
|
+
return False
|
|
978
|
+
|
|
979
|
+
|
|
980
|
+
def _find_histogram_peaks(
|
|
981
|
+
hist: NDArray[np.int64], centers: NDArray[np.float64]
|
|
982
|
+
) -> list[tuple[int, int, float]]:
|
|
983
|
+
"""Find peaks in histogram.
|
|
984
|
+
|
|
985
|
+
Args:
|
|
986
|
+
hist: Histogram counts.
|
|
987
|
+
centers: Bin centers.
|
|
988
|
+
|
|
989
|
+
Returns:
|
|
990
|
+
List of (index, count, center) tuples.
|
|
991
|
+
"""
|
|
777
992
|
threshold = 0.1 * np.max(hist)
|
|
778
993
|
peaks = []
|
|
779
994
|
|
|
780
995
|
# Check first bin (only needs to be > right neighbor)
|
|
781
996
|
if len(hist) > 1 and hist[0] > hist[1] and hist[0] > threshold:
|
|
782
|
-
peaks.append((0, hist[0], centers[0]))
|
|
997
|
+
peaks.append((0, int(hist[0]), float(centers[0])))
|
|
783
998
|
|
|
784
999
|
# Check middle bins (need to be > both neighbors)
|
|
785
1000
|
for i in range(1, len(hist) - 1):
|
|
786
1001
|
if hist[i] > hist[i - 1] and hist[i] > hist[i + 1] and hist[i] > threshold:
|
|
787
|
-
peaks.append((i, hist[i], centers[i]))
|
|
1002
|
+
peaks.append((i, int(hist[i]), float(centers[i])))
|
|
788
1003
|
|
|
789
1004
|
# Check last bin (only needs to be > left neighbor)
|
|
790
1005
|
if len(hist) > 1 and hist[-1] > hist[-2] and hist[-1] > threshold:
|
|
791
|
-
peaks.append((len(hist) - 1, hist[-1], centers[-1]))
|
|
1006
|
+
peaks.append((len(hist) - 1, int(hist[-1]), float(centers[-1])))
|
|
792
1007
|
|
|
793
|
-
|
|
794
|
-
if len(peaks) >= 4:
|
|
795
|
-
return False
|
|
1008
|
+
return peaks
|
|
796
1009
|
|
|
797
|
-
# Bimodal if exactly 2-3 significant peaks that are well-separated
|
|
798
|
-
if len(peaks) == 2 or len(peaks) == 3:
|
|
799
|
-
peaks.sort(key=lambda x: x[1], reverse=True)
|
|
800
1010
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
return False
|
|
1011
|
+
def _is_bimodal_distribution(
|
|
1012
|
+
data: NDArray[np.float64], peaks: list[tuple[int, int, float]]
|
|
1013
|
+
) -> bool:
|
|
1014
|
+
"""Check if peaks indicate bimodal distribution.
|
|
806
1015
|
|
|
807
|
-
|
|
808
|
-
|
|
1016
|
+
Args:
|
|
1017
|
+
data: Signal data.
|
|
1018
|
+
peaks: List of (index, count, center) tuples.
|
|
809
1019
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
1020
|
+
Returns:
|
|
1021
|
+
True if distribution is bimodal.
|
|
1022
|
+
"""
|
|
1023
|
+
# Sort peaks by count (descending)
|
|
1024
|
+
peaks.sort(key=lambda x: x[1], reverse=True)
|
|
1025
|
+
|
|
1026
|
+
# Check if peaks are well-separated
|
|
1027
|
+
v_min, v_max = np.min(data), np.max(data)
|
|
1028
|
+
v_range = v_max - v_min
|
|
1029
|
+
if v_range == 0:
|
|
1030
|
+
return False
|
|
813
1031
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
return True
|
|
1032
|
+
# Normalize peak positions
|
|
1033
|
+
peak_positions = [(p[2] - v_min) / v_range for p in peaks[:2]]
|
|
817
1034
|
|
|
818
|
-
|
|
1035
|
+
# Digital signals have one peak < 0.4 and one peak > 0.6
|
|
1036
|
+
has_low_peak = any(p < 0.4 for p in peak_positions)
|
|
1037
|
+
has_high_peak = any(p > 0.6 for p in peak_positions)
|
|
1038
|
+
|
|
1039
|
+
# Second peak should be significant
|
|
1040
|
+
return has_low_peak and has_high_peak and peaks[1][1] > 0.3 * peaks[0][1]
|
|
819
1041
|
|
|
820
1042
|
|
|
821
1043
|
__all__ = [
|