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
oscura/analyzers/eye/diagram.py
CHANGED
|
@@ -96,19 +96,47 @@ def generate_eye(
|
|
|
96
96
|
"""
|
|
97
97
|
data = trace.data
|
|
98
98
|
sample_rate = trace.metadata.sample_rate
|
|
99
|
-
1.0 / sample_rate
|
|
100
99
|
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
samples_per_ui = _validate_unit_interval(unit_interval, sample_rate)
|
|
101
|
+
total_ui_samples = samples_per_ui * n_ui
|
|
102
|
+
_validate_data_length(len(data), total_ui_samples)
|
|
103
|
+
|
|
104
|
+
trigger_indices = _find_trigger_points(data, trigger_level, trigger_edge)
|
|
105
|
+
eye_traces = _extract_eye_traces(
|
|
106
|
+
data, trigger_indices, samples_per_ui, total_ui_samples, max_traces
|
|
107
|
+
)
|
|
108
|
+
eye_data = np.array(eye_traces, dtype=np.float64)
|
|
109
|
+
time_axis = np.linspace(0, n_ui, total_ui_samples, endpoint=False)
|
|
110
|
+
|
|
111
|
+
histogram, voltage_bins, time_bins = _generate_histogram_if_requested(
|
|
112
|
+
eye_data, time_axis, n_ui, generate_histogram, histogram_bins
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return EyeDiagram(
|
|
116
|
+
data=eye_data,
|
|
117
|
+
time_axis=time_axis,
|
|
118
|
+
unit_interval=unit_interval,
|
|
119
|
+
samples_per_ui=samples_per_ui,
|
|
120
|
+
n_traces=len(eye_traces),
|
|
121
|
+
sample_rate=sample_rate,
|
|
122
|
+
histogram=histogram,
|
|
123
|
+
voltage_bins=voltage_bins,
|
|
124
|
+
time_bins=time_bins,
|
|
125
|
+
)
|
|
103
126
|
|
|
127
|
+
|
|
128
|
+
def _validate_unit_interval(unit_interval: float, sample_rate: float) -> int:
|
|
129
|
+
"""Validate unit interval and calculate samples per UI."""
|
|
130
|
+
samples_per_ui = round(unit_interval * sample_rate)
|
|
104
131
|
if samples_per_ui < 4:
|
|
105
132
|
raise AnalysisError(
|
|
106
133
|
f"Unit interval too short: {samples_per_ui} samples/UI. Need at least 4 samples per UI."
|
|
107
134
|
)
|
|
135
|
+
return samples_per_ui
|
|
108
136
|
|
|
109
|
-
n_samples = len(data)
|
|
110
|
-
total_ui_samples = samples_per_ui * n_ui
|
|
111
137
|
|
|
138
|
+
def _validate_data_length(n_samples: int, total_ui_samples: int) -> None:
|
|
139
|
+
"""Validate that we have enough data for eye generation."""
|
|
112
140
|
if n_samples < total_ui_samples * 2:
|
|
113
141
|
raise InsufficientDataError(
|
|
114
142
|
f"Need at least {total_ui_samples * 2} samples for eye diagram",
|
|
@@ -117,7 +145,13 @@ def generate_eye(
|
|
|
117
145
|
analysis_type="eye_diagram_generation",
|
|
118
146
|
)
|
|
119
147
|
|
|
120
|
-
|
|
148
|
+
|
|
149
|
+
def _find_trigger_points(
|
|
150
|
+
data: NDArray[np.float64],
|
|
151
|
+
trigger_level: float,
|
|
152
|
+
trigger_edge: str,
|
|
153
|
+
) -> NDArray[np.intp]:
|
|
154
|
+
"""Find trigger points in the data."""
|
|
121
155
|
low = np.percentile(data, 10)
|
|
122
156
|
high = np.percentile(data, 90)
|
|
123
157
|
threshold = low + trigger_level * (high - low)
|
|
@@ -137,9 +171,20 @@ def generate_eye(
|
|
|
137
171
|
analysis_type="eye_diagram_generation",
|
|
138
172
|
)
|
|
139
173
|
|
|
140
|
-
|
|
174
|
+
return trigger_indices
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _extract_eye_traces(
|
|
178
|
+
data: NDArray[np.float64],
|
|
179
|
+
trigger_indices: NDArray[np.intp],
|
|
180
|
+
samples_per_ui: int,
|
|
181
|
+
total_ui_samples: int,
|
|
182
|
+
max_traces: int | None,
|
|
183
|
+
) -> list[NDArray[np.float64]]:
|
|
184
|
+
"""Extract eye traces from data using trigger points."""
|
|
141
185
|
eye_traces = []
|
|
142
|
-
half_ui = samples_per_ui // 2
|
|
186
|
+
half_ui = samples_per_ui // 2
|
|
187
|
+
n_samples = len(data)
|
|
143
188
|
|
|
144
189
|
for trig_idx in trigger_indices:
|
|
145
190
|
start_idx = trig_idx - half_ui
|
|
@@ -159,48 +204,35 @@ def generate_eye(
|
|
|
159
204
|
analysis_type="eye_diagram_generation",
|
|
160
205
|
)
|
|
161
206
|
|
|
162
|
-
|
|
163
|
-
eye_data = np.array(eye_traces, dtype=np.float64)
|
|
207
|
+
return eye_traces
|
|
164
208
|
|
|
165
|
-
# Generate time axis in UI
|
|
166
|
-
time_axis = np.linspace(0, n_ui, total_ui_samples, endpoint=False)
|
|
167
209
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
# Create histogram
|
|
179
|
-
voltage_range = (np.min(all_voltages), np.max(all_voltages))
|
|
180
|
-
time_range = (0, n_ui)
|
|
181
|
-
|
|
182
|
-
histogram, voltage_edges, time_edges = np.histogram2d(
|
|
183
|
-
all_voltages,
|
|
184
|
-
all_times,
|
|
185
|
-
bins=histogram_bins,
|
|
186
|
-
range=[voltage_range, time_range],
|
|
187
|
-
)
|
|
210
|
+
def _generate_histogram_if_requested(
|
|
211
|
+
eye_data: NDArray[np.float64],
|
|
212
|
+
time_axis: NDArray[np.float64],
|
|
213
|
+
n_ui: int,
|
|
214
|
+
generate_histogram: bool,
|
|
215
|
+
histogram_bins: tuple[int, int],
|
|
216
|
+
) -> tuple[NDArray[np.float64] | None, NDArray[np.float64] | None, NDArray[np.float64] | None]:
|
|
217
|
+
"""Generate 2D histogram if requested."""
|
|
218
|
+
if not generate_histogram:
|
|
219
|
+
return None, None, None
|
|
188
220
|
|
|
189
|
-
|
|
190
|
-
|
|
221
|
+
all_voltages = eye_data.flatten()
|
|
222
|
+
all_times = np.tile(time_axis, len(eye_data))
|
|
191
223
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
voltage_bins=voltage_bins,
|
|
201
|
-
time_bins=time_bins,
|
|
224
|
+
voltage_range = (np.min(all_voltages), np.max(all_voltages))
|
|
225
|
+
time_range = (0, n_ui)
|
|
226
|
+
|
|
227
|
+
histogram, voltage_edges, time_edges = np.histogram2d(
|
|
228
|
+
all_voltages,
|
|
229
|
+
all_times,
|
|
230
|
+
bins=histogram_bins,
|
|
231
|
+
range=[voltage_range, time_range],
|
|
202
232
|
)
|
|
203
233
|
|
|
234
|
+
return histogram, voltage_edges, time_edges
|
|
235
|
+
|
|
204
236
|
|
|
205
237
|
def generate_eye_from_edges(
|
|
206
238
|
trace: WaveformTrace,
|
|
@@ -301,6 +333,95 @@ def generate_eye_from_edges(
|
|
|
301
333
|
)
|
|
302
334
|
|
|
303
335
|
|
|
336
|
+
def _calculate_trigger_threshold(data: NDArray[np.float64], trigger_fraction: float) -> float:
|
|
337
|
+
"""Calculate trigger threshold from data amplitude.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
data: Eye diagram data.
|
|
341
|
+
trigger_fraction: Trigger level as fraction of amplitude.
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
Threshold value.
|
|
345
|
+
"""
|
|
346
|
+
low = np.percentile(data, 10)
|
|
347
|
+
high = np.percentile(data, 90)
|
|
348
|
+
amplitude_range = high - low
|
|
349
|
+
threshold: float = float(low + trigger_fraction * amplitude_range)
|
|
350
|
+
return threshold
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _find_trace_crossings(data: NDArray[np.float64], threshold: float) -> list[int]:
|
|
354
|
+
"""Find crossing indices for all traces.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
data: Eye diagram trace data (n_traces x samples_per_trace).
|
|
358
|
+
threshold: Crossing threshold.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
List of crossing indices for traces with crossings.
|
|
362
|
+
"""
|
|
363
|
+
n_traces, _samples_per_trace = data.shape
|
|
364
|
+
crossing_indices = []
|
|
365
|
+
|
|
366
|
+
for trace_idx in range(n_traces):
|
|
367
|
+
trace = data[trace_idx, :]
|
|
368
|
+
crossings = np.where((trace[:-1] < threshold) & (trace[1:] >= threshold))[0]
|
|
369
|
+
|
|
370
|
+
if len(crossings) > 0:
|
|
371
|
+
crossing_indices.append(crossings[0])
|
|
372
|
+
|
|
373
|
+
return crossing_indices
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _align_traces_to_target(
|
|
377
|
+
data: NDArray[np.float64], threshold: float, target_crossing: int
|
|
378
|
+
) -> NDArray[np.float64]:
|
|
379
|
+
"""Align all traces to target crossing position.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
data: Eye diagram trace data.
|
|
383
|
+
threshold: Crossing threshold.
|
|
384
|
+
target_crossing: Target crossing position.
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
Aligned trace data.
|
|
388
|
+
"""
|
|
389
|
+
n_traces, _samples_per_trace = data.shape
|
|
390
|
+
aligned_data = np.zeros_like(data)
|
|
391
|
+
|
|
392
|
+
for trace_idx in range(n_traces):
|
|
393
|
+
trace = data[trace_idx, :]
|
|
394
|
+
crossings = np.where((trace[:-1] < threshold) & (trace[1:] >= threshold))[0]
|
|
395
|
+
|
|
396
|
+
if len(crossings) > 0:
|
|
397
|
+
crossing = crossings[0]
|
|
398
|
+
shift = target_crossing - crossing
|
|
399
|
+
|
|
400
|
+
if shift != 0:
|
|
401
|
+
aligned_data[trace_idx, :] = np.roll(trace, shift)
|
|
402
|
+
else:
|
|
403
|
+
aligned_data[trace_idx, :] = trace
|
|
404
|
+
else:
|
|
405
|
+
aligned_data[trace_idx, :] = trace
|
|
406
|
+
|
|
407
|
+
return aligned_data
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _apply_symmetric_centering(data: NDArray[np.float64]) -> NDArray[np.float64]:
|
|
411
|
+
"""Apply symmetric amplitude centering if enabled.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
data: Aligned trace data.
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Symmetrically centered data.
|
|
418
|
+
"""
|
|
419
|
+
max_abs = np.max(np.abs(data))
|
|
420
|
+
if max_abs > 0:
|
|
421
|
+
data = data - np.mean(data)
|
|
422
|
+
return data
|
|
423
|
+
|
|
424
|
+
|
|
304
425
|
def auto_center_eye_diagram(
|
|
305
426
|
eye: EyeDiagram,
|
|
306
427
|
*,
|
|
@@ -335,37 +456,12 @@ def auto_center_eye_diagram(
|
|
|
335
456
|
if not 0 <= trigger_fraction <= 1:
|
|
336
457
|
raise ValueError(f"trigger_fraction must be in [0, 1], got {trigger_fraction}")
|
|
337
458
|
|
|
459
|
+
# Setup: calculate threshold and find crossings
|
|
338
460
|
data = eye.data
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
# Find median value (represents middle level)
|
|
342
|
-
np.median(data)
|
|
343
|
-
|
|
344
|
-
# Calculate amplitude range
|
|
345
|
-
low = np.percentile(data, 10)
|
|
346
|
-
high = np.percentile(data, 90)
|
|
347
|
-
amplitude_range = high - low
|
|
348
|
-
|
|
349
|
-
# Trigger threshold at specified fraction
|
|
350
|
-
threshold = low + trigger_fraction * amplitude_range
|
|
351
|
-
|
|
352
|
-
# Find crossing points for each trace
|
|
353
|
-
# A crossing is where signal crosses threshold
|
|
354
|
-
n_traces, samples_per_trace = data.shape
|
|
355
|
-
crossing_indices = []
|
|
356
|
-
|
|
357
|
-
for trace_idx in range(n_traces):
|
|
358
|
-
trace = data[trace_idx, :]
|
|
359
|
-
|
|
360
|
-
# Find zero-crossings relative to threshold
|
|
361
|
-
crossings = np.where((trace[:-1] < threshold) & (trace[1:] >= threshold))[0]
|
|
362
|
-
|
|
363
|
-
if len(crossings) > 0:
|
|
364
|
-
# Use first crossing in this trace
|
|
365
|
-
crossing_indices.append(crossings[0])
|
|
461
|
+
threshold = _calculate_trigger_threshold(data, trigger_fraction)
|
|
462
|
+
crossing_indices = _find_trace_crossings(data, threshold)
|
|
366
463
|
|
|
367
464
|
if len(crossing_indices) == 0:
|
|
368
|
-
# No crossings found, return original
|
|
369
465
|
import warnings
|
|
370
466
|
|
|
371
467
|
warnings.warn(
|
|
@@ -375,44 +471,15 @@ def auto_center_eye_diagram(
|
|
|
375
471
|
)
|
|
376
472
|
return eye
|
|
377
473
|
|
|
378
|
-
#
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
# This requires resampling/shifting each trace
|
|
383
|
-
aligned_data = np.zeros_like(data)
|
|
384
|
-
target_crossing = samples_per_trace // 2 # Center of trace
|
|
474
|
+
# Processing: align traces to target crossing point
|
|
475
|
+
_n_traces, samples_per_trace = data.shape
|
|
476
|
+
target_crossing = samples_per_trace // 2
|
|
477
|
+
aligned_data = _align_traces_to_target(data, threshold, target_crossing)
|
|
385
478
|
|
|
386
|
-
|
|
387
|
-
trace = data[trace_idx, :]
|
|
388
|
-
|
|
389
|
-
# Find crossing for this trace
|
|
390
|
-
crossings = np.where((trace[:-1] < threshold) & (trace[1:] >= threshold))[0]
|
|
391
|
-
|
|
392
|
-
if len(crossings) > 0:
|
|
393
|
-
crossing = crossings[0]
|
|
394
|
-
shift = target_crossing - crossing
|
|
395
|
-
|
|
396
|
-
# Shift trace by interpolation
|
|
397
|
-
if shift != 0:
|
|
398
|
-
# Simple roll (circular shift)
|
|
399
|
-
aligned_data[trace_idx, :] = np.roll(trace, shift)
|
|
400
|
-
else:
|
|
401
|
-
aligned_data[trace_idx, :] = trace
|
|
402
|
-
else:
|
|
403
|
-
# No crossing, keep original
|
|
404
|
-
aligned_data[trace_idx, :] = trace
|
|
405
|
-
|
|
406
|
-
# Scale amplitude to symmetric range if requested
|
|
479
|
+
# Result building: apply symmetric centering and create result
|
|
407
480
|
if symmetric_range:
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
# Center on zero
|
|
411
|
-
aligned_data = aligned_data - np.mean(aligned_data)
|
|
412
|
-
# Scale to ±max for symmetric range
|
|
413
|
-
# No additional scaling needed, data already centered
|
|
414
|
-
|
|
415
|
-
# Create centered eye diagram
|
|
481
|
+
aligned_data = _apply_symmetric_centering(aligned_data)
|
|
482
|
+
|
|
416
483
|
return EyeDiagram(
|
|
417
484
|
data=aligned_data,
|
|
418
485
|
time_axis=eye.time_axis,
|
|
@@ -420,7 +487,7 @@ def auto_center_eye_diagram(
|
|
|
420
487
|
samples_per_ui=eye.samples_per_ui,
|
|
421
488
|
n_traces=eye.n_traces,
|
|
422
489
|
sample_rate=eye.sample_rate,
|
|
423
|
-
histogram=None,
|
|
490
|
+
histogram=None,
|
|
424
491
|
voltage_bins=None,
|
|
425
492
|
time_bins=None,
|
|
426
493
|
)
|
oscura/analyzers/eye/metrics.py
CHANGED
|
@@ -117,7 +117,7 @@ def eye_height(
|
|
|
117
117
|
break
|
|
118
118
|
else:
|
|
119
119
|
# No position found with eye opening
|
|
120
|
-
return np.nan
|
|
120
|
+
return np.nan
|
|
121
121
|
|
|
122
122
|
if ber is None:
|
|
123
123
|
# Simple min-max eye height
|
|
@@ -198,7 +198,7 @@ def eye_width(
|
|
|
198
198
|
time_indices.append(i)
|
|
199
199
|
|
|
200
200
|
if len(separations) == 0:
|
|
201
|
-
return np.nan
|
|
201
|
+
return np.nan
|
|
202
202
|
|
|
203
203
|
# Find contiguous region with good separation
|
|
204
204
|
if len(time_indices) < 2:
|
|
@@ -285,7 +285,7 @@ def q_factor(eye: EyeDiagram, *, position: float = 0.5) -> float:
|
|
|
285
285
|
break
|
|
286
286
|
else:
|
|
287
287
|
# No position found with eye opening
|
|
288
|
-
return np.nan
|
|
288
|
+
return np.nan
|
|
289
289
|
|
|
290
290
|
mu_high = np.mean(high_voltages)
|
|
291
291
|
mu_low = np.mean(low_voltages)
|
|
@@ -295,7 +295,7 @@ def q_factor(eye: EyeDiagram, *, position: float = 0.5) -> float:
|
|
|
295
295
|
denominator = sigma_high + sigma_low
|
|
296
296
|
|
|
297
297
|
if denominator <= 0:
|
|
298
|
-
return np.inf if mu_high > mu_low else np.nan
|
|
298
|
+
return np.inf if mu_high > mu_low else np.nan
|
|
299
299
|
|
|
300
300
|
q = (mu_high - mu_low) / denominator
|
|
301
301
|
|
|
@@ -331,7 +331,7 @@ def crossing_percentage(eye: EyeDiagram) -> float:
|
|
|
331
331
|
amplitude = all_high - all_low
|
|
332
332
|
|
|
333
333
|
if amplitude <= 0:
|
|
334
|
-
return np.nan
|
|
334
|
+
return np.nan
|
|
335
335
|
|
|
336
336
|
# Find crossing points (where traces cross the center time)
|
|
337
337
|
# Look at the rising and falling edges
|
|
@@ -24,6 +24,10 @@ from oscura.analyzers.jitter.ber import (
|
|
|
24
24
|
q_factor_from_ber,
|
|
25
25
|
tj_at_ber,
|
|
26
26
|
)
|
|
27
|
+
from oscura.analyzers.jitter.classification import (
|
|
28
|
+
JitterClassificationResult,
|
|
29
|
+
JitterComponentEstimate,
|
|
30
|
+
)
|
|
27
31
|
from oscura.analyzers.jitter.decomposition import (
|
|
28
32
|
DataDependentJitterResult,
|
|
29
33
|
DeterministicJitterResult,
|
|
@@ -51,16 +55,14 @@ from oscura.analyzers.jitter.spectrum import (
|
|
|
51
55
|
)
|
|
52
56
|
|
|
53
57
|
__all__ = [
|
|
54
|
-
# BER
|
|
55
58
|
"BathtubCurveResult",
|
|
56
|
-
# Measurements
|
|
57
59
|
"CycleJitterResult",
|
|
58
60
|
"DataDependentJitterResult",
|
|
59
61
|
"DeterministicJitterResult",
|
|
60
62
|
"DutyCycleDistortionResult",
|
|
61
|
-
|
|
63
|
+
"JitterClassificationResult",
|
|
64
|
+
"JitterComponentEstimate",
|
|
62
65
|
"JitterDecomposition",
|
|
63
|
-
# Spectrum
|
|
64
66
|
"JitterSpectrumResult",
|
|
65
67
|
"PeriodicJitterResult",
|
|
66
68
|
"RandomJitterResult",
|
oscura/analyzers/jitter/ber.py
CHANGED
|
@@ -68,7 +68,7 @@ def q_factor_from_ber(ber: float) -> float:
|
|
|
68
68
|
IEEE 2414-2020: Q = sqrt(2) * erfc_inv(2 * BER)
|
|
69
69
|
"""
|
|
70
70
|
if ber <= 0 or ber >= 0.5:
|
|
71
|
-
return np.nan
|
|
71
|
+
return np.nan
|
|
72
72
|
|
|
73
73
|
# BER = 0.5 * erfc(Q / sqrt(2))
|
|
74
74
|
# erfc(Q / sqrt(2)) = 2 * BER
|
|
@@ -141,7 +141,7 @@ def tj_at_ber(
|
|
|
141
141
|
q = q_factor_from_ber(ber)
|
|
142
142
|
|
|
143
143
|
if np.isnan(q):
|
|
144
|
-
return np.nan
|
|
144
|
+
return np.nan
|
|
145
145
|
|
|
146
146
|
# TJ = 2 * Q * RJ_rms + DJ_pp
|
|
147
147
|
tj = 2 * q * rj_rms + dj_pp
|
|
@@ -182,68 +182,25 @@ def bathtub_curve(
|
|
|
182
182
|
References:
|
|
183
183
|
IEEE 2414-2020 Section 6.7
|
|
184
184
|
"""
|
|
185
|
-
from oscura.analyzers.jitter.decomposition import extract_dj, extract_rj
|
|
186
185
|
|
|
187
186
|
valid_data = tie_data[~np.isnan(tie_data)]
|
|
188
187
|
|
|
189
|
-
# Normalize TIE to UI
|
|
190
|
-
valid_data / unit_interval
|
|
191
|
-
|
|
192
188
|
# Extract jitter components if not provided
|
|
193
189
|
if rj_rms is None or dj_delta is None:
|
|
194
|
-
|
|
195
|
-
rj_result = extract_rj(valid_data, min_samples=100)
|
|
196
|
-
rj_rms = rj_result.rj_rms
|
|
197
|
-
except Exception:
|
|
198
|
-
rj_rms = np.std(valid_data)
|
|
199
|
-
|
|
200
|
-
try:
|
|
201
|
-
dj_result = extract_dj(valid_data, min_samples=100)
|
|
202
|
-
dj_delta = dj_result.dj_delta
|
|
203
|
-
except Exception:
|
|
204
|
-
dj_delta = 0.0
|
|
190
|
+
rj_rms, dj_delta = _extract_jitter_components(valid_data)
|
|
205
191
|
|
|
206
|
-
# Convert to UI
|
|
207
|
-
sigma_ui = rj_rms / unit_interval
|
|
208
|
-
delta_ui = dj_delta / unit_interval
|
|
209
|
-
|
|
210
|
-
# Generate sampling positions (0 to 1 UI)
|
|
192
|
+
# Convert to UI and generate positions
|
|
193
|
+
sigma_ui, delta_ui = rj_rms / unit_interval, dj_delta / unit_interval
|
|
211
194
|
positions = np.linspace(0, 1, n_points)
|
|
212
195
|
|
|
213
|
-
# Calculate BER
|
|
214
|
-
|
|
215
|
-
# Right side: probability of sampling a '0' when '1' is sent
|
|
216
|
-
|
|
217
|
-
# For a dual-Dirac distribution centered at 0.5 UI:
|
|
218
|
-
# Left Dirac at 0.5 - delta, Right Dirac at 0.5 + delta
|
|
219
|
-
|
|
220
|
-
ber_left = np.zeros(n_points)
|
|
221
|
-
ber_right = np.zeros(n_points)
|
|
222
|
-
|
|
223
|
-
for i, pos in enumerate(positions):
|
|
224
|
-
# Left BER: Q-function from left edge
|
|
225
|
-
if sigma_ui > 0:
|
|
226
|
-
# Distance from left edge to sampling point
|
|
227
|
-
q_left = (pos - delta_ui) / sigma_ui
|
|
228
|
-
ber_left[i] = 0.5 * special.erfc(q_left / np.sqrt(2))
|
|
229
|
-
|
|
230
|
-
# Distance from right edge to sampling point
|
|
231
|
-
q_right = (1 - pos - delta_ui) / sigma_ui
|
|
232
|
-
ber_right[i] = 0.5 * special.erfc(q_right / np.sqrt(2))
|
|
233
|
-
else:
|
|
234
|
-
# No random jitter - step function
|
|
235
|
-
ber_left[i] = 0.5 if pos <= delta_ui else 0
|
|
236
|
-
ber_right[i] = 0.5 if pos >= (1 - delta_ui) else 0
|
|
237
|
-
|
|
238
|
-
# Total BER is sum of left and right
|
|
239
|
-
ber_total = ber_left + ber_right
|
|
196
|
+
# Calculate BER arrays using dual-Dirac model
|
|
197
|
+
ber_left, ber_right = _compute_ber_arrays(positions, sigma_ui, delta_ui)
|
|
240
198
|
|
|
241
|
-
#
|
|
242
|
-
ber_total = np.clip(
|
|
199
|
+
# Combine and clip BER values
|
|
200
|
+
ber_total = np.clip(ber_left + ber_right, 1e-18, 0.5)
|
|
243
201
|
ber_left = np.clip(ber_left, 1e-18, 0.5)
|
|
244
202
|
ber_right = np.clip(ber_right, 1e-18, 0.5)
|
|
245
203
|
|
|
246
|
-
# Calculate eye opening at target BER
|
|
247
204
|
eye_opening = _calculate_eye_opening(positions, ber_total, target_ber)
|
|
248
205
|
|
|
249
206
|
return BathtubCurveResult(
|
|
@@ -257,6 +214,49 @@ def bathtub_curve(
|
|
|
257
214
|
)
|
|
258
215
|
|
|
259
216
|
|
|
217
|
+
def _extract_jitter_components(valid_data: NDArray[np.float64]) -> tuple[float, float]:
|
|
218
|
+
"""Extract RJ and DJ components from TIE data."""
|
|
219
|
+
from oscura.analyzers.jitter.decomposition import extract_dj, extract_rj
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
rj_result = extract_rj(valid_data, min_samples=100)
|
|
223
|
+
rj_rms = rj_result.rj_rms
|
|
224
|
+
except Exception:
|
|
225
|
+
rj_rms_raw = np.std(valid_data)
|
|
226
|
+
rj_rms = float(rj_rms_raw)
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
dj_result = extract_dj(valid_data, min_samples=100)
|
|
230
|
+
dj_delta = dj_result.dj_delta
|
|
231
|
+
except Exception:
|
|
232
|
+
dj_delta = 0.0
|
|
233
|
+
|
|
234
|
+
return rj_rms, dj_delta
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _compute_ber_arrays(
|
|
238
|
+
positions: NDArray[np.float64], sigma_ui: float, delta_ui: float
|
|
239
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
240
|
+
"""Compute BER arrays for left and right edges using dual-Dirac model."""
|
|
241
|
+
n_points = len(positions)
|
|
242
|
+
ber_left, ber_right = np.zeros(n_points), np.zeros(n_points)
|
|
243
|
+
|
|
244
|
+
for i, pos in enumerate(positions):
|
|
245
|
+
if sigma_ui > 0:
|
|
246
|
+
# Q-function for Gaussian random jitter
|
|
247
|
+
q_left = (pos - delta_ui) / sigma_ui
|
|
248
|
+
ber_left[i] = 0.5 * special.erfc(q_left / np.sqrt(2))
|
|
249
|
+
|
|
250
|
+
q_right = (1 - pos - delta_ui) / sigma_ui
|
|
251
|
+
ber_right[i] = 0.5 * special.erfc(q_right / np.sqrt(2))
|
|
252
|
+
else:
|
|
253
|
+
# No random jitter - step function
|
|
254
|
+
ber_left[i] = 0.5 if pos <= delta_ui else 0
|
|
255
|
+
ber_right[i] = 0.5 if pos >= (1 - delta_ui) else 0
|
|
256
|
+
|
|
257
|
+
return ber_left, ber_right
|
|
258
|
+
|
|
259
|
+
|
|
260
260
|
def _calculate_eye_opening(
|
|
261
261
|
positions: NDArray[np.float64],
|
|
262
262
|
ber: NDArray[np.float64],
|