oscura 0.5.0__py3-none-any.whl → 0.6.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/__init__.py +0 -48
- oscura/analyzers/digital/edges.py +325 -65
- oscura/analyzers/digital/extraction.py +0 -195
- 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/__init__.py +1 -22
- 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 +2763 -0
- 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/core/schemas/bus_configuration.json +322 -0
- oscura/core/schemas/device_mapping.json +182 -0
- oscura/core/schemas/packet_format.json +418 -0
- oscura/core/schemas/protocol_definition.json +363 -0
- 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 -20
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- oscura/export/wireshark/README.md +15 -15
- 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/README.md +7 -7
- 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 +171 -63
- 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/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -7
- 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/reporting/templates/index.md +13 -13
- 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/autodetect.py +1 -5
- 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 +11 -3
- 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.6.0.dist-info/METADATA +643 -0
- oscura-0.6.0.dist-info/RECORD +590 -0
- oscura/analyzers/digital/ic_database.py +0 -498
- oscura/analyzers/digital/timing_paths.py +0 -339
- oscura/analyzers/digital/vintage.py +0 -377
- oscura/analyzers/digital/vintage_result.py +0 -148
- oscura/analyzers/protocols/parallel_bus.py +0 -449
- 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/export/wavedrom.py +0 -430
- 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 -338
- 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/exporters/vintage_logic_csv.py +0 -247
- oscura/reporting/vintage_logic_report.py +0 -523
- 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/visualization/digital_advanced.py +0 -718
- oscura/visualization/figure_manager.py +0 -156
- oscura/workflow/__init__.py +0 -13
- oscura-0.5.0.dist-info/METADATA +0 -407
- oscura-0.5.0.dist-info/RECORD +0 -486
- /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/{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.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -92,62 +92,19 @@ class ZoomState:
|
|
|
92
92
|
home_ylim: tuple[float, float] | None = None
|
|
93
93
|
|
|
94
94
|
|
|
95
|
-
def
|
|
96
|
-
|
|
97
|
-
*,
|
|
98
|
-
enable_zoom: bool = True,
|
|
99
|
-
enable_pan: bool = True,
|
|
100
|
-
zoom_factor: float = 1.5,
|
|
101
|
-
) -> ZoomState:
|
|
102
|
-
"""Enable interactive zoom and pan on an axes.
|
|
103
|
-
|
|
104
|
-
Adds scroll wheel zoom and click-drag pan functionality.
|
|
105
|
-
|
|
106
|
-
Args:
|
|
107
|
-
ax: Matplotlib axes to enable zoom/pan on.
|
|
108
|
-
enable_zoom: Enable scroll wheel zoom.
|
|
109
|
-
enable_pan: Enable click-drag pan.
|
|
110
|
-
zoom_factor: Zoom factor per scroll step.
|
|
111
|
-
|
|
112
|
-
Returns:
|
|
113
|
-
ZoomState object tracking zoom history.
|
|
114
|
-
|
|
115
|
-
Raises:
|
|
116
|
-
ImportError: If matplotlib is not available.
|
|
117
|
-
|
|
118
|
-
Example:
|
|
119
|
-
>>> fig, ax = plt.subplots()
|
|
120
|
-
>>> ax.plot(trace.time_vector, trace.data)
|
|
121
|
-
>>> state = enable_zoom_pan(ax)
|
|
122
|
-
|
|
123
|
-
References:
|
|
124
|
-
VIS-007
|
|
125
|
-
"""
|
|
126
|
-
if not MATPLOTLIB_AVAILABLE:
|
|
127
|
-
raise ImportError("matplotlib is required for interactive visualization")
|
|
128
|
-
|
|
129
|
-
# Store initial state
|
|
130
|
-
xlim = ax.get_xlim()
|
|
131
|
-
ylim = ax.get_ylim()
|
|
132
|
-
state = ZoomState(
|
|
133
|
-
xlim=xlim,
|
|
134
|
-
ylim=ylim,
|
|
135
|
-
home_xlim=xlim,
|
|
136
|
-
home_ylim=ylim,
|
|
137
|
-
)
|
|
95
|
+
def _create_scroll_handler(ax: Axes, state: ZoomState, zoom_factor: float) -> Any:
|
|
96
|
+
"""Create scroll event handler for zooming."""
|
|
138
97
|
|
|
139
98
|
def on_scroll(event): # type: ignore[no-untyped-def]
|
|
140
99
|
if event.inaxes != ax:
|
|
141
100
|
return
|
|
142
101
|
|
|
143
|
-
# Get mouse position
|
|
144
102
|
x_data = event.xdata
|
|
145
103
|
y_data = event.ydata
|
|
146
104
|
|
|
147
105
|
if x_data is None or y_data is None:
|
|
148
106
|
return
|
|
149
107
|
|
|
150
|
-
# Determine zoom direction
|
|
151
108
|
if event.button == "up":
|
|
152
109
|
factor = 1 / zoom_factor
|
|
153
110
|
elif event.button == "down":
|
|
@@ -155,10 +112,8 @@ def enable_zoom_pan(
|
|
|
155
112
|
else:
|
|
156
113
|
return
|
|
157
114
|
|
|
158
|
-
# Save current state
|
|
159
115
|
state.history.append((state.xlim, state.ylim))
|
|
160
116
|
|
|
161
|
-
# Calculate new limits centered on mouse position
|
|
162
117
|
cur_xlim = ax.get_xlim()
|
|
163
118
|
cur_ylim = ax.get_ylim()
|
|
164
119
|
|
|
@@ -184,17 +139,18 @@ def enable_zoom_pan(
|
|
|
184
139
|
|
|
185
140
|
ax.figure.canvas.draw_idle()
|
|
186
141
|
|
|
187
|
-
|
|
188
|
-
|
|
142
|
+
return on_scroll
|
|
143
|
+
|
|
189
144
|
|
|
190
|
-
|
|
145
|
+
def _create_pan_handlers(ax: Axes, state: ZoomState) -> tuple[Any, Any, Any]:
|
|
146
|
+
"""Create pan event handlers for click-drag panning."""
|
|
191
147
|
pan_active = [False]
|
|
192
148
|
pan_start: list[float | None] = [None, None]
|
|
193
149
|
|
|
194
150
|
def on_press(event): # type: ignore[no-untyped-def]
|
|
195
151
|
if event.inaxes != ax:
|
|
196
152
|
return
|
|
197
|
-
if event.button == 1:
|
|
153
|
+
if event.button == 1:
|
|
198
154
|
pan_active[0] = True
|
|
199
155
|
pan_start[0] = event.xdata
|
|
200
156
|
pan_start[1] = event.ydata
|
|
@@ -228,10 +184,56 @@ def enable_zoom_pan(
|
|
|
228
184
|
|
|
229
185
|
ax.figure.canvas.draw_idle()
|
|
230
186
|
|
|
187
|
+
return on_press, on_release, on_motion
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def enable_zoom_pan(
|
|
191
|
+
ax: Axes,
|
|
192
|
+
*,
|
|
193
|
+
enable_zoom: bool = True,
|
|
194
|
+
enable_pan: bool = True,
|
|
195
|
+
zoom_factor: float = 1.5,
|
|
196
|
+
) -> ZoomState:
|
|
197
|
+
"""Enable interactive zoom and pan on an axes.
|
|
198
|
+
|
|
199
|
+
Adds scroll wheel zoom and click-drag pan functionality.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
ax: Matplotlib axes to enable zoom/pan on.
|
|
203
|
+
enable_zoom: Enable scroll wheel zoom.
|
|
204
|
+
enable_pan: Enable click-drag pan.
|
|
205
|
+
zoom_factor: Zoom factor per scroll step.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
ZoomState object tracking zoom history.
|
|
209
|
+
|
|
210
|
+
Raises:
|
|
211
|
+
ImportError: If matplotlib is not available.
|
|
212
|
+
|
|
213
|
+
Example:
|
|
214
|
+
>>> fig, ax = plt.subplots()
|
|
215
|
+
>>> ax.plot(trace.time_vector, trace.data)
|
|
216
|
+
>>> state = enable_zoom_pan(ax)
|
|
217
|
+
|
|
218
|
+
References:
|
|
219
|
+
VIS-007
|
|
220
|
+
"""
|
|
221
|
+
if not MATPLOTLIB_AVAILABLE:
|
|
222
|
+
raise ImportError("matplotlib is required for interactive visualization")
|
|
223
|
+
|
|
224
|
+
xlim = ax.get_xlim()
|
|
225
|
+
ylim = ax.get_ylim()
|
|
226
|
+
state = ZoomState(xlim=xlim, ylim=ylim, home_xlim=xlim, home_ylim=ylim)
|
|
227
|
+
|
|
228
|
+
if enable_zoom:
|
|
229
|
+
on_scroll = _create_scroll_handler(ax, state, zoom_factor)
|
|
230
|
+
ax.figure.canvas.mpl_connect("scroll_event", on_scroll)
|
|
231
|
+
|
|
231
232
|
if enable_pan:
|
|
233
|
+
on_press, on_release, on_motion = _create_pan_handlers(ax, state)
|
|
232
234
|
ax.figure.canvas.mpl_connect("button_press_event", on_press)
|
|
233
|
-
ax.figure.canvas.mpl_connect("button_release_event", on_release)
|
|
234
|
-
ax.figure.canvas.mpl_connect("motion_notify_event", on_motion)
|
|
235
|
+
ax.figure.canvas.mpl_connect("button_release_event", on_release)
|
|
236
|
+
ax.figure.canvas.mpl_connect("motion_notify_event", on_motion)
|
|
235
237
|
|
|
236
238
|
return state
|
|
237
239
|
|
|
@@ -339,42 +341,70 @@ def add_measurement_cursors(
|
|
|
339
341
|
if not MATPLOTLIB_AVAILABLE:
|
|
340
342
|
raise ImportError("matplotlib is required for interactive visualization")
|
|
341
343
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
344
|
+
# Setup: initialize state
|
|
345
|
+
state = _create_cursor_state()
|
|
346
|
+
|
|
347
|
+
# Processing: create selector with callback
|
|
348
|
+
onselect_callback = _create_cursor_select_handler(ax, state)
|
|
349
|
+
span = SpanSelector(
|
|
350
|
+
ax,
|
|
351
|
+
onselect_callback,
|
|
352
|
+
"horizontal",
|
|
353
|
+
useblit=True,
|
|
354
|
+
props={"alpha": 0.3, "facecolor": color},
|
|
355
|
+
interactive=True,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
# Formatting: create measurement accessor
|
|
359
|
+
get_measurement = _create_measurement_getter(state)
|
|
360
|
+
|
|
361
|
+
return {"span": span, "state": state, "get_measurement": get_measurement}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _create_cursor_state() -> dict[str, float | None | Any]:
|
|
365
|
+
"""Create cursor state dictionary.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
State dictionary with x/y positions and line references.
|
|
369
|
+
"""
|
|
370
|
+
return {"x1": None, "x2": None, "y1": None, "y2": None, "line1": None, "line2": None}
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _create_cursor_select_handler(ax: Axes, state: dict[str, float | None | Any]) -> Any:
|
|
374
|
+
"""Create cursor selection callback.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
ax: Axes to interpolate data from.
|
|
378
|
+
state: Cursor state dictionary.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
Selection callback function.
|
|
382
|
+
"""
|
|
350
383
|
|
|
351
384
|
def onselect(xmin: float, xmax: float) -> None:
|
|
352
385
|
state["x1"] = xmin
|
|
353
386
|
state["x2"] = xmax
|
|
354
387
|
|
|
355
|
-
# Get Y values at cursor positions
|
|
356
388
|
for line in ax.get_lines():
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
# Type narrowing: these return ArrayLike from Line2D
|
|
360
|
-
xdata_arr = np.asarray(xdata)
|
|
361
|
-
ydata_arr = np.asarray(ydata)
|
|
389
|
+
xdata_arr = np.asarray(line.get_xdata())
|
|
390
|
+
ydata_arr = np.asarray(line.get_ydata())
|
|
362
391
|
if len(xdata_arr) > 0:
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
y2_interp: float = float(np.interp(xmax, xdata_arr, ydata_arr))
|
|
366
|
-
state["y1"] = y1_interp
|
|
367
|
-
state["y2"] = y2_interp
|
|
392
|
+
state["y1"] = float(np.interp(xmin, xdata_arr, ydata_arr))
|
|
393
|
+
state["y2"] = float(np.interp(xmax, xdata_arr, ydata_arr))
|
|
368
394
|
break
|
|
369
395
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
396
|
+
return onselect
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _create_measurement_getter(state: dict[str, float | None | Any]) -> Any:
|
|
400
|
+
"""Create measurement getter function.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
state: Cursor state dictionary.
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
Function that returns CursorMeasurement or None.
|
|
407
|
+
"""
|
|
378
408
|
|
|
379
409
|
def get_measurement() -> CursorMeasurement | None:
|
|
380
410
|
x1 = state["x1"]
|
|
@@ -406,11 +436,7 @@ def add_measurement_cursors(
|
|
|
406
436
|
slope=delta_y / delta_x if delta_x != 0 else None,
|
|
407
437
|
)
|
|
408
438
|
|
|
409
|
-
return
|
|
410
|
-
"span": span,
|
|
411
|
-
"state": state,
|
|
412
|
-
"get_measurement": get_measurement,
|
|
413
|
-
}
|
|
439
|
+
return get_measurement
|
|
414
440
|
|
|
415
441
|
|
|
416
442
|
def plot_phase(
|
|
@@ -505,6 +531,7 @@ def plot_bode(
|
|
|
505
531
|
*,
|
|
506
532
|
magnitude_db: bool = True,
|
|
507
533
|
phase_degrees: bool = True,
|
|
534
|
+
show_margins: bool = False,
|
|
508
535
|
fig: Figure | None = None,
|
|
509
536
|
**plot_kwargs: Any,
|
|
510
537
|
) -> Figure:
|
|
@@ -520,6 +547,7 @@ def plot_bode(
|
|
|
520
547
|
phase: Phase array in radians (optional). Ignored if magnitude is complex.
|
|
521
548
|
magnitude_db: If True, magnitude is already in dB. Ignored if complex input.
|
|
522
549
|
phase_degrees: If True, convert phase to degrees.
|
|
550
|
+
show_margins: If True, annotate stability margins (currently unused, reserved for future).
|
|
523
551
|
fig: Existing figure to plot on.
|
|
524
552
|
**plot_kwargs: Additional arguments to plot().
|
|
525
553
|
|
|
@@ -649,35 +677,81 @@ def plot_waterfall(
|
|
|
649
677
|
raise ImportError("matplotlib is required for interactive visualization")
|
|
650
678
|
|
|
651
679
|
data = np.asarray(data)
|
|
680
|
+
Sxx_db, frequencies, times = _prepare_waterfall_data(
|
|
681
|
+
data, time_axis, freq_axis, sample_rate, nperseg, noverlap
|
|
682
|
+
)
|
|
683
|
+
fig, ax = _create_waterfall_figure(ax)
|
|
684
|
+
T, F = np.meshgrid(times, frequencies)
|
|
685
|
+
Sxx_db = _align_waterfall_dimensions(Sxx_db, T)
|
|
686
|
+
surf = _plot_waterfall_surface(ax, T, F, Sxx_db, cmap)
|
|
687
|
+
_format_waterfall_axes(ax, fig, surf)
|
|
688
|
+
|
|
689
|
+
return fig, ax
|
|
690
|
+
|
|
652
691
|
|
|
653
|
-
|
|
692
|
+
def _prepare_waterfall_data(
|
|
693
|
+
data: NDArray[np.floating[Any]],
|
|
694
|
+
time_axis: NDArray[np.floating[Any]] | None,
|
|
695
|
+
freq_axis: NDArray[np.floating[Any]] | None,
|
|
696
|
+
sample_rate: float,
|
|
697
|
+
nperseg: int,
|
|
698
|
+
noverlap: int | None,
|
|
699
|
+
) -> tuple[NDArray[np.floating[Any]], NDArray[np.floating[Any]], NDArray[np.floating[Any]]]:
|
|
700
|
+
"""Prepare spectrogram data for waterfall plot.
|
|
701
|
+
|
|
702
|
+
Args:
|
|
703
|
+
data: Input data array.
|
|
704
|
+
time_axis: Time axis.
|
|
705
|
+
freq_axis: Frequency axis.
|
|
706
|
+
sample_rate: Sample rate.
|
|
707
|
+
nperseg: Segment length.
|
|
708
|
+
noverlap: Overlap length.
|
|
709
|
+
|
|
710
|
+
Returns:
|
|
711
|
+
Tuple of (Sxx_db, frequencies, times).
|
|
712
|
+
"""
|
|
654
713
|
if data.ndim == 2:
|
|
655
|
-
# Treat as precomputed spectrogram (n_traces, n_points)
|
|
656
714
|
Sxx_db = data
|
|
657
715
|
n_traces, n_points = data.shape
|
|
658
|
-
frequencies =
|
|
659
|
-
|
|
716
|
+
frequencies: NDArray[np.floating[Any]] = (
|
|
717
|
+
freq_axis if freq_axis is not None else np.arange(n_points, dtype=np.float64)
|
|
718
|
+
)
|
|
719
|
+
times: NDArray[np.floating[Any]] = (
|
|
720
|
+
time_axis if time_axis is not None else np.arange(n_traces, dtype=np.float64)
|
|
721
|
+
)
|
|
660
722
|
elif freq_axis is not None:
|
|
661
|
-
# 1D data with explicit freq_axis means precomputed
|
|
662
723
|
Sxx_db = data
|
|
663
724
|
frequencies = freq_axis
|
|
664
725
|
times = (
|
|
665
726
|
time_axis
|
|
666
727
|
if time_axis is not None
|
|
667
|
-
else np.arange(Sxx_db.shape[1] if Sxx_db.ndim > 1 else 1)
|
|
728
|
+
else np.arange(Sxx_db.shape[1] if Sxx_db.ndim > 1 else 1, dtype=np.float64)
|
|
668
729
|
)
|
|
669
730
|
else:
|
|
670
|
-
# Compute spectrogram from 1D signal
|
|
671
731
|
if noverlap is None:
|
|
672
732
|
noverlap = nperseg // 2
|
|
673
|
-
|
|
674
|
-
frequencies, times, Sxx = scipy_signal.spectrogram(
|
|
733
|
+
frequencies_raw, times_raw, Sxx = scipy_signal.spectrogram(
|
|
675
734
|
data, fs=sample_rate, nperseg=nperseg, noverlap=noverlap
|
|
676
735
|
)
|
|
736
|
+
frequencies = np.asarray(frequencies_raw, dtype=np.float64)
|
|
677
737
|
Sxx_db = 10 * np.log10(Sxx + 1e-10)
|
|
678
|
-
times = time_axis if time_axis is not None else np.arange(Sxx_db.shape[1])
|
|
738
|
+
times = time_axis if time_axis is not None else np.arange(Sxx_db.shape[1], dtype=np.float64)
|
|
679
739
|
|
|
680
|
-
|
|
740
|
+
return Sxx_db, frequencies, times
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def _create_waterfall_figure(ax: Axes | None) -> tuple[Figure, Axes]:
|
|
744
|
+
"""Create 3D figure for waterfall plot.
|
|
745
|
+
|
|
746
|
+
Args:
|
|
747
|
+
ax: Existing axes or None.
|
|
748
|
+
|
|
749
|
+
Returns:
|
|
750
|
+
Tuple of (figure, axes).
|
|
751
|
+
|
|
752
|
+
Raises:
|
|
753
|
+
ValueError: If axes has no figure.
|
|
754
|
+
"""
|
|
681
755
|
if ax is None:
|
|
682
756
|
fig = plt.figure(figsize=(12, 8))
|
|
683
757
|
ax = fig.add_subplot(111, projection="3d")
|
|
@@ -686,41 +760,71 @@ def plot_waterfall(
|
|
|
686
760
|
if fig_temp is None:
|
|
687
761
|
raise ValueError("Axes must have an associated figure")
|
|
688
762
|
fig = cast("Figure", fig_temp)
|
|
763
|
+
return fig, ax
|
|
689
764
|
|
|
690
|
-
# Create meshgrid
|
|
691
|
-
T, F = np.meshgrid(times, frequencies)
|
|
692
765
|
|
|
693
|
-
|
|
766
|
+
def _align_waterfall_dimensions(
|
|
767
|
+
Sxx_db: NDArray[np.floating[Any]], T: NDArray[np.floating[Any]]
|
|
768
|
+
) -> NDArray[np.floating[Any]]:
|
|
769
|
+
"""Align spectrogram dimensions to match meshgrid.
|
|
770
|
+
|
|
771
|
+
Args:
|
|
772
|
+
Sxx_db: Spectrogram data.
|
|
773
|
+
T: Meshgrid time array.
|
|
774
|
+
|
|
775
|
+
Returns:
|
|
776
|
+
Aligned spectrogram.
|
|
777
|
+
"""
|
|
694
778
|
if Sxx_db.shape != T.shape:
|
|
695
779
|
if Sxx_db.T.shape == T.shape:
|
|
696
780
|
Sxx_db = Sxx_db.T
|
|
697
|
-
|
|
698
|
-
|
|
781
|
+
return Sxx_db
|
|
782
|
+
|
|
699
783
|
|
|
700
|
-
|
|
701
|
-
|
|
784
|
+
def _plot_waterfall_surface(
|
|
785
|
+
ax: Axes,
|
|
786
|
+
T: NDArray[np.floating[Any]],
|
|
787
|
+
F: NDArray[np.floating[Any]],
|
|
788
|
+
Sxx_db: NDArray[np.floating[Any]],
|
|
789
|
+
cmap: str,
|
|
790
|
+
) -> Any:
|
|
791
|
+
"""Plot waterfall surface.
|
|
792
|
+
|
|
793
|
+
Args:
|
|
794
|
+
ax: 3D axes.
|
|
795
|
+
T: Time meshgrid.
|
|
796
|
+
F: Frequency meshgrid.
|
|
797
|
+
Sxx_db: Spectrogram data.
|
|
798
|
+
cmap: Colormap.
|
|
799
|
+
|
|
800
|
+
Returns:
|
|
801
|
+
Surface object.
|
|
802
|
+
|
|
803
|
+
Raises:
|
|
804
|
+
TypeError: If axes is not 3D.
|
|
805
|
+
"""
|
|
702
806
|
if not hasattr(ax, "plot_surface"):
|
|
703
807
|
raise TypeError("Axes must be a 3D axes for waterfall plot")
|
|
704
|
-
|
|
705
|
-
T,
|
|
706
|
-
F,
|
|
707
|
-
Sxx_db,
|
|
708
|
-
cmap=cmap,
|
|
709
|
-
linewidth=0,
|
|
710
|
-
antialiased=True,
|
|
711
|
-
alpha=0.8,
|
|
808
|
+
return cast("Any", ax).plot_surface(
|
|
809
|
+
T, F, Sxx_db, cmap=cmap, linewidth=0, antialiased=True, alpha=0.8
|
|
712
810
|
)
|
|
713
811
|
|
|
812
|
+
|
|
813
|
+
def _format_waterfall_axes(ax: Axes, fig: Figure, surf: Any) -> None:
|
|
814
|
+
"""Format waterfall plot axes.
|
|
815
|
+
|
|
816
|
+
Args:
|
|
817
|
+
ax: 3D axes.
|
|
818
|
+
fig: Figure object.
|
|
819
|
+
surf: Surface object.
|
|
820
|
+
"""
|
|
714
821
|
ax.set_xlabel("Time (s)")
|
|
715
822
|
ax.set_ylabel("Frequency (Hz)")
|
|
716
823
|
if hasattr(ax, "set_zlabel"):
|
|
717
|
-
ax.set_zlabel("Power (dB)")
|
|
824
|
+
ax.set_zlabel("Power (dB)")
|
|
718
825
|
ax.set_title("Waterfall Plot (Spectrogram)")
|
|
719
|
-
|
|
720
826
|
fig.colorbar(surf, ax=ax, label="Power (dB)", shrink=0.5)
|
|
721
827
|
|
|
722
|
-
return fig, ax
|
|
723
|
-
|
|
724
828
|
|
|
725
829
|
def plot_histogram(
|
|
726
830
|
trace: WaveformTrace | NDArray[np.floating[Any]],
|
|
@@ -767,10 +871,30 @@ def plot_histogram(
|
|
|
767
871
|
if not MATPLOTLIB_AVAILABLE:
|
|
768
872
|
raise ImportError("matplotlib is required for interactive visualization")
|
|
769
873
|
|
|
770
|
-
# Get data
|
|
771
874
|
data = trace.data if isinstance(trace, WaveformTrace) else np.asarray(trace)
|
|
875
|
+
fig, ax = _setup_histogram_figure(ax)
|
|
876
|
+
stats = _calculate_histogram_statistics(data)
|
|
877
|
+
bin_edges = _plot_histogram_data(ax, data, bins, density, hist_kwargs)
|
|
878
|
+
stats["bins"] = len(bin_edges) - 1
|
|
879
|
+
_add_histogram_overlays(ax, data, stats, bin_edges, density, show_stats, show_kde)
|
|
880
|
+
_format_histogram_axes(ax, density, show_stats, show_kde)
|
|
881
|
+
_handle_histogram_output(fig, save_path, show)
|
|
772
882
|
|
|
773
|
-
|
|
883
|
+
return fig, ax, stats
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def _setup_histogram_figure(ax: Axes | None) -> tuple[Figure, Axes]:
|
|
887
|
+
"""Setup figure and axes for histogram.
|
|
888
|
+
|
|
889
|
+
Args:
|
|
890
|
+
ax: Existing axes or None.
|
|
891
|
+
|
|
892
|
+
Returns:
|
|
893
|
+
Tuple of (figure, axes).
|
|
894
|
+
|
|
895
|
+
Raises:
|
|
896
|
+
ValueError: If axes has no figure.
|
|
897
|
+
"""
|
|
774
898
|
if ax is None:
|
|
775
899
|
fig, ax = plt.subplots(figsize=(10, 6))
|
|
776
900
|
else:
|
|
@@ -778,72 +902,125 @@ def plot_histogram(
|
|
|
778
902
|
if fig_temp is None:
|
|
779
903
|
raise ValueError("Axes must have an associated figure")
|
|
780
904
|
fig = cast("Figure", fig_temp)
|
|
905
|
+
return fig, ax
|
|
781
906
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
"
|
|
794
|
-
"
|
|
907
|
+
|
|
908
|
+
def _calculate_histogram_statistics(data: NDArray[np.floating[Any]]) -> dict[str, Any]:
|
|
909
|
+
"""Calculate histogram statistics.
|
|
910
|
+
|
|
911
|
+
Args:
|
|
912
|
+
data: Data array.
|
|
913
|
+
|
|
914
|
+
Returns:
|
|
915
|
+
Statistics dictionary.
|
|
916
|
+
"""
|
|
917
|
+
return {
|
|
918
|
+
"mean": float(np.mean(data)),
|
|
919
|
+
"std": float(np.std(data)),
|
|
920
|
+
"median": float(np.median(data)),
|
|
921
|
+
"min": float(np.min(data)),
|
|
922
|
+
"max": float(np.max(data)),
|
|
795
923
|
"count": len(data),
|
|
796
924
|
}
|
|
797
925
|
|
|
798
|
-
|
|
926
|
+
|
|
927
|
+
def _plot_histogram_data(
|
|
928
|
+
ax: Axes,
|
|
929
|
+
data: NDArray[np.floating[Any]],
|
|
930
|
+
bins: int | str | NDArray[np.floating[Any]],
|
|
931
|
+
density: bool,
|
|
932
|
+
hist_kwargs: dict[str, Any],
|
|
933
|
+
) -> NDArray[Any]:
|
|
934
|
+
"""Plot histogram data.
|
|
935
|
+
|
|
936
|
+
Args:
|
|
937
|
+
ax: Axes to plot on.
|
|
938
|
+
data: Data array.
|
|
939
|
+
bins: Bin specification.
|
|
940
|
+
density: Normalize to density.
|
|
941
|
+
hist_kwargs: Additional histogram arguments.
|
|
942
|
+
|
|
943
|
+
Returns:
|
|
944
|
+
Bin edges array.
|
|
945
|
+
"""
|
|
799
946
|
defaults: dict[str, Any] = {"alpha": 0.7, "edgecolor": "black", "linewidth": 0.5}
|
|
800
947
|
defaults.update(hist_kwargs)
|
|
801
948
|
_counts, bin_edges, _patches = ax.hist(data, bins=bins, density=density, **defaults) # type: ignore[arg-type]
|
|
949
|
+
return bin_edges
|
|
802
950
|
|
|
803
|
-
stats["bins"] = len(bin_edges) - 1
|
|
804
951
|
|
|
805
|
-
|
|
952
|
+
def _add_histogram_overlays(
|
|
953
|
+
ax: Axes,
|
|
954
|
+
data: NDArray[np.floating[Any]],
|
|
955
|
+
stats: dict[str, Any],
|
|
956
|
+
bin_edges: NDArray[Any],
|
|
957
|
+
density: bool,
|
|
958
|
+
show_stats: bool,
|
|
959
|
+
show_kde: bool,
|
|
960
|
+
) -> None:
|
|
961
|
+
"""Add overlays to histogram.
|
|
962
|
+
|
|
963
|
+
Args:
|
|
964
|
+
ax: Axes object.
|
|
965
|
+
data: Data array.
|
|
966
|
+
stats: Statistics dict.
|
|
967
|
+
bin_edges: Bin edges.
|
|
968
|
+
density: Whether density normalized.
|
|
969
|
+
show_stats: Show statistics lines.
|
|
970
|
+
show_kde: Show KDE overlay.
|
|
971
|
+
"""
|
|
806
972
|
if show_stats:
|
|
807
|
-
|
|
973
|
+
mean, std = stats["mean"], stats["std"]
|
|
808
974
|
ax.axvline(mean, color="red", linestyle="--", linewidth=2, label=f"Mean: {mean:.3g}")
|
|
809
975
|
ax.axvline(mean - std, color="orange", linestyle=":", linewidth=1.5, label="Mean - Std")
|
|
810
976
|
ax.axvline(mean + std, color="orange", linestyle=":", linewidth=1.5, label="Mean + Std")
|
|
811
977
|
|
|
812
|
-
# Show KDE
|
|
813
978
|
if show_kde:
|
|
814
979
|
from scipy.stats import gaussian_kde
|
|
815
980
|
|
|
816
981
|
kde = gaussian_kde(data)
|
|
817
|
-
x_kde = np.linspace(
|
|
982
|
+
x_kde = np.linspace(stats["min"], stats["max"], 200)
|
|
818
983
|
y_kde = kde(x_kde)
|
|
819
984
|
|
|
820
985
|
if density:
|
|
821
986
|
ax.plot(x_kde, y_kde, "r-", linewidth=2, label="KDE")
|
|
822
987
|
else:
|
|
823
|
-
# Scale KDE to histogram
|
|
824
988
|
bin_width = bin_edges[1] - bin_edges[0]
|
|
825
989
|
ax.plot(x_kde, y_kde * len(data) * bin_width, "r-", linewidth=2, label="KDE")
|
|
826
990
|
|
|
991
|
+
|
|
992
|
+
def _format_histogram_axes(ax: Axes, density: bool, show_stats: bool, show_kde: bool) -> None:
|
|
993
|
+
"""Format histogram axes.
|
|
994
|
+
|
|
995
|
+
Args:
|
|
996
|
+
ax: Axes object.
|
|
997
|
+
density: Whether density normalized.
|
|
998
|
+
show_stats: Whether stats shown.
|
|
999
|
+
show_kde: Whether KDE shown.
|
|
1000
|
+
"""
|
|
827
1001
|
ax.set_xlabel("Amplitude")
|
|
828
1002
|
ax.set_ylabel("Density" if density else "Count")
|
|
829
1003
|
ax.set_title("Amplitude Distribution")
|
|
830
|
-
# Only show legend if there are labeled artists
|
|
831
1004
|
if show_stats or show_kde:
|
|
832
1005
|
ax.legend(loc="upper right")
|
|
833
1006
|
ax.grid(True, alpha=0.3)
|
|
834
1007
|
|
|
835
|
-
|
|
1008
|
+
|
|
1009
|
+
def _handle_histogram_output(fig: Figure, save_path: str | None, show: bool) -> None:
|
|
1010
|
+
"""Handle histogram output.
|
|
1011
|
+
|
|
1012
|
+
Args:
|
|
1013
|
+
fig: Figure object.
|
|
1014
|
+
save_path: Save path.
|
|
1015
|
+
show: Whether to show.
|
|
1016
|
+
"""
|
|
836
1017
|
if save_path is not None:
|
|
837
1018
|
fig.savefig(save_path, dpi=150, bbox_inches="tight")
|
|
838
|
-
|
|
839
|
-
# Show or close
|
|
840
1019
|
if show:
|
|
841
1020
|
plt.show()
|
|
842
1021
|
else:
|
|
843
1022
|
plt.close(fig)
|
|
844
1023
|
|
|
845
|
-
return fig, ax, stats
|
|
846
|
-
|
|
847
1024
|
|
|
848
1025
|
__all__ = [
|
|
849
1026
|
"CursorMeasurement",
|