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
|
@@ -15,7 +15,7 @@ References:
|
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
|
-
from typing import TYPE_CHECKING
|
|
18
|
+
from typing import TYPE_CHECKING, Any
|
|
19
19
|
|
|
20
20
|
import numpy as np
|
|
21
21
|
|
|
@@ -83,10 +83,25 @@ def render_thumbnail(
|
|
|
83
83
|
if not HAS_MATPLOTLIB:
|
|
84
84
|
raise ImportError("matplotlib is required for visualization")
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
sample_rate = sample_rate if sample_rate is not None else 1.0
|
|
87
|
+
_validate_thumbnail_params(signal, sample_rate, max_samples)
|
|
88
|
+
size = _compute_thumbnail_size(size, width, height)
|
|
89
89
|
|
|
90
|
+
with plt.rc_context(_get_fast_rendering_config()):
|
|
91
|
+
fig, ax = _create_thumbnail_figure(size, dpi)
|
|
92
|
+
decimated_signal = _decimate_uniform(signal, max_samples)
|
|
93
|
+
total_time = len(signal) / sample_rate
|
|
94
|
+
time_scaled, time_unit = _prepare_time_axis(decimated_signal, total_time, time_unit)
|
|
95
|
+
_plot_thumbnail_signal(ax, time_scaled, decimated_signal, time_unit, title)
|
|
96
|
+
fig.tight_layout(pad=0.5)
|
|
97
|
+
|
|
98
|
+
return fig
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _validate_thumbnail_params(
|
|
102
|
+
signal: NDArray[np.float64], sample_rate: float, max_samples: int
|
|
103
|
+
) -> None:
|
|
104
|
+
"""Validate thumbnail rendering parameters."""
|
|
90
105
|
if len(signal) == 0:
|
|
91
106
|
raise ValueError("Signal cannot be empty")
|
|
92
107
|
if sample_rate <= 0:
|
|
@@ -94,70 +109,75 @@ def render_thumbnail(
|
|
|
94
109
|
if max_samples < 10:
|
|
95
110
|
raise ValueError("max_samples must be >= 10")
|
|
96
111
|
|
|
97
|
-
|
|
112
|
+
|
|
113
|
+
def _compute_thumbnail_size(
|
|
114
|
+
size: tuple[int, int], width: int | None, height: int | None
|
|
115
|
+
) -> tuple[int, int]:
|
|
116
|
+
"""Compute thumbnail size from width/height or size tuple."""
|
|
98
117
|
if width is not None:
|
|
99
118
|
h = height if height is not None else int(width * 0.75)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
119
|
+
return (width, h)
|
|
120
|
+
if height is not None:
|
|
121
|
+
return (int(height * 4 / 3), height)
|
|
122
|
+
return size
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _get_fast_rendering_config() -> dict[str, bool | float]:
|
|
126
|
+
"""Get matplotlib configuration for fast rendering."""
|
|
127
|
+
return {
|
|
128
|
+
"path.simplify": True,
|
|
129
|
+
"path.simplify_threshold": 1.0,
|
|
130
|
+
"agg.path.chunksize": 1000,
|
|
131
|
+
"lines.antialiased": False,
|
|
132
|
+
"patch.antialiased": False,
|
|
133
|
+
"text.antialiased": False,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _create_thumbnail_figure(size: tuple[int, int], dpi: int) -> tuple[Figure, Any]:
|
|
138
|
+
"""Create matplotlib figure for thumbnail."""
|
|
139
|
+
width_inches = size[0] / dpi
|
|
140
|
+
height_inches = size[1] / dpi
|
|
141
|
+
return plt.subplots(figsize=(width_inches, height_inches), dpi=dpi)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _prepare_time_axis(
|
|
145
|
+
decimated_signal: NDArray[np.float64], total_time: float, time_unit: str
|
|
146
|
+
) -> tuple[NDArray[np.float64], str]:
|
|
147
|
+
"""Prepare time axis with auto unit selection."""
|
|
148
|
+
time = np.linspace(0, total_time, len(decimated_signal))
|
|
149
|
+
if time_unit == "auto":
|
|
150
|
+
time_unit = _auto_select_time_unit(total_time)
|
|
151
|
+
time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
|
|
152
|
+
multiplier = time_multipliers.get(time_unit, 1.0)
|
|
153
|
+
return time * multiplier, time_unit
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _auto_select_time_unit(total_time: float) -> str:
|
|
157
|
+
"""Auto-select appropriate time unit based on signal duration."""
|
|
158
|
+
if total_time < 1e-6:
|
|
159
|
+
return "ns"
|
|
160
|
+
if total_time < 1e-3:
|
|
161
|
+
return "us"
|
|
162
|
+
if total_time < 1:
|
|
163
|
+
return "ms"
|
|
164
|
+
return "s"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _plot_thumbnail_signal(
|
|
168
|
+
ax: Any,
|
|
169
|
+
time_scaled: NDArray[np.float64],
|
|
170
|
+
decimated_signal: NDArray[np.float64],
|
|
171
|
+
time_unit: str,
|
|
172
|
+
title: str | None,
|
|
173
|
+
) -> None:
|
|
174
|
+
"""Plot signal on thumbnail axes."""
|
|
175
|
+
ax.plot(time_scaled, decimated_signal, "b-", linewidth=0.5, antialiased=False)
|
|
176
|
+
ax.set_xlabel(f"Time ({time_unit})", fontsize=8)
|
|
177
|
+
ax.set_ylabel("Amplitude", fontsize=8)
|
|
178
|
+
if title:
|
|
179
|
+
ax.set_title(title, fontsize=9)
|
|
180
|
+
ax.tick_params(labelsize=7)
|
|
161
181
|
|
|
162
182
|
|
|
163
183
|
def _decimate_uniform(signal: NDArray[np.float64], target_samples: int) -> NDArray[np.float64]:
|
|
@@ -220,89 +240,98 @@ def render_thumbnail_multichannel(
|
|
|
220
240
|
References:
|
|
221
241
|
VIS-018: Thumbnail Mode
|
|
222
242
|
"""
|
|
243
|
+
_validate_multichannel_params(signals, sample_rate)
|
|
244
|
+
n_channels = len(signals)
|
|
245
|
+
names = channel_names if channel_names is not None else _default_channel_names(n_channels)
|
|
246
|
+
time_unit_resolved, multiplier = _resolve_time_unit(signals[0], sample_rate, time_unit)
|
|
247
|
+
|
|
248
|
+
with plt.rc_context(_get_fast_rendering_config()):
|
|
249
|
+
fig, axes = _create_multichannel_figure(n_channels, size, dpi)
|
|
250
|
+
_plot_multichannel_signals(
|
|
251
|
+
axes, signals, names, sample_rate, max_samples, multiplier, time_unit_resolved
|
|
252
|
+
)
|
|
253
|
+
fig.tight_layout(pad=0.3)
|
|
254
|
+
|
|
255
|
+
return fig
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _validate_multichannel_params(signals: list[NDArray[np.float64]], sample_rate: float) -> None:
|
|
259
|
+
"""Validate multichannel thumbnail parameters."""
|
|
223
260
|
if not HAS_MATPLOTLIB:
|
|
224
261
|
raise ImportError("matplotlib is required for visualization")
|
|
225
|
-
|
|
226
262
|
if len(signals) == 0:
|
|
227
263
|
raise ValueError("Must provide at least one signal")
|
|
228
264
|
if sample_rate <= 0:
|
|
229
265
|
raise ValueError("Sample rate must be positive")
|
|
230
266
|
|
|
267
|
+
|
|
268
|
+
def _default_channel_names(n_channels: int) -> list[str]:
|
|
269
|
+
"""Generate default channel names."""
|
|
270
|
+
return [f"CH{i + 1}" for i in range(n_channels)]
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _resolve_time_unit(
|
|
274
|
+
first_signal: NDArray[np.float64], sample_rate: float, time_unit: str
|
|
275
|
+
) -> tuple[str, float]:
|
|
276
|
+
"""Resolve time unit and multiplier."""
|
|
277
|
+
if len(first_signal) > 0 and time_unit == "auto":
|
|
278
|
+
total_time = len(first_signal) / sample_rate
|
|
279
|
+
time_unit = _auto_select_time_unit(total_time)
|
|
280
|
+
elif time_unit == "auto":
|
|
281
|
+
time_unit = "s"
|
|
282
|
+
|
|
283
|
+
time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
|
|
284
|
+
multiplier = time_multipliers.get(time_unit, 1.0)
|
|
285
|
+
return time_unit, multiplier
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _create_multichannel_figure(
|
|
289
|
+
n_channels: int, size: tuple[int, int], dpi: int
|
|
290
|
+
) -> tuple[Figure, Any]:
|
|
291
|
+
"""Create matplotlib figure for multichannel display."""
|
|
292
|
+
width_inches = size[0] / dpi
|
|
293
|
+
height_inches = size[1] / dpi
|
|
294
|
+
|
|
295
|
+
fig, axes = plt.subplots(
|
|
296
|
+
n_channels,
|
|
297
|
+
1,
|
|
298
|
+
figsize=(width_inches, height_inches),
|
|
299
|
+
dpi=dpi,
|
|
300
|
+
sharex=True,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
if n_channels == 1:
|
|
304
|
+
axes = [axes]
|
|
305
|
+
|
|
306
|
+
return fig, axes
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _plot_multichannel_signals(
|
|
310
|
+
axes: Any,
|
|
311
|
+
signals: list[NDArray[np.float64]],
|
|
312
|
+
names: list[str],
|
|
313
|
+
sample_rate: float,
|
|
314
|
+
max_samples: int,
|
|
315
|
+
multiplier: float,
|
|
316
|
+
time_unit: str,
|
|
317
|
+
) -> None:
|
|
318
|
+
"""Plot all channels on their respective axes."""
|
|
231
319
|
n_channels = len(signals)
|
|
232
320
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
# Configure matplotlib for fast rendering
|
|
237
|
-
with plt.rc_context(
|
|
238
|
-
{
|
|
239
|
-
"path.simplify": True,
|
|
240
|
-
"path.simplify_threshold": 1.0,
|
|
241
|
-
"agg.path.chunksize": 1000,
|
|
242
|
-
"lines.antialiased": False,
|
|
243
|
-
"patch.antialiased": False,
|
|
244
|
-
"text.antialiased": False,
|
|
245
|
-
}
|
|
246
|
-
):
|
|
247
|
-
# Calculate figure size
|
|
248
|
-
width_inches = size[0] / dpi
|
|
249
|
-
height_inches = size[1] / dpi
|
|
250
|
-
|
|
251
|
-
fig, axes = plt.subplots(
|
|
252
|
-
n_channels,
|
|
253
|
-
1,
|
|
254
|
-
figsize=(width_inches, height_inches),
|
|
255
|
-
dpi=dpi,
|
|
256
|
-
sharex=True,
|
|
257
|
-
)
|
|
321
|
+
for i, (sig, name, ax) in enumerate(zip(signals, names, axes, strict=False)):
|
|
322
|
+
if len(sig) == 0:
|
|
323
|
+
continue
|
|
258
324
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
# Get time unit from first signal
|
|
263
|
-
if len(signals[0]) > 0:
|
|
264
|
-
total_time = len(signals[0]) / sample_rate
|
|
265
|
-
if time_unit == "auto":
|
|
266
|
-
if total_time < 1e-6:
|
|
267
|
-
time_unit = "ns"
|
|
268
|
-
elif total_time < 1e-3:
|
|
269
|
-
time_unit = "us"
|
|
270
|
-
elif total_time < 1:
|
|
271
|
-
time_unit = "ms"
|
|
272
|
-
else:
|
|
273
|
-
time_unit = "s"
|
|
274
|
-
else:
|
|
275
|
-
time_unit = "s"
|
|
276
|
-
|
|
277
|
-
time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
|
|
278
|
-
multiplier = time_multipliers.get(time_unit, 1.0)
|
|
279
|
-
|
|
280
|
-
# Plot each channel
|
|
281
|
-
for i, (sig, name, ax) in enumerate(zip(signals, channel_names, axes, strict=False)):
|
|
282
|
-
if len(sig) == 0:
|
|
283
|
-
continue
|
|
284
|
-
|
|
285
|
-
# Decimate signal
|
|
286
|
-
decimated = _decimate_uniform(sig, max_samples)
|
|
287
|
-
|
|
288
|
-
# Time vector
|
|
289
|
-
total_time = len(sig) / sample_rate
|
|
290
|
-
time = np.linspace(0, total_time, len(decimated)) * multiplier
|
|
291
|
-
|
|
292
|
-
# Plot
|
|
293
|
-
ax.plot(time, decimated, "b-", linewidth=0.5, antialiased=False)
|
|
294
|
-
|
|
295
|
-
# Channel label
|
|
296
|
-
ax.set_ylabel(name, fontsize=7, rotation=0, ha="right", va="center")
|
|
297
|
-
ax.tick_params(labelsize=6)
|
|
298
|
-
|
|
299
|
-
# Only x-label on bottom
|
|
300
|
-
if i == n_channels - 1:
|
|
301
|
-
ax.set_xlabel(f"Time ({time_unit})", fontsize=8)
|
|
325
|
+
decimated = _decimate_uniform(sig, max_samples)
|
|
326
|
+
total_time = len(sig) / sample_rate
|
|
327
|
+
time = np.linspace(0, total_time, len(decimated)) * multiplier
|
|
302
328
|
|
|
303
|
-
|
|
329
|
+
ax.plot(time, decimated, "b-", linewidth=0.5, antialiased=False)
|
|
330
|
+
ax.set_ylabel(name, fontsize=7, rotation=0, ha="right", va="center")
|
|
331
|
+
ax.tick_params(labelsize=6)
|
|
304
332
|
|
|
305
|
-
|
|
333
|
+
if i == n_channels - 1:
|
|
334
|
+
ax.set_xlabel(f"Time ({time_unit})", fontsize=8)
|
|
306
335
|
|
|
307
336
|
|
|
308
337
|
__all__ = [
|
oscura/visualization/waveform.py
CHANGED
|
@@ -92,15 +92,58 @@ def plot_waveform(
|
|
|
92
92
|
if not HAS_MATPLOTLIB:
|
|
93
93
|
raise ImportError("matplotlib is required for visualization")
|
|
94
94
|
|
|
95
|
+
# Setup figure and axes
|
|
96
|
+
fig, ax = _setup_waveform_figure(ax, figsize)
|
|
97
|
+
|
|
98
|
+
# Prepare time axis
|
|
99
|
+
time_unit_final, time_info = _prepare_time_axis(trace, time_unit)
|
|
100
|
+
time_scaled, _ = time_info
|
|
101
|
+
|
|
102
|
+
# Plot waveform
|
|
103
|
+
_plot_waveform_data(ax, time_scaled, trace.data, color, label)
|
|
104
|
+
|
|
105
|
+
# Apply styling and formatting
|
|
106
|
+
_apply_waveform_formatting(
|
|
107
|
+
ax,
|
|
108
|
+
time_range,
|
|
109
|
+
time_unit_final,
|
|
110
|
+
time_info,
|
|
111
|
+
xlabel,
|
|
112
|
+
ylabel,
|
|
113
|
+
title,
|
|
114
|
+
trace.metadata.channel_name,
|
|
115
|
+
show_grid,
|
|
116
|
+
label,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Add measurements if provided
|
|
120
|
+
if show_measurements:
|
|
121
|
+
_add_measurement_annotations(ax, trace, show_measurements, time_unit_final, time_info[1])
|
|
122
|
+
|
|
123
|
+
fig.tight_layout()
|
|
124
|
+
|
|
125
|
+
# Save and show
|
|
126
|
+
_save_and_show_figure(fig, save_path, show)
|
|
127
|
+
|
|
128
|
+
return fig
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _setup_waveform_figure(ax: Axes | None, figsize: tuple[float, float]) -> tuple[Figure, Axes]:
|
|
132
|
+
"""Setup figure and axes for waveform plot."""
|
|
95
133
|
if ax is None:
|
|
96
134
|
fig, ax = plt.subplots(figsize=figsize)
|
|
97
|
-
|
|
98
|
-
fig_temp = ax.get_figure()
|
|
99
|
-
if fig_temp is None:
|
|
100
|
-
raise ValueError("Axes must have an associated figure")
|
|
101
|
-
fig = cast("Figure", fig_temp)
|
|
135
|
+
return fig, ax
|
|
102
136
|
|
|
103
|
-
|
|
137
|
+
fig_temp = ax.get_figure()
|
|
138
|
+
if fig_temp is None:
|
|
139
|
+
raise ValueError("Axes must have an associated figure")
|
|
140
|
+
return cast("Figure", fig_temp), ax
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _prepare_time_axis(
|
|
144
|
+
trace: WaveformTrace, time_unit: str
|
|
145
|
+
) -> tuple[str, tuple[NDArray[np.float64], float]]:
|
|
146
|
+
"""Prepare time axis with appropriate unit and scaling."""
|
|
104
147
|
time = trace.time_vector
|
|
105
148
|
|
|
106
149
|
# Auto-select time unit
|
|
@@ -119,8 +162,34 @@ def plot_waveform(
|
|
|
119
162
|
multiplier = time_multipliers.get(time_unit, 1.0)
|
|
120
163
|
time_scaled = time * multiplier
|
|
121
164
|
|
|
122
|
-
|
|
123
|
-
|
|
165
|
+
return time_unit, (time_scaled, multiplier)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _plot_waveform_data(
|
|
169
|
+
ax: Axes,
|
|
170
|
+
time_scaled: NDArray[np.float64],
|
|
171
|
+
data: NDArray[np.float64],
|
|
172
|
+
color: str,
|
|
173
|
+
label: str | None,
|
|
174
|
+
) -> None:
|
|
175
|
+
"""Plot waveform data on axes."""
|
|
176
|
+
ax.plot(time_scaled, data, color=color, label=label, linewidth=0.8)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _apply_waveform_formatting(
|
|
180
|
+
ax: Axes,
|
|
181
|
+
time_range: tuple[float, float] | None,
|
|
182
|
+
time_unit: str,
|
|
183
|
+
time_info: tuple[NDArray[np.float64], float],
|
|
184
|
+
xlabel: str,
|
|
185
|
+
ylabel: str,
|
|
186
|
+
title: str | None,
|
|
187
|
+
channel_name: str | None,
|
|
188
|
+
show_grid: bool,
|
|
189
|
+
label: str | None,
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Apply formatting to waveform plot."""
|
|
192
|
+
_, multiplier = time_info
|
|
124
193
|
|
|
125
194
|
# Apply time range if specified
|
|
126
195
|
if time_range is not None:
|
|
@@ -130,33 +199,28 @@ def plot_waveform(
|
|
|
130
199
|
ax.set_xlabel(f"{xlabel} ({time_unit})")
|
|
131
200
|
ax.set_ylabel(ylabel)
|
|
132
201
|
|
|
202
|
+
# Title
|
|
133
203
|
if title:
|
|
134
204
|
ax.set_title(title)
|
|
135
|
-
elif
|
|
136
|
-
ax.set_title(f"Waveform - {
|
|
205
|
+
elif channel_name:
|
|
206
|
+
ax.set_title(f"Waveform - {channel_name}")
|
|
137
207
|
|
|
208
|
+
# Grid and legend
|
|
138
209
|
if show_grid:
|
|
139
210
|
ax.grid(True, alpha=0.3)
|
|
140
211
|
|
|
141
212
|
if label:
|
|
142
213
|
ax.legend()
|
|
143
214
|
|
|
144
|
-
# Add measurement annotations
|
|
145
|
-
if show_measurements:
|
|
146
|
-
_add_measurement_annotations(ax, trace, show_measurements, time_unit, multiplier)
|
|
147
215
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
# Save if path provided
|
|
216
|
+
def _save_and_show_figure(fig: Figure, save_path: str | None, show: bool) -> None:
|
|
217
|
+
"""Save and/or display the figure."""
|
|
151
218
|
if save_path is not None:
|
|
152
219
|
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
153
220
|
|
|
154
|
-
# Show if requested
|
|
155
221
|
if show:
|
|
156
222
|
plt.show()
|
|
157
223
|
|
|
158
|
-
return fig
|
|
159
|
-
|
|
160
224
|
|
|
161
225
|
def plot_multi_channel(
|
|
162
226
|
traces: list[WaveformTrace | DigitalTrace],
|
|
@@ -193,74 +257,97 @@ def plot_multi_channel(
|
|
|
193
257
|
>>> fig = plot_multi_channel([ch1, ch2, ch3], names=["CLK", "DATA", "CS"])
|
|
194
258
|
>>> plt.show()
|
|
195
259
|
"""
|
|
196
|
-
# Handle share_x alias
|
|
197
|
-
if share_x is not None:
|
|
198
|
-
shared_x = share_x
|
|
199
260
|
if not HAS_MATPLOTLIB:
|
|
200
261
|
raise ImportError("matplotlib is required for visualization")
|
|
201
262
|
|
|
263
|
+
shared_x = share_x if share_x is not None else shared_x
|
|
202
264
|
n_channels = len(traces)
|
|
265
|
+
names = names or [f"CH{i + 1}" for i in range(n_channels)]
|
|
266
|
+
figsize = figsize or (10, 2 * n_channels)
|
|
203
267
|
|
|
204
|
-
|
|
205
|
-
|
|
268
|
+
fig, axes = plt.subplots(n_channels, 1, figsize=figsize, sharex=shared_x)
|
|
269
|
+
axes = [axes] if n_channels == 1 else axes
|
|
206
270
|
|
|
207
|
-
|
|
208
|
-
figsize = (10, 2 * n_channels)
|
|
271
|
+
time_unit, multiplier = _determine_time_unit_and_multiplier(time_unit, traces)
|
|
209
272
|
|
|
210
|
-
|
|
211
|
-
n_channels,
|
|
212
|
-
1,
|
|
213
|
-
figsize=figsize,
|
|
214
|
-
sharex=shared_x,
|
|
215
|
-
)
|
|
273
|
+
_plot_channels(traces, names, axes, colors, time_unit, multiplier, show_grid, n_channels)
|
|
216
274
|
|
|
217
|
-
if
|
|
218
|
-
|
|
275
|
+
if title:
|
|
276
|
+
fig.suptitle(title)
|
|
277
|
+
|
|
278
|
+
fig.tight_layout()
|
|
279
|
+
return fig
|
|
219
280
|
|
|
220
|
-
|
|
281
|
+
|
|
282
|
+
def _determine_time_unit_and_multiplier(
|
|
283
|
+
time_unit: str, traces: list[WaveformTrace | DigitalTrace]
|
|
284
|
+
) -> tuple[str, float]:
|
|
285
|
+
"""Determine time unit and multiplier for plotting."""
|
|
221
286
|
if time_unit == "auto" and len(traces) > 0:
|
|
222
287
|
ref_trace = traces[0]
|
|
223
288
|
duration = len(ref_trace.data) * ref_trace.metadata.time_base
|
|
224
|
-
|
|
225
|
-
time_unit = "ns"
|
|
226
|
-
elif duration < 1e-3:
|
|
227
|
-
time_unit = "us"
|
|
228
|
-
elif duration < 1:
|
|
229
|
-
time_unit = "ms"
|
|
230
|
-
else:
|
|
231
|
-
time_unit = "s"
|
|
289
|
+
time_unit = _select_time_unit_from_duration(duration)
|
|
232
290
|
|
|
233
291
|
time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
|
|
234
292
|
multiplier = time_multipliers.get(time_unit, 1.0)
|
|
235
293
|
|
|
236
|
-
|
|
237
|
-
time = trace.time_vector * multiplier
|
|
238
|
-
color = colors[i] if colors is not None and i < len(colors) else f"C{i}"
|
|
294
|
+
return time_unit, multiplier
|
|
239
295
|
|
|
240
|
-
if isinstance(trace, WaveformTrace):
|
|
241
|
-
ax.plot(time, trace.data, color=color, linewidth=0.8)
|
|
242
|
-
ax.set_ylabel("V")
|
|
243
|
-
else:
|
|
244
|
-
# Digital trace - step plot
|
|
245
|
-
ax.step(time, trace.data.astype(int), color=color, where="post", linewidth=1.0)
|
|
246
|
-
ax.set_ylim(-0.1, 1.1)
|
|
247
|
-
ax.set_yticks([0, 1])
|
|
248
|
-
ax.set_yticklabels(["L", "H"])
|
|
249
296
|
|
|
250
|
-
|
|
297
|
+
def _select_time_unit_from_duration(duration: float) -> str:
|
|
298
|
+
"""Select appropriate time unit based on duration."""
|
|
299
|
+
if duration < 1e-6:
|
|
300
|
+
return "ns"
|
|
301
|
+
if duration < 1e-3:
|
|
302
|
+
return "us"
|
|
303
|
+
if duration < 1:
|
|
304
|
+
return "ms"
|
|
305
|
+
return "s"
|
|
251
306
|
|
|
252
|
-
if show_grid:
|
|
253
|
-
ax.grid(True, alpha=0.3)
|
|
254
307
|
|
|
255
|
-
|
|
308
|
+
def _plot_channels(
|
|
309
|
+
traces: list[WaveformTrace | DigitalTrace],
|
|
310
|
+
names: list[str],
|
|
311
|
+
axes: list[Any],
|
|
312
|
+
colors: list[str] | None,
|
|
313
|
+
time_unit: str,
|
|
314
|
+
multiplier: float,
|
|
315
|
+
show_grid: bool,
|
|
316
|
+
n_channels: int,
|
|
317
|
+
) -> None:
|
|
318
|
+
"""Plot each channel on its subplot."""
|
|
319
|
+
for i, (trace, name, ax) in enumerate(zip(traces, names, axes, strict=False)):
|
|
320
|
+
time = trace.time_vector * multiplier
|
|
321
|
+
color = colors[i] if colors and i < len(colors) else f"C{i}"
|
|
322
|
+
|
|
323
|
+
_plot_single_channel(ax, trace, time, color, name, show_grid)
|
|
324
|
+
|
|
256
325
|
if i == n_channels - 1:
|
|
257
326
|
ax.set_xlabel(f"Time ({time_unit})")
|
|
258
327
|
|
|
259
|
-
if title:
|
|
260
|
-
fig.suptitle(title)
|
|
261
328
|
|
|
262
|
-
|
|
263
|
-
|
|
329
|
+
def _plot_single_channel(
|
|
330
|
+
ax: Any,
|
|
331
|
+
trace: WaveformTrace | DigitalTrace,
|
|
332
|
+
time: Any,
|
|
333
|
+
color: str,
|
|
334
|
+
name: str,
|
|
335
|
+
show_grid: bool,
|
|
336
|
+
) -> None:
|
|
337
|
+
"""Plot a single channel (analog or digital)."""
|
|
338
|
+
if isinstance(trace, WaveformTrace):
|
|
339
|
+
ax.plot(time, trace.data, color=color, linewidth=0.8)
|
|
340
|
+
ax.set_ylabel("V")
|
|
341
|
+
else:
|
|
342
|
+
ax.step(time, trace.data.astype(int), color=color, where="post", linewidth=1.0)
|
|
343
|
+
ax.set_ylim(-0.1, 1.1)
|
|
344
|
+
ax.set_yticks([0, 1])
|
|
345
|
+
ax.set_yticklabels(["L", "H"])
|
|
346
|
+
|
|
347
|
+
ax.set_ylabel(name, rotation=0, ha="right", va="center")
|
|
348
|
+
|
|
349
|
+
if show_grid:
|
|
350
|
+
ax.grid(True, alpha=0.3)
|
|
264
351
|
|
|
265
352
|
|
|
266
353
|
def plot_xy(
|
oscura/workflows/__init__.py
CHANGED
|
@@ -16,6 +16,7 @@ Example:
|
|
|
16
16
|
>>> stats = osc.workflows.load_all(["trace1.wfm", "trace2.wfm"])
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
+
from oscura.workflows.complete_re import CompleteREResult, full_protocol_re
|
|
19
20
|
from oscura.workflows.compliance import emc_compliance_test
|
|
20
21
|
from oscura.workflows.digital import characterize_buffer
|
|
21
22
|
from oscura.workflows.multi_trace import (
|
|
@@ -40,6 +41,7 @@ __all__ = [
|
|
|
40
41
|
# Multi-trace
|
|
41
42
|
"AlignmentMethod",
|
|
42
43
|
# Reverse engineering
|
|
44
|
+
"CompleteREResult",
|
|
43
45
|
"FieldSpec",
|
|
44
46
|
"InferredFrame",
|
|
45
47
|
"MultiTraceResults",
|
|
@@ -51,6 +53,7 @@ __all__ = [
|
|
|
51
53
|
"characterize_buffer",
|
|
52
54
|
"debug_protocol",
|
|
53
55
|
"emc_compliance_test",
|
|
56
|
+
"full_protocol_re",
|
|
54
57
|
"load_all",
|
|
55
58
|
"power_analysis",
|
|
56
59
|
"reverse_engineer_signal",
|
|
@@ -5,23 +5,23 @@ This module enables efficient batch analysis of multiple signal files
|
|
|
5
5
|
with parallel execution support and comprehensive result aggregation.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from oscura.batch.advanced import (
|
|
8
|
+
from oscura.workflows.batch.advanced import (
|
|
9
9
|
AdvancedBatchProcessor,
|
|
10
10
|
BatchCheckpoint,
|
|
11
11
|
BatchConfig,
|
|
12
12
|
FileResult,
|
|
13
13
|
resume_batch,
|
|
14
14
|
)
|
|
15
|
-
from oscura.batch.aggregate import aggregate_results
|
|
16
|
-
from oscura.batch.analyze import batch_analyze
|
|
17
|
-
from oscura.batch.logging import (
|
|
15
|
+
from oscura.workflows.batch.aggregate import aggregate_results
|
|
16
|
+
from oscura.workflows.batch.analyze import batch_analyze
|
|
17
|
+
from oscura.workflows.batch.logging import (
|
|
18
18
|
BatchLogger,
|
|
19
19
|
BatchSummary,
|
|
20
20
|
FileLogEntry,
|
|
21
21
|
FileLogger,
|
|
22
22
|
aggregate_batch_logs,
|
|
23
23
|
)
|
|
24
|
-
from oscura.batch.metrics import (
|
|
24
|
+
from oscura.workflows.batch.metrics import (
|
|
25
25
|
BatchMetrics,
|
|
26
26
|
BatchMetricsSummary,
|
|
27
27
|
ErrorBreakdown,
|