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
oscura/utils/__init__.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"""Oscura utilities package.
|
|
2
2
|
|
|
3
|
-
Provides utility functions for memory management, windowing,
|
|
3
|
+
Provides utility functions for memory management, windowing, progressive analysis,
|
|
4
|
+
geometry, serial communication, validation, and bitwise operations.
|
|
4
5
|
"""
|
|
5
6
|
|
|
7
|
+
from oscura.utils.bitwise import bits_to_byte, bits_to_value
|
|
8
|
+
from oscura.utils.geometry import generate_leader_line
|
|
6
9
|
from oscura.utils.memory import (
|
|
7
10
|
MemoryCheck,
|
|
8
11
|
MemoryCheckError,
|
|
@@ -50,6 +53,8 @@ from oscura.utils.progressive import (
|
|
|
50
53
|
progressive_analysis,
|
|
51
54
|
select_roi,
|
|
52
55
|
)
|
|
56
|
+
from oscura.utils.serial import connect_serial_port
|
|
57
|
+
from oscura.utils.validation import validate_protocol_spec
|
|
53
58
|
|
|
54
59
|
__all__ = [
|
|
55
60
|
# Advanced memory management (MEM-014, 020, 023, 025, 028, 030-033)
|
|
@@ -75,13 +80,20 @@ __all__ = [
|
|
|
75
80
|
"ROISelection",
|
|
76
81
|
"WSLSwapChecker",
|
|
77
82
|
"analyze_roi",
|
|
83
|
+
# Bitwise operations
|
|
84
|
+
"bits_to_byte",
|
|
85
|
+
"bits_to_value",
|
|
78
86
|
"check_memory_available",
|
|
79
87
|
"configure_memory",
|
|
88
|
+
# Serial communication
|
|
89
|
+
"connect_serial_port",
|
|
80
90
|
"create_preview",
|
|
81
91
|
"detect_wsl",
|
|
82
92
|
"estimate_memory",
|
|
83
93
|
"estimate_optimal_preview_factor",
|
|
84
94
|
"gc_aggressive",
|
|
95
|
+
# Geometry
|
|
96
|
+
"generate_leader_line",
|
|
85
97
|
"get_available_memory",
|
|
86
98
|
"get_max_memory",
|
|
87
99
|
"get_memory_config",
|
|
@@ -96,4 +108,6 @@ __all__ = [
|
|
|
96
108
|
"select_roi",
|
|
97
109
|
"set_max_memory",
|
|
98
110
|
"suggest_downsampling",
|
|
111
|
+
# Validation
|
|
112
|
+
"validate_protocol_spec",
|
|
99
113
|
]
|
oscura/utils/autodetect.py
CHANGED
|
@@ -317,11 +317,7 @@ def detect_logic_family(
|
|
|
317
317
|
# Score based on how well levels match
|
|
318
318
|
low_match = 1.0 - min(1.0, abs(v_low - vol) / 0.5)
|
|
319
319
|
high_match = 1.0 - min(1.0, abs(v_high - voh) / 0.5)
|
|
320
|
-
|
|
321
|
-
if vcc != 0:
|
|
322
|
-
vcc_match = 1.0 - min(1.0, abs(v_cc_est - vcc) / abs(vcc))
|
|
323
|
-
else:
|
|
324
|
-
vcc_match = 1.0 if abs(v_cc_est) < 0.5 else 0.0
|
|
320
|
+
vcc_match = 1.0 - min(1.0, abs(v_cc_est - vcc) / vcc)
|
|
325
321
|
|
|
326
322
|
score = (low_match + high_match + vcc_match) / 3
|
|
327
323
|
|
oscura/utils/bitwise.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Bitwise operation utilities for protocol decoding.
|
|
2
|
+
|
|
3
|
+
This module provides bitwise conversion utilities used across protocol analyzers.
|
|
4
|
+
Performance-optimized with NumPy vectorized operations (10-100x faster for large arrays).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from numpy.typing import NDArray
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def bits_to_byte(bits: list[int] | NDArray[np.uint8], lsb_first: bool = True) -> int:
|
|
12
|
+
"""Convert up to 8 bits to byte value.
|
|
13
|
+
|
|
14
|
+
Performance: Uses NumPy vectorized operations for 10-100x speedup vs loops.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
bits: List or array of bits (0 or 1)
|
|
18
|
+
lsb_first: If True, bits[0] is LSB. If False, bits[0] is MSB.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Byte value (0-255)
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
ValueError: If bits contain values other than 0 or 1
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
>>> bits_to_byte([1, 0, 1, 0, 1, 0, 1, 0]) # LSB first
|
|
28
|
+
85
|
|
29
|
+
>>> bits_to_byte([1, 0, 1, 0, 1, 0, 1, 0], lsb_first=False) # MSB first
|
|
30
|
+
170
|
|
31
|
+
"""
|
|
32
|
+
# Validate input types before conversion
|
|
33
|
+
if isinstance(bits, (list, tuple)):
|
|
34
|
+
# Check for non-integer types in list/tuple
|
|
35
|
+
if any(not isinstance(b, (int, np.integer)) for b in bits):
|
|
36
|
+
raise ValueError("All bits must be 0 or 1")
|
|
37
|
+
|
|
38
|
+
# Convert to numpy array for vectorized operations
|
|
39
|
+
bits_arr = np.asarray(bits, dtype=np.uint8)
|
|
40
|
+
|
|
41
|
+
if not np.all((bits_arr == 0) | (bits_arr == 1)):
|
|
42
|
+
raise ValueError("All bits must be 0 or 1")
|
|
43
|
+
|
|
44
|
+
num_bits = min(8, len(bits_arr))
|
|
45
|
+
bits_arr = bits_arr[:num_bits]
|
|
46
|
+
|
|
47
|
+
if lsb_first:
|
|
48
|
+
# Vectorized: bits[i] * 2^i
|
|
49
|
+
shifts = np.arange(num_bits, dtype=np.uint8)
|
|
50
|
+
value = np.sum(bits_arr << shifts)
|
|
51
|
+
else:
|
|
52
|
+
# Vectorized: bits[i] * 2^(7-i)
|
|
53
|
+
shifts = np.arange(7, 7 - num_bits, -1, dtype=np.uint8)
|
|
54
|
+
value = np.sum(bits_arr << shifts)
|
|
55
|
+
|
|
56
|
+
return int(value)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def bits_to_value(bits: list[int] | NDArray[np.uint8], lsb_first: bool = True) -> int:
|
|
60
|
+
"""Convert arbitrary number of bits to integer value.
|
|
61
|
+
|
|
62
|
+
Performance: Uses NumPy vectorized operations for 10-100x speedup vs loops.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
bits: List or array of bits (0 or 1)
|
|
66
|
+
lsb_first: If True, bits[0] is LSB. If False, bits[0] is MSB.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Integer value
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
ValueError: If bits contain values other than 0 or 1
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
>>> bits_to_value([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) # 10 bits
|
|
76
|
+
1023
|
|
77
|
+
>>> bits_to_value([1, 0, 1, 0], lsb_first=False)
|
|
78
|
+
10
|
|
79
|
+
"""
|
|
80
|
+
# Validate input types before conversion
|
|
81
|
+
if isinstance(bits, (list, tuple)):
|
|
82
|
+
# Check for non-integer types in list/tuple
|
|
83
|
+
if any(not isinstance(b, (int, np.integer)) for b in bits):
|
|
84
|
+
raise ValueError("All bits must be 0 or 1")
|
|
85
|
+
|
|
86
|
+
# Convert to numpy array for vectorized operations
|
|
87
|
+
bits_arr = np.asarray(bits, dtype=np.uint8)
|
|
88
|
+
|
|
89
|
+
if not np.all((bits_arr == 0) | (bits_arr == 1)):
|
|
90
|
+
raise ValueError("All bits must be 0 or 1")
|
|
91
|
+
|
|
92
|
+
num_bits = len(bits_arr)
|
|
93
|
+
|
|
94
|
+
# For very large bit arrays (>64 bits), use packbits for efficiency
|
|
95
|
+
if num_bits > 64:
|
|
96
|
+
if lsb_first:
|
|
97
|
+
# Reverse for LSB-first interpretation
|
|
98
|
+
bits_arr = bits_arr[::-1]
|
|
99
|
+
# Pack into bytes (MSB first)
|
|
100
|
+
packed = np.packbits(bits_arr, bitorder="big")
|
|
101
|
+
# Convert bytes to integer
|
|
102
|
+
value = int.from_bytes(packed.tobytes(), byteorder="big")
|
|
103
|
+
# Adjust for partial byte
|
|
104
|
+
if not lsb_first:
|
|
105
|
+
value >>= (8 - (num_bits % 8)) % 8
|
|
106
|
+
return value
|
|
107
|
+
|
|
108
|
+
# For smaller arrays, use vectorized shift and sum
|
|
109
|
+
if lsb_first:
|
|
110
|
+
# Vectorized: bits[i] * 2^i
|
|
111
|
+
shifts = np.arange(num_bits, dtype=np.uint64)
|
|
112
|
+
value = np.sum(bits_arr.astype(np.uint64) << shifts)
|
|
113
|
+
else:
|
|
114
|
+
# Vectorized: bits[i] * 2^(num_bits-1-i)
|
|
115
|
+
shifts = np.arange(num_bits - 1, -1, -1, dtype=np.uint64)
|
|
116
|
+
value = np.sum(bits_arr.astype(np.uint64) << shifts)
|
|
117
|
+
|
|
118
|
+
return int(value)
|
|
@@ -4,31 +4,31 @@ This module provides waveform comparison, limit testing, mask testing,
|
|
|
4
4
|
and golden waveform comparison functionality.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from oscura.comparison.compare import (
|
|
7
|
+
from oscura.utils.comparison.compare import (
|
|
8
8
|
compare_traces,
|
|
9
9
|
correlation,
|
|
10
10
|
difference,
|
|
11
11
|
similarity_score,
|
|
12
12
|
)
|
|
13
|
-
from oscura.comparison.golden import (
|
|
13
|
+
from oscura.utils.comparison.golden import (
|
|
14
14
|
GoldenReference,
|
|
15
15
|
compare_to_golden,
|
|
16
16
|
create_golden,
|
|
17
17
|
tolerance_envelope,
|
|
18
18
|
)
|
|
19
|
-
from oscura.comparison.limits import (
|
|
19
|
+
from oscura.utils.comparison.limits import (
|
|
20
20
|
LimitSpec,
|
|
21
21
|
check_limits,
|
|
22
22
|
create_limit_spec,
|
|
23
23
|
margin_analysis,
|
|
24
24
|
)
|
|
25
|
-
from oscura.comparison.mask import (
|
|
25
|
+
from oscura.utils.comparison.mask import (
|
|
26
26
|
Mask,
|
|
27
27
|
create_mask,
|
|
28
28
|
eye_mask,
|
|
29
29
|
mask_test,
|
|
30
30
|
)
|
|
31
|
-
from oscura.comparison.trace_diff import (
|
|
31
|
+
from oscura.utils.comparison.trace_diff import (
|
|
32
32
|
Difference,
|
|
33
33
|
TraceDiff,
|
|
34
34
|
)
|
|
@@ -36,7 +36,7 @@ from oscura.comparison.trace_diff import (
|
|
|
36
36
|
# Note: compare_traces is imported from both compare.py and trace_diff.py
|
|
37
37
|
# The trace_diff version is from discovery.comparison (intelligent comparison)
|
|
38
38
|
# Import as compare_traces_intelligent to avoid conflict
|
|
39
|
-
from oscura.comparison.trace_diff import compare_traces as compare_traces_intelligent
|
|
39
|
+
from oscura.utils.comparison.trace_diff import compare_traces as compare_traces_intelligent
|
|
40
40
|
|
|
41
41
|
__all__ = [
|
|
42
42
|
# Intelligent trace diff (DISC-004)
|
|
@@ -5,7 +5,7 @@ difference calculation, correlation, and similarity scoring.
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
Example:
|
|
8
|
-
>>> from oscura.comparison import compare_traces, similarity_score
|
|
8
|
+
>>> from oscura.utils.comparison import compare_traces, similarity_score
|
|
9
9
|
>>> result = compare_traces(trace1, trace2)
|
|
10
10
|
>>> score = similarity_score(trace1, trace2)
|
|
11
11
|
|
|
@@ -205,11 +205,30 @@ def similarity_score(
|
|
|
205
205
|
>>> if score > 0.95:
|
|
206
206
|
... print("Traces match")
|
|
207
207
|
"""
|
|
208
|
-
|
|
208
|
+
data1, data2 = _prepare_trace_data(trace1, trace2, normalize_offset, normalize_amplitude)
|
|
209
|
+
|
|
210
|
+
if method == "correlation":
|
|
211
|
+
return _correlation_similarity(data1, data2)
|
|
212
|
+
elif method == "rms":
|
|
213
|
+
return _rms_similarity(data1, data2)
|
|
214
|
+
elif method == "mse":
|
|
215
|
+
return _mse_similarity(data1, data2)
|
|
216
|
+
elif method == "cosine":
|
|
217
|
+
return _cosine_similarity(data1, data2)
|
|
218
|
+
else:
|
|
219
|
+
raise ValueError(f"Unknown similarity method: {method}")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _prepare_trace_data(
|
|
223
|
+
trace1: WaveformTrace,
|
|
224
|
+
trace2: WaveformTrace,
|
|
225
|
+
normalize_offset: bool,
|
|
226
|
+
normalize_amplitude: bool,
|
|
227
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
228
|
+
"""Prepare trace data for similarity comparison."""
|
|
209
229
|
data1 = trace1.data.astype(np.float64).copy()
|
|
210
230
|
data2 = trace2.data.astype(np.float64).copy()
|
|
211
231
|
|
|
212
|
-
# Check for NaN/Inf values
|
|
213
232
|
if np.any(~np.isfinite(data1)) or np.any(~np.isfinite(data2)):
|
|
214
233
|
raise ValueError("Input traces contain NaN or Inf values")
|
|
215
234
|
|
|
@@ -232,151 +251,180 @@ def similarity_score(
|
|
|
232
251
|
if std2 > 0:
|
|
233
252
|
data2 = data2 / std2
|
|
234
253
|
|
|
235
|
-
|
|
236
|
-
# Pearson correlation coefficient
|
|
237
|
-
# Handle constant inputs gracefully
|
|
238
|
-
with warnings.catch_warnings():
|
|
239
|
-
warnings.filterwarnings("ignore", category=stats.ConstantInputWarning)
|
|
240
|
-
try:
|
|
241
|
-
r, _ = stats.pearsonr(data1, data2)
|
|
242
|
-
# Handle NaN result (constant traces after normalization)
|
|
243
|
-
if np.isnan(r):
|
|
244
|
-
# If both traces are constant and identical, perfect match
|
|
245
|
-
if np.allclose(data1, data2, equal_nan=False):
|
|
246
|
-
r = 1.0
|
|
247
|
-
else:
|
|
248
|
-
r = 0.0
|
|
249
|
-
except Exception:
|
|
250
|
-
r = 0.0
|
|
251
|
-
# Map from [-1, 1] to [0, 1]
|
|
252
|
-
return float((r + 1) / 2)
|
|
254
|
+
return data1, data2
|
|
253
255
|
|
|
254
|
-
elif method == "rms":
|
|
255
|
-
# RMS-based similarity
|
|
256
|
-
rms_diff = np.sqrt(np.mean((data1 - data2) ** 2))
|
|
257
|
-
rms_ref = np.sqrt(np.mean(data2**2)) + 1e-10
|
|
258
|
-
return float(max(0, 1 - rms_diff / rms_ref))
|
|
259
256
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
257
|
+
def _correlation_similarity(data1: NDArray[np.float64], data2: NDArray[np.float64]) -> float:
|
|
258
|
+
"""Compute Pearson correlation-based similarity."""
|
|
259
|
+
with warnings.catch_warnings():
|
|
260
|
+
warnings.filterwarnings("ignore", category=stats.ConstantInputWarning)
|
|
261
|
+
try:
|
|
262
|
+
r, _ = stats.pearsonr(data1, data2)
|
|
263
|
+
if np.isnan(r):
|
|
264
|
+
r = 1.0 if np.allclose(data1, data2, equal_nan=False) else 0.0
|
|
265
|
+
except Exception:
|
|
266
|
+
r = 0.0
|
|
267
|
+
return float((r + 1) / 2)
|
|
265
268
|
|
|
266
|
-
elif method == "cosine":
|
|
267
|
-
# Cosine similarity
|
|
268
|
-
dot = np.dot(data1, data2)
|
|
269
|
-
norm1 = np.linalg.norm(data1) + 1e-10
|
|
270
|
-
norm2 = np.linalg.norm(data2) + 1e-10
|
|
271
|
-
cosine = dot / (norm1 * norm2)
|
|
272
|
-
# Map from [-1, 1] to [0, 1]
|
|
273
|
-
return float((cosine + 1) / 2)
|
|
274
269
|
|
|
275
|
-
|
|
276
|
-
|
|
270
|
+
def _rms_similarity(data1: NDArray[np.float64], data2: NDArray[np.float64]) -> float:
|
|
271
|
+
"""Compute RMS-based similarity."""
|
|
272
|
+
rms_diff = np.sqrt(np.mean((data1 - data2) ** 2))
|
|
273
|
+
rms_ref = np.sqrt(np.mean(data2**2)) + 1e-10
|
|
274
|
+
return float(max(0, 1 - rms_diff / rms_ref))
|
|
277
275
|
|
|
278
276
|
|
|
279
|
-
def
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
tolerance_pct: float | None = None,
|
|
285
|
-
method: Literal["absolute", "relative", "statistical"] = "absolute",
|
|
286
|
-
include_difference: bool = True,
|
|
287
|
-
) -> ComparisonResult:
|
|
288
|
-
"""Compare two traces and determine if they match.
|
|
277
|
+
def _mse_similarity(data1: NDArray[np.float64], data2: NDArray[np.float64]) -> float:
|
|
278
|
+
"""Compute MSE-based similarity."""
|
|
279
|
+
mse = np.mean((data1 - data2) ** 2)
|
|
280
|
+
var_ref = np.var(data2) + 1e-10
|
|
281
|
+
return float(max(0, 1 - mse / var_ref))
|
|
289
282
|
|
|
290
|
-
Comprehensive comparison of two waveforms including difference
|
|
291
|
-
analysis, correlation, and match determination.
|
|
292
283
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
- "relative": Compare relative to reference
|
|
301
|
-
- "statistical": Use statistical tests
|
|
302
|
-
include_difference: Include difference trace in result.
|
|
284
|
+
def _cosine_similarity(data1: NDArray[np.float64], data2: NDArray[np.float64]) -> float:
|
|
285
|
+
"""Compute cosine similarity."""
|
|
286
|
+
dot = np.dot(data1, data2)
|
|
287
|
+
norm1 = np.linalg.norm(data1) + 1e-10
|
|
288
|
+
norm2 = np.linalg.norm(data2) + 1e-10
|
|
289
|
+
cosine = dot / (norm1 * norm2)
|
|
290
|
+
return float((cosine + 1) / 2)
|
|
303
291
|
|
|
304
|
-
Returns:
|
|
305
|
-
ComparisonResult with match status and statistics.
|
|
306
292
|
|
|
307
|
-
|
|
308
|
-
|
|
293
|
+
def _align_trace_data(
|
|
294
|
+
trace1: WaveformTrace, trace2: WaveformTrace
|
|
295
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64], int]:
|
|
296
|
+
"""Align trace data to same length.
|
|
309
297
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
298
|
+
Args:
|
|
299
|
+
trace1: First trace.
|
|
300
|
+
trace2: Second trace.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Tuple of (data1, data2, min_len).
|
|
314
304
|
"""
|
|
315
|
-
# Get data
|
|
316
305
|
data1 = trace1.data.astype(np.float64)
|
|
317
306
|
data2 = trace2.data.astype(np.float64)
|
|
318
|
-
|
|
319
|
-
# Align lengths
|
|
320
307
|
min_len = min(len(data1), len(data2))
|
|
321
|
-
|
|
322
|
-
data2 = data2[:min_len]
|
|
308
|
+
return data1[:min_len], data2[:min_len], min_len
|
|
323
309
|
|
|
324
|
-
# Compute difference
|
|
325
|
-
diff = data1 - data2
|
|
326
310
|
|
|
327
|
-
|
|
311
|
+
def _compute_difference_stats(diff: NDArray[np.float64]) -> tuple[float, float]:
|
|
312
|
+
"""Compute max and RMS of difference.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
diff: Difference array.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
Tuple of (max_diff, rms_diff).
|
|
319
|
+
"""
|
|
328
320
|
max_diff = float(np.max(np.abs(diff)))
|
|
329
321
|
rms_diff = float(np.sqrt(np.mean(diff**2)))
|
|
322
|
+
return max_diff, rms_diff
|
|
323
|
+
|
|
330
324
|
|
|
331
|
-
|
|
325
|
+
def _compute_correlation_coefficient(
|
|
326
|
+
data1: NDArray[np.float64], data2: NDArray[np.float64]
|
|
327
|
+
) -> float:
|
|
328
|
+
"""Compute Pearson correlation coefficient.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
data1: First data array.
|
|
332
|
+
data2: Second data array.
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Correlation coefficient.
|
|
336
|
+
"""
|
|
332
337
|
if len(data1) > 1:
|
|
333
|
-
# Handle constant inputs (e.g., DC signals) gracefully
|
|
334
338
|
with warnings.catch_warnings():
|
|
335
339
|
warnings.filterwarnings("ignore", category=stats.ConstantInputWarning)
|
|
336
340
|
try:
|
|
337
341
|
corr, _ = stats.pearsonr(data1, data2)
|
|
338
342
|
except Exception:
|
|
339
|
-
# Fallback for any correlation computation issues
|
|
340
343
|
corr = 0.0
|
|
341
344
|
else:
|
|
342
345
|
corr = 1.0 if data1[0] == data2[0] else 0.0
|
|
346
|
+
return float(corr)
|
|
343
347
|
|
|
344
|
-
# Compute similarity score
|
|
345
|
-
sim_score = similarity_score(trace1, trace2)
|
|
346
348
|
|
|
347
|
-
|
|
349
|
+
def _determine_tolerance(
|
|
350
|
+
tolerance: float | None, tolerance_pct: float | None, data2: NDArray[np.float64]
|
|
351
|
+
) -> float:
|
|
352
|
+
"""Determine effective tolerance value.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
tolerance: Absolute tolerance value.
|
|
356
|
+
tolerance_pct: Percentage tolerance.
|
|
357
|
+
data2: Reference data array.
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
Effective tolerance value.
|
|
361
|
+
"""
|
|
348
362
|
if tolerance is None and tolerance_pct is not None:
|
|
349
363
|
ref_range = float(np.ptp(data2))
|
|
350
|
-
|
|
364
|
+
return ref_range * tolerance_pct / 100.0
|
|
351
365
|
elif tolerance is None:
|
|
352
|
-
# Default: 1% of reference range
|
|
353
366
|
ref_range = float(np.ptp(data2))
|
|
354
|
-
|
|
367
|
+
return ref_range * 0.01
|
|
368
|
+
return tolerance
|
|
355
369
|
|
|
356
|
-
# Find violations
|
|
357
|
-
violations = np.where(np.abs(diff) > tolerance)[0]
|
|
358
370
|
|
|
359
|
-
|
|
371
|
+
def _determine_match(
|
|
372
|
+
method: str,
|
|
373
|
+
max_diff: float,
|
|
374
|
+
tolerance: float,
|
|
375
|
+
tolerance_pct: float | None,
|
|
376
|
+
data1: NDArray[np.float64],
|
|
377
|
+
data2: NDArray[np.float64],
|
|
378
|
+
) -> bool:
|
|
379
|
+
"""Determine if traces match based on method.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
method: Comparison method.
|
|
383
|
+
max_diff: Maximum difference.
|
|
384
|
+
tolerance: Tolerance value.
|
|
385
|
+
tolerance_pct: Percentage tolerance.
|
|
386
|
+
data1: First data array.
|
|
387
|
+
data2: Second data array.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
True if traces match.
|
|
391
|
+
|
|
392
|
+
Raises:
|
|
393
|
+
ValueError: If method is unknown.
|
|
394
|
+
"""
|
|
360
395
|
if method == "absolute":
|
|
361
|
-
|
|
396
|
+
return max_diff <= tolerance
|
|
362
397
|
elif method == "relative":
|
|
363
398
|
ref_range = float(np.ptp(data2)) + 1e-10
|
|
364
399
|
relative_max = max_diff / ref_range
|
|
365
|
-
|
|
400
|
+
return relative_max <= (tolerance_pct or 1.0) / 100.0
|
|
366
401
|
elif method == "statistical":
|
|
367
|
-
# Use t-test for statistical matching
|
|
368
402
|
_, p_value = stats.ttest_rel(data1, data2)
|
|
369
|
-
|
|
403
|
+
return bool(p_value > 0.05)
|
|
370
404
|
else:
|
|
371
405
|
raise ValueError(f"Unknown method: {method}")
|
|
372
406
|
|
|
373
|
-
# Create difference trace if requested
|
|
374
|
-
diff_trace = None
|
|
375
|
-
if include_difference:
|
|
376
|
-
diff_trace = difference(trace1, trace2, channel_name="comparison_diff")
|
|
377
407
|
|
|
378
|
-
|
|
379
|
-
|
|
408
|
+
def _compute_comparison_statistics(
|
|
409
|
+
diff: NDArray[np.float64],
|
|
410
|
+
violations: NDArray[np.int64],
|
|
411
|
+
min_len: int,
|
|
412
|
+
data1: NDArray[np.float64],
|
|
413
|
+
data2: NDArray[np.float64],
|
|
414
|
+
) -> dict[str, float]:
|
|
415
|
+
"""Compute additional comparison statistics.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
diff: Difference array.
|
|
419
|
+
violations: Violation indices.
|
|
420
|
+
min_len: Minimum length.
|
|
421
|
+
data1: First data array.
|
|
422
|
+
data2: Second data array.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
Dictionary of statistics.
|
|
426
|
+
"""
|
|
427
|
+
return {
|
|
380
428
|
"mean_difference": float(np.mean(diff)),
|
|
381
429
|
"std_difference": float(np.std(diff)),
|
|
382
430
|
"median_difference": float(np.median(diff)),
|
|
@@ -385,6 +433,59 @@ def compare_traces(
|
|
|
385
433
|
"p_value": float(stats.ttest_rel(data1, data2)[1]) if len(data1) > 1 else 1.0,
|
|
386
434
|
}
|
|
387
435
|
|
|
436
|
+
|
|
437
|
+
def compare_traces(
|
|
438
|
+
trace1: WaveformTrace,
|
|
439
|
+
trace2: WaveformTrace,
|
|
440
|
+
*,
|
|
441
|
+
tolerance: float | None = None,
|
|
442
|
+
tolerance_pct: float | None = None,
|
|
443
|
+
method: Literal["absolute", "relative", "statistical"] = "absolute",
|
|
444
|
+
include_difference: bool = True,
|
|
445
|
+
) -> ComparisonResult:
|
|
446
|
+
"""Compare two traces and determine if they match.
|
|
447
|
+
|
|
448
|
+
Comprehensive comparison of two waveforms including difference
|
|
449
|
+
analysis, correlation, and match determination.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
trace1: First trace (typically measured).
|
|
453
|
+
trace2: Second trace (typically reference).
|
|
454
|
+
tolerance: Absolute tolerance for matching.
|
|
455
|
+
tolerance_pct: Percentage tolerance (0-100) relative to reference range.
|
|
456
|
+
method: Comparison method:
|
|
457
|
+
- "absolute": Compare absolute values
|
|
458
|
+
- "relative": Compare relative to reference
|
|
459
|
+
- "statistical": Use statistical tests
|
|
460
|
+
include_difference: Include difference trace in result.
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
ComparisonResult with match status and statistics.
|
|
464
|
+
|
|
465
|
+
Raises:
|
|
466
|
+
ValueError: If method is unknown.
|
|
467
|
+
|
|
468
|
+
Example:
|
|
469
|
+
>>> result = compare_traces(measured, golden, tolerance=0.01)
|
|
470
|
+
>>> if result.match:
|
|
471
|
+
... print(f"Match! Similarity: {result.similarity:.1%}")
|
|
472
|
+
"""
|
|
473
|
+
data1, data2, min_len = _align_trace_data(trace1, trace2)
|
|
474
|
+
diff = data1 - data2
|
|
475
|
+
|
|
476
|
+
max_diff, rms_diff = _compute_difference_stats(diff)
|
|
477
|
+
corr = _compute_correlation_coefficient(data1, data2)
|
|
478
|
+
sim_score = similarity_score(trace1, trace2)
|
|
479
|
+
|
|
480
|
+
tolerance = _determine_tolerance(tolerance, tolerance_pct, data2)
|
|
481
|
+
violations = np.where(np.abs(diff) > tolerance)[0]
|
|
482
|
+
match = _determine_match(method, max_diff, tolerance, tolerance_pct, data1, data2)
|
|
483
|
+
|
|
484
|
+
diff_trace = (
|
|
485
|
+
difference(trace1, trace2, channel_name="comparison_diff") if include_difference else None
|
|
486
|
+
)
|
|
487
|
+
statistics = _compute_comparison_statistics(diff, violations, min_len, data1, data2)
|
|
488
|
+
|
|
388
489
|
return ComparisonResult(
|
|
389
490
|
match=match,
|
|
390
491
|
similarity=sim_score,
|