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
|
@@ -323,10 +323,10 @@ def iqr_outliers(
|
|
|
323
323
|
|
|
324
324
|
|
|
325
325
|
def detect_outliers(
|
|
326
|
-
trace: WaveformTrace | NDArray[np.floating[Any]],
|
|
326
|
+
trace: WaveformTrace | NDArray[np.floating[Any]],
|
|
327
327
|
*,
|
|
328
328
|
method: str = "modified_zscore",
|
|
329
|
-
**kwargs: Any,
|
|
329
|
+
**kwargs: Any,
|
|
330
330
|
) -> OutlierResult:
|
|
331
331
|
"""Detect outliers using specified method.
|
|
332
332
|
|
|
@@ -46,6 +46,8 @@ class StreamingStats:
|
|
|
46
46
|
self.m2 = 0.0 # Sum of squared differences from mean
|
|
47
47
|
self.min_val = float("inf")
|
|
48
48
|
self.max_val = float("-inf")
|
|
49
|
+
self.has_pos_inf = False
|
|
50
|
+
self.has_neg_inf = False
|
|
49
51
|
|
|
50
52
|
def update(self, data: NDArray[np.floating[Any]]) -> None:
|
|
51
53
|
"""Update statistics with new data chunk.
|
|
@@ -59,10 +61,19 @@ class StreamingStats:
|
|
|
59
61
|
|
|
60
62
|
for value in data:
|
|
61
63
|
self.count += 1
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
|
|
65
|
+
# Track infinities for proper mean calculation
|
|
66
|
+
if np.isposinf(value):
|
|
67
|
+
self.has_pos_inf = True
|
|
68
|
+
elif np.isneginf(value):
|
|
69
|
+
self.has_neg_inf = True
|
|
70
|
+
|
|
71
|
+
# Only update Welford's algorithm for finite values when no inf present
|
|
72
|
+
if not (self.has_pos_inf or self.has_neg_inf):
|
|
73
|
+
delta = value - self.mean
|
|
74
|
+
self.mean += delta / self.count
|
|
75
|
+
delta2 = value - self.mean
|
|
76
|
+
self.m2 += delta * delta2
|
|
66
77
|
|
|
67
78
|
# Update min/max
|
|
68
79
|
if value < self.min_val:
|
|
@@ -76,6 +87,20 @@ class StreamingStats:
|
|
|
76
87
|
Returns:
|
|
77
88
|
StreamingStatsResult with mean, variance, std, min, max, count.
|
|
78
89
|
"""
|
|
90
|
+
# Handle infinities in mean calculation
|
|
91
|
+
if self.has_pos_inf and self.has_neg_inf:
|
|
92
|
+
# Both +inf and -inf present: mean is undefined (NaN)
|
|
93
|
+
mean = float("nan")
|
|
94
|
+
elif self.has_pos_inf:
|
|
95
|
+
# Only +inf present: mean is +inf
|
|
96
|
+
mean = float("inf")
|
|
97
|
+
elif self.has_neg_inf:
|
|
98
|
+
# Only -inf present: mean is -inf
|
|
99
|
+
mean = float("-inf")
|
|
100
|
+
else:
|
|
101
|
+
# No infinities: use Welford's result
|
|
102
|
+
mean = self.mean
|
|
103
|
+
|
|
79
104
|
if self.count < 2:
|
|
80
105
|
variance = 0.0
|
|
81
106
|
std = 0.0
|
|
@@ -84,7 +109,7 @@ class StreamingStats:
|
|
|
84
109
|
std = np.sqrt(variance)
|
|
85
110
|
|
|
86
111
|
return StreamingStatsResult(
|
|
87
|
-
mean=
|
|
112
|
+
mean=mean,
|
|
88
113
|
variance=variance,
|
|
89
114
|
std=std,
|
|
90
115
|
min=self.min_val if self.min_val != float("inf") else 0.0,
|
oscura/analyzers/validation.py
CHANGED
|
@@ -20,6 +20,8 @@ from typing import TYPE_CHECKING
|
|
|
20
20
|
import numpy as np
|
|
21
21
|
|
|
22
22
|
if TYPE_CHECKING:
|
|
23
|
+
from numpy.typing import NDArray
|
|
24
|
+
|
|
23
25
|
from oscura.core.types import WaveformTrace
|
|
24
26
|
|
|
25
27
|
|
|
@@ -260,40 +262,66 @@ def get_valid_measurements(trace: WaveformTrace) -> list[str]:
|
|
|
260
262
|
... func = getattr(tk, meas_name)
|
|
261
263
|
... result = func(trace)
|
|
262
264
|
"""
|
|
263
|
-
valid = []
|
|
265
|
+
valid: list[str] = []
|
|
266
|
+
|
|
267
|
+
_add_basic_measurements(valid, trace)
|
|
268
|
+
_add_edge_measurements(valid, trace)
|
|
269
|
+
_add_frequency_measurements(valid, trace)
|
|
270
|
+
_add_pulse_measurements(valid, trace)
|
|
271
|
+
_add_overshoot_measurements(valid, trace)
|
|
272
|
+
_add_slew_rate_measurement(valid)
|
|
273
|
+
|
|
274
|
+
return valid
|
|
275
|
+
|
|
264
276
|
|
|
265
|
-
|
|
277
|
+
def _add_basic_measurements(valid: list[str], trace: WaveformTrace) -> None:
|
|
278
|
+
"""Add basic measurements that almost always work.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
valid: List to append to.
|
|
282
|
+
trace: Input trace.
|
|
283
|
+
"""
|
|
266
284
|
if len(trace.data) > 0:
|
|
267
285
|
valid.extend(["mean", "rms"])
|
|
268
|
-
|
|
269
286
|
if len(trace.data) >= 2:
|
|
270
287
|
valid.append("amplitude")
|
|
271
288
|
|
|
272
|
-
# Check edge-based measurements
|
|
273
|
-
suitable, _ = is_suitable_for_rise_time_measurement(trace)
|
|
274
|
-
if suitable:
|
|
275
|
-
valid.append("rise_time")
|
|
276
289
|
|
|
277
|
-
|
|
278
|
-
|
|
290
|
+
def _add_edge_measurements(valid: list[str], trace: WaveformTrace) -> None:
|
|
291
|
+
"""Add edge-based measurements.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
valid: List to append to.
|
|
295
|
+
trace: Input trace.
|
|
296
|
+
"""
|
|
297
|
+
if is_suitable_for_rise_time_measurement(trace)[0]:
|
|
298
|
+
valid.append("rise_time")
|
|
299
|
+
if is_suitable_for_fall_time_measurement(trace)[0]:
|
|
279
300
|
valid.append("fall_time")
|
|
280
301
|
|
|
281
|
-
# Check frequency/period
|
|
282
|
-
suitable, _ = is_suitable_for_frequency_measurement(trace)
|
|
283
|
-
if suitable:
|
|
284
|
-
valid.extend(["frequency", "period"])
|
|
285
302
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
if suitable:
|
|
289
|
-
valid.append("duty_cycle")
|
|
303
|
+
def _add_frequency_measurements(valid: list[str], trace: WaveformTrace) -> None:
|
|
304
|
+
"""Add frequency/period measurements.
|
|
290
305
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
306
|
+
Args:
|
|
307
|
+
valid: List to append to.
|
|
308
|
+
trace: Input trace.
|
|
309
|
+
"""
|
|
310
|
+
if is_suitable_for_frequency_measurement(trace)[0]:
|
|
311
|
+
valid.extend(["frequency", "period"])
|
|
312
|
+
if is_suitable_for_duty_cycle_measurement(trace)[0]:
|
|
313
|
+
valid.append("duty_cycle")
|
|
314
|
+
if is_suitable_for_jitter_measurement(trace)[0]:
|
|
294
315
|
valid.extend(["rms_jitter", "peak_to_peak_jitter"])
|
|
295
316
|
|
|
296
|
-
|
|
317
|
+
|
|
318
|
+
def _add_pulse_measurements(valid: list[str], trace: WaveformTrace) -> None:
|
|
319
|
+
"""Add pulse width measurements.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
valid: List to append to.
|
|
323
|
+
trace: Input trace.
|
|
324
|
+
"""
|
|
297
325
|
from oscura.analyzers.waveform.measurements import _find_edges
|
|
298
326
|
|
|
299
327
|
rising = _find_edges(trace, "rising")
|
|
@@ -302,7 +330,14 @@ def get_valid_measurements(trace: WaveformTrace) -> list[str]:
|
|
|
302
330
|
if len(rising) > 0 and len(falling) > 0:
|
|
303
331
|
valid.append("pulse_width")
|
|
304
332
|
|
|
305
|
-
|
|
333
|
+
|
|
334
|
+
def _add_overshoot_measurements(valid: list[str], trace: WaveformTrace) -> None:
|
|
335
|
+
"""Add overshoot/undershoot measurements.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
valid: List to append to.
|
|
339
|
+
trace: Input trace.
|
|
340
|
+
"""
|
|
306
341
|
from oscura.analyzers.waveform.measurements import _find_levels
|
|
307
342
|
|
|
308
343
|
if len(trace.data) >= 3:
|
|
@@ -310,12 +345,16 @@ def get_valid_measurements(trace: WaveformTrace) -> list[str]:
|
|
|
310
345
|
if high - low > 0:
|
|
311
346
|
valid.extend(["overshoot", "undershoot", "preshoot"])
|
|
312
347
|
|
|
313
|
-
|
|
348
|
+
|
|
349
|
+
def _add_slew_rate_measurement(valid: list[str]) -> None:
|
|
350
|
+
"""Add slew rate if edge measurements available.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
valid: List to check and append to.
|
|
354
|
+
"""
|
|
314
355
|
if "rise_time" in valid or "fall_time" in valid:
|
|
315
356
|
valid.append("slew_rate")
|
|
316
357
|
|
|
317
|
-
return valid
|
|
318
|
-
|
|
319
358
|
|
|
320
359
|
def analyze_signal_characteristics(trace: WaveformTrace) -> dict[str, bool | int | str | list[str]]:
|
|
321
360
|
"""Perform comprehensive signal characteristic analysis.
|
|
@@ -327,59 +366,75 @@ def analyze_signal_characteristics(trace: WaveformTrace) -> dict[str, bool | int
|
|
|
327
366
|
trace: Input waveform trace.
|
|
328
367
|
|
|
329
368
|
Returns:
|
|
330
|
-
Dictionary containing
|
|
331
|
-
|
|
332
|
-
- has_amplitude: bool - signal has variation
|
|
333
|
-
- has_variation: bool - standard deviation > 0
|
|
334
|
-
- has_edges: bool - rising or falling edges detected
|
|
335
|
-
- is_periodic: bool - signal appears periodic
|
|
336
|
-
- edge_count: int - total edges (rising + falling)
|
|
337
|
-
- rising_edge_count: int - number of rising edges
|
|
338
|
-
- falling_edge_count: int - number of falling edges
|
|
339
|
-
- signal_type: str - classified type (dc, periodic_digital, etc.)
|
|
340
|
-
- recommended_measurements: list[str] - suggested measurements
|
|
369
|
+
Dictionary containing signal characteristics including sufficient_samples,
|
|
370
|
+
has_amplitude, has_variation, edge counts, periodicity, signal_type.
|
|
341
371
|
|
|
342
372
|
Example:
|
|
343
373
|
>>> chars = analyze_signal_characteristics(trace)
|
|
344
374
|
>>> if chars['is_periodic']:
|
|
345
375
|
... print("Signal is periodic")
|
|
346
|
-
... print(f"Frequency measurement recommended: {'frequency' in chars['recommended_measurements']}")
|
|
347
376
|
"""
|
|
348
377
|
from oscura.analyzers.waveform.measurements import _find_edges
|
|
349
378
|
|
|
350
379
|
data = trace.data
|
|
380
|
+
characteristics = _init_characteristics(data)
|
|
381
|
+
|
|
382
|
+
if not characteristics["has_variation"]:
|
|
383
|
+
characteristics["signal_type"] = "dc"
|
|
384
|
+
characteristics["recommended_measurements"] = ["mean", "rms"]
|
|
385
|
+
return characteristics
|
|
386
|
+
|
|
387
|
+
rising_edges = _find_edges(trace, "rising")
|
|
388
|
+
falling_edges = _find_edges(trace, "falling")
|
|
389
|
+
_update_edge_counts(characteristics, rising_edges, falling_edges)
|
|
390
|
+
_check_periodicity(characteristics, rising_edges)
|
|
391
|
+
_classify_signal_type(characteristics, data, len(data))
|
|
392
|
+
|
|
393
|
+
characteristics["recommended_measurements"] = get_valid_measurements(trace)
|
|
394
|
+
|
|
395
|
+
return characteristics
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _init_characteristics(data: NDArray[np.float64]) -> dict[str, bool | int | str | list[str]]:
|
|
399
|
+
"""Initialize characteristics dictionary with basic signal properties.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
data: Signal data array.
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
Dictionary with initial characteristics.
|
|
406
|
+
"""
|
|
351
407
|
n = len(data)
|
|
408
|
+
std = np.std(data)
|
|
409
|
+
amplitude = np.max(data) - np.min(data)
|
|
352
410
|
|
|
353
|
-
characteristics: dict[str,
|
|
354
|
-
"sufficient_samples": n >= 16,
|
|
355
|
-
"has_amplitude":
|
|
356
|
-
"has_variation":
|
|
357
|
-
"has_edges": False,
|
|
358
|
-
"is_periodic": False,
|
|
411
|
+
characteristics: dict[str, int | str | list[str]] = {
|
|
412
|
+
"sufficient_samples": int(n >= 16),
|
|
413
|
+
"has_amplitude": int(amplitude > 1e-12),
|
|
414
|
+
"has_variation": int(std > 1e-12),
|
|
415
|
+
"has_edges": int(False),
|
|
416
|
+
"is_periodic": int(False),
|
|
359
417
|
"edge_count": 0,
|
|
360
418
|
"rising_edge_count": 0,
|
|
361
419
|
"falling_edge_count": 0,
|
|
362
420
|
"signal_type": "unknown",
|
|
363
421
|
"recommended_measurements": [],
|
|
364
422
|
}
|
|
423
|
+
return characteristics
|
|
365
424
|
|
|
366
|
-
# Check variation
|
|
367
|
-
std = np.std(data)
|
|
368
|
-
characteristics["has_variation"] = std > 1e-12
|
|
369
|
-
|
|
370
|
-
# Check amplitude
|
|
371
|
-
amplitude = np.max(data) - np.min(data)
|
|
372
|
-
characteristics["has_amplitude"] = amplitude > 1e-12
|
|
373
|
-
|
|
374
|
-
if not characteristics["has_variation"]:
|
|
375
|
-
characteristics["signal_type"] = "dc"
|
|
376
|
-
characteristics["recommended_measurements"] = ["mean", "rms"]
|
|
377
|
-
return characteristics
|
|
378
425
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
426
|
+
def _update_edge_counts(
|
|
427
|
+
characteristics: dict[str, bool | int | str | list[str]],
|
|
428
|
+
rising_edges: NDArray[np.float64],
|
|
429
|
+
falling_edges: NDArray[np.float64],
|
|
430
|
+
) -> None:
|
|
431
|
+
"""Update edge count statistics in characteristics dictionary.
|
|
382
432
|
|
|
433
|
+
Args:
|
|
434
|
+
characteristics: Dictionary to update.
|
|
435
|
+
rising_edges: Array of rising edge indices.
|
|
436
|
+
falling_edges: Array of falling edge indices.
|
|
437
|
+
"""
|
|
383
438
|
rising_edge_count = len(rising_edges)
|
|
384
439
|
falling_edge_count = len(falling_edges)
|
|
385
440
|
edge_count = rising_edge_count + falling_edge_count
|
|
@@ -389,7 +444,16 @@ def analyze_signal_characteristics(trace: WaveformTrace) -> dict[str, bool | int
|
|
|
389
444
|
characteristics["edge_count"] = edge_count
|
|
390
445
|
characteristics["has_edges"] = edge_count > 0
|
|
391
446
|
|
|
392
|
-
|
|
447
|
+
|
|
448
|
+
def _check_periodicity(
|
|
449
|
+
characteristics: dict[str, bool | int | str | list[str]], rising_edges: NDArray[np.float64]
|
|
450
|
+
) -> None:
|
|
451
|
+
"""Check if signal is periodic based on edge spacing.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
characteristics: Dictionary to update.
|
|
455
|
+
rising_edges: Array of rising edge indices.
|
|
456
|
+
"""
|
|
393
457
|
if len(rising_edges) >= 3:
|
|
394
458
|
periods = np.diff(rising_edges)
|
|
395
459
|
period_cv = np.std(periods) / np.mean(periods) if np.mean(periods) > 0 else float("inf")
|
|
@@ -397,30 +461,45 @@ def analyze_signal_characteristics(trace: WaveformTrace) -> dict[str, bool | int
|
|
|
397
461
|
if period_cv < 0.2: # Less than 20% variation
|
|
398
462
|
characteristics["is_periodic"] = True
|
|
399
463
|
|
|
400
|
-
|
|
464
|
+
|
|
465
|
+
def _classify_signal_type(
|
|
466
|
+
characteristics: dict[str, bool | int | str | list[str]], data: NDArray[np.float64], n: int
|
|
467
|
+
) -> None:
|
|
468
|
+
"""Classify signal type based on characteristics.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
characteristics: Dictionary to update.
|
|
472
|
+
data: Signal data array.
|
|
473
|
+
n: Number of samples.
|
|
474
|
+
"""
|
|
401
475
|
if not characteristics["has_edges"]:
|
|
402
|
-
|
|
403
|
-
if n >= 16:
|
|
404
|
-
fft_result = np.abs(np.fft.rfft(data - np.mean(data)))
|
|
405
|
-
peak_power = np.max(fft_result[1:]) if len(fft_result) > 1 else 0
|
|
406
|
-
avg_power = np.mean(fft_result[1:]) if len(fft_result) > 1 else 0
|
|
407
|
-
|
|
408
|
-
if peak_power > 10 * avg_power:
|
|
409
|
-
characteristics["signal_type"] = "periodic_analog"
|
|
410
|
-
else:
|
|
411
|
-
characteristics["signal_type"] = "noise"
|
|
412
|
-
else:
|
|
413
|
-
characteristics["signal_type"] = "unknown"
|
|
476
|
+
characteristics["signal_type"] = _classify_no_edge_signal(data, n)
|
|
414
477
|
elif characteristics["is_periodic"]:
|
|
415
478
|
characteristics["signal_type"] = "periodic_digital"
|
|
416
479
|
else:
|
|
417
480
|
characteristics["signal_type"] = "aperiodic_digital"
|
|
418
481
|
|
|
419
|
-
# Recommend measurements
|
|
420
|
-
recommended = get_valid_measurements(trace)
|
|
421
|
-
characteristics["recommended_measurements"] = recommended
|
|
422
482
|
|
|
423
|
-
|
|
483
|
+
def _classify_no_edge_signal(data: NDArray[np.float64], n: int) -> str:
|
|
484
|
+
"""Classify signal without edges (analog or noise).
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
data: Signal data array.
|
|
488
|
+
n: Number of samples.
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
Signal type classification string.
|
|
492
|
+
"""
|
|
493
|
+
if n >= 16:
|
|
494
|
+
fft_result = np.abs(np.fft.rfft(data - np.mean(data)))
|
|
495
|
+
peak_power = np.max(fft_result[1:]) if len(fft_result) > 1 else 0
|
|
496
|
+
avg_power = np.mean(fft_result[1:]) if len(fft_result) > 1 else 0
|
|
497
|
+
|
|
498
|
+
if peak_power > 10 * avg_power:
|
|
499
|
+
return "periodic_analog"
|
|
500
|
+
else:
|
|
501
|
+
return "noise"
|
|
502
|
+
return "unknown"
|
|
424
503
|
|
|
425
504
|
|
|
426
505
|
def get_measurement_requirements(measurement_name: str) -> dict[str, str | int | list[str]]:
|
|
@@ -442,7 +521,24 @@ def get_measurement_requirements(measurement_name: str) -> dict[str, str | int |
|
|
|
442
521
|
>>> print(f"Minimum samples: {reqs['min_samples']}")
|
|
443
522
|
>>> print(f"Required features: {', '.join(reqs['required_features'])}")
|
|
444
523
|
"""
|
|
445
|
-
requirements =
|
|
524
|
+
requirements = _get_all_measurement_requirements()
|
|
525
|
+
default = _get_default_measurement_requirements()
|
|
526
|
+
return requirements.get(measurement_name, default)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _get_all_measurement_requirements() -> dict[str, dict[str, str | int | list[str]]]:
|
|
530
|
+
"""Get complete measurement requirements dictionary."""
|
|
531
|
+
timing_reqs = _get_timing_measurement_requirements()
|
|
532
|
+
amplitude_reqs = _get_amplitude_measurement_requirements()
|
|
533
|
+
jitter_reqs = _get_jitter_measurement_requirements()
|
|
534
|
+
statistical_reqs = _get_statistical_measurement_requirements()
|
|
535
|
+
|
|
536
|
+
return {**timing_reqs, **amplitude_reqs, **jitter_reqs, **statistical_reqs}
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _get_timing_measurement_requirements() -> dict[str, dict[str, str | int | list[str]]]:
|
|
540
|
+
"""Get requirements for timing-related measurements."""
|
|
541
|
+
return {
|
|
446
542
|
"frequency": {
|
|
447
543
|
"description": "Measures the repetition rate of a periodic signal",
|
|
448
544
|
"min_samples": 3,
|
|
@@ -509,6 +605,19 @@ def get_measurement_requirements(measurement_name: str) -> dict[str, str | int |
|
|
|
509
605
|
"Incomplete pulses",
|
|
510
606
|
],
|
|
511
607
|
},
|
|
608
|
+
"slew_rate": {
|
|
609
|
+
"description": "Measures dV/dt during transitions",
|
|
610
|
+
"min_samples": 3,
|
|
611
|
+
"required_signal_types": ["periodic_digital", "aperiodic_digital"],
|
|
612
|
+
"required_features": ["edges", "amplitude"],
|
|
613
|
+
"common_nan_causes": ["No edges", "No amplitude", "DC signal"],
|
|
614
|
+
},
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def _get_amplitude_measurement_requirements() -> dict[str, dict[str, str | int | list[str]]]:
|
|
619
|
+
"""Get requirements for amplitude-related measurements."""
|
|
620
|
+
return {
|
|
512
621
|
"amplitude": {
|
|
513
622
|
"description": "Measures peak-to-peak voltage",
|
|
514
623
|
"min_samples": 2,
|
|
@@ -516,20 +625,6 @@ def get_measurement_requirements(measurement_name: str) -> dict[str, str | int |
|
|
|
516
625
|
"required_features": [],
|
|
517
626
|
"common_nan_causes": ["Fewer than 2 samples"],
|
|
518
627
|
},
|
|
519
|
-
"mean": {
|
|
520
|
-
"description": "Calculates DC level (average voltage)",
|
|
521
|
-
"min_samples": 1,
|
|
522
|
-
"required_signal_types": ["all"],
|
|
523
|
-
"required_features": [],
|
|
524
|
-
"common_nan_causes": ["No data"],
|
|
525
|
-
},
|
|
526
|
-
"rms": {
|
|
527
|
-
"description": "Calculates root-mean-square voltage",
|
|
528
|
-
"min_samples": 1,
|
|
529
|
-
"required_signal_types": ["all"],
|
|
530
|
-
"required_features": [],
|
|
531
|
-
"common_nan_causes": ["No data"],
|
|
532
|
-
},
|
|
533
628
|
"overshoot": {
|
|
534
629
|
"description": "Measures overshoot above high level",
|
|
535
630
|
"min_samples": 3,
|
|
@@ -544,13 +639,12 @@ def get_measurement_requirements(measurement_name: str) -> dict[str, str | int |
|
|
|
544
639
|
"required_features": ["amplitude"],
|
|
545
640
|
"common_nan_causes": ["No amplitude", "DC signal"],
|
|
546
641
|
},
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
},
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _get_jitter_measurement_requirements() -> dict[str, dict[str, str | int | list[str]]]:
|
|
646
|
+
"""Get requirements for jitter measurements."""
|
|
647
|
+
return {
|
|
554
648
|
"rms_jitter": {
|
|
555
649
|
"description": "Measures timing uncertainty (RMS)",
|
|
556
650
|
"min_samples": 3,
|
|
@@ -575,7 +669,30 @@ def get_measurement_requirements(measurement_name: str) -> dict[str, str | int |
|
|
|
575
669
|
},
|
|
576
670
|
}
|
|
577
671
|
|
|
578
|
-
|
|
672
|
+
|
|
673
|
+
def _get_statistical_measurement_requirements() -> dict[str, dict[str, str | int | list[str]]]:
|
|
674
|
+
"""Get requirements for statistical measurements."""
|
|
675
|
+
return {
|
|
676
|
+
"mean": {
|
|
677
|
+
"description": "Calculates DC level (average voltage)",
|
|
678
|
+
"min_samples": 1,
|
|
679
|
+
"required_signal_types": ["all"],
|
|
680
|
+
"required_features": [],
|
|
681
|
+
"common_nan_causes": ["No data"],
|
|
682
|
+
},
|
|
683
|
+
"rms": {
|
|
684
|
+
"description": "Calculates root-mean-square voltage",
|
|
685
|
+
"min_samples": 1,
|
|
686
|
+
"required_signal_types": ["all"],
|
|
687
|
+
"required_features": [],
|
|
688
|
+
"common_nan_causes": ["No data"],
|
|
689
|
+
},
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def _get_default_measurement_requirements() -> dict[str, str | int | list[str]]:
|
|
694
|
+
"""Get default requirements for undocumented measurements."""
|
|
695
|
+
return {
|
|
579
696
|
"description": "Measurement not documented",
|
|
580
697
|
"min_samples": 1,
|
|
581
698
|
"required_signal_types": ["unknown"],
|
|
@@ -583,8 +700,6 @@ def get_measurement_requirements(measurement_name: str) -> dict[str, str | int |
|
|
|
583
700
|
"common_nan_causes": ["Check measurement documentation"],
|
|
584
701
|
}
|
|
585
702
|
|
|
586
|
-
return requirements.get(measurement_name, default) # type: ignore[return-value]
|
|
587
|
-
|
|
588
703
|
|
|
589
704
|
__all__ = [
|
|
590
705
|
"analyze_signal_characteristics",
|
|
@@ -789,9 +789,18 @@ def _find_levels(data: NDArray[np_floating[Any]]) -> tuple[float, float]:
|
|
|
789
789
|
if data.dtype == np.bool_:
|
|
790
790
|
data = data.astype(np.float64)
|
|
791
791
|
|
|
792
|
+
# Check for all-NaN data
|
|
793
|
+
if np.all(np.isnan(data)):
|
|
794
|
+
return float(np.nan), float(np.nan)
|
|
795
|
+
|
|
792
796
|
# Use percentiles for robust level detection
|
|
793
797
|
p10, p90 = np.percentile(data, [10, 90])
|
|
794
798
|
|
|
799
|
+
# Check for constant or near-constant signal
|
|
800
|
+
data_range = p90 - p10
|
|
801
|
+
if data_range < 1e-10 or np.isnan(data_range): # Essentially constant or NaN
|
|
802
|
+
return float(p10), float(p10)
|
|
803
|
+
|
|
795
804
|
# Refine using histogram peaks
|
|
796
805
|
hist, bin_edges = np.histogram(data, bins=50)
|
|
797
806
|
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
@@ -70,16 +70,26 @@ def rise_time(
|
|
|
70
70
|
uncertainties = []
|
|
71
71
|
|
|
72
72
|
# 1. Time base uncertainty (Type B)
|
|
73
|
-
if
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
if (
|
|
74
|
+
trace.metadata.calibration_info is not None
|
|
75
|
+
and trace.metadata.calibration_info.timebase_accuracy is not None
|
|
76
|
+
):
|
|
77
|
+
# Use calibration timebase accuracy if available
|
|
78
|
+
timebase_ppm = trace.metadata.calibration_info.timebase_accuracy
|
|
77
79
|
u_timebase = UncertaintyEstimator.time_base_uncertainty(
|
|
78
80
|
trace.metadata.sample_rate, timebase_ppm
|
|
79
81
|
)
|
|
80
82
|
# Rise time involves 2 samples (start and stop), so uncertainty scales
|
|
81
83
|
u_timebase_rise = u_timebase * np.sqrt(2)
|
|
82
84
|
uncertainties.append(u_timebase_rise)
|
|
85
|
+
elif trace.metadata.calibration_info is not None:
|
|
86
|
+
# Use conservative estimate if calibration present but no timebase accuracy
|
|
87
|
+
timebase_ppm = 25.0 # Typical scope: 25-50 ppm
|
|
88
|
+
u_timebase = UncertaintyEstimator.time_base_uncertainty(
|
|
89
|
+
trace.metadata.sample_rate, timebase_ppm
|
|
90
|
+
)
|
|
91
|
+
u_timebase_rise = u_timebase * np.sqrt(2)
|
|
92
|
+
uncertainties.append(u_timebase_rise)
|
|
83
93
|
else:
|
|
84
94
|
# No calibration info - use conservative estimate
|
|
85
95
|
u_timebase_rise = (1.0 / trace.metadata.sample_rate) * 50e-6 # 50 ppm
|
|
@@ -146,7 +156,13 @@ def fall_time(
|
|
|
146
156
|
uncertainties = []
|
|
147
157
|
|
|
148
158
|
# Time base uncertainty
|
|
149
|
-
|
|
159
|
+
if (
|
|
160
|
+
trace.metadata.calibration_info is not None
|
|
161
|
+
and trace.metadata.calibration_info.timebase_accuracy is not None
|
|
162
|
+
):
|
|
163
|
+
timebase_ppm = trace.metadata.calibration_info.timebase_accuracy
|
|
164
|
+
else:
|
|
165
|
+
timebase_ppm = 25.0 # Conservative default
|
|
150
166
|
u_timebase = UncertaintyEstimator.time_base_uncertainty(
|
|
151
167
|
trace.metadata.sample_rate, timebase_ppm
|
|
152
168
|
)
|
|
@@ -208,7 +224,13 @@ def frequency(
|
|
|
208
224
|
uncertainties = []
|
|
209
225
|
|
|
210
226
|
# Time base uncertainty
|
|
211
|
-
|
|
227
|
+
if (
|
|
228
|
+
trace.metadata.calibration_info is not None
|
|
229
|
+
and trace.metadata.calibration_info.timebase_accuracy is not None
|
|
230
|
+
):
|
|
231
|
+
timebase_ppm = trace.metadata.calibration_info.timebase_accuracy
|
|
232
|
+
else:
|
|
233
|
+
timebase_ppm = 25.0 # Conservative default
|
|
212
234
|
# Period measurement spans multiple cycles, typically more accurate
|
|
213
235
|
u_period_timebase = period * (timebase_ppm * 1e-6)
|
|
214
236
|
uncertainties.append(u_period_timebase)
|
|
@@ -292,15 +314,9 @@ def amplitude(
|
|
|
292
314
|
uncertainties.append(u_quant)
|
|
293
315
|
|
|
294
316
|
# 3. Signal noise (Type A)
|
|
295
|
-
#
|
|
296
|
-
#
|
|
297
|
-
|
|
298
|
-
# Sample first and last 50 points (assume flat regions)
|
|
299
|
-
noise_start = np.std(trace.data[:50])
|
|
300
|
-
noise_end = np.std(trace.data[-50:])
|
|
301
|
-
u_noise = np.mean([noise_start, noise_end])
|
|
302
|
-
# Amplitude involves max and min, so sqrt(2) factor
|
|
303
|
-
uncertainties.append(u_noise * np.sqrt(2))
|
|
317
|
+
# For amplitude (Vpp) measurements, noise is already captured in the peak detection
|
|
318
|
+
# uncertainty. Adding additional noise estimation from "flat regions" is inappropriate
|
|
319
|
+
# for periodic signals where no regions are truly flat. Skip noise term for amplitude.
|
|
304
320
|
|
|
305
321
|
total_uncertainty = UncertaintyEstimator.combined_uncertainty(uncertainties)
|
|
306
322
|
|