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
|
@@ -14,12 +14,12 @@ These functions return WaveformTrace objects ready for use:
|
|
|
14
14
|
- generate_pulse: Single pulse with configurable rise/fall times
|
|
15
15
|
|
|
16
16
|
Example:
|
|
17
|
-
>>> from oscura.testing import generate_sine_wave, generate_square_wave
|
|
17
|
+
>>> from oscura.validation.testing import generate_sine_wave, generate_square_wave
|
|
18
18
|
>>> sine = generate_sine_wave(frequency=1e6, amplitude=1.0)
|
|
19
19
|
>>> square = generate_square_wave(frequency=500e3, duty_cycle=0.3)
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
-
from oscura.testing.synthetic import (
|
|
22
|
+
from oscura.validation.testing.synthetic import (
|
|
23
23
|
GroundTruth,
|
|
24
24
|
SyntheticDataGenerator,
|
|
25
25
|
SyntheticMessageConfig,
|
|
@@ -443,7 +443,7 @@ def generate_sine_wave(
|
|
|
443
443
|
WaveformTrace containing the sine wave.
|
|
444
444
|
|
|
445
445
|
Example:
|
|
446
|
-
>>> from oscura.testing import generate_sine_wave
|
|
446
|
+
>>> from oscura.validation.testing import generate_sine_wave
|
|
447
447
|
>>> trace = generate_sine_wave(frequency=1e6, amplitude=1.0)
|
|
448
448
|
>>> print(f"Samples: {len(trace.data)}")
|
|
449
449
|
"""
|
|
@@ -483,7 +483,7 @@ def generate_square_wave(
|
|
|
483
483
|
WaveformTrace containing the square wave.
|
|
484
484
|
|
|
485
485
|
Example:
|
|
486
|
-
>>> from oscura.testing import generate_square_wave
|
|
486
|
+
>>> from oscura.validation.testing import generate_square_wave
|
|
487
487
|
>>> trace = generate_square_wave(frequency=500e3, duty_cycle=0.3)
|
|
488
488
|
"""
|
|
489
489
|
num_samples = int(sample_rate * duration)
|
|
@@ -520,7 +520,7 @@ def generate_dc(
|
|
|
520
520
|
WaveformTrace containing the DC signal.
|
|
521
521
|
|
|
522
522
|
Example:
|
|
523
|
-
>>> from oscura.testing import generate_dc
|
|
523
|
+
>>> from oscura.validation.testing import generate_dc
|
|
524
524
|
>>> trace = generate_dc(level=1.5, duration=10e-6)
|
|
525
525
|
"""
|
|
526
526
|
num_samples = int(sample_rate * duration)
|
|
@@ -559,7 +559,7 @@ def generate_multi_tone(
|
|
|
559
559
|
ValueError: If frequencies, amplitudes, and phases have different lengths.
|
|
560
560
|
|
|
561
561
|
Example:
|
|
562
|
-
>>> from oscura.testing import generate_multi_tone
|
|
562
|
+
>>> from oscura.validation.testing import generate_multi_tone
|
|
563
563
|
>>> trace = generate_multi_tone(
|
|
564
564
|
... frequencies=[1e6, 2.5e6, 4e6],
|
|
565
565
|
... amplitudes=[1.0, 0.5, 0.25]
|
|
@@ -616,7 +616,7 @@ def generate_pulse(
|
|
|
616
616
|
WaveformTrace containing the pulse.
|
|
617
617
|
|
|
618
618
|
Example:
|
|
619
|
-
>>> from oscura.testing import generate_pulse
|
|
619
|
+
>>> from oscura.validation.testing import generate_pulse
|
|
620
620
|
>>> trace = generate_pulse(width=1e-6, rise_time=10e-9)
|
|
621
621
|
"""
|
|
622
622
|
num_samples = int(sample_rate * duration)
|
oscura/visualization/__init__.py
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
Provides plotting functions for waveforms, spectra, and other signal data,
|
|
4
4
|
plus optimization utilities, style presets, and intelligent rendering.
|
|
5
5
|
|
|
6
|
+
**Requires matplotlib:**
|
|
7
|
+
This module requires matplotlib to be installed. Install with:
|
|
8
|
+
pip install oscura[visualization] # Just visualization
|
|
9
|
+
pip install oscura[standard] # Recommended
|
|
10
|
+
pip install oscura[all] # Everything
|
|
6
11
|
|
|
7
12
|
Example:
|
|
8
13
|
>>> from oscura.visualization import plot_waveform, plot_spectrum
|
|
@@ -15,6 +20,10 @@ Example:
|
|
|
15
20
|
... plt.savefig("figure.pdf")
|
|
16
21
|
"""
|
|
17
22
|
|
|
23
|
+
# NOTE: Matplotlib is optional - individual functions will check and raise
|
|
24
|
+
# helpful errors if matplotlib is not installed when they're called.
|
|
25
|
+
# The module itself can be imported without matplotlib.
|
|
26
|
+
|
|
18
27
|
# Import plot module as namespace for DSL compatibility
|
|
19
28
|
from oscura.visualization import plot
|
|
20
29
|
from oscura.visualization.accessibility import (
|
|
@@ -111,7 +111,7 @@ def get_multi_line_styles(n_lines: int) -> list[tuple[tuple[float, float, float,
|
|
|
111
111
|
for i in range(n_lines):
|
|
112
112
|
linestyle = LINE_STYLES[i % len(LINE_STYLES)]
|
|
113
113
|
# Colors from colormap are RGBA tuples
|
|
114
|
-
rgba_color = tuple(colors[i])
|
|
114
|
+
rgba_color = tuple(colors[i])
|
|
115
115
|
styles.append((rgba_color, linestyle)) # type: ignore[arg-type]
|
|
116
116
|
|
|
117
117
|
return styles
|
|
@@ -20,6 +20,8 @@ from dataclasses import dataclass
|
|
|
20
20
|
|
|
21
21
|
import numpy as np
|
|
22
22
|
|
|
23
|
+
from oscura.utils.geometry import generate_leader_line
|
|
24
|
+
|
|
23
25
|
|
|
24
26
|
@dataclass
|
|
25
27
|
class Annotation:
|
|
@@ -106,57 +108,84 @@ def place_annotations(
|
|
|
106
108
|
if len(annotations) == 0:
|
|
107
109
|
return []
|
|
108
110
|
|
|
109
|
-
# Filter
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
#
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
111
|
+
# Filter and limit annotations
|
|
112
|
+
visible_annots = _filter_by_viewport(annotations, viewport)
|
|
113
|
+
visible_annots = _apply_density_limit(visible_annots, density_limit)
|
|
114
|
+
|
|
115
|
+
# Initialize placement
|
|
116
|
+
placed = _initialize_placements(visible_annots)
|
|
117
|
+
|
|
118
|
+
# Resolve collisions
|
|
119
|
+
_resolve_collisions(placed, collision_threshold, max_iterations)
|
|
120
|
+
|
|
121
|
+
# Add leader lines where needed
|
|
122
|
+
_add_leader_lines(placed, leader_threshold=30.0)
|
|
123
|
+
|
|
124
|
+
return placed
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _filter_by_viewport(
|
|
128
|
+
annotations: list[Annotation],
|
|
129
|
+
viewport: tuple[float, float] | None,
|
|
130
|
+
) -> list[Annotation]:
|
|
131
|
+
"""Filter annotations by viewport range."""
|
|
132
|
+
if viewport is None:
|
|
133
|
+
return annotations
|
|
134
|
+
|
|
135
|
+
x_min, x_max = viewport
|
|
136
|
+
return [a for a in annotations if x_min <= a.x <= x_max]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _apply_density_limit(
|
|
140
|
+
annotations: list[Annotation],
|
|
141
|
+
density_limit: int,
|
|
142
|
+
) -> list[Annotation]:
|
|
143
|
+
"""Apply density limiting by keeping top priority annotations."""
|
|
144
|
+
if len(annotations) <= density_limit:
|
|
145
|
+
return annotations
|
|
146
|
+
|
|
147
|
+
sorted_annots = sorted(annotations, key=lambda a: a.priority, reverse=True)
|
|
148
|
+
return sorted_annots[:density_limit]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _initialize_placements(annotations: list[Annotation]) -> list[PlacedAnnotation]:
|
|
152
|
+
"""Initialize placed annotations at anchor points."""
|
|
153
|
+
return [
|
|
154
|
+
PlacedAnnotation(
|
|
155
|
+
annotation=annot,
|
|
156
|
+
display_x=annot.x,
|
|
157
|
+
display_y=annot.y,
|
|
158
|
+
visible=True,
|
|
159
|
+
needs_leader=False,
|
|
137
160
|
)
|
|
161
|
+
for annot in annotations
|
|
162
|
+
]
|
|
138
163
|
|
|
139
|
-
|
|
164
|
+
|
|
165
|
+
def _resolve_collisions(
|
|
166
|
+
placed: list[PlacedAnnotation],
|
|
167
|
+
collision_threshold: float,
|
|
168
|
+
max_iterations: int,
|
|
169
|
+
) -> None:
|
|
170
|
+
"""Resolve collisions using iterative adjustment."""
|
|
140
171
|
for _iteration in range(max_iterations):
|
|
141
172
|
moved = False
|
|
142
173
|
|
|
143
|
-
# Check all pairs for collisions
|
|
144
174
|
for i in range(len(placed)):
|
|
145
175
|
for j in range(i + 1, len(placed)):
|
|
146
176
|
if _check_collision(placed[i], placed[j], collision_threshold):
|
|
147
|
-
#
|
|
177
|
+
# Move lower-priority annotation
|
|
148
178
|
if placed[i].annotation.priority >= placed[j].annotation.priority:
|
|
149
179
|
moved = _move_annotation(placed[j], placed[i], collision_threshold) or moved
|
|
150
180
|
else:
|
|
151
181
|
moved = _move_annotation(placed[i], placed[j], collision_threshold) or moved
|
|
152
182
|
|
|
153
|
-
# Converged if nothing moved
|
|
154
183
|
if not moved:
|
|
155
184
|
break
|
|
156
185
|
|
|
157
|
-
# Determine which annotations need leader lines
|
|
158
|
-
leader_threshold = 30.0 # pixels
|
|
159
186
|
|
|
187
|
+
def _add_leader_lines(placed: list[PlacedAnnotation], leader_threshold: float) -> None:
|
|
188
|
+
"""Add leader lines for displaced annotations."""
|
|
160
189
|
for p in placed:
|
|
161
190
|
dx = abs(p.display_x - p.annotation.x)
|
|
162
191
|
dy = abs(p.display_y - p.annotation.y)
|
|
@@ -164,13 +193,11 @@ def place_annotations(
|
|
|
164
193
|
|
|
165
194
|
if displacement > leader_threshold:
|
|
166
195
|
p.needs_leader = True
|
|
167
|
-
p.leader_points =
|
|
196
|
+
p.leader_points = generate_leader_line(
|
|
168
197
|
(p.annotation.x, p.annotation.y),
|
|
169
198
|
(p.display_x, p.display_y),
|
|
170
199
|
)
|
|
171
200
|
|
|
172
|
-
return placed
|
|
173
|
-
|
|
174
201
|
|
|
175
202
|
def _check_collision(
|
|
176
203
|
p1: PlacedAnnotation,
|
|
@@ -246,36 +273,6 @@ def _move_annotation(
|
|
|
246
273
|
return False
|
|
247
274
|
|
|
248
275
|
|
|
249
|
-
def _generate_leader_line(
|
|
250
|
-
anchor: tuple[float, float],
|
|
251
|
-
label: tuple[float, float],
|
|
252
|
-
) -> list[tuple[float, float]]:
|
|
253
|
-
"""Generate orthogonal leader line from anchor to label.
|
|
254
|
-
|
|
255
|
-
Args:
|
|
256
|
-
anchor: Anchor point (x, y)
|
|
257
|
-
label: Label position (x, y)
|
|
258
|
-
|
|
259
|
-
Returns:
|
|
260
|
-
List of points for leader line
|
|
261
|
-
"""
|
|
262
|
-
ax, ay = anchor
|
|
263
|
-
lx, ly = label
|
|
264
|
-
|
|
265
|
-
# L-shaped leader line
|
|
266
|
-
dx = abs(lx - ax)
|
|
267
|
-
dy = abs(ly - ay)
|
|
268
|
-
|
|
269
|
-
if dx > dy:
|
|
270
|
-
# Horizontal-first
|
|
271
|
-
mid = (lx, ay)
|
|
272
|
-
else:
|
|
273
|
-
# Vertical-first
|
|
274
|
-
mid = (ax, ly)
|
|
275
|
-
|
|
276
|
-
return [anchor, mid, label]
|
|
277
|
-
|
|
278
|
-
|
|
279
276
|
def filter_by_zoom_level(
|
|
280
277
|
placed: list[PlacedAnnotation],
|
|
281
278
|
zoom_range: tuple[float, float],
|
oscura/visualization/colors.py
CHANGED
|
@@ -256,7 +256,7 @@ def _adjust_for_contrast(
|
|
|
256
256
|
b = int(color_val[4:6], 16)
|
|
257
257
|
|
|
258
258
|
# Convert to HSL for easier lightness adjustment
|
|
259
|
-
h, s, l = _rgb_to_hsl(r, g, b)
|
|
259
|
+
h, s, l = _rgb_to_hsl(r, g, b)
|
|
260
260
|
|
|
261
261
|
bg_lum = _relative_luminance(background)
|
|
262
262
|
|
|
@@ -280,18 +280,18 @@ def _adjust_for_contrast(
|
|
|
280
280
|
if bg_lum > 0.5:
|
|
281
281
|
# Dark background - make lighter
|
|
282
282
|
l_min = l
|
|
283
|
-
l = (l + l_max) / 2
|
|
283
|
+
l = (l + l_max) / 2
|
|
284
284
|
else:
|
|
285
285
|
# Light background - make darker
|
|
286
286
|
l_max = l
|
|
287
|
-
l = (l_min + l) / 2
|
|
287
|
+
l = (l_min + l) / 2
|
|
288
288
|
# Too much contrast - move back
|
|
289
289
|
elif bg_lum > 0.5:
|
|
290
290
|
l_max = l
|
|
291
|
-
l = (l_min + l) / 2
|
|
291
|
+
l = (l_min + l) / 2
|
|
292
292
|
else:
|
|
293
293
|
l_min = l
|
|
294
|
-
l = (l + l_max) / 2
|
|
294
|
+
l = (l + l_max) / 2
|
|
295
295
|
|
|
296
296
|
iterations += 1
|
|
297
297
|
|
|
@@ -319,7 +319,7 @@ def _rgb_to_hsl(r: int, g: int, b: int) -> tuple[float, float, float]:
|
|
|
319
319
|
delta = max_c - min_c
|
|
320
320
|
|
|
321
321
|
# Lightness
|
|
322
|
-
l = (max_c + min_c) / 2.0
|
|
322
|
+
l = (max_c + min_c) / 2.0
|
|
323
323
|
|
|
324
324
|
if delta == 0:
|
|
325
325
|
# Achromatic
|
|
@@ -341,7 +341,7 @@ def _rgb_to_hsl(r: int, g: int, b: int) -> tuple[float, float, float]:
|
|
|
341
341
|
return (h, s, l)
|
|
342
342
|
|
|
343
343
|
|
|
344
|
-
def _hsl_to_rgb(h: float, s: float, l: float) -> tuple[int, int, int]:
|
|
344
|
+
def _hsl_to_rgb(h: float, s: float, l: float) -> tuple[int, int, int]:
|
|
345
345
|
"""Convert HSL to RGB color space.
|
|
346
346
|
|
|
347
347
|
Args:
|
oscura/visualization/digital.py
CHANGED
|
@@ -38,6 +38,172 @@ if TYPE_CHECKING:
|
|
|
38
38
|
from oscura.analyzers.protocols.base import Annotation
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
def _validate_timing_inputs(
|
|
42
|
+
traces: Sequence[WaveformTrace | DigitalTrace],
|
|
43
|
+
names: list[str] | None,
|
|
44
|
+
) -> tuple[int, list[str]]:
|
|
45
|
+
"""Validate plot_timing inputs and generate default names.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
traces: List of traces to validate.
|
|
49
|
+
names: Channel names or None for defaults.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Tuple of (n_channels, validated_names).
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
ValueError: If traces empty or names length mismatch.
|
|
56
|
+
"""
|
|
57
|
+
if len(traces) == 0:
|
|
58
|
+
raise ValueError("traces list cannot be empty")
|
|
59
|
+
|
|
60
|
+
n_channels = len(traces)
|
|
61
|
+
|
|
62
|
+
if names is None:
|
|
63
|
+
names = [f"CH{i + 1}" for i in range(n_channels)]
|
|
64
|
+
|
|
65
|
+
if len(names) != n_channels:
|
|
66
|
+
raise ValueError(f"names length ({len(names)}) must match traces ({n_channels})")
|
|
67
|
+
|
|
68
|
+
return n_channels, names
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _convert_to_digital_traces(
|
|
72
|
+
traces: Sequence[WaveformTrace | DigitalTrace],
|
|
73
|
+
threshold: float | str,
|
|
74
|
+
) -> list[DigitalTrace]:
|
|
75
|
+
"""Convert analog traces to digital using threshold.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
traces: List of analog or digital traces.
|
|
79
|
+
threshold: Threshold for analog-to-digital conversion.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
List of digital traces.
|
|
83
|
+
"""
|
|
84
|
+
from oscura.analyzers.digital.extraction import to_digital
|
|
85
|
+
|
|
86
|
+
digital_traces: list[DigitalTrace] = []
|
|
87
|
+
for trace in traces:
|
|
88
|
+
if isinstance(trace, WaveformTrace):
|
|
89
|
+
digital_traces.append(to_digital(trace, threshold=threshold)) # type: ignore[arg-type]
|
|
90
|
+
else:
|
|
91
|
+
digital_traces.append(trace)
|
|
92
|
+
|
|
93
|
+
return digital_traces
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _select_time_unit_and_multiplier(
|
|
97
|
+
digital_traces: list[DigitalTrace],
|
|
98
|
+
time_unit: str,
|
|
99
|
+
) -> tuple[str, float]:
|
|
100
|
+
"""Select appropriate time unit based on signal duration.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
digital_traces: List of digital traces.
|
|
104
|
+
time_unit: Time unit ("auto" or specific unit).
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Tuple of (time_unit, multiplier).
|
|
108
|
+
"""
|
|
109
|
+
if time_unit == "auto" and len(digital_traces) > 0:
|
|
110
|
+
ref_trace = digital_traces[0]
|
|
111
|
+
duration = len(ref_trace.data) * ref_trace.metadata.time_base
|
|
112
|
+
if duration < 1e-6:
|
|
113
|
+
time_unit = "ns"
|
|
114
|
+
elif duration < 1e-3:
|
|
115
|
+
time_unit = "us"
|
|
116
|
+
elif duration < 1:
|
|
117
|
+
time_unit = "ms"
|
|
118
|
+
else:
|
|
119
|
+
time_unit = "s"
|
|
120
|
+
|
|
121
|
+
time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
|
|
122
|
+
multiplier = time_multipliers.get(time_unit, 1.0)
|
|
123
|
+
|
|
124
|
+
return time_unit, multiplier
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _determine_plot_time_range(
|
|
128
|
+
digital_traces: list[DigitalTrace],
|
|
129
|
+
time_range: tuple[float, float] | None,
|
|
130
|
+
) -> tuple[float, float]:
|
|
131
|
+
"""Determine start and end times for plot.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
digital_traces: List of digital traces.
|
|
135
|
+
time_range: User-specified time range or None for auto.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Tuple of (start_time, end_time) in seconds.
|
|
139
|
+
"""
|
|
140
|
+
if time_range is not None:
|
|
141
|
+
return time_range
|
|
142
|
+
|
|
143
|
+
start_time = 0.0
|
|
144
|
+
end_time = max(trace.duration for trace in digital_traces if len(trace.data) > 0)
|
|
145
|
+
return start_time, end_time
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _plot_timing_channel(
|
|
149
|
+
ax: Axes,
|
|
150
|
+
trace: DigitalTrace,
|
|
151
|
+
name: str,
|
|
152
|
+
channel_index: int,
|
|
153
|
+
multiplier: float,
|
|
154
|
+
time_range: tuple[float, float] | None,
|
|
155
|
+
show_grid: bool,
|
|
156
|
+
annotations: list[Annotation] | None,
|
|
157
|
+
time_unit: str,
|
|
158
|
+
) -> None:
|
|
159
|
+
"""Plot a single channel in the timing diagram.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
ax: Matplotlib axes to plot on.
|
|
163
|
+
trace: Digital trace to plot.
|
|
164
|
+
name: Channel name for label.
|
|
165
|
+
channel_index: Index for color selection.
|
|
166
|
+
multiplier: Time unit multiplier.
|
|
167
|
+
time_range: Optional time range to display.
|
|
168
|
+
show_grid: Show vertical grid lines.
|
|
169
|
+
annotations: Optional protocol annotations.
|
|
170
|
+
time_unit: Time unit string.
|
|
171
|
+
"""
|
|
172
|
+
time = trace.time_vector * multiplier
|
|
173
|
+
|
|
174
|
+
# Filter to time range
|
|
175
|
+
if time_range is not None:
|
|
176
|
+
start_time, end_time = time_range
|
|
177
|
+
start_idx = int(np.searchsorted(trace.time_vector, start_time))
|
|
178
|
+
end_idx = int(np.searchsorted(trace.time_vector, end_time))
|
|
179
|
+
time = time[start_idx:end_idx]
|
|
180
|
+
data_slice = trace.data[start_idx:end_idx]
|
|
181
|
+
else:
|
|
182
|
+
data_slice = trace.data
|
|
183
|
+
|
|
184
|
+
# Plot digital waveform as step function
|
|
185
|
+
ax.step(
|
|
186
|
+
time,
|
|
187
|
+
data_slice.astype(int),
|
|
188
|
+
where="post",
|
|
189
|
+
color=f"C{channel_index}",
|
|
190
|
+
linewidth=1.5,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Set up digital signal display
|
|
194
|
+
ax.set_ylim(-0.2, 1.2)
|
|
195
|
+
ax.set_yticks([0, 1])
|
|
196
|
+
ax.set_yticklabels(["0", "1"])
|
|
197
|
+
ax.set_ylabel(name, rotation=0, ha="right", va="center", fontweight="bold")
|
|
198
|
+
|
|
199
|
+
if show_grid:
|
|
200
|
+
ax.grid(True, alpha=0.2, axis="x")
|
|
201
|
+
|
|
202
|
+
# Add protocol annotations if provided
|
|
203
|
+
if annotations:
|
|
204
|
+
_add_protocol_annotations(ax, annotations, multiplier, time_unit)
|
|
205
|
+
|
|
206
|
+
|
|
41
207
|
def plot_timing(
|
|
42
208
|
traces: Sequence[WaveformTrace | DigitalTrace],
|
|
43
209
|
*,
|
|
@@ -87,108 +253,41 @@ def plot_timing(
|
|
|
87
253
|
if not HAS_MATPLOTLIB:
|
|
88
254
|
raise ImportError("matplotlib is required for visualization")
|
|
89
255
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
n_channels = len(traces)
|
|
94
|
-
|
|
95
|
-
if names is None:
|
|
96
|
-
names = [f"CH{i + 1}" for i in range(n_channels)]
|
|
256
|
+
# Data preparation/validation
|
|
257
|
+
n_channels, names = _validate_timing_inputs(traces, names)
|
|
258
|
+
digital_traces = _convert_to_digital_traces(traces, threshold)
|
|
97
259
|
|
|
98
|
-
|
|
99
|
-
|
|
260
|
+
# Unit/scale selection
|
|
261
|
+
time_unit, multiplier = _select_time_unit_and_multiplier(digital_traces, time_unit)
|
|
262
|
+
start_time, end_time = _determine_plot_time_range(digital_traces, time_range)
|
|
100
263
|
|
|
264
|
+
# Figure/axes creation
|
|
101
265
|
if figsize is None:
|
|
102
266
|
figsize = (12, 1.5 * n_channels)
|
|
103
267
|
|
|
104
|
-
|
|
105
|
-
from oscura.analyzers.digital.extraction import to_digital
|
|
106
|
-
|
|
107
|
-
digital_traces: list[DigitalTrace] = []
|
|
108
|
-
for trace in traces:
|
|
109
|
-
if isinstance(trace, WaveformTrace):
|
|
110
|
-
digital_traces.append(to_digital(trace, threshold=threshold)) # type: ignore[arg-type]
|
|
111
|
-
else:
|
|
112
|
-
digital_traces.append(trace)
|
|
113
|
-
|
|
114
|
-
# Auto-select time unit from first trace
|
|
115
|
-
if time_unit == "auto" and len(digital_traces) > 0:
|
|
116
|
-
ref_trace = digital_traces[0]
|
|
117
|
-
duration = len(ref_trace.data) * ref_trace.metadata.time_base
|
|
118
|
-
if duration < 1e-6:
|
|
119
|
-
time_unit = "ns"
|
|
120
|
-
elif duration < 1e-3:
|
|
121
|
-
time_unit = "us"
|
|
122
|
-
elif duration < 1:
|
|
123
|
-
time_unit = "ms"
|
|
124
|
-
else:
|
|
125
|
-
time_unit = "s"
|
|
126
|
-
|
|
127
|
-
time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
|
|
128
|
-
multiplier = time_multipliers.get(time_unit, 1.0)
|
|
129
|
-
|
|
130
|
-
# Create figure
|
|
131
|
-
fig, axes = plt.subplots(
|
|
132
|
-
n_channels,
|
|
133
|
-
1,
|
|
134
|
-
figsize=figsize,
|
|
135
|
-
sharex=True,
|
|
136
|
-
)
|
|
268
|
+
fig, axes = plt.subplots(n_channels, 1, figsize=figsize, sharex=True)
|
|
137
269
|
|
|
138
270
|
if n_channels == 1:
|
|
139
271
|
axes = [axes]
|
|
140
272
|
|
|
141
|
-
#
|
|
142
|
-
if time_range is not None:
|
|
143
|
-
start_time, end_time = time_range
|
|
144
|
-
else:
|
|
145
|
-
start_time = 0.0
|
|
146
|
-
end_time = max(trace.duration for trace in digital_traces if len(trace.data) > 0)
|
|
147
|
-
|
|
273
|
+
# Plotting/rendering
|
|
148
274
|
for i, (trace, name, ax) in enumerate(zip(digital_traces, names, axes, strict=False)):
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if time_range is not None:
|
|
153
|
-
start_idx = int(np.searchsorted(trace.time_vector, start_time))
|
|
154
|
-
end_idx = int(np.searchsorted(trace.time_vector, end_time))
|
|
155
|
-
time = time[start_idx:end_idx]
|
|
156
|
-
data_slice = trace.data[start_idx:end_idx]
|
|
157
|
-
else:
|
|
158
|
-
data_slice = trace.data
|
|
159
|
-
|
|
160
|
-
# Plot digital waveform as step function
|
|
161
|
-
ax.step(
|
|
162
|
-
time,
|
|
163
|
-
data_slice.astype(int),
|
|
164
|
-
where="post",
|
|
165
|
-
color=f"C{i}",
|
|
166
|
-
linewidth=1.5,
|
|
275
|
+
channel_annotations = annotations[i] if annotations and i < len(annotations) else None
|
|
276
|
+
_plot_timing_channel(
|
|
277
|
+
ax, trace, name, i, multiplier, time_range, show_grid, channel_annotations, time_unit
|
|
167
278
|
)
|
|
168
279
|
|
|
169
|
-
# Set up digital signal display
|
|
170
|
-
ax.set_ylim(-0.2, 1.2)
|
|
171
|
-
ax.set_yticks([0, 1])
|
|
172
|
-
ax.set_yticklabels(["0", "1"])
|
|
173
|
-
ax.set_ylabel(name, rotation=0, ha="right", va="center", fontweight="bold")
|
|
174
|
-
|
|
175
|
-
if show_grid:
|
|
176
|
-
ax.grid(True, alpha=0.2, axis="x")
|
|
177
|
-
|
|
178
|
-
# Add protocol annotations if provided
|
|
179
|
-
if annotations is not None and i < len(annotations) and annotations[i]:
|
|
180
|
-
_add_protocol_annotations(ax, annotations[i], multiplier, time_unit)
|
|
181
|
-
|
|
182
280
|
# Remove x-axis labels except for bottom plot
|
|
183
281
|
if i < n_channels - 1:
|
|
184
282
|
ax.set_xticklabels([])
|
|
185
283
|
|
|
186
|
-
#
|
|
284
|
+
# Annotation/labeling
|
|
187
285
|
axes[-1].set_xlabel(f"Time ({time_unit})")
|
|
188
286
|
|
|
189
287
|
if title:
|
|
190
288
|
fig.suptitle(title, fontsize=14, fontweight="bold")
|
|
191
289
|
|
|
290
|
+
# Layout/formatting
|
|
192
291
|
fig.tight_layout()
|
|
193
292
|
return fig
|
|
194
293
|
|