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/visualization/jitter.py
CHANGED
|
@@ -44,6 +44,138 @@ __all__ = [
|
|
|
44
44
|
]
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
def _determine_tie_time_unit(
|
|
48
|
+
tie_data: NDArray[np.floating[Any]], time_unit: str
|
|
49
|
+
) -> tuple[str, float]:
|
|
50
|
+
"""Determine time unit and multiplier for TIE display.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
tie_data: TIE values in seconds.
|
|
54
|
+
time_unit: Requested time unit or "auto".
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Tuple of (time_unit, time_multiplier).
|
|
58
|
+
"""
|
|
59
|
+
if time_unit == "auto":
|
|
60
|
+
max_tie = np.max(np.abs(tie_data))
|
|
61
|
+
if max_tie < 1e-12:
|
|
62
|
+
return "fs", 1e15
|
|
63
|
+
elif max_tie < 1e-9:
|
|
64
|
+
return "ps", 1e12
|
|
65
|
+
elif max_tie < 1e-6:
|
|
66
|
+
return "ns", 1e9
|
|
67
|
+
else:
|
|
68
|
+
return "us", 1e6
|
|
69
|
+
else:
|
|
70
|
+
time_mult_map = {
|
|
71
|
+
"s": 1,
|
|
72
|
+
"ms": 1e3,
|
|
73
|
+
"us": 1e6,
|
|
74
|
+
"ns": 1e9,
|
|
75
|
+
"ps": 1e12,
|
|
76
|
+
"fs": 1e15,
|
|
77
|
+
}
|
|
78
|
+
if time_unit in time_mult_map:
|
|
79
|
+
return time_unit, time_mult_map[time_unit]
|
|
80
|
+
else:
|
|
81
|
+
# Fallback to ps for invalid unit
|
|
82
|
+
return "ps", 1e12
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _calculate_tie_statistics(
|
|
86
|
+
tie_scaled: NDArray[np.floating[Any]],
|
|
87
|
+
) -> tuple[float, float, float, float]:
|
|
88
|
+
"""Calculate TIE statistical metrics.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
tie_scaled: Scaled TIE values.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Tuple of (mean, std, peak-to-peak, rms).
|
|
95
|
+
"""
|
|
96
|
+
mean_val = float(np.mean(tie_scaled))
|
|
97
|
+
std_val = float(np.std(tie_scaled))
|
|
98
|
+
pp_val = float(np.ptp(tie_scaled))
|
|
99
|
+
rms_val = float(np.sqrt(np.mean(tie_scaled**2)))
|
|
100
|
+
return mean_val, std_val, pp_val, rms_val
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _add_gaussian_fit(
|
|
104
|
+
ax: Axes, bin_edges: NDArray[np.floating[Any]], mean_val: float, std_val: float, time_unit: str
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Add Gaussian fit overlay to histogram.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
ax: Matplotlib axes to plot on.
|
|
110
|
+
bin_edges: Histogram bin edges.
|
|
111
|
+
mean_val: Mean value.
|
|
112
|
+
std_val: Standard deviation.
|
|
113
|
+
time_unit: Time unit string for label.
|
|
114
|
+
"""
|
|
115
|
+
if not HAS_SCIPY:
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
x_fit = np.linspace(bin_edges[0], bin_edges[-1], 200)
|
|
119
|
+
y_fit = norm.pdf(x_fit, mean_val, std_val)
|
|
120
|
+
ax.plot(
|
|
121
|
+
x_fit, y_fit, "r-", linewidth=2, label=f"Gaussian Fit (sigma={std_val:.2f} {time_unit})"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _add_rj_dj_indicators(ax: Axes, mean_val: float, std_val: float) -> None:
|
|
126
|
+
"""Add RJ/DJ separation indicators to plot.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
ax: Matplotlib axes to plot on.
|
|
130
|
+
mean_val: Mean value.
|
|
131
|
+
std_val: Standard deviation.
|
|
132
|
+
"""
|
|
133
|
+
# Mark ±3sigma region (RJ contribution)
|
|
134
|
+
ax.axvline(mean_val - 3 * std_val, color="#E74C3C", linestyle="--", linewidth=1.5, alpha=0.7)
|
|
135
|
+
ax.axvline(mean_val + 3 * std_val, color="#E74C3C", linestyle="--", linewidth=1.5, alpha=0.7)
|
|
136
|
+
|
|
137
|
+
# Shade RJ region
|
|
138
|
+
ax.axvspan(
|
|
139
|
+
mean_val - 3 * std_val,
|
|
140
|
+
mean_val + 3 * std_val,
|
|
141
|
+
alpha=0.1,
|
|
142
|
+
color="#E74C3C",
|
|
143
|
+
label="±3sigma (99.7% RJ)",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _add_statistics_box(
|
|
148
|
+
ax: Axes, mean_val: float, rms_val: float, std_val: float, pp_val: float, time_unit: str
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Add statistics text box to plot.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
ax: Matplotlib axes to plot on.
|
|
154
|
+
mean_val: Mean value.
|
|
155
|
+
rms_val: RMS value.
|
|
156
|
+
std_val: Standard deviation.
|
|
157
|
+
pp_val: Peak-to-peak value.
|
|
158
|
+
time_unit: Time unit string.
|
|
159
|
+
"""
|
|
160
|
+
stats_text = (
|
|
161
|
+
f"Mean: {mean_val:.2f} {time_unit}\n"
|
|
162
|
+
f"RMS: {rms_val:.2f} {time_unit}\n"
|
|
163
|
+
f"Std Dev: {std_val:.2f} {time_unit}\n"
|
|
164
|
+
f"Peak-Peak: {pp_val:.2f} {time_unit}"
|
|
165
|
+
)
|
|
166
|
+
ax.text(
|
|
167
|
+
0.98,
|
|
168
|
+
0.98,
|
|
169
|
+
stats_text,
|
|
170
|
+
transform=ax.transAxes,
|
|
171
|
+
fontsize=9,
|
|
172
|
+
verticalalignment="top",
|
|
173
|
+
horizontalalignment="right",
|
|
174
|
+
bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.9},
|
|
175
|
+
fontfamily="monospace",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
47
179
|
def plot_tie_histogram(
|
|
48
180
|
tie_data: NDArray[np.floating[Any]],
|
|
49
181
|
*,
|
|
@@ -68,7 +200,7 @@ def plot_tie_histogram(
|
|
|
68
200
|
ax: Matplotlib axes. If None, creates new figure.
|
|
69
201
|
figsize: Figure size in inches.
|
|
70
202
|
title: Plot title.
|
|
71
|
-
time_unit: Time unit ("s", "ms", "us", "ns", "ps", "auto").
|
|
203
|
+
time_unit: Time unit ("s", "ms", "us", "ns", "ps", "fs", "auto").
|
|
72
204
|
bins: Number of bins or "auto" for automatic selection.
|
|
73
205
|
show_gaussian_fit: Overlay Gaussian fit for RJ estimation.
|
|
74
206
|
show_statistics: Show statistics box.
|
|
@@ -86,7 +218,34 @@ def plot_tie_histogram(
|
|
|
86
218
|
if not HAS_MATPLOTLIB:
|
|
87
219
|
raise ImportError("matplotlib is required for visualization")
|
|
88
220
|
|
|
89
|
-
|
|
221
|
+
fig, ax = _setup_tie_figure(ax, figsize)
|
|
222
|
+
time_unit, time_mult = _determine_tie_time_unit(tie_data, time_unit)
|
|
223
|
+
tie_scaled = tie_data * time_mult
|
|
224
|
+
mean_val, std_val, pp_val, rms_val = _calculate_tie_statistics(tie_scaled)
|
|
225
|
+
|
|
226
|
+
counts, bin_edges, patches = _plot_tie_histogram_data(ax, tie_scaled, bins)
|
|
227
|
+
_add_tie_overlays(ax, show_gaussian_fit, show_rj_dj, bin_edges, mean_val, std_val, time_unit)
|
|
228
|
+
_format_tie_plot(ax, show_statistics, mean_val, rms_val, std_val, pp_val, time_unit, title)
|
|
229
|
+
|
|
230
|
+
fig.tight_layout()
|
|
231
|
+
_save_and_show_tie_plot(fig, save_path, show)
|
|
232
|
+
|
|
233
|
+
return fig
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _setup_tie_figure(ax: Axes | None, figsize: tuple[float, float]) -> tuple[Figure, Axes]:
|
|
237
|
+
"""Setup figure and axes for TIE plot.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
ax: Existing axes or None.
|
|
241
|
+
figsize: Figure size.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Tuple of (figure, axes).
|
|
245
|
+
|
|
246
|
+
Raises:
|
|
247
|
+
ValueError: If axes has no figure.
|
|
248
|
+
"""
|
|
90
249
|
if ax is None:
|
|
91
250
|
fig, ax = plt.subplots(figsize=figsize)
|
|
92
251
|
else:
|
|
@@ -94,42 +253,23 @@ def plot_tie_histogram(
|
|
|
94
253
|
if fig_temp is None:
|
|
95
254
|
raise ValueError("Axes must have an associated figure")
|
|
96
255
|
fig = cast("Figure", fig_temp)
|
|
256
|
+
return fig, ax
|
|
97
257
|
|
|
98
|
-
# Select time unit
|
|
99
|
-
if time_unit == "auto":
|
|
100
|
-
max_tie = np.max(np.abs(tie_data))
|
|
101
|
-
if max_tie < 1e-12:
|
|
102
|
-
time_unit = "fs"
|
|
103
|
-
time_mult = 1e15
|
|
104
|
-
elif max_tie < 1e-9:
|
|
105
|
-
time_unit = "ps"
|
|
106
|
-
time_mult = 1e12
|
|
107
|
-
elif max_tie < 1e-6:
|
|
108
|
-
time_unit = "ns"
|
|
109
|
-
time_mult = 1e9
|
|
110
|
-
else:
|
|
111
|
-
time_unit = "us"
|
|
112
|
-
time_mult = 1e6
|
|
113
|
-
else:
|
|
114
|
-
time_mult = {
|
|
115
|
-
"s": 1,
|
|
116
|
-
"ms": 1e3,
|
|
117
|
-
"us": 1e6,
|
|
118
|
-
"ns": 1e9,
|
|
119
|
-
"ps": 1e12,
|
|
120
|
-
"fs": 1e15,
|
|
121
|
-
}.get(time_unit, 1e12)
|
|
122
258
|
|
|
123
|
-
|
|
259
|
+
def _plot_tie_histogram_data(
|
|
260
|
+
ax: Axes, tie_scaled: NDArray[np.floating[Any]], bins: int | str
|
|
261
|
+
) -> tuple[Any, NDArray[Any], Any]:
|
|
262
|
+
"""Plot histogram data.
|
|
124
263
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
rms_val = np.sqrt(np.mean(tie_scaled**2))
|
|
264
|
+
Args:
|
|
265
|
+
ax: Matplotlib axes.
|
|
266
|
+
tie_scaled: Scaled TIE data.
|
|
267
|
+
bins: Bin specification.
|
|
130
268
|
|
|
131
|
-
|
|
132
|
-
|
|
269
|
+
Returns:
|
|
270
|
+
Tuple of (counts, bin_edges, patches) from matplotlib hist.
|
|
271
|
+
"""
|
|
272
|
+
result: tuple[Any, NDArray[Any], Any] = ax.hist(
|
|
133
273
|
tie_scaled,
|
|
134
274
|
bins=bins,
|
|
135
275
|
density=True,
|
|
@@ -138,75 +278,82 @@ def plot_tie_histogram(
|
|
|
138
278
|
edgecolor="black",
|
|
139
279
|
linewidth=0.5,
|
|
140
280
|
)
|
|
281
|
+
return result
|
|
141
282
|
|
|
142
|
-
# Gaussian fit overlay
|
|
143
|
-
if show_gaussian_fit and HAS_SCIPY:
|
|
144
|
-
x_fit = np.linspace(bin_edges[0], bin_edges[-1], 200)
|
|
145
|
-
y_fit = norm.pdf(x_fit, mean_val, std_val)
|
|
146
|
-
ax.plot(
|
|
147
|
-
x_fit, y_fit, "r-", linewidth=2, label=f"Gaussian Fit (sigma={std_val:.2f} {time_unit})"
|
|
148
|
-
)
|
|
149
283
|
|
|
150
|
-
|
|
284
|
+
def _add_tie_overlays(
|
|
285
|
+
ax: Axes,
|
|
286
|
+
show_gaussian_fit: bool,
|
|
287
|
+
show_rj_dj: bool,
|
|
288
|
+
bin_edges: NDArray[Any],
|
|
289
|
+
mean_val: float,
|
|
290
|
+
std_val: float,
|
|
291
|
+
time_unit: str,
|
|
292
|
+
) -> None:
|
|
293
|
+
"""Add Gaussian fit and RJ/DJ overlays to TIE plot.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
ax: Matplotlib axes.
|
|
297
|
+
show_gaussian_fit: Whether to show Gaussian fit.
|
|
298
|
+
show_rj_dj: Whether to show RJ/DJ indicators.
|
|
299
|
+
bin_edges: Histogram bin edges.
|
|
300
|
+
mean_val: Mean TIE value.
|
|
301
|
+
std_val: Standard deviation.
|
|
302
|
+
time_unit: Time unit string.
|
|
303
|
+
"""
|
|
304
|
+
if show_gaussian_fit:
|
|
305
|
+
_add_gaussian_fit(ax, bin_edges, mean_val, std_val, time_unit)
|
|
151
306
|
if show_rj_dj:
|
|
152
|
-
|
|
153
|
-
ax.axvline(
|
|
154
|
-
mean_val - 3 * std_val, color="#E74C3C", linestyle="--", linewidth=1.5, alpha=0.7
|
|
155
|
-
)
|
|
156
|
-
ax.axvline(
|
|
157
|
-
mean_val + 3 * std_val, color="#E74C3C", linestyle="--", linewidth=1.5, alpha=0.7
|
|
158
|
-
)
|
|
307
|
+
_add_rj_dj_indicators(ax, mean_val, std_val)
|
|
159
308
|
|
|
160
|
-
# Shade RJ region
|
|
161
|
-
ax.axvspan(
|
|
162
|
-
mean_val - 3 * std_val,
|
|
163
|
-
mean_val + 3 * std_val,
|
|
164
|
-
alpha=0.1,
|
|
165
|
-
color="#E74C3C",
|
|
166
|
-
label="±3sigma (99.7% RJ)",
|
|
167
|
-
)
|
|
168
309
|
|
|
169
|
-
|
|
310
|
+
def _format_tie_plot(
|
|
311
|
+
ax: Axes,
|
|
312
|
+
show_statistics: bool,
|
|
313
|
+
mean_val: float,
|
|
314
|
+
rms_val: float,
|
|
315
|
+
std_val: float,
|
|
316
|
+
pp_val: float,
|
|
317
|
+
time_unit: str,
|
|
318
|
+
title: str | None,
|
|
319
|
+
) -> None:
|
|
320
|
+
"""Format TIE plot axes and labels.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
ax: Matplotlib axes.
|
|
324
|
+
show_statistics: Whether to show statistics box.
|
|
325
|
+
mean_val: Mean value.
|
|
326
|
+
rms_val: RMS value.
|
|
327
|
+
std_val: Standard deviation.
|
|
328
|
+
pp_val: Peak-to-peak value.
|
|
329
|
+
time_unit: Time unit.
|
|
330
|
+
title: Plot title.
|
|
331
|
+
"""
|
|
170
332
|
if show_statistics:
|
|
171
|
-
|
|
172
|
-
f"Mean: {mean_val:.2f} {time_unit}\n"
|
|
173
|
-
f"RMS: {rms_val:.2f} {time_unit}\n"
|
|
174
|
-
f"Std Dev: {std_val:.2f} {time_unit}\n"
|
|
175
|
-
f"Peak-Peak: {pp_val:.2f} {time_unit}"
|
|
176
|
-
)
|
|
177
|
-
ax.text(
|
|
178
|
-
0.98,
|
|
179
|
-
0.98,
|
|
180
|
-
stats_text,
|
|
181
|
-
transform=ax.transAxes,
|
|
182
|
-
fontsize=9,
|
|
183
|
-
verticalalignment="top",
|
|
184
|
-
horizontalalignment="right",
|
|
185
|
-
bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.9},
|
|
186
|
-
fontfamily="monospace",
|
|
187
|
-
)
|
|
333
|
+
_add_statistics_box(ax, mean_val, rms_val, std_val, pp_val, time_unit)
|
|
188
334
|
|
|
189
|
-
# Labels
|
|
190
335
|
ax.set_xlabel(f"TIE ({time_unit})", fontsize=11)
|
|
191
336
|
ax.set_ylabel("Probability Density", fontsize=11)
|
|
192
337
|
ax.grid(True, alpha=0.3)
|
|
193
338
|
ax.legend(loc="upper left")
|
|
194
339
|
|
|
195
|
-
if title
|
|
196
|
-
|
|
197
|
-
else:
|
|
198
|
-
ax.set_title("Time Interval Error Distribution", fontsize=12, fontweight="bold")
|
|
340
|
+
final_title = title if title else "Time Interval Error Distribution"
|
|
341
|
+
ax.set_title(final_title, fontsize=12, fontweight="bold")
|
|
199
342
|
|
|
200
|
-
fig.tight_layout()
|
|
201
343
|
|
|
344
|
+
def _save_and_show_tie_plot(fig: Figure, save_path: str | Path | None, show: bool) -> None:
|
|
345
|
+
"""Save and/or show TIE plot.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
fig: Matplotlib figure.
|
|
349
|
+
save_path: Path to save file.
|
|
350
|
+
show: Whether to display interactively.
|
|
351
|
+
"""
|
|
202
352
|
if save_path is not None:
|
|
203
353
|
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
204
|
-
|
|
205
354
|
if show:
|
|
206
355
|
plt.show()
|
|
207
356
|
|
|
208
|
-
return fig
|
|
209
|
-
|
|
210
357
|
|
|
211
358
|
def plot_bathtub_full(
|
|
212
359
|
positions: NDArray[np.floating[Any]],
|
|
@@ -257,6 +404,35 @@ def plot_bathtub_full(
|
|
|
257
404
|
if not HAS_MATPLOTLIB:
|
|
258
405
|
raise ImportError("matplotlib is required for visualization")
|
|
259
406
|
|
|
407
|
+
fig, ax = _get_or_create_figure(ax, figsize)
|
|
408
|
+
ber_total = ber_total if ber_total is not None else ber_left + ber_right
|
|
409
|
+
|
|
410
|
+
# Plot BER curves
|
|
411
|
+
ber_total_plot = _plot_bathtub_ber_curves(ax, positions, ber_left, ber_right, ber_total)
|
|
412
|
+
|
|
413
|
+
# Optional annotations
|
|
414
|
+
if show_target:
|
|
415
|
+
_add_target_ber_line(ax, target_ber)
|
|
416
|
+
|
|
417
|
+
if show_eye_opening:
|
|
418
|
+
_add_eye_opening_annotation(ax, positions, ber_total_plot, target_ber, eye_opening)
|
|
419
|
+
|
|
420
|
+
# Styling
|
|
421
|
+
_style_bathtub_plot(ax, positions, ber_total_plot, title)
|
|
422
|
+
|
|
423
|
+
fig.tight_layout()
|
|
424
|
+
|
|
425
|
+
if save_path is not None:
|
|
426
|
+
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
427
|
+
|
|
428
|
+
if show:
|
|
429
|
+
plt.show()
|
|
430
|
+
|
|
431
|
+
return fig
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _get_or_create_figure(ax: Axes | None, figsize: tuple[float, float]) -> tuple[Figure, Axes]:
|
|
435
|
+
"""Get existing figure or create new one."""
|
|
260
436
|
if ax is None:
|
|
261
437
|
fig, ax = plt.subplots(figsize=figsize)
|
|
262
438
|
else:
|
|
@@ -264,92 +440,104 @@ def plot_bathtub_full(
|
|
|
264
440
|
if fig_temp is None:
|
|
265
441
|
raise ValueError("Axes must have an associated figure")
|
|
266
442
|
fig = cast("Figure", fig_temp)
|
|
443
|
+
return fig, ax
|
|
267
444
|
|
|
268
|
-
# Compute total BER if not provided
|
|
269
|
-
if ber_total is None:
|
|
270
|
-
ber_total = ber_left + ber_right
|
|
271
445
|
|
|
272
|
-
|
|
446
|
+
def _plot_bathtub_ber_curves(
|
|
447
|
+
ax: Axes,
|
|
448
|
+
positions: NDArray[np.floating[Any]],
|
|
449
|
+
ber_left: NDArray[np.floating[Any]],
|
|
450
|
+
ber_right: NDArray[np.floating[Any]],
|
|
451
|
+
ber_total: NDArray[np.floating[Any]],
|
|
452
|
+
) -> NDArray[np.floating[Any]]:
|
|
453
|
+
"""Plot BER curves and return clipped total BER."""
|
|
273
454
|
ber_left_plot = np.clip(ber_left, 1e-18, 1)
|
|
274
455
|
ber_right_plot = np.clip(ber_right, 1e-18, 1)
|
|
275
456
|
ber_total_plot = np.clip(ber_total, 1e-18, 1)
|
|
276
457
|
|
|
277
|
-
# Plot BER curves
|
|
278
458
|
ax.semilogy(positions, ber_left_plot, "b-", linewidth=2, label="BER Left", alpha=0.8)
|
|
279
459
|
ax.semilogy(positions, ber_right_plot, "r-", linewidth=2, label="BER Right", alpha=0.8)
|
|
280
460
|
ax.semilogy(positions, ber_total_plot, "k-", linewidth=2.5, label="BER Total")
|
|
281
461
|
|
|
282
|
-
|
|
283
|
-
if show_target:
|
|
284
|
-
ax.axhline(
|
|
285
|
-
target_ber,
|
|
286
|
-
color="#27AE60",
|
|
287
|
-
linestyle="--",
|
|
288
|
-
linewidth=2,
|
|
289
|
-
label=f"Target BER = {target_ber:.0e}",
|
|
290
|
-
)
|
|
462
|
+
return ber_total_plot
|
|
291
463
|
|
|
292
|
-
# Eye opening annotation
|
|
293
|
-
if show_eye_opening:
|
|
294
|
-
# Find eye opening at target BER
|
|
295
|
-
if eye_opening is None:
|
|
296
|
-
# Find crossover points
|
|
297
|
-
left_cross = np.where(ber_total_plot < target_ber)[0]
|
|
298
|
-
if len(left_cross) > 0:
|
|
299
|
-
left_edge = positions[left_cross[0]]
|
|
300
|
-
right_edge = positions[left_cross[-1]]
|
|
301
|
-
eye_opening = right_edge - left_edge
|
|
302
|
-
else:
|
|
303
|
-
eye_opening = 0
|
|
304
|
-
|
|
305
|
-
if eye_opening > 0:
|
|
306
|
-
# Draw eye opening bracket
|
|
307
|
-
center = 0.5
|
|
308
|
-
left_edge = center - eye_opening / 2
|
|
309
|
-
right_edge = center + eye_opening / 2
|
|
310
|
-
|
|
311
|
-
ax.annotate(
|
|
312
|
-
"",
|
|
313
|
-
xy=(right_edge, target_ber),
|
|
314
|
-
xytext=(left_edge, target_ber),
|
|
315
|
-
arrowprops={"arrowstyle": "<->", "color": "#27AE60", "lw": 2},
|
|
316
|
-
)
|
|
317
|
-
ax.text(
|
|
318
|
-
center,
|
|
319
|
-
target_ber * 0.1,
|
|
320
|
-
f"Eye Opening: {eye_opening:.3f} UI",
|
|
321
|
-
ha="center",
|
|
322
|
-
va="top",
|
|
323
|
-
fontsize=10,
|
|
324
|
-
fontweight="bold",
|
|
325
|
-
color="#27AE60",
|
|
326
|
-
)
|
|
327
|
-
|
|
328
|
-
# Shading for bathtub
|
|
329
|
-
ax.fill_between(positions, 1e-18, ber_total_plot, alpha=0.1, color="gray")
|
|
330
464
|
|
|
331
|
-
|
|
465
|
+
def _add_target_ber_line(ax: Axes, target_ber: float) -> None:
|
|
466
|
+
"""Add target BER horizontal line."""
|
|
467
|
+
ax.axhline(
|
|
468
|
+
target_ber,
|
|
469
|
+
color="#27AE60",
|
|
470
|
+
linestyle="--",
|
|
471
|
+
linewidth=2,
|
|
472
|
+
label=f"Target BER = {target_ber:.0e}",
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _add_eye_opening_annotation(
|
|
477
|
+
ax: Axes,
|
|
478
|
+
positions: NDArray[np.floating[Any]],
|
|
479
|
+
ber_total_plot: NDArray[np.floating[Any]],
|
|
480
|
+
target_ber: float,
|
|
481
|
+
eye_opening: float | None,
|
|
482
|
+
) -> None:
|
|
483
|
+
"""Add eye opening annotation if applicable."""
|
|
484
|
+
if eye_opening is None:
|
|
485
|
+
eye_opening = _calculate_eye_opening(positions, ber_total_plot, target_ber)
|
|
486
|
+
|
|
487
|
+
if eye_opening <= 0:
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
center = 0.5
|
|
491
|
+
left_edge = center - eye_opening / 2
|
|
492
|
+
right_edge = center + eye_opening / 2
|
|
493
|
+
|
|
494
|
+
ax.annotate(
|
|
495
|
+
"",
|
|
496
|
+
xy=(right_edge, target_ber),
|
|
497
|
+
xytext=(left_edge, target_ber),
|
|
498
|
+
arrowprops={"arrowstyle": "<->", "color": "#27AE60", "lw": 2},
|
|
499
|
+
)
|
|
500
|
+
ax.text(
|
|
501
|
+
center,
|
|
502
|
+
target_ber * 0.1,
|
|
503
|
+
f"Eye Opening: {eye_opening:.3f} UI",
|
|
504
|
+
ha="center",
|
|
505
|
+
va="top",
|
|
506
|
+
fontsize=10,
|
|
507
|
+
fontweight="bold",
|
|
508
|
+
color="#27AE60",
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _calculate_eye_opening(
|
|
513
|
+
positions: NDArray[np.floating[Any]],
|
|
514
|
+
ber_total: NDArray[np.floating[Any]],
|
|
515
|
+
target_ber: float,
|
|
516
|
+
) -> float:
|
|
517
|
+
"""Calculate eye opening at target BER."""
|
|
518
|
+
left_cross = np.where(ber_total < target_ber)[0]
|
|
519
|
+
if len(left_cross) > 0:
|
|
520
|
+
left_edge = positions[left_cross[0]]
|
|
521
|
+
right_edge = positions[left_cross[-1]]
|
|
522
|
+
return float(right_edge - left_edge)
|
|
523
|
+
return 0.0
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _style_bathtub_plot(
|
|
527
|
+
ax: Axes,
|
|
528
|
+
positions: NDArray[np.floating[Any]],
|
|
529
|
+
ber_total_plot: NDArray[np.floating[Any]],
|
|
530
|
+
title: str | None,
|
|
531
|
+
) -> None:
|
|
532
|
+
"""Apply styling to bathtub plot."""
|
|
533
|
+
ax.fill_between(positions, 1e-18, ber_total_plot, alpha=0.1, color="gray")
|
|
332
534
|
ax.set_xlabel("Sample Position (UI)", fontsize=11)
|
|
333
535
|
ax.set_ylabel("Bit Error Rate", fontsize=11)
|
|
334
536
|
ax.set_xlim(0, 1)
|
|
335
537
|
ax.set_ylim(1e-15, 1)
|
|
336
538
|
ax.grid(True, which="both", alpha=0.3)
|
|
337
539
|
ax.legend(loc="upper right")
|
|
338
|
-
|
|
339
|
-
if title:
|
|
340
|
-
ax.set_title(title, fontsize=12, fontweight="bold")
|
|
341
|
-
else:
|
|
342
|
-
ax.set_title("Bathtub Curve", fontsize=12, fontweight="bold")
|
|
343
|
-
|
|
344
|
-
fig.tight_layout()
|
|
345
|
-
|
|
346
|
-
if save_path is not None:
|
|
347
|
-
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
348
|
-
|
|
349
|
-
if show:
|
|
350
|
-
plt.show()
|
|
351
|
-
|
|
352
|
-
return fig
|
|
540
|
+
ax.set_title(title or "Bathtub Curve", fontsize=12, fontweight="bold")
|
|
353
541
|
|
|
354
542
|
|
|
355
543
|
def plot_ddj(
|
|
@@ -389,6 +577,13 @@ def plot_ddj(
|
|
|
389
577
|
if not HAS_MATPLOTLIB:
|
|
390
578
|
raise ImportError("matplotlib is required for visualization")
|
|
391
579
|
|
|
580
|
+
# Validate input lengths match
|
|
581
|
+
if len(patterns) != len(jitter_values):
|
|
582
|
+
raise ValueError(
|
|
583
|
+
f"Mismatched lengths: patterns has {len(patterns)} elements "
|
|
584
|
+
f"but jitter_values has {len(jitter_values)} elements"
|
|
585
|
+
)
|
|
586
|
+
|
|
392
587
|
if ax is None:
|
|
393
588
|
fig, ax = plt.subplots(figsize=figsize)
|
|
394
589
|
else:
|
|
@@ -443,76 +638,71 @@ def plot_ddj(
|
|
|
443
638
|
return fig
|
|
444
639
|
|
|
445
640
|
|
|
446
|
-
def
|
|
447
|
-
high_times: NDArray[np.floating[Any]],
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
ax: Axes | None = None,
|
|
451
|
-
figsize: tuple[float, float] = (10, 6),
|
|
452
|
-
title: str | None = None,
|
|
453
|
-
time_unit: str = "auto",
|
|
454
|
-
show: bool = True,
|
|
455
|
-
save_path: str | Path | None = None,
|
|
456
|
-
) -> Figure:
|
|
457
|
-
"""Plot Duty Cycle Distortion (DCD) analysis.
|
|
458
|
-
|
|
459
|
-
Creates overlaid histograms of high and low pulse times to visualize
|
|
460
|
-
duty cycle distortion.
|
|
641
|
+
def _determine_dcd_time_unit(
|
|
642
|
+
high_times: NDArray[np.floating[Any]], low_times: NDArray[np.floating[Any]], time_unit: str
|
|
643
|
+
) -> tuple[str, float]:
|
|
644
|
+
"""Determine time unit and scaling for DCD plot.
|
|
461
645
|
|
|
462
646
|
Args:
|
|
463
|
-
high_times:
|
|
464
|
-
low_times:
|
|
465
|
-
|
|
466
|
-
figsize: Figure size.
|
|
467
|
-
title: Plot title.
|
|
468
|
-
time_unit: Time unit.
|
|
469
|
-
show: Display plot.
|
|
470
|
-
save_path: Save path.
|
|
647
|
+
high_times: High-state durations.
|
|
648
|
+
low_times: Low-state durations.
|
|
649
|
+
time_unit: Requested time unit or "auto".
|
|
471
650
|
|
|
472
651
|
Returns:
|
|
473
|
-
|
|
652
|
+
Tuple of (time_unit, time_multiplier).
|
|
474
653
|
"""
|
|
475
|
-
if not HAS_MATPLOTLIB:
|
|
476
|
-
raise ImportError("matplotlib is required for visualization")
|
|
477
|
-
|
|
478
|
-
if ax is None:
|
|
479
|
-
fig, ax = plt.subplots(figsize=figsize)
|
|
480
|
-
else:
|
|
481
|
-
fig_temp = ax.get_figure()
|
|
482
|
-
if fig_temp is None:
|
|
483
|
-
raise ValueError("Axes must have an associated figure")
|
|
484
|
-
fig = cast("Figure", fig_temp)
|
|
485
|
-
|
|
486
|
-
# Select time unit
|
|
487
654
|
if time_unit == "auto":
|
|
488
655
|
max_time = max(np.max(high_times), np.max(low_times))
|
|
489
656
|
if max_time < 1e-9:
|
|
490
|
-
|
|
491
|
-
time_mult = 1e12
|
|
657
|
+
return "ps", 1e12
|
|
492
658
|
elif max_time < 1e-6:
|
|
493
|
-
|
|
494
|
-
time_mult = 1e9
|
|
659
|
+
return "ns", 1e9
|
|
495
660
|
else:
|
|
496
|
-
|
|
497
|
-
time_mult = 1e6
|
|
661
|
+
return "us", 1e6
|
|
498
662
|
else:
|
|
499
663
|
time_mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9, "ps": 1e12}.get(time_unit, 1e9)
|
|
664
|
+
return time_unit, time_mult
|
|
500
665
|
|
|
501
|
-
high_scaled = high_times * time_mult
|
|
502
|
-
low_scaled = low_times * time_mult
|
|
503
666
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
667
|
+
def _compute_dcd_statistics(
|
|
668
|
+
high_scaled: NDArray[np.floating[Any]], low_scaled: NDArray[np.floating[Any]]
|
|
669
|
+
) -> tuple[float, float, float, float]:
|
|
670
|
+
"""Compute DCD statistics.
|
|
671
|
+
|
|
672
|
+
Args:
|
|
673
|
+
high_scaled: Scaled high-state durations.
|
|
674
|
+
low_scaled: Scaled low-state durations.
|
|
675
|
+
|
|
676
|
+
Returns:
|
|
677
|
+
Tuple of (mean_high, mean_low, duty_cycle, dcd).
|
|
678
|
+
"""
|
|
679
|
+
mean_high = float(np.mean(high_scaled))
|
|
680
|
+
mean_low = float(np.mean(low_scaled))
|
|
507
681
|
period = mean_high + mean_low
|
|
508
682
|
duty_cycle = mean_high / period * 100
|
|
509
683
|
dcd = (mean_high - mean_low) / 2
|
|
684
|
+
return mean_high, mean_low, duty_cycle, dcd
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def _plot_dcd_histograms(
|
|
688
|
+
ax: Axes,
|
|
689
|
+
high_scaled: NDArray[np.floating[Any]],
|
|
690
|
+
low_scaled: NDArray[np.floating[Any]],
|
|
691
|
+
mean_high: float,
|
|
692
|
+
mean_low: float,
|
|
693
|
+
) -> None:
|
|
694
|
+
"""Plot DCD histograms with mean lines.
|
|
510
695
|
|
|
511
|
-
|
|
696
|
+
Args:
|
|
697
|
+
ax: Matplotlib axes.
|
|
698
|
+
high_scaled: Scaled high-state durations.
|
|
699
|
+
low_scaled: Scaled low-state durations.
|
|
700
|
+
mean_high: Mean high value.
|
|
701
|
+
mean_low: Mean low value.
|
|
702
|
+
"""
|
|
512
703
|
all_times = np.concatenate([high_scaled, low_scaled])
|
|
513
704
|
bins = np.linspace(np.min(all_times) * 0.95, np.max(all_times) * 1.05, 50)
|
|
514
705
|
|
|
515
|
-
# Plot histograms
|
|
516
706
|
ax.hist(
|
|
517
707
|
high_scaled,
|
|
518
708
|
bins=bins,
|
|
@@ -532,10 +722,55 @@ def plot_dcd(
|
|
|
532
722
|
linewidth=0.5,
|
|
533
723
|
)
|
|
534
724
|
|
|
535
|
-
# Mean lines
|
|
536
725
|
ax.axvline(mean_high, color="#E74C3C", linestyle="--", linewidth=2, alpha=0.8)
|
|
537
726
|
ax.axvline(mean_low, color="#3498DB", linestyle="--", linewidth=2, alpha=0.8)
|
|
538
727
|
|
|
728
|
+
|
|
729
|
+
def plot_dcd(
|
|
730
|
+
high_times: NDArray[np.floating[Any]],
|
|
731
|
+
low_times: NDArray[np.floating[Any]],
|
|
732
|
+
*,
|
|
733
|
+
ax: Axes | None = None,
|
|
734
|
+
figsize: tuple[float, float] = (10, 6),
|
|
735
|
+
title: str | None = None,
|
|
736
|
+
time_unit: str = "auto",
|
|
737
|
+
show: bool = True,
|
|
738
|
+
save_path: str | Path | None = None,
|
|
739
|
+
) -> Figure:
|
|
740
|
+
"""Plot Duty Cycle Distortion (DCD) analysis.
|
|
741
|
+
|
|
742
|
+
Creates overlaid histograms of high and low pulse times to visualize
|
|
743
|
+
duty cycle distortion.
|
|
744
|
+
|
|
745
|
+
Args:
|
|
746
|
+
high_times: Array of high-state durations.
|
|
747
|
+
low_times: Array of low-state durations.
|
|
748
|
+
ax: Matplotlib axes.
|
|
749
|
+
figsize: Figure size.
|
|
750
|
+
title: Plot title.
|
|
751
|
+
time_unit: Time unit.
|
|
752
|
+
show: Display plot.
|
|
753
|
+
save_path: Save path.
|
|
754
|
+
|
|
755
|
+
Returns:
|
|
756
|
+
Matplotlib Figure object.
|
|
757
|
+
"""
|
|
758
|
+
if not HAS_MATPLOTLIB:
|
|
759
|
+
raise ImportError("matplotlib is required for visualization")
|
|
760
|
+
|
|
761
|
+
fig, ax = _get_or_create_figure(ax, figsize)
|
|
762
|
+
|
|
763
|
+
# Scale times
|
|
764
|
+
time_unit, time_mult = _determine_dcd_time_unit(high_times, low_times, time_unit)
|
|
765
|
+
high_scaled = high_times * time_mult
|
|
766
|
+
low_scaled = low_times * time_mult
|
|
767
|
+
|
|
768
|
+
# Calculate statistics
|
|
769
|
+
mean_high, mean_low, duty_cycle, dcd = _compute_dcd_statistics(high_scaled, low_scaled)
|
|
770
|
+
|
|
771
|
+
# Plot histograms
|
|
772
|
+
_plot_dcd_histograms(ax, high_scaled, low_scaled, mean_high, mean_low)
|
|
773
|
+
|
|
539
774
|
# Statistics box
|
|
540
775
|
stats_text = (
|
|
541
776
|
f"Mean High: {mean_high:.2f} {time_unit}\n"
|
|
@@ -614,6 +849,36 @@ def plot_jitter_trend(
|
|
|
614
849
|
if not HAS_MATPLOTLIB:
|
|
615
850
|
raise ImportError("matplotlib is required for visualization")
|
|
616
851
|
|
|
852
|
+
fig, ax = _setup_jitter_trend_figure(ax, figsize)
|
|
853
|
+
jitter_unit, jitter_mult = _determine_jitter_unit(jitter_values, jitter_unit)
|
|
854
|
+
jitter_scaled = jitter_values * jitter_mult
|
|
855
|
+
|
|
856
|
+
mean_val, std_val = _plot_jitter_data(ax, time_axis, jitter_scaled, jitter_unit)
|
|
857
|
+
_add_jitter_bounds(ax, time_axis, mean_val, std_val, jitter_unit, show_bounds)
|
|
858
|
+
_add_jitter_trend(ax, time_axis, jitter_scaled, jitter_unit, show_trend)
|
|
859
|
+
_format_jitter_trend_plot(ax, time_unit, jitter_unit, title)
|
|
860
|
+
|
|
861
|
+
fig.tight_layout()
|
|
862
|
+
_save_and_show_jitter_trend(fig, save_path, show)
|
|
863
|
+
|
|
864
|
+
return fig
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def _setup_jitter_trend_figure(
|
|
868
|
+
ax: Axes | None, figsize: tuple[float, float]
|
|
869
|
+
) -> tuple[Figure, Axes]:
|
|
870
|
+
"""Setup figure and axes for jitter trend plot.
|
|
871
|
+
|
|
872
|
+
Args:
|
|
873
|
+
ax: Existing axes or None.
|
|
874
|
+
figsize: Figure size.
|
|
875
|
+
|
|
876
|
+
Returns:
|
|
877
|
+
Tuple of (figure, axes).
|
|
878
|
+
|
|
879
|
+
Raises:
|
|
880
|
+
ValueError: If axes has no figure.
|
|
881
|
+
"""
|
|
617
882
|
if ax is None:
|
|
618
883
|
fig, ax = plt.subplots(figsize=figsize)
|
|
619
884
|
else:
|
|
@@ -621,31 +886,56 @@ def plot_jitter_trend(
|
|
|
621
886
|
if fig_temp is None:
|
|
622
887
|
raise ValueError("Axes must have an associated figure")
|
|
623
888
|
fig = cast("Figure", fig_temp)
|
|
889
|
+
return fig, ax
|
|
890
|
+
|
|
624
891
|
|
|
625
|
-
|
|
892
|
+
def _determine_jitter_unit(
|
|
893
|
+
jitter_values: NDArray[np.floating[Any]], jitter_unit: str
|
|
894
|
+
) -> tuple[str, float]:
|
|
895
|
+
"""Determine jitter unit and multiplier.
|
|
896
|
+
|
|
897
|
+
Args:
|
|
898
|
+
jitter_values: Jitter value array.
|
|
899
|
+
jitter_unit: Requested unit or "auto".
|
|
900
|
+
|
|
901
|
+
Returns:
|
|
902
|
+
Tuple of (unit_str, multiplier).
|
|
903
|
+
"""
|
|
626
904
|
if jitter_unit == "auto":
|
|
627
905
|
max_jitter = np.max(np.abs(jitter_values))
|
|
628
906
|
if max_jitter < 1e-9:
|
|
629
|
-
|
|
630
|
-
jitter_mult = 1e12
|
|
907
|
+
return "ps", 1e12
|
|
631
908
|
elif max_jitter < 1e-6:
|
|
632
|
-
|
|
633
|
-
jitter_mult = 1e9
|
|
909
|
+
return "ns", 1e9
|
|
634
910
|
else:
|
|
635
|
-
|
|
636
|
-
jitter_mult = 1e6
|
|
911
|
+
return "us", 1e6
|
|
637
912
|
else:
|
|
638
913
|
jitter_mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9, "ps": 1e12}.get(jitter_unit, 1e12)
|
|
914
|
+
return jitter_unit, jitter_mult
|
|
639
915
|
|
|
640
|
-
jitter_scaled = jitter_values * jitter_mult
|
|
641
916
|
|
|
642
|
-
|
|
917
|
+
def _plot_jitter_data(
|
|
918
|
+
ax: Axes,
|
|
919
|
+
time_axis: NDArray[np.floating[Any]],
|
|
920
|
+
jitter_scaled: NDArray[np.floating[Any]],
|
|
921
|
+
jitter_unit: str,
|
|
922
|
+
) -> tuple[float, float]:
|
|
923
|
+
"""Plot jitter data and mean line.
|
|
924
|
+
|
|
925
|
+
Args:
|
|
926
|
+
ax: Matplotlib axes.
|
|
927
|
+
time_axis: Time array.
|
|
928
|
+
jitter_scaled: Scaled jitter values.
|
|
929
|
+
jitter_unit: Jitter unit string.
|
|
930
|
+
|
|
931
|
+
Returns:
|
|
932
|
+
Tuple of (mean_val, std_val).
|
|
933
|
+
"""
|
|
643
934
|
ax.plot(time_axis, jitter_scaled, "b-", linewidth=0.8, alpha=0.7, label="Jitter")
|
|
644
935
|
|
|
645
|
-
mean_val = np.mean(jitter_scaled)
|
|
646
|
-
std_val = np.std(jitter_scaled)
|
|
936
|
+
mean_val = float(np.mean(jitter_scaled))
|
|
937
|
+
std_val = float(np.std(jitter_scaled))
|
|
647
938
|
|
|
648
|
-
# Mean line
|
|
649
939
|
ax.axhline(
|
|
650
940
|
mean_val,
|
|
651
941
|
color="gray",
|
|
@@ -654,49 +944,99 @@ def plot_jitter_trend(
|
|
|
654
944
|
label=f"Mean: {mean_val:.2f} {jitter_unit}",
|
|
655
945
|
)
|
|
656
946
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
947
|
+
return mean_val, std_val
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def _add_jitter_bounds(
|
|
951
|
+
ax: Axes,
|
|
952
|
+
time_axis: NDArray[np.floating[Any]],
|
|
953
|
+
mean_val: float,
|
|
954
|
+
std_val: float,
|
|
955
|
+
jitter_unit: str,
|
|
956
|
+
show_bounds: bool,
|
|
957
|
+
) -> None:
|
|
958
|
+
"""Add statistical bounds to plot.
|
|
959
|
+
|
|
960
|
+
Args:
|
|
961
|
+
ax: Matplotlib axes.
|
|
962
|
+
time_axis: Time array.
|
|
963
|
+
mean_val: Mean value.
|
|
964
|
+
std_val: Standard deviation.
|
|
965
|
+
jitter_unit: Unit string.
|
|
966
|
+
show_bounds: Whether to show bounds.
|
|
967
|
+
"""
|
|
968
|
+
if not show_bounds:
|
|
969
|
+
return
|
|
970
|
+
|
|
971
|
+
ax.axhline(mean_val + 3 * std_val, color="#E74C3C", linestyle="--", linewidth=1, alpha=0.7)
|
|
972
|
+
ax.axhline(
|
|
973
|
+
mean_val - 3 * std_val,
|
|
974
|
+
color="#E74C3C",
|
|
975
|
+
linestyle="--",
|
|
976
|
+
linewidth=1,
|
|
977
|
+
alpha=0.7,
|
|
978
|
+
label=f"±3sigma: {3 * std_val:.2f} {jitter_unit}",
|
|
979
|
+
)
|
|
980
|
+
ax.fill_between(
|
|
981
|
+
time_axis, mean_val - 3 * std_val, mean_val + 3 * std_val, alpha=0.1, color="#E74C3C"
|
|
982
|
+
)
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
def _add_jitter_trend(
|
|
986
|
+
ax: Axes,
|
|
987
|
+
time_axis: NDArray[np.floating[Any]],
|
|
988
|
+
jitter_scaled: NDArray[np.floating[Any]],
|
|
989
|
+
jitter_unit: str,
|
|
990
|
+
show_trend: bool,
|
|
991
|
+
) -> None:
|
|
992
|
+
"""Add trend line to plot.
|
|
993
|
+
|
|
994
|
+
Args:
|
|
995
|
+
ax: Matplotlib axes.
|
|
996
|
+
time_axis: Time array.
|
|
997
|
+
jitter_scaled: Scaled jitter values.
|
|
998
|
+
jitter_unit: Unit string.
|
|
999
|
+
show_trend: Whether to show trend.
|
|
1000
|
+
"""
|
|
1001
|
+
if not show_trend:
|
|
1002
|
+
return
|
|
1003
|
+
|
|
1004
|
+
z = np.polyfit(time_axis, jitter_scaled, 1)
|
|
1005
|
+
p = np.poly1d(z)
|
|
1006
|
+
ax.plot(
|
|
1007
|
+
time_axis, p(time_axis), "g-", linewidth=2, label=f"Trend: {z[0]:.2e} {jitter_unit}/unit"
|
|
1008
|
+
)
|
|
671
1009
|
|
|
672
|
-
# Trend line
|
|
673
|
-
if show_trend:
|
|
674
|
-
z = np.polyfit(time_axis, jitter_scaled, 1)
|
|
675
|
-
p = np.poly1d(z)
|
|
676
|
-
ax.plot(
|
|
677
|
-
time_axis,
|
|
678
|
-
p(time_axis),
|
|
679
|
-
"g-",
|
|
680
|
-
linewidth=2,
|
|
681
|
-
label=f"Trend: {z[0]:.2e} {jitter_unit}/unit",
|
|
682
|
-
)
|
|
683
1010
|
|
|
1011
|
+
def _format_jitter_trend_plot(
|
|
1012
|
+
ax: Axes, time_unit: str, jitter_unit: str, title: str | None
|
|
1013
|
+
) -> None:
|
|
1014
|
+
"""Format jitter trend plot axes and labels.
|
|
1015
|
+
|
|
1016
|
+
Args:
|
|
1017
|
+
ax: Matplotlib axes.
|
|
1018
|
+
time_unit: Time unit string.
|
|
1019
|
+
jitter_unit: Jitter unit string.
|
|
1020
|
+
title: Plot title.
|
|
1021
|
+
"""
|
|
684
1022
|
ax.set_xlabel(f"Time ({time_unit})" if time_unit != "auto" else "Sample Index", fontsize=11)
|
|
685
1023
|
ax.set_ylabel(f"Jitter ({jitter_unit})", fontsize=11)
|
|
686
1024
|
ax.grid(True, alpha=0.3)
|
|
687
1025
|
ax.legend(loc="upper right")
|
|
688
1026
|
|
|
689
|
-
if title
|
|
690
|
-
|
|
691
|
-
else:
|
|
692
|
-
ax.set_title("Jitter Trend Analysis", fontsize=12, fontweight="bold")
|
|
1027
|
+
final_title = title if title else "Jitter Trend Analysis"
|
|
1028
|
+
ax.set_title(final_title, fontsize=12, fontweight="bold")
|
|
693
1029
|
|
|
694
|
-
fig.tight_layout()
|
|
695
1030
|
|
|
1031
|
+
def _save_and_show_jitter_trend(fig: Figure, save_path: str | Path | None, show: bool) -> None:
|
|
1032
|
+
"""Save and/or show jitter trend plot.
|
|
1033
|
+
|
|
1034
|
+
Args:
|
|
1035
|
+
fig: Matplotlib figure.
|
|
1036
|
+
save_path: Path to save file.
|
|
1037
|
+
show: Whether to display interactively.
|
|
1038
|
+
"""
|
|
696
1039
|
if save_path is not None:
|
|
697
1040
|
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
698
|
-
|
|
699
1041
|
if show:
|
|
700
1042
|
plt.show()
|
|
701
|
-
|
|
702
|
-
return fig
|