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
|
@@ -5,7 +5,7 @@ functions for pass/fail testing against known-good waveforms.
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
Example:
|
|
8
|
-
>>> from oscura.comparison import create_golden, compare_to_golden
|
|
8
|
+
>>> from oscura.utils.comparison import create_golden, compare_to_golden
|
|
9
9
|
>>> golden = create_golden(reference_trace)
|
|
10
10
|
>>> result = compare_to_golden(measured_trace, golden)
|
|
11
11
|
|
|
@@ -301,8 +301,7 @@ def compare_to_golden(
|
|
|
301
301
|
) -> GoldenComparisonResult:
|
|
302
302
|
"""Compare a trace to a golden reference.
|
|
303
303
|
|
|
304
|
-
Tests if the measured trace falls within the tolerance bounds
|
|
305
|
-
of the golden reference.
|
|
304
|
+
Tests if the measured trace falls within the tolerance bounds.
|
|
306
305
|
|
|
307
306
|
Args:
|
|
308
307
|
trace: Measured trace to compare.
|
|
@@ -315,73 +314,107 @@ def compare_to_golden(
|
|
|
315
314
|
|
|
316
315
|
Example:
|
|
317
316
|
>>> result = compare_to_golden(measured, golden)
|
|
318
|
-
>>> if result.passed:
|
|
319
|
-
... print("PASS")
|
|
317
|
+
>>> if result.passed: print("PASS")
|
|
320
318
|
"""
|
|
319
|
+
measured, reference, upper, lower = _prepare_data(trace, golden, interpolate)
|
|
320
|
+
|
|
321
|
+
if align and len(measured) > 10:
|
|
322
|
+
measured = _align_signals(measured, reference)
|
|
323
|
+
|
|
324
|
+
violations = _find_violations(measured, upper, lower)
|
|
325
|
+
deviation = measured - reference
|
|
326
|
+
margin = _compute_margin(measured, upper, lower)
|
|
327
|
+
margin_pct = (margin / golden.tolerance * 100) if golden.tolerance > 0 else None
|
|
328
|
+
|
|
329
|
+
statistics = _compute_statistics(measured, reference, deviation)
|
|
330
|
+
|
|
331
|
+
return GoldenComparisonResult(
|
|
332
|
+
passed=violations["count"] == 0,
|
|
333
|
+
num_violations=violations["count"],
|
|
334
|
+
violation_rate=violations["rate"],
|
|
335
|
+
max_deviation=float(np.max(np.abs(deviation))),
|
|
336
|
+
rms_deviation=float(np.sqrt(np.mean(deviation**2))),
|
|
337
|
+
upper_violations=violations["upper"] if len(violations["upper"]) > 0 else None,
|
|
338
|
+
lower_violations=violations["lower"] if len(violations["lower"]) > 0 else None,
|
|
339
|
+
margin=margin,
|
|
340
|
+
margin_percentage=margin_pct,
|
|
341
|
+
statistics=statistics,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _prepare_data(
|
|
346
|
+
trace: WaveformTrace, golden: GoldenReference, interpolate: bool
|
|
347
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]:
|
|
348
|
+
"""Prepare and align data arrays."""
|
|
321
349
|
measured = trace.data.astype(np.float64)
|
|
322
|
-
reference =
|
|
323
|
-
|
|
324
|
-
|
|
350
|
+
reference, upper, lower = (
|
|
351
|
+
golden.data.copy(),
|
|
352
|
+
golden.upper_bound.copy(),
|
|
353
|
+
golden.lower_bound.copy(),
|
|
354
|
+
)
|
|
325
355
|
|
|
326
|
-
# Handle length mismatch
|
|
327
356
|
if len(measured) != len(reference):
|
|
328
357
|
if interpolate:
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
358
|
+
x_measured, x_reference = (
|
|
359
|
+
np.linspace(0, 1, len(measured)),
|
|
360
|
+
np.linspace(0, 1, len(reference)),
|
|
361
|
+
)
|
|
332
362
|
measured = np.interp(x_reference, x_measured, measured)
|
|
333
363
|
else:
|
|
334
|
-
# Truncate to shorter length
|
|
335
364
|
min_len = min(len(measured), len(reference))
|
|
336
|
-
measured =
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
365
|
+
measured, reference, upper, lower = (
|
|
366
|
+
measured[:min_len],
|
|
367
|
+
reference[:min_len],
|
|
368
|
+
upper[:min_len],
|
|
369
|
+
lower[:min_len],
|
|
370
|
+
)
|
|
340
371
|
|
|
341
|
-
|
|
342
|
-
if align and len(measured) > 10:
|
|
343
|
-
from scipy import signal as sp_signal
|
|
372
|
+
return measured, reference, upper, lower
|
|
344
373
|
|
|
345
|
-
corr = sp_signal.correlate(measured, reference, mode="same")
|
|
346
|
-
shift = len(measured) // 2 - np.argmax(corr)
|
|
347
|
-
if abs(shift) < len(measured) // 4: # Only shift if reasonable
|
|
348
|
-
measured = np.roll(measured, -shift)
|
|
349
374
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
375
|
+
def _align_signals(
|
|
376
|
+
measured: NDArray[np.float64], reference: NDArray[np.float64]
|
|
377
|
+
) -> NDArray[np.float64]:
|
|
378
|
+
"""Align signals using cross-correlation."""
|
|
379
|
+
from scipy import signal as sp_signal
|
|
354
380
|
|
|
355
|
-
|
|
356
|
-
|
|
381
|
+
corr = sp_signal.correlate(measured, reference, mode="same")
|
|
382
|
+
shift = len(measured) // 2 - np.argmax(corr)
|
|
383
|
+
return np.roll(measured, -shift) if abs(shift) < len(measured) // 4 else measured
|
|
357
384
|
|
|
358
|
-
# Compute deviation statistics
|
|
359
|
-
deviation = measured - reference
|
|
360
|
-
max_deviation = float(np.max(np.abs(deviation)))
|
|
361
|
-
rms_deviation = float(np.sqrt(np.mean(deviation**2)))
|
|
362
385
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
386
|
+
def _find_violations(
|
|
387
|
+
measured: NDArray[np.float64], upper: NDArray[np.float64], lower: NDArray[np.float64]
|
|
388
|
+
) -> dict[str, Any]:
|
|
389
|
+
"""Find tolerance violations."""
|
|
390
|
+
upper_viol, lower_viol = np.where(measured > upper)[0], np.where(measured < lower)[0]
|
|
391
|
+
all_violations = np.union1d(upper_viol, lower_viol)
|
|
392
|
+
return {
|
|
393
|
+
"upper": upper_viol,
|
|
394
|
+
"lower": lower_viol,
|
|
395
|
+
"count": len(all_violations),
|
|
396
|
+
"rate": len(all_violations) / len(measured) if len(measured) > 0 else 0.0,
|
|
397
|
+
}
|
|
367
398
|
|
|
368
|
-
# Margin as percentage of tolerance
|
|
369
|
-
margin_pct = (margin / golden.tolerance * 100) if golden.tolerance > 0 else None
|
|
370
399
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
400
|
+
def _compute_margin(
|
|
401
|
+
measured: NDArray[np.float64], upper: NDArray[np.float64], lower: NDArray[np.float64]
|
|
402
|
+
) -> float:
|
|
403
|
+
"""Compute minimum margin to tolerance bounds."""
|
|
404
|
+
return min(float(np.min(upper - measured)), float(np.min(measured - lower)))
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _compute_statistics(
|
|
408
|
+
measured: NDArray[np.float64], reference: NDArray[np.float64], deviation: NDArray[np.float64]
|
|
409
|
+
) -> dict[str, float]:
|
|
410
|
+
"""Compute deviation and correlation statistics."""
|
|
411
|
+
measured_std, reference_std = np.std(measured), np.std(reference)
|
|
375
412
|
if measured_std == 0 or reference_std == 0:
|
|
376
|
-
|
|
377
|
-
if np.allclose(measured, reference):
|
|
378
|
-
correlation = 1.0
|
|
379
|
-
else:
|
|
380
|
-
correlation = float("nan")
|
|
413
|
+
correlation = 1.0 if np.allclose(measured, reference) else float("nan")
|
|
381
414
|
else:
|
|
382
415
|
correlation = float(np.corrcoef(measured, reference)[0, 1])
|
|
383
416
|
|
|
384
|
-
|
|
417
|
+
return {
|
|
385
418
|
"mean_deviation": float(np.mean(deviation)),
|
|
386
419
|
"std_deviation": float(np.std(deviation)),
|
|
387
420
|
"max_positive_deviation": float(np.max(deviation)),
|
|
@@ -389,19 +422,6 @@ def compare_to_golden(
|
|
|
389
422
|
"correlation": correlation,
|
|
390
423
|
}
|
|
391
424
|
|
|
392
|
-
return GoldenComparisonResult(
|
|
393
|
-
passed=num_violations == 0,
|
|
394
|
-
num_violations=num_violations,
|
|
395
|
-
violation_rate=violation_rate,
|
|
396
|
-
max_deviation=max_deviation,
|
|
397
|
-
rms_deviation=rms_deviation,
|
|
398
|
-
upper_violations=upper_viol if len(upper_viol) > 0 else None,
|
|
399
|
-
lower_violations=lower_viol if len(lower_viol) > 0 else None,
|
|
400
|
-
margin=margin,
|
|
401
|
-
margin_percentage=margin_pct,
|
|
402
|
-
statistics=statistics,
|
|
403
|
-
)
|
|
404
|
-
|
|
405
425
|
|
|
406
426
|
def batch_compare_to_golden(
|
|
407
427
|
traces: list[WaveformTrace],
|
|
@@ -5,7 +5,7 @@ bounds, pass/fail determination, and margin analysis.
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
Example:
|
|
8
|
-
>>> from oscura.comparison import check_limits, margin_analysis
|
|
8
|
+
>>> from oscura.utils.comparison import check_limits, margin_analysis
|
|
9
9
|
>>> result = check_limits(trace, upper=1.5, lower=-0.5)
|
|
10
10
|
>>> margins = margin_analysis(trace, limits)
|
|
11
11
|
"""
|
|
@@ -188,74 +188,35 @@ def check_limits(
|
|
|
188
188
|
>>> if not result.passed:
|
|
189
189
|
... print(f"{result.num_violations} violations found")
|
|
190
190
|
"""
|
|
191
|
-
#
|
|
192
|
-
|
|
193
|
-
data = trace.data.astype(np.float64)
|
|
194
|
-
else:
|
|
195
|
-
data = np.asarray(trace, dtype=np.float64)
|
|
191
|
+
# Extract data array
|
|
192
|
+
data = _extract_data_array(trace)
|
|
196
193
|
|
|
197
|
-
#
|
|
198
|
-
|
|
199
|
-
if upper is None and lower is None:
|
|
200
|
-
raise ValueError("Must specify limits or upper/lower bounds")
|
|
201
|
-
limits = LimitSpec(upper=upper, lower=lower)
|
|
194
|
+
# Get or create limit specification
|
|
195
|
+
limits = _get_or_create_limits(limits, upper, lower)
|
|
202
196
|
|
|
203
|
-
#
|
|
204
|
-
actual_upper = limits
|
|
205
|
-
actual_lower = limits.lower
|
|
206
|
-
if limits.mode == "relative" and reference is not None:
|
|
207
|
-
if actual_upper is not None:
|
|
208
|
-
actual_upper = reference + actual_upper
|
|
209
|
-
if actual_lower is not None:
|
|
210
|
-
actual_lower = reference + actual_lower
|
|
197
|
+
# Apply relative mode adjustment if needed
|
|
198
|
+
actual_upper, actual_lower = _apply_relative_limits(limits, reference)
|
|
211
199
|
|
|
212
|
-
# Find violations
|
|
213
|
-
upper_viol =
|
|
214
|
-
lower_viol = np.array([], dtype=np.int64)
|
|
200
|
+
# Find violations in data
|
|
201
|
+
upper_viol, lower_viol = _find_violations(data, actual_upper, actual_lower)
|
|
215
202
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if actual_lower is not None:
|
|
219
|
-
lower_viol = np.where(data < actual_lower)[0]
|
|
203
|
+
# Compute violation statistics
|
|
204
|
+
num_violations, violation_rate = _compute_violation_stats(upper_viol, lower_viol, len(data))
|
|
220
205
|
|
|
221
|
-
#
|
|
222
|
-
all_violations = np.union1d(upper_viol, lower_viol)
|
|
223
|
-
num_violations = len(all_violations)
|
|
224
|
-
violation_rate = num_violations / len(data) if len(data) > 0 else 0.0
|
|
225
|
-
|
|
226
|
-
# Compute statistics
|
|
206
|
+
# Compute data range statistics
|
|
227
207
|
max_val = float(np.max(data))
|
|
228
208
|
min_val = float(np.min(data))
|
|
229
209
|
|
|
230
|
-
# Compute margins
|
|
231
|
-
upper_margin =
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
upper_margin = float(actual_upper - max_val)
|
|
235
|
-
if actual_lower is not None:
|
|
236
|
-
lower_margin = float(min_val - actual_lower)
|
|
210
|
+
# Compute margins to limits
|
|
211
|
+
upper_margin, lower_margin = _compute_limit_margins(
|
|
212
|
+
actual_upper, actual_lower, max_val, min_val
|
|
213
|
+
)
|
|
237
214
|
|
|
238
215
|
# Compute margin percentage
|
|
239
|
-
margin_pct =
|
|
240
|
-
if actual_upper is not None and actual_lower is not None:
|
|
241
|
-
limit_range = actual_upper - actual_lower
|
|
242
|
-
if limit_range > 0:
|
|
243
|
-
min_margin = min(
|
|
244
|
-
upper_margin if upper_margin is not None else float("inf"),
|
|
245
|
-
lower_margin if lower_margin is not None else float("inf"),
|
|
246
|
-
)
|
|
247
|
-
margin_pct = (min_margin / limit_range) * 100.0
|
|
216
|
+
margin_pct = _compute_margin_pct(actual_upper, actual_lower, upper_margin, lower_margin)
|
|
248
217
|
|
|
249
|
-
# Check guardband
|
|
250
|
-
within_guardband =
|
|
251
|
-
if num_violations == 0:
|
|
252
|
-
# Check if within guardband
|
|
253
|
-
if limits.upper_guardband > 0 and upper_margin is not None:
|
|
254
|
-
if upper_margin < limits.upper_guardband:
|
|
255
|
-
within_guardband = True
|
|
256
|
-
if limits.lower_guardband > 0 and lower_margin is not None:
|
|
257
|
-
if lower_margin < limits.lower_guardband:
|
|
258
|
-
within_guardband = True
|
|
218
|
+
# Check guardband status
|
|
219
|
+
within_guardband = _check_guardband_status(num_violations, limits, upper_margin, lower_margin)
|
|
259
220
|
|
|
260
221
|
return LimitTestResult(
|
|
261
222
|
passed=num_violations == 0,
|
|
@@ -321,16 +282,238 @@ def margin_analysis(
|
|
|
321
282
|
>>> margins = margin_analysis(trace, limits)
|
|
322
283
|
>>> print(f"Margin: {margins.margin_percentage:.1f}%")
|
|
323
284
|
"""
|
|
324
|
-
#
|
|
285
|
+
# Extract data array
|
|
286
|
+
data = _extract_data_array(trace)
|
|
287
|
+
max_val = float(np.max(data))
|
|
288
|
+
min_val = float(np.min(data))
|
|
289
|
+
|
|
290
|
+
# Calculate margins to limits
|
|
291
|
+
upper_margin, lower_margin = _calculate_margins(limits, max_val, min_val)
|
|
292
|
+
|
|
293
|
+
# Determine critical limit and minimum margin
|
|
294
|
+
min_margin, critical_limit = _find_critical_limit(upper_margin, lower_margin)
|
|
295
|
+
|
|
296
|
+
# Calculate margin as percentage of limit range
|
|
297
|
+
margin_pct = _calculate_margin_percentage(limits, upper_margin, lower_margin, min_margin)
|
|
298
|
+
|
|
299
|
+
# Determine pass/warning/fail status
|
|
300
|
+
margin_status, warning = _determine_margin_status(min_margin, margin_pct, warning_threshold_pct)
|
|
301
|
+
|
|
302
|
+
return MarginAnalysis(
|
|
303
|
+
upper_margin=upper_margin,
|
|
304
|
+
lower_margin=lower_margin,
|
|
305
|
+
min_margin=min_margin,
|
|
306
|
+
margin_percentage=margin_pct,
|
|
307
|
+
critical_limit=critical_limit,
|
|
308
|
+
warning=warning,
|
|
309
|
+
margin_status=margin_status,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _get_or_create_limits(
|
|
314
|
+
limits: LimitSpec | None, upper: float | None, lower: float | None
|
|
315
|
+
) -> LimitSpec:
|
|
316
|
+
"""Get existing limits or create from upper/lower bounds.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
limits: Existing LimitSpec or None.
|
|
320
|
+
upper: Upper limit value.
|
|
321
|
+
lower: Lower limit value.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
LimitSpec instance.
|
|
325
|
+
|
|
326
|
+
Raises:
|
|
327
|
+
ValueError: If no limits specified.
|
|
328
|
+
"""
|
|
329
|
+
if limits is None:
|
|
330
|
+
if upper is None and lower is None:
|
|
331
|
+
raise ValueError("Must specify limits or upper/lower bounds")
|
|
332
|
+
limits = LimitSpec(upper=upper, lower=lower)
|
|
333
|
+
return limits
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _apply_relative_limits(
|
|
337
|
+
limits: LimitSpec, reference: float | None
|
|
338
|
+
) -> tuple[float | None, float | None]:
|
|
339
|
+
"""Apply relative mode adjustment to limits.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
limits: Limit specification.
|
|
343
|
+
reference: Reference value for relative limits.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
Tuple of (actual_upper, actual_lower).
|
|
347
|
+
"""
|
|
348
|
+
actual_upper = limits.upper
|
|
349
|
+
actual_lower = limits.lower
|
|
350
|
+
|
|
351
|
+
if limits.mode == "relative" and reference is not None:
|
|
352
|
+
if actual_upper is not None:
|
|
353
|
+
actual_upper = reference + actual_upper
|
|
354
|
+
if actual_lower is not None:
|
|
355
|
+
actual_lower = reference + actual_lower
|
|
356
|
+
|
|
357
|
+
return (actual_upper, actual_lower)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _find_violations(
|
|
361
|
+
data: NDArray[np.float64], actual_upper: float | None, actual_lower: float | None
|
|
362
|
+
) -> tuple[NDArray[np.int64], NDArray[np.int64]]:
|
|
363
|
+
"""Find samples violating upper and lower limits.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
data: Data array to check.
|
|
367
|
+
actual_upper: Upper limit (None if no upper limit).
|
|
368
|
+
actual_lower: Lower limit (None if no lower limit).
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
Tuple of (upper_violations, lower_violations) index arrays.
|
|
372
|
+
"""
|
|
373
|
+
upper_viol = np.array([], dtype=np.int64)
|
|
374
|
+
lower_viol = np.array([], dtype=np.int64)
|
|
375
|
+
|
|
376
|
+
if actual_upper is not None:
|
|
377
|
+
upper_viol = np.where(data > actual_upper)[0]
|
|
378
|
+
if actual_lower is not None:
|
|
379
|
+
lower_viol = np.where(data < actual_lower)[0]
|
|
380
|
+
|
|
381
|
+
return (upper_viol, lower_viol)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _compute_violation_stats(
|
|
385
|
+
upper_viol: NDArray[np.int64], lower_viol: NDArray[np.int64], data_length: int
|
|
386
|
+
) -> tuple[int, float]:
|
|
387
|
+
"""Compute violation count and rate.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
upper_viol: Upper limit violations.
|
|
391
|
+
lower_viol: Lower limit violations.
|
|
392
|
+
data_length: Total number of samples.
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
Tuple of (num_violations, violation_rate).
|
|
396
|
+
"""
|
|
397
|
+
all_violations = np.union1d(upper_viol, lower_viol)
|
|
398
|
+
num_violations = len(all_violations)
|
|
399
|
+
violation_rate = num_violations / data_length if data_length > 0 else 0.0
|
|
400
|
+
return (num_violations, violation_rate)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _compute_limit_margins(
|
|
404
|
+
actual_upper: float | None, actual_lower: float | None, max_val: float, min_val: float
|
|
405
|
+
) -> tuple[float | None, float | None]:
|
|
406
|
+
"""Compute margins to upper and lower limits.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
actual_upper: Upper limit.
|
|
410
|
+
actual_lower: Lower limit.
|
|
411
|
+
max_val: Maximum value in data.
|
|
412
|
+
min_val: Minimum value in data.
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
Tuple of (upper_margin, lower_margin).
|
|
416
|
+
"""
|
|
417
|
+
upper_margin = None
|
|
418
|
+
lower_margin = None
|
|
419
|
+
|
|
420
|
+
if actual_upper is not None:
|
|
421
|
+
upper_margin = float(actual_upper - max_val)
|
|
422
|
+
if actual_lower is not None:
|
|
423
|
+
lower_margin = float(min_val - actual_lower)
|
|
424
|
+
|
|
425
|
+
return (upper_margin, lower_margin)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _compute_margin_pct(
|
|
429
|
+
actual_upper: float | None,
|
|
430
|
+
actual_lower: float | None,
|
|
431
|
+
upper_margin: float | None,
|
|
432
|
+
lower_margin: float | None,
|
|
433
|
+
) -> float | None:
|
|
434
|
+
"""Compute margin as percentage of limit range.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
actual_upper: Upper limit.
|
|
438
|
+
actual_lower: Lower limit.
|
|
439
|
+
upper_margin: Margin to upper limit.
|
|
440
|
+
lower_margin: Margin to lower limit.
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
Margin percentage or None if cannot compute.
|
|
444
|
+
"""
|
|
445
|
+
if actual_upper is not None and actual_lower is not None:
|
|
446
|
+
limit_range = actual_upper - actual_lower
|
|
447
|
+
if limit_range > 0:
|
|
448
|
+
min_margin = min(
|
|
449
|
+
upper_margin if upper_margin is not None else float("inf"),
|
|
450
|
+
lower_margin if lower_margin is not None else float("inf"),
|
|
451
|
+
)
|
|
452
|
+
return (min_margin / limit_range) * 100.0
|
|
453
|
+
return None
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _check_guardband_status(
|
|
457
|
+
num_violations: int,
|
|
458
|
+
limits: LimitSpec,
|
|
459
|
+
upper_margin: float | None,
|
|
460
|
+
lower_margin: float | None,
|
|
461
|
+
) -> bool:
|
|
462
|
+
"""Check if data is within guardband region.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
num_violations: Number of limit violations.
|
|
466
|
+
limits: Limit specification with guardbands.
|
|
467
|
+
upper_margin: Margin to upper limit.
|
|
468
|
+
lower_margin: Margin to lower limit.
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
True if within guardband but outside tight limits.
|
|
472
|
+
"""
|
|
473
|
+
if num_violations > 0:
|
|
474
|
+
return False
|
|
475
|
+
|
|
476
|
+
within_guardband = False
|
|
477
|
+
|
|
478
|
+
if limits.upper_guardband > 0 and upper_margin is not None:
|
|
479
|
+
if upper_margin < limits.upper_guardband:
|
|
480
|
+
within_guardband = True
|
|
481
|
+
|
|
482
|
+
if limits.lower_guardband > 0 and lower_margin is not None:
|
|
483
|
+
if lower_margin < limits.lower_guardband:
|
|
484
|
+
within_guardband = True
|
|
485
|
+
|
|
486
|
+
return within_guardband
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _extract_data_array(trace: WaveformTrace | NDArray[np.floating[Any]]) -> NDArray[np.float64]:
|
|
490
|
+
"""Extract numpy array from trace or array input.
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
trace: WaveformTrace object or numpy array.
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
Data as float64 numpy array.
|
|
497
|
+
"""
|
|
325
498
|
if isinstance(trace, WaveformTrace):
|
|
326
|
-
|
|
499
|
+
return trace.data.astype(np.float64)
|
|
327
500
|
else:
|
|
328
|
-
|
|
501
|
+
return np.asarray(trace, dtype=np.float64)
|
|
329
502
|
|
|
330
|
-
max_val = float(np.max(data))
|
|
331
|
-
min_val = float(np.min(data))
|
|
332
503
|
|
|
333
|
-
|
|
504
|
+
def _calculate_margins(
|
|
505
|
+
limits: LimitSpec, max_val: float, min_val: float
|
|
506
|
+
) -> tuple[float | None, float | None]:
|
|
507
|
+
"""Calculate margin to upper and lower limits.
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
limits: Specification limits.
|
|
511
|
+
max_val: Maximum value in data.
|
|
512
|
+
min_val: Minimum value in data.
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
Tuple of (upper_margin, lower_margin). None if limit not defined.
|
|
516
|
+
"""
|
|
334
517
|
upper_margin = None
|
|
335
518
|
lower_margin = None
|
|
336
519
|
|
|
@@ -339,8 +522,25 @@ def margin_analysis(
|
|
|
339
522
|
if limits.lower is not None:
|
|
340
523
|
lower_margin = min_val - limits.lower
|
|
341
524
|
|
|
342
|
-
|
|
343
|
-
|
|
525
|
+
return (upper_margin, lower_margin)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _find_critical_limit(
|
|
529
|
+
upper_margin: float | None, lower_margin: float | None
|
|
530
|
+
) -> tuple[float, Literal["upper", "lower", "both", "none"]]:
|
|
531
|
+
"""Find minimum margin and identify critical limit.
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
upper_margin: Margin to upper limit (None if no upper limit).
|
|
535
|
+
lower_margin: Margin to lower limit (None if no lower limit).
|
|
536
|
+
|
|
537
|
+
Returns:
|
|
538
|
+
Tuple of (minimum_margin, critical_limit_name).
|
|
539
|
+
|
|
540
|
+
Raises:
|
|
541
|
+
AnalysisError: If no limits defined.
|
|
542
|
+
"""
|
|
543
|
+
margins: list[tuple[str, float]] = []
|
|
344
544
|
if upper_margin is not None:
|
|
345
545
|
margins.append(("upper", upper_margin))
|
|
346
546
|
if lower_margin is not None:
|
|
@@ -353,39 +553,63 @@ def margin_analysis(
|
|
|
353
553
|
min_margin_tuple = min(margins, key=lambda x: x[1])
|
|
354
554
|
min_margin = min_margin_tuple[1]
|
|
355
555
|
|
|
356
|
-
# Determine critical limit
|
|
556
|
+
# Determine critical limit (both if equal margins)
|
|
357
557
|
if len(margins) == 2 and abs(margins[0][1] - margins[1][1]) < 1e-10:
|
|
358
558
|
critical_limit: Literal["upper", "lower", "both", "none"] = "both"
|
|
359
559
|
else:
|
|
360
560
|
critical_limit = min_margin_tuple[0] # type: ignore[assignment]
|
|
361
561
|
|
|
362
|
-
|
|
363
|
-
|
|
562
|
+
return (min_margin, critical_limit)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _calculate_margin_percentage(
|
|
566
|
+
limits: LimitSpec,
|
|
567
|
+
upper_margin: float | None,
|
|
568
|
+
lower_margin: float | None,
|
|
569
|
+
min_margin: float,
|
|
570
|
+
) -> float:
|
|
571
|
+
"""Calculate margin as percentage of limit range.
|
|
572
|
+
|
|
573
|
+
Args:
|
|
574
|
+
limits: Specification limits.
|
|
575
|
+
upper_margin: Margin to upper limit.
|
|
576
|
+
lower_margin: Margin to lower limit.
|
|
577
|
+
min_margin: Minimum of upper/lower margins.
|
|
578
|
+
|
|
579
|
+
Returns:
|
|
580
|
+
Margin percentage (0-100+).
|
|
581
|
+
"""
|
|
582
|
+
# Prefer range-based percentage if both limits defined
|
|
364
583
|
if limits.upper is not None and limits.lower is not None:
|
|
365
584
|
limit_range = limits.upper - limits.lower
|
|
366
585
|
if limit_range > 0:
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
586
|
+
return (min_margin / limit_range) * 100.0
|
|
587
|
+
|
|
588
|
+
# Single limit: use absolute value
|
|
589
|
+
if limits.upper is not None and upper_margin is not None:
|
|
590
|
+
return (upper_margin / abs(limits.upper)) * 100.0 if limits.upper != 0 else 0.0
|
|
370
591
|
elif limits.lower is not None and lower_margin is not None:
|
|
371
|
-
|
|
592
|
+
return (lower_margin / abs(limits.lower)) * 100.0 if limits.lower != 0 else 0.0
|
|
593
|
+
|
|
594
|
+
return 0.0
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _determine_margin_status(
|
|
598
|
+
min_margin: float, margin_pct: float, warning_threshold_pct: float
|
|
599
|
+
) -> tuple[Literal["pass", "warning", "fail"], bool]:
|
|
600
|
+
"""Determine margin status (pass/warning/fail).
|
|
601
|
+
|
|
602
|
+
Args:
|
|
603
|
+
min_margin: Minimum margin value.
|
|
604
|
+
margin_pct: Margin as percentage.
|
|
605
|
+
warning_threshold_pct: Warning threshold percentage.
|
|
372
606
|
|
|
373
|
-
|
|
374
|
-
|
|
607
|
+
Returns:
|
|
608
|
+
Tuple of (status, warning_flag).
|
|
609
|
+
"""
|
|
375
610
|
if min_margin < 0:
|
|
376
|
-
|
|
611
|
+
return ("fail", False)
|
|
377
612
|
elif margin_pct < warning_threshold_pct:
|
|
378
|
-
|
|
379
|
-
warning = True
|
|
613
|
+
return ("warning", True)
|
|
380
614
|
else:
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
return MarginAnalysis(
|
|
384
|
-
upper_margin=upper_margin,
|
|
385
|
-
lower_margin=lower_margin,
|
|
386
|
-
min_margin=min_margin,
|
|
387
|
-
margin_percentage=margin_pct,
|
|
388
|
-
critical_limit=critical_limit,
|
|
389
|
-
warning=warning,
|
|
390
|
-
margin_status=margin_status,
|
|
391
|
-
)
|
|
615
|
+
return ("pass", False)
|