oscura 0.5.1__py3-none-any.whl → 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oscura/__init__.py +169 -167
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/classification.py +659 -0
- oscura/analyzers/digital/edges.py +325 -65
- oscura/analyzers/digital/quality.py +293 -166
- oscura/analyzers/digital/timing.py +260 -115
- oscura/analyzers/digital/timing_numba.py +334 -0
- oscura/analyzers/entropy.py +605 -0
- oscura/analyzers/eye/diagram.py +176 -109
- oscura/analyzers/eye/metrics.py +5 -5
- oscura/analyzers/jitter/__init__.py +6 -4
- oscura/analyzers/jitter/ber.py +52 -52
- oscura/analyzers/jitter/classification.py +156 -0
- oscura/analyzers/jitter/decomposition.py +163 -113
- oscura/analyzers/jitter/spectrum.py +80 -64
- oscura/analyzers/ml/__init__.py +39 -0
- oscura/analyzers/ml/features.py +600 -0
- oscura/analyzers/ml/signal_classifier.py +604 -0
- oscura/analyzers/packet/daq.py +246 -158
- oscura/analyzers/packet/parser.py +12 -1
- oscura/analyzers/packet/payload.py +50 -2110
- oscura/analyzers/packet/payload_analysis.py +361 -181
- oscura/analyzers/packet/payload_patterns.py +133 -70
- oscura/analyzers/packet/stream.py +84 -23
- oscura/analyzers/patterns/__init__.py +26 -5
- oscura/analyzers/patterns/anomaly_detection.py +908 -0
- oscura/analyzers/patterns/clustering.py +169 -108
- oscura/analyzers/patterns/clustering_optimized.py +227 -0
- oscura/analyzers/patterns/discovery.py +1 -1
- oscura/analyzers/patterns/matching.py +581 -197
- oscura/analyzers/patterns/pattern_mining.py +778 -0
- oscura/analyzers/patterns/periodic.py +121 -38
- oscura/analyzers/patterns/sequences.py +175 -78
- oscura/analyzers/power/conduction.py +1 -1
- oscura/analyzers/power/soa.py +6 -6
- oscura/analyzers/power/switching.py +250 -110
- oscura/analyzers/protocol/__init__.py +17 -1
- oscura/analyzers/protocols/base.py +6 -6
- oscura/analyzers/protocols/ble/__init__.py +38 -0
- oscura/analyzers/protocols/ble/analyzer.py +809 -0
- oscura/analyzers/protocols/ble/uuids.py +288 -0
- oscura/analyzers/protocols/can.py +257 -127
- oscura/analyzers/protocols/can_fd.py +107 -80
- oscura/analyzers/protocols/flexray.py +139 -80
- oscura/analyzers/protocols/hdlc.py +93 -58
- oscura/analyzers/protocols/i2c.py +247 -106
- oscura/analyzers/protocols/i2s.py +138 -86
- oscura/analyzers/protocols/industrial/__init__.py +40 -0
- oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
- oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
- oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
- oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
- oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
- oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
- oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
- oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
- oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
- oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
- oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
- oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
- oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
- oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
- oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
- oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
- oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
- oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
- oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
- oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
- oscura/analyzers/protocols/jtag.py +180 -98
- oscura/analyzers/protocols/lin.py +219 -114
- oscura/analyzers/protocols/manchester.py +4 -4
- oscura/analyzers/protocols/onewire.py +253 -149
- oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
- oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
- oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
- oscura/analyzers/protocols/spi.py +192 -95
- oscura/analyzers/protocols/swd.py +321 -167
- oscura/analyzers/protocols/uart.py +267 -125
- oscura/analyzers/protocols/usb.py +235 -131
- oscura/analyzers/side_channel/power.py +17 -12
- oscura/analyzers/signal/__init__.py +15 -0
- oscura/analyzers/signal/timing_analysis.py +1086 -0
- oscura/analyzers/signal_integrity/__init__.py +4 -1
- oscura/analyzers/signal_integrity/sparams.py +2 -19
- oscura/analyzers/spectral/chunked.py +129 -60
- oscura/analyzers/spectral/chunked_fft.py +300 -94
- oscura/analyzers/spectral/chunked_wavelet.py +100 -80
- oscura/analyzers/statistical/checksum.py +376 -217
- oscura/analyzers/statistical/classification.py +229 -107
- oscura/analyzers/statistical/entropy.py +78 -53
- oscura/analyzers/statistics/correlation.py +407 -211
- oscura/analyzers/statistics/outliers.py +2 -2
- oscura/analyzers/statistics/streaming.py +30 -5
- oscura/analyzers/validation.py +216 -101
- oscura/analyzers/waveform/measurements.py +9 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
- oscura/analyzers/waveform/spectral.py +500 -228
- oscura/api/__init__.py +31 -5
- oscura/api/dsl/__init__.py +582 -0
- oscura/{dsl → api/dsl}/commands.py +43 -76
- oscura/{dsl → api/dsl}/interpreter.py +26 -51
- oscura/{dsl → api/dsl}/parser.py +107 -77
- oscura/{dsl → api/dsl}/repl.py +2 -2
- oscura/api/dsl.py +1 -1
- oscura/{integrations → api/integrations}/__init__.py +1 -1
- oscura/{integrations → api/integrations}/llm.py +201 -102
- oscura/api/operators.py +3 -3
- oscura/api/optimization.py +144 -30
- oscura/api/rest_server.py +921 -0
- oscura/api/server/__init__.py +17 -0
- oscura/api/server/dashboard.py +850 -0
- oscura/api/server/static/README.md +34 -0
- oscura/api/server/templates/base.html +181 -0
- oscura/api/server/templates/export.html +120 -0
- oscura/api/server/templates/home.html +284 -0
- oscura/api/server/templates/protocols.html +58 -0
- oscura/api/server/templates/reports.html +43 -0
- oscura/api/server/templates/session_detail.html +89 -0
- oscura/api/server/templates/sessions.html +83 -0
- oscura/api/server/templates/waveforms.html +73 -0
- oscura/automotive/__init__.py +8 -1
- oscura/automotive/can/__init__.py +10 -0
- oscura/automotive/can/checksum.py +3 -1
- oscura/automotive/can/dbc_generator.py +590 -0
- oscura/automotive/can/message_wrapper.py +121 -74
- oscura/automotive/can/patterns.py +98 -21
- oscura/automotive/can/session.py +292 -56
- oscura/automotive/can/state_machine.py +6 -3
- oscura/automotive/can/stimulus_response.py +97 -75
- oscura/automotive/dbc/__init__.py +10 -2
- oscura/automotive/dbc/generator.py +84 -56
- oscura/automotive/dbc/parser.py +6 -6
- oscura/automotive/dtc/data.json +17 -102
- oscura/automotive/dtc/database.py +2 -2
- oscura/automotive/flexray/__init__.py +31 -0
- oscura/automotive/flexray/analyzer.py +504 -0
- oscura/automotive/flexray/crc.py +185 -0
- oscura/automotive/flexray/fibex.py +449 -0
- oscura/automotive/j1939/__init__.py +45 -8
- oscura/automotive/j1939/analyzer.py +605 -0
- oscura/automotive/j1939/spns.py +326 -0
- oscura/automotive/j1939/transport.py +306 -0
- oscura/automotive/lin/__init__.py +47 -0
- oscura/automotive/lin/analyzer.py +612 -0
- oscura/automotive/loaders/blf.py +13 -2
- oscura/automotive/loaders/csv_can.py +143 -72
- oscura/automotive/loaders/dispatcher.py +50 -2
- oscura/automotive/loaders/mdf.py +86 -45
- oscura/automotive/loaders/pcap.py +111 -61
- oscura/automotive/uds/__init__.py +4 -0
- oscura/automotive/uds/analyzer.py +725 -0
- oscura/automotive/uds/decoder.py +140 -58
- oscura/automotive/uds/models.py +7 -1
- oscura/automotive/visualization.py +1 -1
- oscura/cli/analyze.py +348 -0
- oscura/cli/batch.py +142 -122
- oscura/cli/benchmark.py +275 -0
- oscura/cli/characterize.py +137 -82
- oscura/cli/compare.py +224 -131
- oscura/cli/completion.py +250 -0
- oscura/cli/config_cmd.py +361 -0
- oscura/cli/decode.py +164 -87
- oscura/cli/export.py +286 -0
- oscura/cli/main.py +115 -31
- oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
- oscura/{onboarding → cli/onboarding}/help.py +80 -58
- oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
- oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
- oscura/cli/progress.py +147 -0
- oscura/cli/shell.py +157 -135
- oscura/cli/validate_cmd.py +204 -0
- oscura/cli/visualize.py +158 -0
- oscura/convenience.py +125 -79
- oscura/core/__init__.py +4 -2
- oscura/core/backend_selector.py +3 -3
- oscura/core/cache.py +126 -15
- oscura/core/cancellation.py +1 -1
- oscura/{config → core/config}/__init__.py +20 -11
- oscura/{config → core/config}/defaults.py +1 -1
- oscura/{config → core/config}/loader.py +7 -5
- oscura/{config → core/config}/memory.py +5 -5
- oscura/{config → core/config}/migration.py +1 -1
- oscura/{config → core/config}/pipeline.py +99 -23
- oscura/{config → core/config}/preferences.py +1 -1
- oscura/{config → core/config}/protocol.py +3 -3
- oscura/{config → core/config}/schema.py +426 -272
- oscura/{config → core/config}/settings.py +1 -1
- oscura/{config → core/config}/thresholds.py +195 -153
- oscura/core/correlation.py +5 -6
- oscura/core/cross_domain.py +0 -2
- oscura/core/debug.py +9 -5
- oscura/{extensibility → core/extensibility}/docs.py +158 -70
- oscura/{extensibility → core/extensibility}/extensions.py +160 -76
- oscura/{extensibility → core/extensibility}/logging.py +1 -1
- oscura/{extensibility → core/extensibility}/measurements.py +1 -1
- oscura/{extensibility → core/extensibility}/plugins.py +1 -1
- oscura/{extensibility → core/extensibility}/templates.py +73 -3
- oscura/{extensibility → core/extensibility}/validation.py +1 -1
- oscura/core/gpu_backend.py +11 -7
- oscura/core/log_query.py +101 -11
- oscura/core/logging.py +126 -54
- oscura/core/logging_advanced.py +5 -5
- oscura/core/memory_limits.py +108 -70
- oscura/core/memory_monitor.py +2 -2
- oscura/core/memory_progress.py +7 -7
- oscura/core/memory_warnings.py +1 -1
- oscura/core/numba_backend.py +13 -13
- oscura/{plugins → core/plugins}/__init__.py +9 -9
- oscura/{plugins → core/plugins}/base.py +7 -7
- oscura/{plugins → core/plugins}/cli.py +3 -3
- oscura/{plugins → core/plugins}/discovery.py +186 -106
- oscura/{plugins → core/plugins}/lifecycle.py +1 -1
- oscura/{plugins → core/plugins}/manager.py +7 -7
- oscura/{plugins → core/plugins}/registry.py +3 -3
- oscura/{plugins → core/plugins}/versioning.py +1 -1
- oscura/core/progress.py +16 -1
- oscura/core/provenance.py +8 -2
- oscura/{schemas → core/schemas}/__init__.py +2 -2
- oscura/{schemas → core/schemas}/device_mapping.json +2 -8
- oscura/{schemas → core/schemas}/packet_format.json +4 -24
- oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
- oscura/core/types.py +4 -0
- oscura/core/uncertainty.py +3 -3
- oscura/correlation/__init__.py +52 -0
- oscura/correlation/multi_protocol.py +811 -0
- oscura/discovery/auto_decoder.py +117 -35
- oscura/discovery/comparison.py +191 -86
- oscura/discovery/quality_validator.py +155 -68
- oscura/discovery/signal_detector.py +196 -79
- oscura/export/__init__.py +18 -8
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- oscura/export/wireshark/generator.py +1 -1
- oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
- oscura/export/wireshark_dissector.py +746 -0
- oscura/guidance/wizard.py +207 -111
- oscura/hardware/__init__.py +19 -0
- oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
- oscura/{acquisition → hardware/acquisition}/file.py +2 -2
- oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
- oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
- oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
- oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
- oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
- oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
- oscura/hardware/firmware/__init__.py +29 -0
- oscura/hardware/firmware/pattern_recognition.py +874 -0
- oscura/hardware/hal_detector.py +736 -0
- oscura/hardware/security/__init__.py +37 -0
- oscura/hardware/security/side_channel_detector.py +1126 -0
- oscura/inference/__init__.py +4 -0
- oscura/inference/active_learning/observation_table.py +4 -1
- oscura/inference/alignment.py +216 -123
- oscura/inference/bayesian.py +113 -33
- oscura/inference/crc_reverse.py +101 -55
- oscura/inference/logic.py +6 -2
- oscura/inference/message_format.py +342 -183
- oscura/inference/protocol.py +95 -44
- oscura/inference/protocol_dsl.py +180 -82
- oscura/inference/signal_intelligence.py +1439 -706
- oscura/inference/spectral.py +99 -57
- oscura/inference/state_machine.py +810 -158
- oscura/inference/stream.py +270 -110
- oscura/iot/__init__.py +34 -0
- oscura/iot/coap/__init__.py +32 -0
- oscura/iot/coap/analyzer.py +668 -0
- oscura/iot/coap/options.py +212 -0
- oscura/iot/lorawan/__init__.py +21 -0
- oscura/iot/lorawan/crypto.py +206 -0
- oscura/iot/lorawan/decoder.py +801 -0
- oscura/iot/lorawan/mac_commands.py +341 -0
- oscura/iot/mqtt/__init__.py +27 -0
- oscura/iot/mqtt/analyzer.py +999 -0
- oscura/iot/mqtt/properties.py +315 -0
- oscura/iot/zigbee/__init__.py +31 -0
- oscura/iot/zigbee/analyzer.py +615 -0
- oscura/iot/zigbee/security.py +153 -0
- oscura/iot/zigbee/zcl.py +349 -0
- oscura/jupyter/display.py +125 -45
- oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
- oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
- oscura/jupyter/exploratory/fuzzy.py +746 -0
- oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
- oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
- oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
- oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
- oscura/jupyter/exploratory/sync.py +612 -0
- oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
- oscura/jupyter/magic.py +4 -4
- oscura/{ui → jupyter/ui}/__init__.py +2 -2
- oscura/{ui → jupyter/ui}/formatters.py +3 -3
- oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
- oscura/loaders/__init__.py +183 -67
- oscura/loaders/binary.py +88 -1
- oscura/loaders/chipwhisperer.py +153 -137
- oscura/loaders/configurable.py +208 -86
- oscura/loaders/csv_loader.py +458 -215
- oscura/loaders/hdf5_loader.py +278 -119
- oscura/loaders/lazy.py +87 -54
- oscura/loaders/mmap_loader.py +1 -1
- oscura/loaders/numpy_loader.py +253 -116
- oscura/loaders/pcap.py +226 -151
- oscura/loaders/rigol.py +110 -49
- oscura/loaders/sigrok.py +201 -78
- oscura/loaders/tdms.py +81 -58
- oscura/loaders/tektronix.py +291 -174
- oscura/loaders/touchstone.py +182 -87
- oscura/loaders/tss.py +456 -0
- oscura/loaders/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -0
- oscura/reporting/analyze.py +352 -146
- oscura/reporting/argument_preparer.py +69 -14
- oscura/reporting/auto_report.py +97 -61
- oscura/reporting/batch.py +131 -58
- oscura/reporting/chart_selection.py +57 -45
- oscura/reporting/comparison.py +63 -17
- oscura/reporting/content/executive.py +76 -24
- oscura/reporting/core_formats/multi_format.py +11 -8
- oscura/reporting/engine.py +312 -158
- oscura/reporting/enhanced_reports.py +949 -0
- oscura/reporting/export.py +86 -43
- oscura/reporting/formatting/numbers.py +69 -42
- oscura/reporting/html.py +139 -58
- oscura/reporting/index.py +137 -65
- oscura/reporting/output.py +158 -67
- oscura/reporting/pdf.py +67 -102
- oscura/reporting/plots.py +191 -112
- oscura/reporting/sections.py +88 -47
- oscura/reporting/standards.py +104 -61
- oscura/reporting/summary_generator.py +75 -55
- oscura/reporting/tables.py +138 -54
- oscura/reporting/templates/enhanced/protocol_re.html +525 -0
- oscura/sessions/__init__.py +14 -23
- oscura/sessions/base.py +3 -3
- oscura/sessions/blackbox.py +106 -10
- oscura/sessions/generic.py +2 -2
- oscura/sessions/legacy.py +783 -0
- oscura/side_channel/__init__.py +63 -0
- oscura/side_channel/dpa.py +1025 -0
- oscura/utils/__init__.py +15 -1
- oscura/utils/bitwise.py +118 -0
- oscura/{builders → utils/builders}/__init__.py +1 -1
- oscura/{comparison → utils/comparison}/__init__.py +6 -6
- oscura/{comparison → utils/comparison}/compare.py +202 -101
- oscura/{comparison → utils/comparison}/golden.py +83 -63
- oscura/{comparison → utils/comparison}/limits.py +313 -89
- oscura/{comparison → utils/comparison}/mask.py +151 -45
- oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
- oscura/{comparison → utils/comparison}/visualization.py +147 -89
- oscura/{component → utils/component}/__init__.py +3 -3
- oscura/{component → utils/component}/impedance.py +122 -58
- oscura/{component → utils/component}/reactive.py +165 -168
- oscura/{component → utils/component}/transmission_line.py +3 -3
- oscura/{filtering → utils/filtering}/__init__.py +6 -6
- oscura/{filtering → utils/filtering}/base.py +1 -1
- oscura/{filtering → utils/filtering}/convenience.py +2 -2
- oscura/{filtering → utils/filtering}/design.py +169 -93
- oscura/{filtering → utils/filtering}/filters.py +2 -2
- oscura/{filtering → utils/filtering}/introspection.py +2 -2
- oscura/utils/geometry.py +31 -0
- oscura/utils/imports.py +184 -0
- oscura/utils/lazy.py +1 -1
- oscura/{math → utils/math}/__init__.py +2 -2
- oscura/{math → utils/math}/arithmetic.py +114 -48
- oscura/{math → utils/math}/interpolation.py +139 -106
- oscura/utils/memory.py +129 -66
- oscura/utils/memory_advanced.py +92 -9
- oscura/utils/memory_extensions.py +10 -8
- oscura/{optimization → utils/optimization}/__init__.py +1 -1
- oscura/{optimization → utils/optimization}/search.py +2 -2
- oscura/utils/performance/__init__.py +58 -0
- oscura/utils/performance/caching.py +889 -0
- oscura/utils/performance/lsh_clustering.py +333 -0
- oscura/utils/performance/memory_optimizer.py +699 -0
- oscura/utils/performance/optimizations.py +675 -0
- oscura/utils/performance/parallel.py +654 -0
- oscura/utils/performance/profiling.py +661 -0
- oscura/{pipeline → utils/pipeline}/base.py +1 -1
- oscura/{pipeline → utils/pipeline}/composition.py +1 -1
- oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
- oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
- oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
- oscura/{search → utils/search}/__init__.py +3 -3
- oscura/{search → utils/search}/anomaly.py +188 -58
- oscura/utils/search/context.py +294 -0
- oscura/{search → utils/search}/pattern.py +138 -10
- oscura/utils/serial.py +51 -0
- oscura/utils/storage/__init__.py +61 -0
- oscura/utils/storage/database.py +1166 -0
- oscura/{streaming → utils/streaming}/chunked.py +302 -143
- oscura/{streaming → utils/streaming}/progressive.py +1 -1
- oscura/{streaming → utils/streaming}/realtime.py +3 -2
- oscura/{triggering → utils/triggering}/__init__.py +6 -6
- oscura/{triggering → utils/triggering}/base.py +6 -6
- oscura/{triggering → utils/triggering}/edge.py +2 -2
- oscura/{triggering → utils/triggering}/pattern.py +2 -2
- oscura/{triggering → utils/triggering}/pulse.py +115 -74
- oscura/{triggering → utils/triggering}/window.py +2 -2
- oscura/utils/validation.py +32 -0
- oscura/validation/__init__.py +121 -0
- oscura/{compliance → validation/compliance}/__init__.py +5 -5
- oscura/{compliance → validation/compliance}/advanced.py +5 -5
- oscura/{compliance → validation/compliance}/masks.py +1 -1
- oscura/{compliance → validation/compliance}/reporting.py +127 -53
- oscura/{compliance → validation/compliance}/testing.py +114 -52
- oscura/validation/compliance_tests.py +915 -0
- oscura/validation/fuzzer.py +990 -0
- oscura/validation/grammar_tests.py +596 -0
- oscura/validation/grammar_validator.py +904 -0
- oscura/validation/hil_testing.py +977 -0
- oscura/{quality → validation/quality}/__init__.py +4 -4
- oscura/{quality → validation/quality}/ensemble.py +251 -171
- oscura/{quality → validation/quality}/explainer.py +3 -3
- oscura/{quality → validation/quality}/scoring.py +1 -1
- oscura/{quality → validation/quality}/warnings.py +4 -4
- oscura/validation/regression_suite.py +808 -0
- oscura/validation/replay.py +788 -0
- oscura/{testing → validation/testing}/__init__.py +2 -2
- oscura/{testing → validation/testing}/synthetic.py +5 -5
- oscura/visualization/__init__.py +9 -0
- oscura/visualization/accessibility.py +1 -1
- oscura/visualization/annotations.py +64 -67
- oscura/visualization/colors.py +7 -7
- oscura/visualization/digital.py +180 -81
- oscura/visualization/eye.py +236 -85
- oscura/visualization/interactive.py +320 -143
- oscura/visualization/jitter.py +587 -247
- oscura/visualization/layout.py +169 -134
- oscura/visualization/optimization.py +103 -52
- oscura/visualization/palettes.py +1 -1
- oscura/visualization/power.py +427 -211
- oscura/visualization/power_extended.py +626 -297
- oscura/visualization/presets.py +2 -0
- oscura/visualization/protocols.py +495 -181
- oscura/visualization/render.py +79 -63
- oscura/visualization/reverse_engineering.py +171 -124
- oscura/visualization/signal_integrity.py +460 -279
- oscura/visualization/specialized.py +190 -100
- oscura/visualization/spectral.py +670 -255
- oscura/visualization/thumbnails.py +166 -137
- oscura/visualization/waveform.py +150 -63
- oscura/workflows/__init__.py +3 -0
- oscura/{batch → workflows/batch}/__init__.py +5 -5
- oscura/{batch → workflows/batch}/advanced.py +150 -75
- oscura/workflows/batch/aggregate.py +531 -0
- oscura/workflows/batch/analyze.py +236 -0
- oscura/{batch → workflows/batch}/logging.py +2 -2
- oscura/{batch → workflows/batch}/metrics.py +1 -1
- oscura/workflows/complete_re.py +1144 -0
- oscura/workflows/compliance.py +44 -54
- oscura/workflows/digital.py +197 -51
- oscura/workflows/legacy/__init__.py +12 -0
- oscura/{workflow → workflows/legacy}/dag.py +4 -1
- oscura/workflows/multi_trace.py +9 -9
- oscura/workflows/power.py +42 -62
- oscura/workflows/protocol.py +82 -49
- oscura/workflows/reverse_engineering.py +351 -150
- oscura/workflows/signal_integrity.py +157 -82
- oscura-0.7.0.dist-info/METADATA +661 -0
- oscura-0.7.0.dist-info/RECORD +591 -0
- oscura/batch/aggregate.py +0 -300
- oscura/batch/analyze.py +0 -139
- oscura/dsl/__init__.py +0 -73
- oscura/exceptions.py +0 -59
- oscura/exploratory/fuzzy.py +0 -513
- oscura/exploratory/sync.py +0 -384
- oscura/exporters/__init__.py +0 -94
- oscura/exporters/csv.py +0 -303
- oscura/exporters/exporters.py +0 -44
- oscura/exporters/hdf5.py +0 -217
- oscura/exporters/html_export.py +0 -701
- oscura/exporters/json_export.py +0 -291
- oscura/exporters/markdown_export.py +0 -367
- oscura/exporters/matlab_export.py +0 -354
- oscura/exporters/npz_export.py +0 -219
- oscura/exporters/spice_export.py +0 -210
- oscura/search/context.py +0 -149
- oscura/session/__init__.py +0 -34
- oscura/session/annotations.py +0 -289
- oscura/session/history.py +0 -313
- oscura/session/session.py +0 -520
- oscura/workflow/__init__.py +0 -13
- oscura-0.5.1.dist-info/METADATA +0 -583
- oscura-0.5.1.dist-info/RECORD +0 -481
- /oscura/core/{config.py → config/legacy.py} +0 -0
- /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
- /oscura/{extensibility → core/extensibility}/registry.py +0 -0
- /oscura/{plugins → core/plugins}/isolation.py +0 -0
- /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
- /oscura/{builders → utils/builders}/signal_builder.py +0 -0
- /oscura/{optimization → utils/optimization}/parallel.py +0 -0
- /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
- /oscura/{streaming → utils/streaming}/__init__.py +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Jitter classification result types and utilities.
|
|
2
|
+
|
|
3
|
+
This module provides result types for jitter classification analysis,
|
|
4
|
+
allowing unified representation of jitter component estimates with
|
|
5
|
+
confidence metrics and classification methods.
|
|
6
|
+
|
|
7
|
+
The classification results support IEEE 2414-2020 compliant jitter
|
|
8
|
+
decomposition workflows by providing structured outputs for RJ, DJ,
|
|
9
|
+
PJ, and TJ estimates with associated confidence levels.
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
>>> from oscura.analyzers.jitter.classification import (
|
|
13
|
+
... JitterComponentEstimate,
|
|
14
|
+
... JitterClassificationResult,
|
|
15
|
+
... )
|
|
16
|
+
>>> rj_est = JitterComponentEstimate(value=0.05, confidence=0.92, unit="UI")
|
|
17
|
+
>>> dj_est = JitterComponentEstimate(value=0.12, confidence=0.88, unit="UI")
|
|
18
|
+
>>> result = JitterClassificationResult(
|
|
19
|
+
... rj_estimate=rj_est,
|
|
20
|
+
... dj_estimate=dj_est,
|
|
21
|
+
... tj_estimate=0.17,
|
|
22
|
+
... classification_method="dual_dirac",
|
|
23
|
+
... ber_target=1e-12,
|
|
24
|
+
... )
|
|
25
|
+
>>> print(f"Total Jitter at BER={result.ber_target}: {result.tj_estimate} UI")
|
|
26
|
+
|
|
27
|
+
References:
|
|
28
|
+
IEEE 2414-2020: Standard for Jitter and Phase Noise
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from dataclasses import dataclass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class JitterComponentEstimate:
|
|
38
|
+
"""Estimate of a single jitter component with confidence metrics.
|
|
39
|
+
|
|
40
|
+
Represents the estimated magnitude of a jitter component (RJ, DJ, PJ, etc.)
|
|
41
|
+
along with a confidence score indicating the reliability of the estimate
|
|
42
|
+
and the unit of measurement.
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
value: Estimated magnitude of the jitter component
|
|
46
|
+
confidence: Confidence score for this estimate (0.0-1.0, where 1.0 is
|
|
47
|
+
highest confidence). Based on fit quality, sample size, and
|
|
48
|
+
statistical significance.
|
|
49
|
+
unit: Unit of measurement (e.g., "UI" for unit intervals, "s" for
|
|
50
|
+
seconds, "ps" for picoseconds)
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
>>> rj = JitterComponentEstimate(value=5.2e-12, confidence=0.94, unit="s")
|
|
54
|
+
>>> print(f"RJ: {rj.value*1e12:.2f} ps (confidence: {rj.confidence:.1%})")
|
|
55
|
+
RJ: 5.20 ps (confidence: 94.0%)
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
value: float
|
|
59
|
+
confidence: float
|
|
60
|
+
unit: str
|
|
61
|
+
|
|
62
|
+
def __post_init__(self) -> None:
|
|
63
|
+
"""Validate component estimate fields."""
|
|
64
|
+
if self.confidence < 0 or self.confidence > 1:
|
|
65
|
+
raise ValueError(f"Confidence must be in [0,1], got {self.confidence}")
|
|
66
|
+
if not isinstance(self.unit, str) or not self.unit:
|
|
67
|
+
raise ValueError(f"Unit must be non-empty string, got {self.unit!r}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class JitterClassificationResult:
|
|
72
|
+
"""Complete jitter classification result with all components.
|
|
73
|
+
|
|
74
|
+
Represents the outcome of a comprehensive jitter analysis, including
|
|
75
|
+
estimates for random jitter (RJ), deterministic jitter (DJ), and
|
|
76
|
+
total jitter (TJ) at a specified BER target. Includes metadata about
|
|
77
|
+
the classification method used.
|
|
78
|
+
|
|
79
|
+
This result type is useful for timing closure analysis, link budget
|
|
80
|
+
calculations, and system-level jitter characterization.
|
|
81
|
+
|
|
82
|
+
Attributes:
|
|
83
|
+
rj_estimate: Random jitter component estimate with confidence
|
|
84
|
+
dj_estimate: Deterministic jitter component estimate with confidence
|
|
85
|
+
tj_estimate: Total jitter at the specified BER (RJ + DJ convolution)
|
|
86
|
+
classification_method: Method used for classification (e.g.,
|
|
87
|
+
"dual_dirac", "spectral_fit", "tail_fit"). Indicates the
|
|
88
|
+
algorithm used for RJ/DJ separation.
|
|
89
|
+
ber_target: Target bit error rate for TJ calculation (e.g., 1e-12)
|
|
90
|
+
pj_estimate: Optional periodic jitter component estimate
|
|
91
|
+
ddj_estimate: Optional data-dependent jitter component estimate
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
>>> result = JitterClassificationResult(
|
|
95
|
+
... rj_estimate=JitterComponentEstimate(0.05, 0.92, "UI"),
|
|
96
|
+
... dj_estimate=JitterComponentEstimate(0.12, 0.88, "UI"),
|
|
97
|
+
... tj_estimate=0.17,
|
|
98
|
+
... classification_method="dual_dirac",
|
|
99
|
+
... ber_target=1e-12,
|
|
100
|
+
... )
|
|
101
|
+
>>> print(f"TJ@{result.ber_target}: {result.tj_estimate} {result.rj_estimate.unit}")
|
|
102
|
+
TJ@1e-12: 0.17 UI
|
|
103
|
+
|
|
104
|
+
Notes:
|
|
105
|
+
For IEEE 2414-2020 compliance, TJ should be calculated using:
|
|
106
|
+
TJ = DJ_pp + n*RJ_rms, where n = Q(BER/2) and Q is the inverse
|
|
107
|
+
Q-function (Gaussian tail probability).
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
rj_estimate: JitterComponentEstimate
|
|
111
|
+
dj_estimate: JitterComponentEstimate
|
|
112
|
+
tj_estimate: float
|
|
113
|
+
classification_method: str
|
|
114
|
+
ber_target: float
|
|
115
|
+
pj_estimate: JitterComponentEstimate | None = None
|
|
116
|
+
ddj_estimate: JitterComponentEstimate | None = None
|
|
117
|
+
|
|
118
|
+
def __post_init__(self) -> None:
|
|
119
|
+
"""Validate classification result fields."""
|
|
120
|
+
if self.ber_target <= 0 or self.ber_target >= 1:
|
|
121
|
+
raise ValueError(f"BER target must be in (0,1), got {self.ber_target}")
|
|
122
|
+
if not isinstance(self.classification_method, str) or not self.classification_method:
|
|
123
|
+
raise ValueError(
|
|
124
|
+
f"Classification method must be non-empty string, got "
|
|
125
|
+
f"{self.classification_method!r}"
|
|
126
|
+
)
|
|
127
|
+
if self.tj_estimate < 0:
|
|
128
|
+
raise ValueError(f"TJ estimate cannot be negative, got {self.tj_estimate}")
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def rj_confidence(self) -> float:
|
|
132
|
+
"""Convenience accessor for RJ confidence score."""
|
|
133
|
+
return self.rj_estimate.confidence
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def dj_confidence(self) -> float:
|
|
137
|
+
"""Convenience accessor for DJ confidence score."""
|
|
138
|
+
return self.dj_estimate.confidence
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def overall_confidence(self) -> float:
|
|
142
|
+
"""Overall confidence as minimum of RJ and DJ confidence scores.
|
|
143
|
+
|
|
144
|
+
Returns the more conservative (lower) of the two confidence values,
|
|
145
|
+
as the TJ estimate depends on both RJ and DJ being accurate.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Minimum confidence score between RJ and DJ estimates
|
|
149
|
+
"""
|
|
150
|
+
return min(self.rj_estimate.confidence, self.dj_estimate.confidence)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
__all__ = [
|
|
154
|
+
"JitterClassificationResult",
|
|
155
|
+
"JitterComponentEstimate",
|
|
156
|
+
]
|
|
@@ -220,7 +220,7 @@ def _extract_rj_tail_fit(tie_data: NDArray[np.float64]) -> RandomJitterResult:
|
|
|
220
220
|
n = len(sorted_data)
|
|
221
221
|
|
|
222
222
|
# Use percentiles to estimate Gaussian parameters
|
|
223
|
-
# For a Gaussian: P16 = μ - σ, P50 = μ, P84 = μ + σ
|
|
223
|
+
# For a Gaussian: P16 = μ - σ, P50 = μ, P84 = μ + σ
|
|
224
224
|
p16 = np.percentile(sorted_data, 16)
|
|
225
225
|
p50 = np.percentile(sorted_data, 50)
|
|
226
226
|
p84 = np.percentile(sorted_data, 84)
|
|
@@ -339,6 +339,98 @@ def _extract_rj_q_scale(tie_data: NDArray[np.float64]) -> RandomJitterResult:
|
|
|
339
339
|
)
|
|
340
340
|
|
|
341
341
|
|
|
342
|
+
def _prepare_dj_histogram(
|
|
343
|
+
valid_data: NDArray[np.float64],
|
|
344
|
+
) -> tuple[NDArray[np.int_], NDArray[np.float64]]:
|
|
345
|
+
"""Create histogram for DJ analysis.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
valid_data: Valid TIE data array.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Tuple of (histogram, bin_centers).
|
|
352
|
+
"""
|
|
353
|
+
n_bins = min(100, len(valid_data) // 50)
|
|
354
|
+
hist, bin_edges = np.histogram(valid_data, bins=n_bins, density=False)
|
|
355
|
+
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
356
|
+
return hist, bin_centers
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _detect_bimodal_peaks(hist: NDArray[np.int_], bin_centers: NDArray[np.float64]) -> float | None:
|
|
360
|
+
"""Detect bimodal peaks in histogram for DJ estimation.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
hist: Histogram counts.
|
|
364
|
+
bin_centers: Bin center positions.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Peak separation distance or None if not bimodal.
|
|
368
|
+
"""
|
|
369
|
+
from scipy.ndimage import gaussian_filter1d
|
|
370
|
+
from scipy.signal import find_peaks
|
|
371
|
+
|
|
372
|
+
if len(hist) < 5:
|
|
373
|
+
return None
|
|
374
|
+
|
|
375
|
+
hist_smooth = gaussian_filter1d(hist.astype(float), sigma=2)
|
|
376
|
+
peaks, properties = find_peaks(hist_smooth, prominence=np.max(hist_smooth) * 0.1)
|
|
377
|
+
|
|
378
|
+
if len(peaks) >= 2:
|
|
379
|
+
prominences = properties.get("prominences", np.ones(len(peaks)))
|
|
380
|
+
sorted_peak_idx = np.argsort(prominences)[::-1][:2]
|
|
381
|
+
top_peaks = peaks[sorted_peak_idx]
|
|
382
|
+
top_peaks = np.sort(top_peaks)
|
|
383
|
+
peak_positions = bin_centers[top_peaks]
|
|
384
|
+
separation: float = float(abs(peak_positions[1] - peak_positions[0]))
|
|
385
|
+
return separation
|
|
386
|
+
|
|
387
|
+
return None
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _calculate_dj_from_quantiles(sorted_data: NDArray[np.float64], rj_rms: float) -> float:
|
|
391
|
+
"""Calculate DJ using quantile-based method.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
sorted_data: Sorted TIE data.
|
|
395
|
+
rj_rms: Random jitter RMS.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
DJ peak-to-peak value.
|
|
399
|
+
"""
|
|
400
|
+
n = len(sorted_data)
|
|
401
|
+
lower_idx = max(0, int(n * 0.0001))
|
|
402
|
+
upper_idx = min(n - 1, int(n * 0.9999))
|
|
403
|
+
tj_at_ber = sorted_data[upper_idx] - sorted_data[lower_idx]
|
|
404
|
+
|
|
405
|
+
# For dual-Dirac + RJ: TJ = 2*Q*RJ + DJ at BER = 1e-4 (Q ≈ 3.72)
|
|
406
|
+
q_factor = 3.72
|
|
407
|
+
rj_contribution = 2 * q_factor * rj_rms
|
|
408
|
+
dj_pp: float = float(max(0.0, tj_at_ber - rj_contribution))
|
|
409
|
+
return dj_pp
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _determine_dj_confidence(
|
|
413
|
+
peak_separation_dj: float | None, dj_pp: float, rj_rms: float, n_peaks: int
|
|
414
|
+
) -> float:
|
|
415
|
+
"""Determine confidence score for DJ extraction.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
peak_separation_dj: Peak separation from histogram.
|
|
419
|
+
dj_pp: Calculated DJ peak-to-peak.
|
|
420
|
+
rj_rms: Random jitter RMS.
|
|
421
|
+
n_peaks: Number of peaks found.
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
Confidence score (0.0 to 1.0).
|
|
425
|
+
"""
|
|
426
|
+
if peak_separation_dj is not None:
|
|
427
|
+
return 0.9 if n_peaks == 2 else 0.7
|
|
428
|
+
elif dj_pp > 2 * rj_rms:
|
|
429
|
+
return 0.5
|
|
430
|
+
else:
|
|
431
|
+
return 0.2
|
|
432
|
+
|
|
433
|
+
|
|
342
434
|
def extract_dj(
|
|
343
435
|
tie_data: NDArray[np.float64],
|
|
344
436
|
rj_result: RandomJitterResult | None = None,
|
|
@@ -376,83 +468,27 @@ def extract_dj(
|
|
|
376
468
|
analysis_type="deterministic_jitter_extraction",
|
|
377
469
|
)
|
|
378
470
|
|
|
471
|
+
# Setup: prepare data and compute RJ if needed
|
|
379
472
|
valid_data = tie_data[~np.isnan(tie_data)]
|
|
380
|
-
|
|
381
|
-
# Get RJ if not provided - use tail fitting for better RJ isolation
|
|
382
473
|
if rj_result is None:
|
|
383
474
|
rj_result = extract_rj(valid_data, method="tail_fit", min_samples=min_samples)
|
|
384
|
-
|
|
385
475
|
rj_rms = rj_result.rj_rms
|
|
386
476
|
|
|
387
|
-
#
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
391
|
-
|
|
392
|
-
# DJ extraction using dual-Dirac model:
|
|
393
|
-
# The dual-Dirac model assumes DJ creates two impulses at ±δ,
|
|
394
|
-
# each convolved with Gaussian RJ.
|
|
395
|
-
# Total distribution is: 0.5*N(μ-δ, σ) + 0.5*N(μ+δ, σ) # noqa: RUF003
|
|
396
|
-
|
|
477
|
+
# Processing: analyze histogram and detect peaks
|
|
478
|
+
hist, bin_centers = _prepare_dj_histogram(valid_data)
|
|
479
|
+
peak_separation_dj = _detect_bimodal_peaks(hist, bin_centers)
|
|
397
480
|
sorted_data = np.sort(valid_data)
|
|
398
|
-
n = len(sorted_data)
|
|
399
481
|
|
|
400
|
-
#
|
|
401
|
-
from scipy.ndimage import gaussian_filter1d
|
|
402
|
-
from scipy.signal import find_peaks
|
|
403
|
-
|
|
404
|
-
dj_pp = 0.0
|
|
405
|
-
peak_separation_dj = None
|
|
406
|
-
|
|
407
|
-
# Smooth histogram for peak detection
|
|
408
|
-
if len(hist) >= 5:
|
|
409
|
-
hist_smooth = gaussian_filter1d(hist.astype(float), sigma=2)
|
|
410
|
-
peaks, properties = find_peaks(hist_smooth, prominence=np.max(hist_smooth) * 0.1)
|
|
411
|
-
|
|
412
|
-
# If we find 2 clear peaks, DJ is their separation
|
|
413
|
-
if len(peaks) >= 2:
|
|
414
|
-
# Sort peaks by prominence and take top 2
|
|
415
|
-
prominences = properties.get("prominences", np.ones(len(peaks)))
|
|
416
|
-
sorted_peak_idx = np.argsort(prominences)[::-1][:2]
|
|
417
|
-
top_peaks = peaks[sorted_peak_idx]
|
|
418
|
-
top_peaks = np.sort(top_peaks)
|
|
419
|
-
|
|
420
|
-
# Peak separation in the histogram
|
|
421
|
-
peak_positions = bin_centers[top_peaks]
|
|
422
|
-
peak_separation_dj = abs(peak_positions[1] - peak_positions[0])
|
|
423
|
-
|
|
424
|
-
# If we found peaks, use that as DJ
|
|
482
|
+
# Result building: calculate DJ and confidence
|
|
425
483
|
if peak_separation_dj is not None and peak_separation_dj > 2 * rj_rms:
|
|
426
484
|
dj_pp = peak_separation_dj
|
|
485
|
+
n_peaks = 2
|
|
427
486
|
else:
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
lower_idx = max(0, int(n * 0.0001))
|
|
431
|
-
upper_idx = min(n - 1, int(n * 0.9999))
|
|
432
|
-
|
|
433
|
-
tj_at_ber = sorted_data[upper_idx] - sorted_data[lower_idx]
|
|
434
|
-
|
|
435
|
-
# For dual-Dirac + RJ: TJ = 2*Q*RJ + DJ
|
|
436
|
-
# Using Q for BER = 1e-4
|
|
437
|
-
q_factor = 3.72
|
|
438
|
-
rj_contribution = 2 * q_factor * rj_rms
|
|
439
|
-
|
|
440
|
-
# DJ is what remains after removing RJ contribution
|
|
441
|
-
dj_pp = max(0.0, tj_at_ber - rj_contribution)
|
|
487
|
+
dj_pp = _calculate_dj_from_quantiles(sorted_data, rj_rms)
|
|
488
|
+
n_peaks = 0
|
|
442
489
|
|
|
443
|
-
# Delta is half the DJ peak-to-peak (dual-Dirac separation)
|
|
444
490
|
dj_delta = dj_pp / 2
|
|
445
|
-
|
|
446
|
-
# Confidence based on whether we found clear DJ
|
|
447
|
-
if peak_separation_dj is not None:
|
|
448
|
-
# Found bimodal peaks
|
|
449
|
-
confidence = 0.9 if len(peaks) == 2 else 0.7
|
|
450
|
-
elif dj_pp > 2 * rj_rms:
|
|
451
|
-
# Significant DJ from quantile method
|
|
452
|
-
confidence = 0.5
|
|
453
|
-
else:
|
|
454
|
-
# Little or no DJ detected
|
|
455
|
-
confidence = 0.2
|
|
491
|
+
confidence = _determine_dj_confidence(peak_separation_dj, dj_pp, rj_rms, n_peaks)
|
|
456
492
|
|
|
457
493
|
return DeterministicJitterResult(
|
|
458
494
|
dj_pp=dj_pp,
|
|
@@ -496,20 +532,40 @@ def extract_pj(
|
|
|
496
532
|
IEEE 2414-2020 Section 6.4
|
|
497
533
|
"""
|
|
498
534
|
valid_data = tie_data[~np.isnan(tie_data)]
|
|
499
|
-
|
|
535
|
+
if len(valid_data) < 32:
|
|
536
|
+
return _empty_pj_result()
|
|
500
537
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
538
|
+
# Compute spectrum
|
|
539
|
+
frequencies, magnitudes = _compute_pj_spectrum(valid_data, sample_rate)
|
|
540
|
+
|
|
541
|
+
# Filter frequency range
|
|
542
|
+
max_frequency = max_frequency or sample_rate / 2
|
|
543
|
+
valid_freqs, valid_mags = _filter_frequency_range(
|
|
544
|
+
frequencies, magnitudes, min_frequency, max_frequency
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
if len(valid_mags) < 3:
|
|
548
|
+
return _empty_pj_result()
|
|
508
549
|
|
|
509
|
-
#
|
|
510
|
-
|
|
550
|
+
# Extract peaks and create result
|
|
551
|
+
components = _extract_pj_peaks(valid_freqs, valid_mags, n_components)
|
|
552
|
+
return _create_pj_result(components)
|
|
511
553
|
|
|
512
|
-
|
|
554
|
+
|
|
555
|
+
def _empty_pj_result() -> PeriodicJitterResult:
|
|
556
|
+
"""Create empty PJ result for insufficient data."""
|
|
557
|
+
return PeriodicJitterResult(
|
|
558
|
+
components=[], pj_pp=0.0, dominant_frequency=None, dominant_amplitude=None
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _compute_pj_spectrum(
|
|
563
|
+
data: NDArray[np.float64], sample_rate: float
|
|
564
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
565
|
+
"""Compute FFT spectrum of TIE data."""
|
|
566
|
+
n = len(data)
|
|
567
|
+
# Remove DC and apply window
|
|
568
|
+
data_centered = data - np.mean(data)
|
|
513
569
|
window = np.hanning(n)
|
|
514
570
|
data_windowed = data_centered * window
|
|
515
571
|
|
|
@@ -518,53 +574,47 @@ def extract_pj(
|
|
|
518
574
|
spectrum = np.fft.rfft(data_windowed, n=nfft)
|
|
519
575
|
frequencies = np.fft.rfftfreq(nfft, d=1.0 / sample_rate)
|
|
520
576
|
magnitudes = np.abs(spectrum) * 2 / n # Scale for amplitude
|
|
577
|
+
return frequencies, magnitudes
|
|
521
578
|
|
|
522
|
-
# Set frequency range
|
|
523
|
-
if max_frequency is None:
|
|
524
|
-
max_frequency = sample_rate / 2
|
|
525
579
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
580
|
+
def _filter_frequency_range(
|
|
581
|
+
frequencies: NDArray[np.float64],
|
|
582
|
+
magnitudes: NDArray[np.float64],
|
|
583
|
+
min_freq: float,
|
|
584
|
+
max_freq: float,
|
|
585
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
586
|
+
"""Filter spectrum to valid frequency range."""
|
|
587
|
+
freq_mask = (frequencies >= min_freq) & (frequencies <= max_freq)
|
|
588
|
+
return frequencies[freq_mask], magnitudes[freq_mask]
|
|
530
589
|
|
|
531
|
-
if len(valid_mags) < 3:
|
|
532
|
-
return PeriodicJitterResult(
|
|
533
|
-
components=[],
|
|
534
|
-
pj_pp=0.0,
|
|
535
|
-
dominant_frequency=None,
|
|
536
|
-
dominant_amplitude=None,
|
|
537
|
-
)
|
|
538
590
|
|
|
539
|
-
|
|
591
|
+
def _extract_pj_peaks(
|
|
592
|
+
frequencies: NDArray[np.float64], magnitudes: NDArray[np.float64], n_components: int
|
|
593
|
+
) -> list[tuple[float, float]]:
|
|
594
|
+
"""Extract top N periodic jitter peaks from spectrum."""
|
|
540
595
|
from scipy.signal import find_peaks
|
|
541
596
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
distance=3, # Minimum separation between peaks
|
|
548
|
-
)
|
|
597
|
+
threshold = 3 * np.median(magnitudes)
|
|
598
|
+
peak_indices, _ = find_peaks(magnitudes, height=threshold, distance=3)
|
|
599
|
+
|
|
600
|
+
if len(peak_indices) == 0:
|
|
601
|
+
return []
|
|
549
602
|
|
|
550
603
|
# Sort by amplitude and take top n_components
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
604
|
+
peak_heights = magnitudes[peak_indices]
|
|
605
|
+
sorted_indices = np.argsort(peak_heights)[::-1][:n_components]
|
|
606
|
+
top_peaks = peak_indices[sorted_indices]
|
|
607
|
+
return [(float(frequencies[idx]), float(magnitudes[idx])) for idx in top_peaks]
|
|
555
608
|
|
|
556
|
-
components = [(float(valid_freqs[idx]), float(valid_mags[idx])) for idx in top_peaks]
|
|
557
609
|
|
|
558
|
-
|
|
559
|
-
|
|
610
|
+
def _create_pj_result(components: list[tuple[float, float]]) -> PeriodicJitterResult:
|
|
611
|
+
"""Create PJ result from extracted components."""
|
|
612
|
+
if not components:
|
|
613
|
+
return _empty_pj_result()
|
|
560
614
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
components = []
|
|
565
|
-
pj_pp = 0.0
|
|
566
|
-
dominant_frequency = None
|
|
567
|
-
dominant_amplitude = None
|
|
615
|
+
pj_pp = 2 * sum(amp for _, amp in components)
|
|
616
|
+
dominant_frequency = components[0][0]
|
|
617
|
+
dominant_amplitude = components[0][1]
|
|
568
618
|
|
|
569
619
|
return PeriodicJitterResult(
|
|
570
620
|
components=components,
|
|
@@ -48,6 +48,75 @@ class JitterSpectrumResult:
|
|
|
48
48
|
peaks: list[tuple[float, float]]
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
def _create_empty_jitter_spectrum() -> JitterSpectrumResult:
|
|
52
|
+
"""Create empty jitter spectrum result for insufficient data.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Empty JitterSpectrumResult.
|
|
56
|
+
"""
|
|
57
|
+
return JitterSpectrumResult(
|
|
58
|
+
frequencies=np.array([]),
|
|
59
|
+
magnitude=np.array([]),
|
|
60
|
+
magnitude_db=np.array([]),
|
|
61
|
+
dominant_frequency=None,
|
|
62
|
+
dominant_magnitude=None,
|
|
63
|
+
noise_floor=0.0,
|
|
64
|
+
peaks=[],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _preprocess_tie_data(
|
|
69
|
+
valid_data: NDArray[np.float64], window: str, detrend: bool
|
|
70
|
+
) -> tuple[NDArray[np.float64], float, int]:
|
|
71
|
+
"""Preprocess TIE data with detrending and windowing.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
valid_data: Valid TIE data (NaNs removed).
|
|
75
|
+
window: Window function name.
|
|
76
|
+
detrend: Whether to detrend data.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Tuple of (windowed_data, window_factor, n_samples).
|
|
80
|
+
"""
|
|
81
|
+
n = len(valid_data)
|
|
82
|
+
|
|
83
|
+
if detrend:
|
|
84
|
+
x = np.arange(n)
|
|
85
|
+
slope, intercept = np.polyfit(x, valid_data, 1)
|
|
86
|
+
data_detrended = valid_data - (slope * x + intercept)
|
|
87
|
+
else:
|
|
88
|
+
data_detrended = valid_data - np.mean(valid_data)
|
|
89
|
+
|
|
90
|
+
win = {"hann": np.hanning(n), "hamming": np.hamming(n), "blackman": np.blackman(n)}.get(
|
|
91
|
+
window, np.ones(n)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
window_factor = np.sqrt(np.mean(win**2))
|
|
95
|
+
return data_detrended * win, float(window_factor), n
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _compute_jitter_fft(
|
|
99
|
+
data_windowed: NDArray[np.float64], n: int, window_factor: float, sample_rate: float
|
|
100
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]:
|
|
101
|
+
"""Compute FFT of windowed jitter data.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
data_windowed: Windowed TIE data.
|
|
105
|
+
n: Original sample count.
|
|
106
|
+
window_factor: Window power factor.
|
|
107
|
+
sample_rate: Sample rate in Hz.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Tuple of (frequencies, magnitude, magnitude_db).
|
|
111
|
+
"""
|
|
112
|
+
nfft = int(2 ** np.ceil(np.log2(n)))
|
|
113
|
+
spectrum = np.fft.rfft(data_windowed, n=nfft)
|
|
114
|
+
frequencies = np.fft.rfftfreq(nfft, d=1.0 / sample_rate)
|
|
115
|
+
magnitude = np.abs(spectrum) * 2 / n / window_factor
|
|
116
|
+
magnitude_db = 20 * np.log10(magnitude / 1e-12 + 1e-20)
|
|
117
|
+
return frequencies, magnitude, magnitude_db
|
|
118
|
+
|
|
119
|
+
|
|
51
120
|
def jitter_spectrum(
|
|
52
121
|
tie_data: NDArray[np.float64],
|
|
53
122
|
sample_rate: float,
|
|
@@ -79,76 +148,23 @@ def jitter_spectrum(
|
|
|
79
148
|
References:
|
|
80
149
|
IEEE 2414-2020 Section 6.8
|
|
81
150
|
"""
|
|
151
|
+
# Setup: validate and prepare data
|
|
82
152
|
valid_data = tie_data[~np.isnan(tie_data)]
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if n < 16:
|
|
86
|
-
return JitterSpectrumResult(
|
|
87
|
-
frequencies=np.array([]),
|
|
88
|
-
magnitude=np.array([]),
|
|
89
|
-
magnitude_db=np.array([]),
|
|
90
|
-
dominant_frequency=None,
|
|
91
|
-
dominant_magnitude=None,
|
|
92
|
-
noise_floor=0.0,
|
|
93
|
-
peaks=[],
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
# Detrend if requested
|
|
97
|
-
if detrend:
|
|
98
|
-
# Remove linear trend
|
|
99
|
-
x = np.arange(n)
|
|
100
|
-
slope, intercept = np.polyfit(x, valid_data, 1)
|
|
101
|
-
data_detrended = valid_data - (slope * x + intercept)
|
|
102
|
-
else:
|
|
103
|
-
data_detrended = valid_data - np.mean(valid_data)
|
|
104
|
-
|
|
105
|
-
# Apply window
|
|
106
|
-
if window == "hann":
|
|
107
|
-
win = np.hanning(n)
|
|
108
|
-
elif window == "hamming":
|
|
109
|
-
win = np.hamming(n)
|
|
110
|
-
elif window == "blackman":
|
|
111
|
-
win = np.blackman(n)
|
|
112
|
-
else:
|
|
113
|
-
win = np.ones(n)
|
|
153
|
+
if len(valid_data) < 16:
|
|
154
|
+
return _create_empty_jitter_spectrum()
|
|
114
155
|
|
|
115
|
-
#
|
|
116
|
-
window_factor =
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
nfft = int(2 ** np.ceil(np.log2(n)))
|
|
121
|
-
|
|
122
|
-
# Compute FFT
|
|
123
|
-
spectrum = np.fft.rfft(data_windowed, n=nfft)
|
|
124
|
-
frequencies = np.fft.rfftfreq(nfft, d=1.0 / sample_rate)
|
|
125
|
-
|
|
126
|
-
# Calculate magnitude spectrum
|
|
127
|
-
# Scale for proper amplitude: 2/N for single-sided, compensate for window
|
|
128
|
-
magnitude = np.abs(spectrum) * 2 / n / window_factor
|
|
129
|
-
|
|
130
|
-
# Convert to dB (relative to 1 ps = 1e-12 s)
|
|
131
|
-
reference = 1e-12 # 1 ps
|
|
132
|
-
magnitude_db = 20 * np.log10(magnitude / reference + 1e-20)
|
|
156
|
+
# Processing: apply preprocessing and FFT
|
|
157
|
+
data_windowed, window_factor, n = _preprocess_tie_data(valid_data, window, detrend)
|
|
158
|
+
frequencies, magnitude, magnitude_db = _compute_jitter_fft(
|
|
159
|
+
data_windowed, n, window_factor, sample_rate
|
|
160
|
+
)
|
|
133
161
|
|
|
134
|
-
#
|
|
162
|
+
# Formatting: identify peaks and dominant frequency
|
|
135
163
|
noise_floor = float(np.median(magnitude))
|
|
136
|
-
|
|
137
|
-
# Find peaks
|
|
138
164
|
peaks = identify_periodic_components(
|
|
139
|
-
frequencies,
|
|
140
|
-
magnitude,
|
|
141
|
-
n_peaks=n_peaks,
|
|
142
|
-
min_height=noise_floor * 3,
|
|
165
|
+
frequencies, magnitude, n_peaks=n_peaks, min_height=noise_floor * 3
|
|
143
166
|
)
|
|
144
|
-
|
|
145
|
-
# Get dominant component
|
|
146
|
-
if len(peaks) > 0:
|
|
147
|
-
dominant_frequency = peaks[0][0]
|
|
148
|
-
dominant_magnitude = peaks[0][1]
|
|
149
|
-
else:
|
|
150
|
-
dominant_frequency = None
|
|
151
|
-
dominant_magnitude = None
|
|
167
|
+
dominant_frequency, dominant_magnitude = (peaks[0][0], peaks[0][1]) if peaks else (None, None)
|
|
152
168
|
|
|
153
169
|
return JitterSpectrumResult(
|
|
154
170
|
frequencies=frequencies,
|