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
|
@@ -90,6 +90,31 @@ def plot_tdr(
|
|
|
90
90
|
>>> fig = plot_tdr(z_profile, dist, z0=50, show=False)
|
|
91
91
|
>>> fig.savefig("tdr_impedance.png")
|
|
92
92
|
"""
|
|
93
|
+
_validate_tdr_inputs(impedance, distance)
|
|
94
|
+
fig, ax = _setup_tdr_figure(ax, figsize)
|
|
95
|
+
|
|
96
|
+
distance_unit_final, dist_scaled = _scale_tdr_distance(distance, distance_unit)
|
|
97
|
+
impedance_display = np.clip(impedance, 0, 500)
|
|
98
|
+
|
|
99
|
+
_plot_tdr_impedance_profile(ax, dist_scaled, impedance_display)
|
|
100
|
+
_fill_tdr_impedance_regions(ax, dist_scaled, impedance_display, z0, discontinuity_threshold)
|
|
101
|
+
|
|
102
|
+
if show_reference:
|
|
103
|
+
_add_tdr_reference_line(ax, z0)
|
|
104
|
+
|
|
105
|
+
if show_discontinuities:
|
|
106
|
+
_annotate_tdr_discontinuities(ax, dist_scaled, impedance_display, discontinuity_threshold)
|
|
107
|
+
|
|
108
|
+
_format_tdr_axes(ax, dist_scaled, impedance_display, distance_unit_final, title)
|
|
109
|
+
_finalize_tdr_plot(fig, save_path, show)
|
|
110
|
+
|
|
111
|
+
return fig
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _validate_tdr_inputs(
|
|
115
|
+
impedance: NDArray[np.floating[Any]], distance: NDArray[np.floating[Any]]
|
|
116
|
+
) -> None:
|
|
117
|
+
"""Validate TDR input arrays."""
|
|
93
118
|
if not HAS_MATPLOTLIB:
|
|
94
119
|
raise ImportError("matplotlib is required for visualization")
|
|
95
120
|
|
|
@@ -99,16 +124,23 @@ def plot_tdr(
|
|
|
99
124
|
f"(got {len(impedance)} and {len(distance)})"
|
|
100
125
|
)
|
|
101
126
|
|
|
102
|
-
|
|
127
|
+
|
|
128
|
+
def _setup_tdr_figure(ax: Axes | None, figsize: tuple[float, float]) -> tuple[Figure, Axes]:
|
|
129
|
+
"""Setup TDR figure and axes."""
|
|
103
130
|
if ax is None:
|
|
104
|
-
fig,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
131
|
+
fig, ax_new = plt.subplots(figsize=figsize)
|
|
132
|
+
return fig, ax_new
|
|
133
|
+
|
|
134
|
+
fig_temp = ax.get_figure()
|
|
135
|
+
if fig_temp is None:
|
|
136
|
+
raise ValueError("Axes must have an associated figure")
|
|
137
|
+
return cast("Figure", fig_temp), ax
|
|
110
138
|
|
|
111
|
-
|
|
139
|
+
|
|
140
|
+
def _scale_tdr_distance(
|
|
141
|
+
distance: NDArray[np.floating[Any]], distance_unit: str
|
|
142
|
+
) -> tuple[str, NDArray[np.floating[Any]]]:
|
|
143
|
+
"""Scale distance to appropriate unit."""
|
|
112
144
|
if distance_unit == "auto":
|
|
113
145
|
max_dist = np.max(distance)
|
|
114
146
|
if max_dist < 0.01:
|
|
@@ -124,25 +156,29 @@ def plot_tdr(
|
|
|
124
156
|
distance_mult = {"m": 1.0, "cm": 100.0, "mm": 1000.0}.get(distance_unit, 1.0)
|
|
125
157
|
|
|
126
158
|
dist_scaled = distance * distance_mult
|
|
159
|
+
return distance_unit, dist_scaled
|
|
127
160
|
|
|
128
|
-
# Clip impedance for display (handle inf values)
|
|
129
|
-
impedance_display = np.clip(impedance, 0, 500)
|
|
130
161
|
|
|
131
|
-
|
|
162
|
+
def _plot_tdr_impedance_profile(
|
|
163
|
+
ax: Axes,
|
|
164
|
+
dist_scaled: NDArray[np.floating[Any]],
|
|
165
|
+
impedance_display: NDArray[np.floating[Any]],
|
|
166
|
+
) -> None:
|
|
167
|
+
"""Plot main impedance profile line."""
|
|
132
168
|
ax.plot(dist_scaled, impedance_display, "b-", linewidth=2, label="Impedance")
|
|
133
169
|
|
|
134
|
-
|
|
170
|
+
|
|
171
|
+
def _fill_tdr_impedance_regions(
|
|
172
|
+
ax: Axes,
|
|
173
|
+
dist_scaled: NDArray[np.floating[Any]],
|
|
174
|
+
impedance_display: NDArray[np.floating[Any]],
|
|
175
|
+
z0: float,
|
|
176
|
+
discontinuity_threshold: float,
|
|
177
|
+
) -> None:
|
|
178
|
+
"""Fill colored regions based on impedance deviation."""
|
|
135
179
|
for i in range(len(dist_scaled) - 1):
|
|
136
180
|
z = impedance_display[i]
|
|
137
|
-
|
|
138
|
-
color = "#FFA500" # Orange for high-Z
|
|
139
|
-
alpha = 0.3
|
|
140
|
-
elif z < z0 - discontinuity_threshold:
|
|
141
|
-
color = "#1E90FF" # Blue for low-Z
|
|
142
|
-
alpha = 0.3
|
|
143
|
-
else:
|
|
144
|
-
color = "#90EE90" # Light green for matched
|
|
145
|
-
alpha = 0.2
|
|
181
|
+
color, alpha = _get_tdr_region_color(z, z0, discontinuity_threshold)
|
|
146
182
|
|
|
147
183
|
ax.fill_between(
|
|
148
184
|
[dist_scaled[i], dist_scaled[i + 1]],
|
|
@@ -152,52 +188,85 @@ def plot_tdr(
|
|
|
152
188
|
alpha=alpha,
|
|
153
189
|
)
|
|
154
190
|
|
|
155
|
-
# Reference line
|
|
156
|
-
if show_reference:
|
|
157
|
-
ax.axhline(z0, color="gray", linestyle="--", linewidth=1.5, label=f"Z0 = {z0} Ω")
|
|
158
191
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
192
|
+
def _get_tdr_region_color(z: float, z0: float, threshold: float) -> tuple[str, float]:
|
|
193
|
+
"""Get color and alpha for impedance region."""
|
|
194
|
+
if z > z0 + threshold:
|
|
195
|
+
return "#FFA500", 0.3 # Orange for high-Z
|
|
196
|
+
elif z < z0 - threshold:
|
|
197
|
+
return "#1E90FF", 0.3 # Blue for low-Z
|
|
198
|
+
else:
|
|
199
|
+
return "#90EE90", 0.2 # Light green for matched
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _add_tdr_reference_line(ax: Axes, z0: float) -> None:
|
|
203
|
+
"""Add reference impedance line."""
|
|
204
|
+
ax.axhline(z0, color="gray", linestyle="--", linewidth=1.5, label=f"Z0 = {z0} Ω")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _annotate_tdr_discontinuities(
|
|
208
|
+
ax: Axes,
|
|
209
|
+
dist_scaled: NDArray[np.floating[Any]],
|
|
210
|
+
impedance_display: NDArray[np.floating[Any]],
|
|
211
|
+
discontinuity_threshold: float,
|
|
212
|
+
) -> None:
|
|
213
|
+
"""Find and annotate impedance discontinuities."""
|
|
214
|
+
z_diff = np.abs(np.diff(impedance_display))
|
|
215
|
+
discontinuities = np.where(z_diff > discontinuity_threshold)[0]
|
|
216
|
+
|
|
217
|
+
for idx in discontinuities:
|
|
218
|
+
z_before = impedance_display[idx]
|
|
219
|
+
z_after = impedance_display[idx + 1]
|
|
220
|
+
d = dist_scaled[idx]
|
|
221
|
+
|
|
222
|
+
disc_type, color = _classify_tdr_discontinuity(z_before, z_after, discontinuity_threshold)
|
|
223
|
+
if disc_type is None:
|
|
224
|
+
continue
|
|
225
|
+
|
|
226
|
+
_add_tdr_discontinuity_marker(ax, d, z_after, disc_type, color)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _classify_tdr_discontinuity(
|
|
230
|
+
z_before: float, z_after: float, threshold: float
|
|
231
|
+
) -> tuple[str | None, str]:
|
|
232
|
+
"""Classify discontinuity type."""
|
|
233
|
+
if z_after > z_before + threshold:
|
|
234
|
+
return "High-Z", "orange"
|
|
235
|
+
elif z_after < z_before - threshold:
|
|
236
|
+
return "Low-Z", "blue"
|
|
237
|
+
return None, ""
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _add_tdr_discontinuity_marker(
|
|
241
|
+
ax: Axes, d: float, z_after: float, disc_type: str, color: str
|
|
242
|
+
) -> None:
|
|
243
|
+
"""Add marker and annotation for discontinuity."""
|
|
244
|
+
ax.plot(d, z_after, "o", color=color, markersize=8)
|
|
245
|
+
|
|
246
|
+
z_str = f"{z_after:.0f}" if z_after < 500 else "Open"
|
|
247
|
+
ax.annotate(
|
|
248
|
+
f"{disc_type}\n{z_str} Ω",
|
|
249
|
+
xy=(d, z_after),
|
|
250
|
+
xytext=(10, 10),
|
|
251
|
+
textcoords="offset points",
|
|
252
|
+
fontsize=8,
|
|
253
|
+
ha="left",
|
|
254
|
+
bbox={"boxstyle": "round,pad=0.3", "facecolor": "white", "alpha": 0.8},
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _format_tdr_axes(
|
|
259
|
+
ax: Axes,
|
|
260
|
+
dist_scaled: NDArray[np.floating[Any]],
|
|
261
|
+
impedance_display: NDArray[np.floating[Any]],
|
|
262
|
+
distance_unit: str,
|
|
263
|
+
title: str | None,
|
|
264
|
+
) -> None:
|
|
265
|
+
"""Format axes labels, limits, and title."""
|
|
196
266
|
ax.set_xlabel(f"Distance ({distance_unit})", fontsize=11)
|
|
197
267
|
ax.set_ylabel("Impedance (Ω)", fontsize=11)
|
|
198
268
|
ax.set_xlim(0, dist_scaled[-1])
|
|
199
269
|
|
|
200
|
-
# Set y-axis limits with padding
|
|
201
270
|
y_min = max(0, np.min(impedance_display) - 10)
|
|
202
271
|
y_max = min(200, np.max(impedance_display) + 10)
|
|
203
272
|
ax.set_ylim(y_min, y_max)
|
|
@@ -205,22 +274,73 @@ def plot_tdr(
|
|
|
205
274
|
ax.grid(True, alpha=0.3)
|
|
206
275
|
ax.legend(loc="upper right")
|
|
207
276
|
|
|
208
|
-
if title
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
ax.set_title("TDR Impedance Profile", fontsize=12, fontweight="bold")
|
|
277
|
+
title_text = title if title else "TDR Impedance Profile"
|
|
278
|
+
ax.set_title(title_text, fontsize=12, fontweight="bold")
|
|
279
|
+
|
|
212
280
|
|
|
281
|
+
def _finalize_tdr_plot(fig: Figure, save_path: str | Path | None, show: bool) -> None:
|
|
282
|
+
"""Finalize plot layout, save, and show."""
|
|
213
283
|
fig.tight_layout()
|
|
214
284
|
|
|
215
|
-
# Save if requested
|
|
216
285
|
if save_path is not None:
|
|
217
286
|
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
218
287
|
|
|
219
|
-
# Show if requested
|
|
220
288
|
if show:
|
|
221
289
|
plt.show()
|
|
222
290
|
|
|
223
|
-
|
|
291
|
+
|
|
292
|
+
def _select_sparams_freq_unit(
|
|
293
|
+
frequencies: NDArray[np.floating[Any]], freq_unit: str
|
|
294
|
+
) -> tuple[str, float]:
|
|
295
|
+
"""Select frequency unit and divisor for S-parameter plots."""
|
|
296
|
+
if freq_unit == "auto":
|
|
297
|
+
max_freq = np.max(frequencies)
|
|
298
|
+
if max_freq >= 1e9:
|
|
299
|
+
return "GHz", 1e9
|
|
300
|
+
elif max_freq >= 1e6:
|
|
301
|
+
return "MHz", 1e6
|
|
302
|
+
elif max_freq >= 1e3:
|
|
303
|
+
return "kHz", 1e3
|
|
304
|
+
else:
|
|
305
|
+
return "Hz", 1.0
|
|
306
|
+
else:
|
|
307
|
+
freq_div = {"Hz": 1.0, "kHz": 1e3, "MHz": 1e6, "GHz": 1e9}.get(freq_unit, 1.0)
|
|
308
|
+
return freq_unit, freq_div
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _convert_sparam_to_db(s: NDArray[Any]) -> NDArray[np.floating[Any]]:
|
|
312
|
+
"""Convert S-parameter to dB."""
|
|
313
|
+
if np.iscomplexobj(s):
|
|
314
|
+
result: NDArray[np.floating[Any]] = 20 * np.log10(np.abs(s) + 1e-12)
|
|
315
|
+
return result
|
|
316
|
+
return np.asarray(s, dtype=np.float64)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _add_3db_marker(
|
|
320
|
+
ax: Axes,
|
|
321
|
+
freq_scaled: NDArray[np.floating[Any]],
|
|
322
|
+
s_db: NDArray[np.floating[Any]],
|
|
323
|
+
freq_unit: str,
|
|
324
|
+
) -> None:
|
|
325
|
+
"""Add -3dB bandwidth marker to S21 plot."""
|
|
326
|
+
max_db = np.max(s_db)
|
|
327
|
+
db_3_level = max_db - 3
|
|
328
|
+
|
|
329
|
+
crossings = np.where(np.diff(np.sign(s_db - db_3_level)))[0]
|
|
330
|
+
if len(crossings) > 0:
|
|
331
|
+
f_3db = float(freq_scaled[crossings[0]])
|
|
332
|
+
db_3_level_float = float(db_3_level)
|
|
333
|
+
ax.axhline(db_3_level_float, color="gray", linestyle=":", alpha=0.7, linewidth=1)
|
|
334
|
+
ax.axvline(f_3db, color="gray", linestyle=":", alpha=0.7, linewidth=1)
|
|
335
|
+
ax.plot(f_3db, db_3_level_float, "ko", markersize=6)
|
|
336
|
+
ax.annotate(
|
|
337
|
+
f"-3dB: {f_3db:.2f} {freq_unit}",
|
|
338
|
+
xy=(f_3db, db_3_level_float),
|
|
339
|
+
xytext=(10, -15),
|
|
340
|
+
textcoords="offset points",
|
|
341
|
+
fontsize=9,
|
|
342
|
+
ha="left",
|
|
343
|
+
)
|
|
224
344
|
|
|
225
345
|
|
|
226
346
|
def plot_sparams_magnitude(
|
|
@@ -270,51 +390,17 @@ def plot_sparams_magnitude(
|
|
|
270
390
|
if not HAS_MATPLOTLIB:
|
|
271
391
|
raise ImportError("matplotlib is required for visualization")
|
|
272
392
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
fig, ax = plt.subplots(figsize=figsize)
|
|
276
|
-
else:
|
|
277
|
-
fig_temp = ax.get_figure()
|
|
278
|
-
if fig_temp is None:
|
|
279
|
-
raise ValueError("Axes must have an associated figure")
|
|
280
|
-
fig = cast("Figure", fig_temp)
|
|
281
|
-
|
|
282
|
-
# Select frequency unit
|
|
283
|
-
if freq_unit == "auto":
|
|
284
|
-
max_freq = np.max(frequencies)
|
|
285
|
-
if max_freq >= 1e9:
|
|
286
|
-
freq_unit = "GHz"
|
|
287
|
-
freq_div = 1e9
|
|
288
|
-
elif max_freq >= 1e6:
|
|
289
|
-
freq_unit = "MHz"
|
|
290
|
-
freq_div = 1e6
|
|
291
|
-
elif max_freq >= 1e3:
|
|
292
|
-
freq_unit = "kHz"
|
|
293
|
-
freq_div = 1e3
|
|
294
|
-
else:
|
|
295
|
-
freq_unit = "Hz"
|
|
296
|
-
freq_div = 1.0
|
|
297
|
-
else:
|
|
298
|
-
freq_div = {"Hz": 1.0, "kHz": 1e3, "MHz": 1e6, "GHz": 1e9}.get(freq_unit, 1.0)
|
|
299
|
-
|
|
393
|
+
fig, ax = _setup_tdr_figure(ax, figsize)
|
|
394
|
+
freq_unit, freq_div = _select_sparams_freq_unit(frequencies, freq_unit)
|
|
300
395
|
freq_scaled = frequencies / freq_div
|
|
301
396
|
|
|
302
|
-
def to_db(s: NDArray[Any]) -> NDArray[np.floating[Any]]:
|
|
303
|
-
"""Convert S-parameter to dB."""
|
|
304
|
-
if np.iscomplexobj(s):
|
|
305
|
-
result: NDArray[np.floating[Any]] = 20 * np.log10(np.abs(s) + 1e-12)
|
|
306
|
-
return result
|
|
307
|
-
return np.asarray(s, dtype=np.float64)
|
|
308
|
-
|
|
309
|
-
# Color scheme
|
|
310
397
|
colors = {"S11": "#E74C3C", "S21": "#3498DB", "S12": "#2ECC71", "S22": "#9B59B6"}
|
|
311
398
|
linestyles = {"S11": "-", "S21": "-", "S12": "--", "S22": "--"}
|
|
312
|
-
|
|
313
399
|
params = [("S11", s11), ("S21", s21), ("S12", s12), ("S22", s22)]
|
|
314
400
|
|
|
315
401
|
for name, s_param in params:
|
|
316
402
|
if s_param is not None:
|
|
317
|
-
s_db =
|
|
403
|
+
s_db = _convert_sparam_to_db(s_param)
|
|
318
404
|
ax.semilogx(
|
|
319
405
|
freq_scaled,
|
|
320
406
|
s_db,
|
|
@@ -324,46 +410,21 @@ def plot_sparams_magnitude(
|
|
|
324
410
|
label=name,
|
|
325
411
|
)
|
|
326
412
|
|
|
327
|
-
# -3dB marker for S21
|
|
328
413
|
if name == "S21" and db_3_marker:
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
# Find -3dB crossover
|
|
333
|
-
crossings = np.where(np.diff(np.sign(s_db - db_3_level)))[0]
|
|
334
|
-
if len(crossings) > 0:
|
|
335
|
-
f_3db = float(freq_scaled[crossings[0]])
|
|
336
|
-
db_3_level_float = float(db_3_level)
|
|
337
|
-
ax.axhline(
|
|
338
|
-
db_3_level_float, color="gray", linestyle=":", alpha=0.7, linewidth=1
|
|
339
|
-
)
|
|
340
|
-
ax.axvline(f_3db, color="gray", linestyle=":", alpha=0.7, linewidth=1)
|
|
341
|
-
ax.plot(f_3db, db_3_level_float, "ko", markersize=6)
|
|
342
|
-
ax.annotate(
|
|
343
|
-
f"-3dB: {f_3db:.2f} {freq_unit}",
|
|
344
|
-
xy=(f_3db, db_3_level_float),
|
|
345
|
-
xytext=(10, -15),
|
|
346
|
-
textcoords="offset points",
|
|
347
|
-
fontsize=9,
|
|
348
|
-
ha="left",
|
|
349
|
-
)
|
|
350
|
-
|
|
351
|
-
# Labels and formatting
|
|
414
|
+
_add_3db_marker(ax, freq_scaled, s_db, freq_unit)
|
|
415
|
+
|
|
352
416
|
ax.set_xlabel(f"Frequency ({freq_unit})", fontsize=11)
|
|
353
417
|
ax.set_ylabel("Magnitude (dB)", fontsize=11)
|
|
354
418
|
ax.grid(True, which="both", alpha=0.3)
|
|
355
419
|
ax.legend(loc="best")
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
else:
|
|
360
|
-
ax.set_title("S-Parameter Magnitude Response", fontsize=12, fontweight="bold")
|
|
420
|
+
ax.set_title(
|
|
421
|
+
title if title else "S-Parameter Magnitude Response", fontsize=12, fontweight="bold"
|
|
422
|
+
)
|
|
361
423
|
|
|
362
424
|
fig.tight_layout()
|
|
363
425
|
|
|
364
426
|
if save_path is not None:
|
|
365
427
|
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
366
|
-
|
|
367
428
|
if show:
|
|
368
429
|
plt.show()
|
|
369
430
|
|
|
@@ -517,44 +578,127 @@ def plot_setup_hold_timing(
|
|
|
517
578
|
if not HAS_MATPLOTLIB:
|
|
518
579
|
raise ImportError("matplotlib is required for visualization")
|
|
519
580
|
|
|
520
|
-
# Create figure
|
|
581
|
+
# Create figure and axes
|
|
582
|
+
fig, axes = _create_timing_figure(ax, clock_data, figsize)
|
|
583
|
+
|
|
584
|
+
# Determine time scaling
|
|
585
|
+
time_unit_final, time_mult = _select_time_unit(time_unit, clock_edges, data_edges)
|
|
586
|
+
setup_scaled = setup_time * time_mult
|
|
587
|
+
hold_scaled = hold_time * time_mult
|
|
588
|
+
|
|
589
|
+
# Plot waveforms if provided
|
|
590
|
+
ax_timing = _plot_timing_waveforms(axes, clock_data, data_data, time_axis, time_mult)
|
|
591
|
+
|
|
592
|
+
# Setup timing annotation panel
|
|
593
|
+
_setup_timing_panel(ax_timing, clock_edges, data_edges, time_mult)
|
|
594
|
+
|
|
595
|
+
# Draw timing arrows
|
|
596
|
+
if show_margins:
|
|
597
|
+
_draw_timing_arrows(
|
|
598
|
+
ax_timing,
|
|
599
|
+
clock_edges,
|
|
600
|
+
data_edges,
|
|
601
|
+
setup_scaled,
|
|
602
|
+
hold_scaled,
|
|
603
|
+
time_mult,
|
|
604
|
+
time_unit_final,
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
# Add pass/fail status
|
|
608
|
+
_add_passfail_status(
|
|
609
|
+
ax_timing, setup_time, hold_time, setup_spec, hold_spec, time_mult, time_unit_final
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
# Finalize plot
|
|
613
|
+
axes[-1].set_xlabel(f"Time ({time_unit_final})", fontsize=11)
|
|
614
|
+
fig.suptitle(title if title else "Setup/Hold Timing Analysis", fontsize=14, fontweight="bold")
|
|
615
|
+
fig.tight_layout()
|
|
616
|
+
|
|
617
|
+
if save_path is not None:
|
|
618
|
+
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
619
|
+
if show:
|
|
620
|
+
plt.show()
|
|
621
|
+
|
|
622
|
+
return fig
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def _create_timing_figure(
|
|
626
|
+
ax: Axes | None,
|
|
627
|
+
clock_data: NDArray[np.floating[Any]] | None,
|
|
628
|
+
figsize: tuple[float, float],
|
|
629
|
+
) -> tuple[Figure, list[Any]]:
|
|
630
|
+
"""Create figure and axes for timing diagram.
|
|
631
|
+
|
|
632
|
+
Args:
|
|
633
|
+
ax: Existing axes or None.
|
|
634
|
+
clock_data: Clock waveform data (determines row count).
|
|
635
|
+
figsize: Figure size.
|
|
636
|
+
|
|
637
|
+
Returns:
|
|
638
|
+
Tuple of (figure, axes_list).
|
|
639
|
+
"""
|
|
521
640
|
if ax is not None:
|
|
522
641
|
fig_temp = ax.get_figure()
|
|
523
642
|
if fig_temp is None:
|
|
524
643
|
raise ValueError("Axes must have an associated figure")
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
n_rows =
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
644
|
+
return cast("Figure", fig_temp), [ax]
|
|
645
|
+
|
|
646
|
+
n_rows = 3 if clock_data is not None else 1
|
|
647
|
+
fig, axes = plt.subplots(
|
|
648
|
+
n_rows, 1, figsize=figsize, sharex=True, gridspec_kw={"height_ratios": [1] * n_rows}
|
|
649
|
+
)
|
|
650
|
+
return fig, [axes] if n_rows == 1 else axes
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def _select_time_unit(
|
|
654
|
+
time_unit: str,
|
|
655
|
+
clock_edges: NDArray[np.floating[Any]],
|
|
656
|
+
data_edges: NDArray[np.floating[Any]],
|
|
657
|
+
) -> tuple[str, float]:
|
|
658
|
+
"""Select appropriate time unit and multiplier.
|
|
535
659
|
|
|
536
|
-
|
|
660
|
+
Args:
|
|
661
|
+
time_unit: Requested unit or "auto".
|
|
662
|
+
clock_edges: Clock edge times.
|
|
663
|
+
data_edges: Data edge times.
|
|
664
|
+
|
|
665
|
+
Returns:
|
|
666
|
+
Tuple of (unit_string, multiplier).
|
|
667
|
+
"""
|
|
537
668
|
if time_unit == "auto":
|
|
538
669
|
max_time = max(np.max(clock_edges), np.max(data_edges))
|
|
539
670
|
if max_time < 1e-9:
|
|
540
|
-
|
|
541
|
-
time_mult = 1e12
|
|
671
|
+
return "ps", 1e12
|
|
542
672
|
elif max_time < 1e-6:
|
|
543
|
-
|
|
544
|
-
time_mult = 1e9
|
|
673
|
+
return "ns", 1e9
|
|
545
674
|
elif max_time < 1e-3:
|
|
546
|
-
|
|
547
|
-
time_mult = 1e6
|
|
675
|
+
return "us", 1e6
|
|
548
676
|
else:
|
|
549
|
-
|
|
550
|
-
time_mult = 1e3
|
|
677
|
+
return "ms", 1e3
|
|
551
678
|
else:
|
|
552
|
-
|
|
679
|
+
mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9, "ps": 1e12}.get(time_unit, 1e9)
|
|
680
|
+
return time_unit, mult
|
|
553
681
|
|
|
554
|
-
setup_scaled = setup_time * time_mult
|
|
555
|
-
hold_scaled = hold_time * time_mult
|
|
556
682
|
|
|
557
|
-
|
|
683
|
+
def _plot_timing_waveforms(
|
|
684
|
+
axes: list[Any],
|
|
685
|
+
clock_data: NDArray[np.floating[Any]] | None,
|
|
686
|
+
data_data: NDArray[np.floating[Any]] | None,
|
|
687
|
+
time_axis: NDArray[np.floating[Any]] | None,
|
|
688
|
+
time_mult: float,
|
|
689
|
+
) -> Any:
|
|
690
|
+
"""Plot clock and data waveforms.
|
|
691
|
+
|
|
692
|
+
Args:
|
|
693
|
+
axes: List of axes.
|
|
694
|
+
clock_data: Clock waveform.
|
|
695
|
+
data_data: Data waveform.
|
|
696
|
+
time_axis: Time axis.
|
|
697
|
+
time_mult: Time multiplier.
|
|
698
|
+
|
|
699
|
+
Returns:
|
|
700
|
+
Axes for timing annotations.
|
|
701
|
+
"""
|
|
558
702
|
if clock_data is not None and data_data is not None and time_axis is not None:
|
|
559
703
|
time_scaled = time_axis * time_mult
|
|
560
704
|
|
|
@@ -578,88 +722,131 @@ def plot_setup_hold_timing(
|
|
|
578
722
|
ax_data.set_yticks([0, 1])
|
|
579
723
|
ax_data.grid(True, axis="x", alpha=0.3)
|
|
580
724
|
|
|
581
|
-
|
|
725
|
+
return axes[2] if len(axes) > 2 else axes[-1]
|
|
582
726
|
else:
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
# Timing annotation panel
|
|
586
|
-
ax_timing.set_ylim(0, 1)
|
|
587
|
-
ax_timing.set_xlim(0, max(clock_edges[-1], data_edges[-1]) * time_mult * 1.1)
|
|
588
|
-
ax_timing.axis("off")
|
|
589
|
-
|
|
590
|
-
# Draw timing arrows for first clock edge
|
|
591
|
-
if len(clock_edges) > 0 and len(data_edges) > 0:
|
|
592
|
-
clk_edge = clock_edges[0] * time_mult
|
|
593
|
-
|
|
594
|
-
# Find nearest data edge before clock
|
|
595
|
-
data_before = data_edges[data_edges < clock_edges[0]]
|
|
596
|
-
if len(data_before) > 0:
|
|
597
|
-
data_edge = data_before[-1] * time_mult
|
|
598
|
-
|
|
599
|
-
# Setup time arrow (data_edge to clk_edge)
|
|
600
|
-
if show_margins:
|
|
601
|
-
y_setup = 0.7
|
|
602
|
-
ax_timing.annotate(
|
|
603
|
-
"",
|
|
604
|
-
xy=(clk_edge, y_setup),
|
|
605
|
-
xytext=(data_edge, y_setup),
|
|
606
|
-
arrowprops={
|
|
607
|
-
"arrowstyle": "<->",
|
|
608
|
-
"color": "#27AE60",
|
|
609
|
-
"lw": 2,
|
|
610
|
-
},
|
|
611
|
-
)
|
|
612
|
-
ax_timing.text(
|
|
613
|
-
(data_edge + clk_edge) / 2,
|
|
614
|
-
y_setup + 0.1,
|
|
615
|
-
f"Setup: {setup_scaled:.2f} {time_unit}",
|
|
616
|
-
ha="center",
|
|
617
|
-
va="bottom",
|
|
618
|
-
fontsize=10,
|
|
619
|
-
fontweight="bold",
|
|
620
|
-
color="#27AE60",
|
|
621
|
-
)
|
|
622
|
-
|
|
623
|
-
# Find nearest data edge after clock
|
|
624
|
-
data_after = data_edges[data_edges > clock_edges[0]]
|
|
625
|
-
if len(data_after) > 0:
|
|
626
|
-
data_edge_after = data_after[0] * time_mult
|
|
627
|
-
|
|
628
|
-
# Hold time arrow (clk_edge to data_edge_after)
|
|
629
|
-
if show_margins:
|
|
630
|
-
y_hold = 0.3
|
|
631
|
-
ax_timing.annotate(
|
|
632
|
-
"",
|
|
633
|
-
xy=(data_edge_after, y_hold),
|
|
634
|
-
xytext=(clk_edge, y_hold),
|
|
635
|
-
arrowprops={
|
|
636
|
-
"arrowstyle": "<->",
|
|
637
|
-
"color": "#E67E22",
|
|
638
|
-
"lw": 2,
|
|
639
|
-
},
|
|
640
|
-
)
|
|
641
|
-
ax_timing.text(
|
|
642
|
-
(clk_edge + data_edge_after) / 2,
|
|
643
|
-
y_hold + 0.1,
|
|
644
|
-
f"Hold: {hold_scaled:.2f} {time_unit}",
|
|
645
|
-
ha="center",
|
|
646
|
-
va="bottom",
|
|
647
|
-
fontsize=10,
|
|
648
|
-
fontweight="bold",
|
|
649
|
-
color="#E67E22",
|
|
650
|
-
)
|
|
727
|
+
return axes[0]
|
|
651
728
|
|
|
652
|
-
|
|
729
|
+
|
|
730
|
+
def _setup_timing_panel(
|
|
731
|
+
ax: Any,
|
|
732
|
+
clock_edges: NDArray[np.floating[Any]],
|
|
733
|
+
data_edges: NDArray[np.floating[Any]],
|
|
734
|
+
time_mult: float,
|
|
735
|
+
) -> None:
|
|
736
|
+
"""Setup timing annotation panel.
|
|
737
|
+
|
|
738
|
+
Args:
|
|
739
|
+
ax: Timing axes.
|
|
740
|
+
clock_edges: Clock edge times.
|
|
741
|
+
data_edges: Data edge times.
|
|
742
|
+
time_mult: Time multiplier.
|
|
743
|
+
"""
|
|
744
|
+
ax.set_ylim(0, 1)
|
|
745
|
+
ax.set_xlim(0, max(clock_edges[-1], data_edges[-1]) * time_mult * 1.1)
|
|
746
|
+
ax.axis("off")
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def _draw_timing_arrows(
|
|
750
|
+
ax: Any,
|
|
751
|
+
clock_edges: NDArray[np.floating[Any]],
|
|
752
|
+
data_edges: NDArray[np.floating[Any]],
|
|
753
|
+
setup_scaled: float,
|
|
754
|
+
hold_scaled: float,
|
|
755
|
+
time_mult: float,
|
|
756
|
+
time_unit: str,
|
|
757
|
+
) -> None:
|
|
758
|
+
"""Draw setup and hold timing arrows.
|
|
759
|
+
|
|
760
|
+
Args:
|
|
761
|
+
ax: Timing axes.
|
|
762
|
+
clock_edges: Clock edge times.
|
|
763
|
+
data_edges: Data edge times.
|
|
764
|
+
setup_scaled: Scaled setup time.
|
|
765
|
+
hold_scaled: Scaled hold time.
|
|
766
|
+
time_mult: Time multiplier.
|
|
767
|
+
time_unit: Time unit string.
|
|
768
|
+
"""
|
|
769
|
+
if len(clock_edges) == 0 or len(data_edges) == 0:
|
|
770
|
+
return
|
|
771
|
+
|
|
772
|
+
clk_edge = clock_edges[0] * time_mult
|
|
773
|
+
|
|
774
|
+
# Setup time arrow
|
|
775
|
+
data_before = data_edges[data_edges < clock_edges[0]]
|
|
776
|
+
if len(data_before) > 0:
|
|
777
|
+
data_edge = data_before[-1] * time_mult
|
|
778
|
+
y_setup = 0.7
|
|
779
|
+
ax.annotate(
|
|
780
|
+
"",
|
|
781
|
+
xy=(clk_edge, y_setup),
|
|
782
|
+
xytext=(data_edge, y_setup),
|
|
783
|
+
arrowprops={"arrowstyle": "<->", "color": "#27AE60", "lw": 2},
|
|
784
|
+
)
|
|
785
|
+
ax.text(
|
|
786
|
+
(data_edge + clk_edge) / 2,
|
|
787
|
+
y_setup + 0.1,
|
|
788
|
+
f"Setup: {setup_scaled:.2f} {time_unit}",
|
|
789
|
+
ha="center",
|
|
790
|
+
va="bottom",
|
|
791
|
+
fontsize=10,
|
|
792
|
+
fontweight="bold",
|
|
793
|
+
color="#27AE60",
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
# Hold time arrow
|
|
797
|
+
data_after = data_edges[data_edges > clock_edges[0]]
|
|
798
|
+
if len(data_after) > 0:
|
|
799
|
+
data_edge_after = data_after[0] * time_mult
|
|
800
|
+
y_hold = 0.3
|
|
801
|
+
ax.annotate(
|
|
802
|
+
"",
|
|
803
|
+
xy=(data_edge_after, y_hold),
|
|
804
|
+
xytext=(clk_edge, y_hold),
|
|
805
|
+
arrowprops={"arrowstyle": "<->", "color": "#E67E22", "lw": 2},
|
|
806
|
+
)
|
|
807
|
+
ax.text(
|
|
808
|
+
(clk_edge + data_edge_after) / 2,
|
|
809
|
+
y_hold + 0.1,
|
|
810
|
+
f"Hold: {hold_scaled:.2f} {time_unit}",
|
|
811
|
+
ha="center",
|
|
812
|
+
va="bottom",
|
|
813
|
+
fontsize=10,
|
|
814
|
+
fontweight="bold",
|
|
815
|
+
color="#E67E22",
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def _add_passfail_status(
|
|
820
|
+
ax: Any,
|
|
821
|
+
setup_time: float,
|
|
822
|
+
hold_time: float,
|
|
823
|
+
setup_spec: float | None,
|
|
824
|
+
hold_spec: float | None,
|
|
825
|
+
time_mult: float,
|
|
826
|
+
time_unit: str,
|
|
827
|
+
) -> None:
|
|
828
|
+
"""Add pass/fail status text.
|
|
829
|
+
|
|
830
|
+
Args:
|
|
831
|
+
ax: Timing axes.
|
|
832
|
+
setup_time: Measured setup time.
|
|
833
|
+
hold_time: Measured hold time.
|
|
834
|
+
setup_spec: Setup specification.
|
|
835
|
+
hold_spec: Hold specification.
|
|
836
|
+
time_mult: Time multiplier.
|
|
837
|
+
time_unit: Time unit string.
|
|
838
|
+
"""
|
|
653
839
|
status_y = 0.9
|
|
840
|
+
|
|
654
841
|
if setup_spec is not None:
|
|
655
842
|
setup_pass = setup_time >= setup_spec
|
|
656
843
|
status = "PASS" if setup_pass else "FAIL"
|
|
657
844
|
color = "#27AE60" if setup_pass else "#E74C3C"
|
|
658
|
-
|
|
845
|
+
ax.text(
|
|
659
846
|
0.02,
|
|
660
847
|
status_y,
|
|
661
848
|
f"Setup: {status} (spec: {setup_spec * time_mult:.2f} {time_unit})",
|
|
662
|
-
transform=
|
|
849
|
+
transform=ax.transAxes,
|
|
663
850
|
fontsize=10,
|
|
664
851
|
color=color,
|
|
665
852
|
fontweight="bold",
|
|
@@ -670,34 +857,16 @@ def plot_setup_hold_timing(
|
|
|
670
857
|
hold_pass = hold_time >= hold_spec
|
|
671
858
|
status = "PASS" if hold_pass else "FAIL"
|
|
672
859
|
color = "#27AE60" if hold_pass else "#E74C3C"
|
|
673
|
-
|
|
860
|
+
ax.text(
|
|
674
861
|
0.02,
|
|
675
862
|
status_y,
|
|
676
863
|
f"Hold: {status} (spec: {hold_spec * time_mult:.2f} {time_unit})",
|
|
677
|
-
transform=
|
|
864
|
+
transform=ax.transAxes,
|
|
678
865
|
fontsize=10,
|
|
679
866
|
color=color,
|
|
680
867
|
fontweight="bold",
|
|
681
868
|
)
|
|
682
869
|
|
|
683
|
-
# Set x-label on bottom axes
|
|
684
|
-
axes[-1].set_xlabel(f"Time ({time_unit})", fontsize=11)
|
|
685
|
-
|
|
686
|
-
if title:
|
|
687
|
-
fig.suptitle(title, fontsize=14, fontweight="bold")
|
|
688
|
-
else:
|
|
689
|
-
fig.suptitle("Setup/Hold Timing Analysis", fontsize=14, fontweight="bold")
|
|
690
|
-
|
|
691
|
-
fig.tight_layout()
|
|
692
|
-
|
|
693
|
-
if save_path is not None:
|
|
694
|
-
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
695
|
-
|
|
696
|
-
if show:
|
|
697
|
-
plt.show()
|
|
698
|
-
|
|
699
|
-
return fig
|
|
700
|
-
|
|
701
870
|
|
|
702
871
|
def plot_timing_margin(
|
|
703
872
|
setup_times: NDArray[np.floating[Any]],
|
|
@@ -735,6 +904,32 @@ def plot_timing_margin(
|
|
|
735
904
|
if not HAS_MATPLOTLIB:
|
|
736
905
|
raise ImportError("matplotlib is required for visualization")
|
|
737
906
|
|
|
907
|
+
fig, ax = _get_or_create_axes(ax, figsize)
|
|
908
|
+
time_mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9, "ps": 1e12}.get(time_unit, 1e9)
|
|
909
|
+
|
|
910
|
+
setup_scaled, hold_scaled = setup_times * time_mult, hold_times * time_mult
|
|
911
|
+
|
|
912
|
+
# Scatter plot
|
|
913
|
+
ax.scatter(setup_scaled, hold_scaled, c="#3498DB", alpha=0.6, s=50)
|
|
914
|
+
|
|
915
|
+
# Add specification lines and regions
|
|
916
|
+
_add_spec_lines(ax, setup_spec, hold_spec, time_mult, time_unit)
|
|
917
|
+
_add_pass_region(ax, setup_spec, hold_spec, time_mult)
|
|
918
|
+
|
|
919
|
+
# Configure axes
|
|
920
|
+
ax.set_xlabel(f"Setup Time ({time_unit})", fontsize=11)
|
|
921
|
+
ax.set_ylabel(f"Hold Time ({time_unit})", fontsize=11)
|
|
922
|
+
ax.grid(True, alpha=0.3)
|
|
923
|
+
ax.legend(loc="best")
|
|
924
|
+
ax.set_title(title or "Setup/Hold Timing Margin", fontsize=12, fontweight="bold")
|
|
925
|
+
|
|
926
|
+
fig.tight_layout()
|
|
927
|
+
_save_and_show_figure(fig, save_path, show)
|
|
928
|
+
return fig
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
def _get_or_create_axes(ax: Axes | None, figsize: tuple[float, float]) -> tuple[Figure, Axes]:
|
|
932
|
+
"""Get existing axes or create new figure with axes."""
|
|
738
933
|
if ax is None:
|
|
739
934
|
fig, ax = plt.subplots(figsize=figsize)
|
|
740
935
|
else:
|
|
@@ -742,16 +937,13 @@ def plot_timing_margin(
|
|
|
742
937
|
if fig_temp is None:
|
|
743
938
|
raise ValueError("Axes must have an associated figure")
|
|
744
939
|
fig = cast("Figure", fig_temp)
|
|
940
|
+
return fig, ax
|
|
745
941
|
|
|
746
|
-
time_mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9, "ps": 1e12}.get(time_unit, 1e9)
|
|
747
|
-
|
|
748
|
-
setup_scaled = setup_times * time_mult
|
|
749
|
-
hold_scaled = hold_times * time_mult
|
|
750
942
|
|
|
751
|
-
|
|
752
|
-
ax
|
|
753
|
-
|
|
754
|
-
|
|
943
|
+
def _add_spec_lines(
|
|
944
|
+
ax: Axes, setup_spec: float | None, hold_spec: float | None, time_mult: float, time_unit: str
|
|
945
|
+
) -> None:
|
|
946
|
+
"""Add specification lines to timing margin plot."""
|
|
755
947
|
if setup_spec is not None:
|
|
756
948
|
spec_scaled = setup_spec * time_mult
|
|
757
949
|
ax.axvline(
|
|
@@ -772,12 +964,13 @@ def plot_timing_margin(
|
|
|
772
964
|
label=f"Hold Spec ({spec_scaled:.2f} {time_unit})",
|
|
773
965
|
)
|
|
774
966
|
|
|
775
|
-
# Mark pass/fail regions
|
|
776
|
-
if setup_spec is not None and hold_spec is not None:
|
|
777
|
-
x_lim = ax.get_xlim()
|
|
778
|
-
y_lim = ax.get_ylim()
|
|
779
967
|
|
|
780
|
-
|
|
968
|
+
def _add_pass_region(
|
|
969
|
+
ax: Axes, setup_spec: float | None, hold_spec: float | None, time_mult: float
|
|
970
|
+
) -> None:
|
|
971
|
+
"""Add pass/fail region shading to timing margin plot."""
|
|
972
|
+
if setup_spec is not None and hold_spec is not None:
|
|
973
|
+
x_lim, y_lim = ax.get_xlim(), ax.get_ylim()
|
|
781
974
|
ax.fill_between(
|
|
782
975
|
[setup_spec * time_mult, x_lim[1]],
|
|
783
976
|
[hold_spec * time_mult, hold_spec * time_mult],
|
|
@@ -787,22 +980,10 @@ def plot_timing_margin(
|
|
|
787
980
|
label="Pass Region",
|
|
788
981
|
)
|
|
789
982
|
|
|
790
|
-
ax.set_xlabel(f"Setup Time ({time_unit})", fontsize=11)
|
|
791
|
-
ax.set_ylabel(f"Hold Time ({time_unit})", fontsize=11)
|
|
792
|
-
ax.grid(True, alpha=0.3)
|
|
793
|
-
ax.legend(loc="best")
|
|
794
|
-
|
|
795
|
-
if title:
|
|
796
|
-
ax.set_title(title, fontsize=12, fontweight="bold")
|
|
797
|
-
else:
|
|
798
|
-
ax.set_title("Setup/Hold Timing Margin", fontsize=12, fontweight="bold")
|
|
799
|
-
|
|
800
|
-
fig.tight_layout()
|
|
801
983
|
|
|
984
|
+
def _save_and_show_figure(fig: Figure, save_path: str | Path | None, show: bool) -> None:
|
|
985
|
+
"""Save and optionally show figure."""
|
|
802
986
|
if save_path is not None:
|
|
803
987
|
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
804
|
-
|
|
805
988
|
if show:
|
|
806
989
|
plt.show()
|
|
807
|
-
|
|
808
|
-
return fig
|