oscura 0.0.1__py3-none-any.whl → 0.1.1__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 +813 -8
- oscura/__main__.py +392 -0
- oscura/analyzers/__init__.py +37 -0
- oscura/analyzers/digital/__init__.py +177 -0
- oscura/analyzers/digital/bus.py +691 -0
- oscura/analyzers/digital/clock.py +805 -0
- oscura/analyzers/digital/correlation.py +720 -0
- oscura/analyzers/digital/edges.py +632 -0
- oscura/analyzers/digital/extraction.py +413 -0
- oscura/analyzers/digital/quality.py +878 -0
- oscura/analyzers/digital/signal_quality.py +877 -0
- oscura/analyzers/digital/thresholds.py +708 -0
- oscura/analyzers/digital/timing.py +1104 -0
- oscura/analyzers/eye/__init__.py +46 -0
- oscura/analyzers/eye/diagram.py +434 -0
- oscura/analyzers/eye/metrics.py +555 -0
- oscura/analyzers/jitter/__init__.py +83 -0
- oscura/analyzers/jitter/ber.py +333 -0
- oscura/analyzers/jitter/decomposition.py +759 -0
- oscura/analyzers/jitter/measurements.py +413 -0
- oscura/analyzers/jitter/spectrum.py +220 -0
- oscura/analyzers/measurements.py +40 -0
- oscura/analyzers/packet/__init__.py +171 -0
- oscura/analyzers/packet/daq.py +1077 -0
- oscura/analyzers/packet/metrics.py +437 -0
- oscura/analyzers/packet/parser.py +327 -0
- oscura/analyzers/packet/payload.py +2156 -0
- oscura/analyzers/packet/payload_analysis.py +1312 -0
- oscura/analyzers/packet/payload_extraction.py +236 -0
- oscura/analyzers/packet/payload_patterns.py +670 -0
- oscura/analyzers/packet/stream.py +359 -0
- oscura/analyzers/patterns/__init__.py +266 -0
- oscura/analyzers/patterns/clustering.py +1036 -0
- oscura/analyzers/patterns/discovery.py +539 -0
- oscura/analyzers/patterns/learning.py +797 -0
- oscura/analyzers/patterns/matching.py +1091 -0
- oscura/analyzers/patterns/periodic.py +650 -0
- oscura/analyzers/patterns/sequences.py +767 -0
- oscura/analyzers/power/__init__.py +116 -0
- oscura/analyzers/power/ac_power.py +391 -0
- oscura/analyzers/power/basic.py +383 -0
- oscura/analyzers/power/conduction.py +314 -0
- oscura/analyzers/power/efficiency.py +297 -0
- oscura/analyzers/power/ripple.py +356 -0
- oscura/analyzers/power/soa.py +372 -0
- oscura/analyzers/power/switching.py +479 -0
- oscura/analyzers/protocol/__init__.py +150 -0
- oscura/analyzers/protocols/__init__.py +150 -0
- oscura/analyzers/protocols/base.py +500 -0
- oscura/analyzers/protocols/can.py +620 -0
- oscura/analyzers/protocols/can_fd.py +448 -0
- oscura/analyzers/protocols/flexray.py +405 -0
- oscura/analyzers/protocols/hdlc.py +399 -0
- oscura/analyzers/protocols/i2c.py +368 -0
- oscura/analyzers/protocols/i2s.py +296 -0
- oscura/analyzers/protocols/jtag.py +393 -0
- oscura/analyzers/protocols/lin.py +445 -0
- oscura/analyzers/protocols/manchester.py +333 -0
- oscura/analyzers/protocols/onewire.py +501 -0
- oscura/analyzers/protocols/spi.py +334 -0
- oscura/analyzers/protocols/swd.py +325 -0
- oscura/analyzers/protocols/uart.py +393 -0
- oscura/analyzers/protocols/usb.py +495 -0
- oscura/analyzers/signal_integrity/__init__.py +63 -0
- oscura/analyzers/signal_integrity/embedding.py +294 -0
- oscura/analyzers/signal_integrity/equalization.py +370 -0
- oscura/analyzers/signal_integrity/sparams.py +484 -0
- oscura/analyzers/spectral/__init__.py +53 -0
- oscura/analyzers/spectral/chunked.py +273 -0
- oscura/analyzers/spectral/chunked_fft.py +571 -0
- oscura/analyzers/spectral/chunked_wavelet.py +391 -0
- oscura/analyzers/spectral/fft.py +92 -0
- oscura/analyzers/statistical/__init__.py +250 -0
- oscura/analyzers/statistical/checksum.py +923 -0
- oscura/analyzers/statistical/chunked_corr.py +228 -0
- oscura/analyzers/statistical/classification.py +778 -0
- oscura/analyzers/statistical/entropy.py +1113 -0
- oscura/analyzers/statistical/ngrams.py +614 -0
- oscura/analyzers/statistics/__init__.py +119 -0
- oscura/analyzers/statistics/advanced.py +885 -0
- oscura/analyzers/statistics/basic.py +263 -0
- oscura/analyzers/statistics/correlation.py +630 -0
- oscura/analyzers/statistics/distribution.py +298 -0
- oscura/analyzers/statistics/outliers.py +463 -0
- oscura/analyzers/statistics/streaming.py +93 -0
- oscura/analyzers/statistics/trend.py +520 -0
- oscura/analyzers/validation.py +598 -0
- oscura/analyzers/waveform/__init__.py +36 -0
- oscura/analyzers/waveform/measurements.py +943 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
- oscura/analyzers/waveform/spectral.py +1689 -0
- oscura/analyzers/waveform/wavelets.py +298 -0
- oscura/api/__init__.py +62 -0
- oscura/api/dsl.py +538 -0
- oscura/api/fluent.py +571 -0
- oscura/api/operators.py +498 -0
- oscura/api/optimization.py +392 -0
- oscura/api/profiling.py +396 -0
- oscura/automotive/__init__.py +73 -0
- oscura/automotive/can/__init__.py +52 -0
- oscura/automotive/can/analysis.py +356 -0
- oscura/automotive/can/checksum.py +250 -0
- oscura/automotive/can/correlation.py +212 -0
- oscura/automotive/can/discovery.py +355 -0
- oscura/automotive/can/message_wrapper.py +375 -0
- oscura/automotive/can/models.py +385 -0
- oscura/automotive/can/patterns.py +381 -0
- oscura/automotive/can/session.py +452 -0
- oscura/automotive/can/state_machine.py +300 -0
- oscura/automotive/can/stimulus_response.py +461 -0
- oscura/automotive/dbc/__init__.py +15 -0
- oscura/automotive/dbc/generator.py +156 -0
- oscura/automotive/dbc/parser.py +146 -0
- oscura/automotive/dtc/__init__.py +30 -0
- oscura/automotive/dtc/database.py +3036 -0
- oscura/automotive/j1939/__init__.py +14 -0
- oscura/automotive/j1939/decoder.py +745 -0
- oscura/automotive/loaders/__init__.py +35 -0
- oscura/automotive/loaders/asc.py +98 -0
- oscura/automotive/loaders/blf.py +77 -0
- oscura/automotive/loaders/csv_can.py +136 -0
- oscura/automotive/loaders/dispatcher.py +136 -0
- oscura/automotive/loaders/mdf.py +331 -0
- oscura/automotive/loaders/pcap.py +132 -0
- oscura/automotive/obd/__init__.py +14 -0
- oscura/automotive/obd/decoder.py +707 -0
- oscura/automotive/uds/__init__.py +48 -0
- oscura/automotive/uds/decoder.py +265 -0
- oscura/automotive/uds/models.py +64 -0
- oscura/automotive/visualization.py +369 -0
- oscura/batch/__init__.py +55 -0
- oscura/batch/advanced.py +627 -0
- oscura/batch/aggregate.py +300 -0
- oscura/batch/analyze.py +139 -0
- oscura/batch/logging.py +487 -0
- oscura/batch/metrics.py +556 -0
- oscura/builders/__init__.py +41 -0
- oscura/builders/signal_builder.py +1131 -0
- oscura/cli/__init__.py +14 -0
- oscura/cli/batch.py +339 -0
- oscura/cli/characterize.py +273 -0
- oscura/cli/compare.py +775 -0
- oscura/cli/decode.py +551 -0
- oscura/cli/main.py +247 -0
- oscura/cli/shell.py +350 -0
- oscura/comparison/__init__.py +66 -0
- oscura/comparison/compare.py +397 -0
- oscura/comparison/golden.py +487 -0
- oscura/comparison/limits.py +391 -0
- oscura/comparison/mask.py +434 -0
- oscura/comparison/trace_diff.py +30 -0
- oscura/comparison/visualization.py +481 -0
- oscura/compliance/__init__.py +70 -0
- oscura/compliance/advanced.py +756 -0
- oscura/compliance/masks.py +363 -0
- oscura/compliance/reporting.py +483 -0
- oscura/compliance/testing.py +298 -0
- oscura/component/__init__.py +38 -0
- oscura/component/impedance.py +365 -0
- oscura/component/reactive.py +598 -0
- oscura/component/transmission_line.py +312 -0
- oscura/config/__init__.py +191 -0
- oscura/config/defaults.py +254 -0
- oscura/config/loader.py +348 -0
- oscura/config/memory.py +271 -0
- oscura/config/migration.py +458 -0
- oscura/config/pipeline.py +1077 -0
- oscura/config/preferences.py +530 -0
- oscura/config/protocol.py +875 -0
- oscura/config/schema.py +713 -0
- oscura/config/settings.py +420 -0
- oscura/config/thresholds.py +599 -0
- oscura/convenience.py +457 -0
- oscura/core/__init__.py +299 -0
- oscura/core/audit.py +457 -0
- oscura/core/backend_selector.py +405 -0
- oscura/core/cache.py +590 -0
- oscura/core/cancellation.py +439 -0
- oscura/core/confidence.py +225 -0
- oscura/core/config.py +506 -0
- oscura/core/correlation.py +216 -0
- oscura/core/cross_domain.py +422 -0
- oscura/core/debug.py +301 -0
- oscura/core/edge_cases.py +541 -0
- oscura/core/exceptions.py +535 -0
- oscura/core/gpu_backend.py +523 -0
- oscura/core/lazy.py +832 -0
- oscura/core/log_query.py +540 -0
- oscura/core/logging.py +931 -0
- oscura/core/logging_advanced.py +952 -0
- oscura/core/memoize.py +171 -0
- oscura/core/memory_check.py +274 -0
- oscura/core/memory_guard.py +290 -0
- oscura/core/memory_limits.py +336 -0
- oscura/core/memory_monitor.py +453 -0
- oscura/core/memory_progress.py +465 -0
- oscura/core/memory_warnings.py +315 -0
- oscura/core/numba_backend.py +362 -0
- oscura/core/performance.py +352 -0
- oscura/core/progress.py +524 -0
- oscura/core/provenance.py +358 -0
- oscura/core/results.py +331 -0
- oscura/core/types.py +504 -0
- oscura/core/uncertainty.py +383 -0
- oscura/discovery/__init__.py +52 -0
- oscura/discovery/anomaly_detector.py +672 -0
- oscura/discovery/auto_decoder.py +415 -0
- oscura/discovery/comparison.py +497 -0
- oscura/discovery/quality_validator.py +528 -0
- oscura/discovery/signal_detector.py +769 -0
- oscura/dsl/__init__.py +73 -0
- oscura/dsl/commands.py +246 -0
- oscura/dsl/interpreter.py +455 -0
- oscura/dsl/parser.py +689 -0
- oscura/dsl/repl.py +172 -0
- oscura/exceptions.py +59 -0
- oscura/exploratory/__init__.py +111 -0
- oscura/exploratory/error_recovery.py +642 -0
- oscura/exploratory/fuzzy.py +513 -0
- oscura/exploratory/fuzzy_advanced.py +786 -0
- oscura/exploratory/legacy.py +831 -0
- oscura/exploratory/parse.py +358 -0
- oscura/exploratory/recovery.py +275 -0
- oscura/exploratory/sync.py +382 -0
- oscura/exploratory/unknown.py +707 -0
- oscura/export/__init__.py +25 -0
- oscura/export/wireshark/README.md +265 -0
- oscura/export/wireshark/__init__.py +47 -0
- oscura/export/wireshark/generator.py +312 -0
- oscura/export/wireshark/lua_builder.py +159 -0
- oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
- oscura/export/wireshark/type_mapping.py +165 -0
- oscura/export/wireshark/validator.py +105 -0
- oscura/exporters/__init__.py +94 -0
- oscura/exporters/csv.py +303 -0
- oscura/exporters/exporters.py +44 -0
- oscura/exporters/hdf5.py +219 -0
- oscura/exporters/html_export.py +701 -0
- oscura/exporters/json_export.py +291 -0
- oscura/exporters/markdown_export.py +367 -0
- oscura/exporters/matlab_export.py +354 -0
- oscura/exporters/npz_export.py +219 -0
- oscura/exporters/spice_export.py +210 -0
- oscura/extensibility/__init__.py +131 -0
- oscura/extensibility/docs.py +752 -0
- oscura/extensibility/extensions.py +1125 -0
- oscura/extensibility/logging.py +259 -0
- oscura/extensibility/measurements.py +485 -0
- oscura/extensibility/plugins.py +414 -0
- oscura/extensibility/registry.py +346 -0
- oscura/extensibility/templates.py +913 -0
- oscura/extensibility/validation.py +651 -0
- oscura/filtering/__init__.py +89 -0
- oscura/filtering/base.py +563 -0
- oscura/filtering/convenience.py +564 -0
- oscura/filtering/design.py +725 -0
- oscura/filtering/filters.py +32 -0
- oscura/filtering/introspection.py +605 -0
- oscura/guidance/__init__.py +24 -0
- oscura/guidance/recommender.py +429 -0
- oscura/guidance/wizard.py +518 -0
- oscura/inference/__init__.py +251 -0
- oscura/inference/active_learning/README.md +153 -0
- oscura/inference/active_learning/__init__.py +38 -0
- oscura/inference/active_learning/lstar.py +257 -0
- oscura/inference/active_learning/observation_table.py +230 -0
- oscura/inference/active_learning/oracle.py +78 -0
- oscura/inference/active_learning/teachers/__init__.py +15 -0
- oscura/inference/active_learning/teachers/simulator.py +192 -0
- oscura/inference/adaptive_tuning.py +453 -0
- oscura/inference/alignment.py +653 -0
- oscura/inference/bayesian.py +943 -0
- oscura/inference/binary.py +1016 -0
- oscura/inference/crc_reverse.py +711 -0
- oscura/inference/logic.py +288 -0
- oscura/inference/message_format.py +1305 -0
- oscura/inference/protocol.py +417 -0
- oscura/inference/protocol_dsl.py +1084 -0
- oscura/inference/protocol_library.py +1230 -0
- oscura/inference/sequences.py +809 -0
- oscura/inference/signal_intelligence.py +1509 -0
- oscura/inference/spectral.py +215 -0
- oscura/inference/state_machine.py +634 -0
- oscura/inference/stream.py +918 -0
- oscura/integrations/__init__.py +59 -0
- oscura/integrations/llm.py +1827 -0
- oscura/jupyter/__init__.py +32 -0
- oscura/jupyter/display.py +268 -0
- oscura/jupyter/magic.py +334 -0
- oscura/loaders/__init__.py +526 -0
- oscura/loaders/binary.py +69 -0
- oscura/loaders/configurable.py +1255 -0
- oscura/loaders/csv.py +26 -0
- oscura/loaders/csv_loader.py +473 -0
- oscura/loaders/hdf5.py +9 -0
- oscura/loaders/hdf5_loader.py +510 -0
- oscura/loaders/lazy.py +370 -0
- oscura/loaders/mmap_loader.py +583 -0
- oscura/loaders/numpy_loader.py +436 -0
- oscura/loaders/pcap.py +432 -0
- oscura/loaders/preprocessing.py +368 -0
- oscura/loaders/rigol.py +287 -0
- oscura/loaders/sigrok.py +321 -0
- oscura/loaders/tdms.py +367 -0
- oscura/loaders/tektronix.py +711 -0
- oscura/loaders/validation.py +584 -0
- oscura/loaders/vcd.py +464 -0
- oscura/loaders/wav.py +233 -0
- oscura/math/__init__.py +45 -0
- oscura/math/arithmetic.py +824 -0
- oscura/math/interpolation.py +413 -0
- oscura/onboarding/__init__.py +39 -0
- oscura/onboarding/help.py +498 -0
- oscura/onboarding/tutorials.py +405 -0
- oscura/onboarding/wizard.py +466 -0
- oscura/optimization/__init__.py +19 -0
- oscura/optimization/parallel.py +440 -0
- oscura/optimization/search.py +532 -0
- oscura/pipeline/__init__.py +43 -0
- oscura/pipeline/base.py +338 -0
- oscura/pipeline/composition.py +242 -0
- oscura/pipeline/parallel.py +448 -0
- oscura/pipeline/pipeline.py +375 -0
- oscura/pipeline/reverse_engineering.py +1119 -0
- oscura/plugins/__init__.py +122 -0
- oscura/plugins/base.py +272 -0
- oscura/plugins/cli.py +497 -0
- oscura/plugins/discovery.py +411 -0
- oscura/plugins/isolation.py +418 -0
- oscura/plugins/lifecycle.py +959 -0
- oscura/plugins/manager.py +493 -0
- oscura/plugins/registry.py +421 -0
- oscura/plugins/versioning.py +372 -0
- oscura/py.typed +0 -0
- oscura/quality/__init__.py +65 -0
- oscura/quality/ensemble.py +740 -0
- oscura/quality/explainer.py +338 -0
- oscura/quality/scoring.py +616 -0
- oscura/quality/warnings.py +456 -0
- oscura/reporting/__init__.py +248 -0
- oscura/reporting/advanced.py +1234 -0
- oscura/reporting/analyze.py +448 -0
- oscura/reporting/argument_preparer.py +596 -0
- oscura/reporting/auto_report.py +507 -0
- oscura/reporting/batch.py +615 -0
- oscura/reporting/chart_selection.py +223 -0
- oscura/reporting/comparison.py +330 -0
- oscura/reporting/config.py +615 -0
- oscura/reporting/content/__init__.py +39 -0
- oscura/reporting/content/executive.py +127 -0
- oscura/reporting/content/filtering.py +191 -0
- oscura/reporting/content/minimal.py +257 -0
- oscura/reporting/content/verbosity.py +162 -0
- oscura/reporting/core.py +508 -0
- oscura/reporting/core_formats/__init__.py +17 -0
- oscura/reporting/core_formats/multi_format.py +210 -0
- oscura/reporting/engine.py +836 -0
- oscura/reporting/export.py +366 -0
- oscura/reporting/formatting/__init__.py +129 -0
- oscura/reporting/formatting/emphasis.py +81 -0
- oscura/reporting/formatting/numbers.py +403 -0
- oscura/reporting/formatting/standards.py +55 -0
- oscura/reporting/formatting.py +466 -0
- oscura/reporting/html.py +578 -0
- oscura/reporting/index.py +590 -0
- oscura/reporting/multichannel.py +296 -0
- oscura/reporting/output.py +379 -0
- oscura/reporting/pdf.py +373 -0
- oscura/reporting/plots.py +731 -0
- oscura/reporting/pptx_export.py +360 -0
- oscura/reporting/renderers/__init__.py +11 -0
- oscura/reporting/renderers/pdf.py +94 -0
- oscura/reporting/sections.py +471 -0
- oscura/reporting/standards.py +680 -0
- oscura/reporting/summary_generator.py +368 -0
- oscura/reporting/tables.py +397 -0
- oscura/reporting/template_system.py +724 -0
- oscura/reporting/templates/__init__.py +15 -0
- oscura/reporting/templates/definition.py +205 -0
- oscura/reporting/templates/index.html +649 -0
- oscura/reporting/templates/index.md +173 -0
- oscura/schemas/__init__.py +158 -0
- oscura/schemas/bus_configuration.json +322 -0
- oscura/schemas/device_mapping.json +182 -0
- oscura/schemas/packet_format.json +418 -0
- oscura/schemas/protocol_definition.json +363 -0
- oscura/search/__init__.py +16 -0
- oscura/search/anomaly.py +292 -0
- oscura/search/context.py +149 -0
- oscura/search/pattern.py +160 -0
- oscura/session/__init__.py +34 -0
- oscura/session/annotations.py +289 -0
- oscura/session/history.py +313 -0
- oscura/session/session.py +445 -0
- oscura/streaming/__init__.py +43 -0
- oscura/streaming/chunked.py +611 -0
- oscura/streaming/progressive.py +393 -0
- oscura/streaming/realtime.py +622 -0
- oscura/testing/__init__.py +54 -0
- oscura/testing/synthetic.py +808 -0
- oscura/triggering/__init__.py +68 -0
- oscura/triggering/base.py +229 -0
- oscura/triggering/edge.py +353 -0
- oscura/triggering/pattern.py +344 -0
- oscura/triggering/pulse.py +581 -0
- oscura/triggering/window.py +453 -0
- oscura/ui/__init__.py +48 -0
- oscura/ui/formatters.py +526 -0
- oscura/ui/progressive_display.py +340 -0
- oscura/utils/__init__.py +99 -0
- oscura/utils/autodetect.py +338 -0
- oscura/utils/buffer.py +389 -0
- oscura/utils/lazy.py +407 -0
- oscura/utils/lazy_imports.py +147 -0
- oscura/utils/memory.py +836 -0
- oscura/utils/memory_advanced.py +1326 -0
- oscura/utils/memory_extensions.py +465 -0
- oscura/utils/progressive.py +352 -0
- oscura/utils/windowing.py +362 -0
- oscura/visualization/__init__.py +321 -0
- oscura/visualization/accessibility.py +526 -0
- oscura/visualization/annotations.py +374 -0
- oscura/visualization/axis_scaling.py +305 -0
- oscura/visualization/colors.py +453 -0
- oscura/visualization/digital.py +337 -0
- oscura/visualization/eye.py +420 -0
- oscura/visualization/histogram.py +281 -0
- oscura/visualization/interactive.py +858 -0
- oscura/visualization/jitter.py +702 -0
- oscura/visualization/keyboard.py +394 -0
- oscura/visualization/layout.py +365 -0
- oscura/visualization/optimization.py +1028 -0
- oscura/visualization/palettes.py +446 -0
- oscura/visualization/plot.py +92 -0
- oscura/visualization/power.py +290 -0
- oscura/visualization/power_extended.py +626 -0
- oscura/visualization/presets.py +467 -0
- oscura/visualization/protocols.py +932 -0
- oscura/visualization/render.py +207 -0
- oscura/visualization/rendering.py +444 -0
- oscura/visualization/reverse_engineering.py +791 -0
- oscura/visualization/signal_integrity.py +808 -0
- oscura/visualization/specialized.py +553 -0
- oscura/visualization/spectral.py +811 -0
- oscura/visualization/styles.py +381 -0
- oscura/visualization/thumbnails.py +311 -0
- oscura/visualization/time_axis.py +351 -0
- oscura/visualization/waveform.py +367 -0
- oscura/workflow/__init__.py +13 -0
- oscura/workflow/dag.py +377 -0
- oscura/workflows/__init__.py +58 -0
- oscura/workflows/compliance.py +280 -0
- oscura/workflows/digital.py +272 -0
- oscura/workflows/multi_trace.py +502 -0
- oscura/workflows/power.py +178 -0
- oscura/workflows/protocol.py +492 -0
- oscura/workflows/reverse_engineering.py +639 -0
- oscura/workflows/signal_integrity.py +227 -0
- oscura-0.1.1.dist-info/METADATA +300 -0
- oscura-0.1.1.dist-info/RECORD +463 -0
- oscura-0.1.1.dist-info/entry_points.txt +2 -0
- {oscura-0.0.1.dist-info → oscura-0.1.1.dist-info}/licenses/LICENSE +1 -1
- oscura-0.0.1.dist-info/METADATA +0 -63
- oscura-0.0.1.dist-info/RECORD +0 -5
- {oscura-0.0.1.dist-info → oscura-0.1.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
"""Automatic anomaly detection and highlighting.
|
|
2
|
+
|
|
3
|
+
This module detects unusual signal features (glitches, dropouts, noise
|
|
4
|
+
spikes, timing violations) to guide user attention.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.discovery import find_anomalies
|
|
9
|
+
>>> anomalies = find_anomalies(trace)
|
|
10
|
+
>>> for anom in anomalies:
|
|
11
|
+
... print(f"{anom.timestamp_us:.2f}us: {anom.type} - {anom.description}")
|
|
12
|
+
|
|
13
|
+
References:
|
|
14
|
+
IEEE 1057-2017: Digitizing Waveform Recorders
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
from oscura.analyzers.statistics.basic import basic_stats
|
|
25
|
+
from oscura.core.types import DigitalTrace, WaveformTrace
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from numpy.typing import NDArray
|
|
29
|
+
|
|
30
|
+
AnomalyType = Literal[
|
|
31
|
+
"glitch",
|
|
32
|
+
"dropout",
|
|
33
|
+
"noise_spike",
|
|
34
|
+
"timing_violation",
|
|
35
|
+
"ringing",
|
|
36
|
+
"overshoot",
|
|
37
|
+
"undershoot",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
Severity = Literal["CRITICAL", "WARNING", "INFO"]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class Anomaly:
|
|
45
|
+
"""Detected signal anomaly.
|
|
46
|
+
|
|
47
|
+
Represents an unusual or interesting signal feature with timing,
|
|
48
|
+
classification, and plain-language explanation.
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
timestamp_us: Anomaly start time in microseconds.
|
|
52
|
+
type: Type of anomaly detected.
|
|
53
|
+
severity: Impact level (CRITICAL, WARNING, INFO).
|
|
54
|
+
description: Plain-language explanation.
|
|
55
|
+
duration_ns: Duration in nanoseconds.
|
|
56
|
+
confidence: Detection confidence (0.0-1.0).
|
|
57
|
+
metadata: Additional type-specific information.
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
>>> anomaly = Anomaly(
|
|
61
|
+
... timestamp_us=45.23,
|
|
62
|
+
... type="glitch",
|
|
63
|
+
... severity="WARNING",
|
|
64
|
+
... description="Brief 35ns pulse, likely noise spike",
|
|
65
|
+
... duration_ns=35.0,
|
|
66
|
+
... confidence=0.92
|
|
67
|
+
... )
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
timestamp_us: float
|
|
71
|
+
type: AnomalyType
|
|
72
|
+
severity: Severity
|
|
73
|
+
description: str
|
|
74
|
+
duration_ns: float = 0.0
|
|
75
|
+
confidence: float = 1.0
|
|
76
|
+
metadata: dict[str, float] = field(default_factory=dict)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def find_anomalies(
|
|
80
|
+
trace: WaveformTrace | DigitalTrace,
|
|
81
|
+
*,
|
|
82
|
+
severity_filter: list[Severity] | None = None,
|
|
83
|
+
min_confidence: float = 0.7,
|
|
84
|
+
anomaly_types: list[AnomalyType] | None = None,
|
|
85
|
+
) -> list[Anomaly]:
|
|
86
|
+
"""Detect anomalies in signal automatically.
|
|
87
|
+
|
|
88
|
+
Identifies glitches, dropouts, noise spikes, timing violations, ringing,
|
|
89
|
+
and overshoot/undershoot without requiring user configuration.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
trace: Input waveform or digital trace.
|
|
93
|
+
severity_filter: Only return specified severity levels (default: all).
|
|
94
|
+
min_confidence: Minimum confidence threshold (0.0-1.0).
|
|
95
|
+
anomaly_types: Specific anomaly types to detect (default: all).
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
List of detected Anomaly objects, sorted by timestamp.
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
ValueError: If trace is empty or invalid.
|
|
102
|
+
|
|
103
|
+
Example:
|
|
104
|
+
>>> anomalies = find_anomalies(trace, severity_filter=['CRITICAL', 'WARNING'])
|
|
105
|
+
>>> print(f"Found {len(anomalies)} critical/warning anomalies")
|
|
106
|
+
>>> for anom in anomalies[:5]:
|
|
107
|
+
... print(f" {anom.timestamp_us:.2f}us: {anom.type} - {anom.description}")
|
|
108
|
+
|
|
109
|
+
References:
|
|
110
|
+
DISC-002: Anomaly Highlighting
|
|
111
|
+
"""
|
|
112
|
+
# Validate input
|
|
113
|
+
if len(trace) == 0:
|
|
114
|
+
raise ValueError("Cannot detect anomalies in empty trace")
|
|
115
|
+
|
|
116
|
+
# Get signal data
|
|
117
|
+
if isinstance(trace, WaveformTrace):
|
|
118
|
+
data = trace.data
|
|
119
|
+
sample_rate = trace.metadata.sample_rate
|
|
120
|
+
else:
|
|
121
|
+
data = trace.data.astype(np.float64)
|
|
122
|
+
sample_rate = trace.metadata.sample_rate
|
|
123
|
+
|
|
124
|
+
# Compute basic statistics for reference
|
|
125
|
+
stats = basic_stats(data)
|
|
126
|
+
voltage_swing = stats["max"] - stats["min"]
|
|
127
|
+
|
|
128
|
+
# Collect all anomalies
|
|
129
|
+
all_anomalies: list[Anomaly] = []
|
|
130
|
+
|
|
131
|
+
# Define which anomaly types to check
|
|
132
|
+
if anomaly_types is None:
|
|
133
|
+
check_types: list[AnomalyType] = [
|
|
134
|
+
"glitch",
|
|
135
|
+
"dropout",
|
|
136
|
+
"noise_spike",
|
|
137
|
+
"timing_violation",
|
|
138
|
+
"ringing",
|
|
139
|
+
"overshoot",
|
|
140
|
+
"undershoot",
|
|
141
|
+
]
|
|
142
|
+
else:
|
|
143
|
+
check_types = anomaly_types
|
|
144
|
+
|
|
145
|
+
# Detect each type
|
|
146
|
+
if "glitch" in check_types:
|
|
147
|
+
all_anomalies.extend(_detect_glitches(data, sample_rate, voltage_swing, stats))
|
|
148
|
+
|
|
149
|
+
if "dropout" in check_types:
|
|
150
|
+
all_anomalies.extend(_detect_dropouts(data, sample_rate, voltage_swing, stats))
|
|
151
|
+
|
|
152
|
+
if "noise_spike" in check_types:
|
|
153
|
+
all_anomalies.extend(_detect_noise_spikes(data, sample_rate, voltage_swing, stats))
|
|
154
|
+
|
|
155
|
+
if "timing_violation" in check_types:
|
|
156
|
+
all_anomalies.extend(_detect_timing_violations(data, sample_rate, stats))
|
|
157
|
+
|
|
158
|
+
if "ringing" in check_types:
|
|
159
|
+
all_anomalies.extend(_detect_ringing(data, sample_rate, voltage_swing, stats))
|
|
160
|
+
|
|
161
|
+
if "overshoot" in check_types:
|
|
162
|
+
all_anomalies.extend(_detect_overshoot(data, sample_rate, voltage_swing, stats))
|
|
163
|
+
|
|
164
|
+
if "undershoot" in check_types:
|
|
165
|
+
all_anomalies.extend(_detect_undershoot(data, sample_rate, voltage_swing, stats))
|
|
166
|
+
|
|
167
|
+
# Filter by confidence
|
|
168
|
+
all_anomalies = [a for a in all_anomalies if a.confidence >= min_confidence]
|
|
169
|
+
|
|
170
|
+
# Filter by severity if requested
|
|
171
|
+
if severity_filter is not None:
|
|
172
|
+
all_anomalies = [a for a in all_anomalies if a.severity in severity_filter]
|
|
173
|
+
|
|
174
|
+
# Sort by timestamp
|
|
175
|
+
all_anomalies.sort(key=lambda a: a.timestamp_us)
|
|
176
|
+
|
|
177
|
+
return all_anomalies
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _detect_glitches(
|
|
181
|
+
data: NDArray[np.floating[Any]],
|
|
182
|
+
sample_rate: float,
|
|
183
|
+
voltage_swing: float,
|
|
184
|
+
stats: dict[str, float],
|
|
185
|
+
) -> list[Anomaly]:
|
|
186
|
+
"""Detect brief narrow pulses (glitches).
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
data: Signal data array.
|
|
190
|
+
sample_rate: Sample rate in Hz.
|
|
191
|
+
voltage_swing: Peak-to-peak voltage.
|
|
192
|
+
stats: Basic statistics.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
List of detected glitch anomalies.
|
|
196
|
+
"""
|
|
197
|
+
anomalies: list[Anomaly] = []
|
|
198
|
+
|
|
199
|
+
if voltage_swing == 0 or len(data) < 10:
|
|
200
|
+
return anomalies
|
|
201
|
+
|
|
202
|
+
# Threshold for glitch detection
|
|
203
|
+
threshold = stats["mean"]
|
|
204
|
+
glitch_threshold = voltage_swing * 0.3 # 30% of swing
|
|
205
|
+
|
|
206
|
+
# Find samples far from mean
|
|
207
|
+
deviations = np.abs(data - threshold)
|
|
208
|
+
glitch_candidates = np.where(deviations > glitch_threshold)[0]
|
|
209
|
+
|
|
210
|
+
if len(glitch_candidates) == 0:
|
|
211
|
+
return anomalies
|
|
212
|
+
|
|
213
|
+
# Group consecutive samples into glitches
|
|
214
|
+
glitch_groups = []
|
|
215
|
+
current_group = [glitch_candidates[0]]
|
|
216
|
+
|
|
217
|
+
for idx in glitch_candidates[1:]:
|
|
218
|
+
if idx == current_group[-1] + 1:
|
|
219
|
+
current_group.append(idx)
|
|
220
|
+
else:
|
|
221
|
+
glitch_groups.append(current_group)
|
|
222
|
+
current_group = [idx]
|
|
223
|
+
|
|
224
|
+
glitch_groups.append(current_group)
|
|
225
|
+
|
|
226
|
+
# Analyze each glitch
|
|
227
|
+
for group in glitch_groups:
|
|
228
|
+
duration_samples = len(group)
|
|
229
|
+
duration_ns = (duration_samples / sample_rate) * 1e9
|
|
230
|
+
|
|
231
|
+
# Only report glitches < 50ns
|
|
232
|
+
if duration_ns < 50:
|
|
233
|
+
timestamp_us = (group[0] / sample_rate) * 1e6
|
|
234
|
+
magnitude = np.max(np.abs(data[group] - threshold))
|
|
235
|
+
|
|
236
|
+
# Determine severity based on magnitude
|
|
237
|
+
if magnitude > voltage_swing * 0.5:
|
|
238
|
+
severity: Severity = "WARNING"
|
|
239
|
+
else:
|
|
240
|
+
severity = "INFO"
|
|
241
|
+
|
|
242
|
+
description = f"Brief {duration_ns:.0f}ns pulse, likely noise spike"
|
|
243
|
+
|
|
244
|
+
anomalies.append(
|
|
245
|
+
Anomaly(
|
|
246
|
+
timestamp_us=timestamp_us,
|
|
247
|
+
type="glitch",
|
|
248
|
+
severity=severity,
|
|
249
|
+
description=description,
|
|
250
|
+
duration_ns=duration_ns,
|
|
251
|
+
confidence=0.85,
|
|
252
|
+
metadata={"magnitude": magnitude},
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
return anomalies
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _detect_dropouts(
|
|
260
|
+
data: NDArray[np.floating[Any]],
|
|
261
|
+
sample_rate: float,
|
|
262
|
+
voltage_swing: float,
|
|
263
|
+
stats: dict[str, float],
|
|
264
|
+
) -> list[Anomaly]:
|
|
265
|
+
"""Detect missing transitions or prolonged holds.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
data: Signal data array.
|
|
269
|
+
sample_rate: Sample rate in Hz.
|
|
270
|
+
voltage_swing: Peak-to-peak voltage.
|
|
271
|
+
stats: Basic statistics.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
List of detected dropout anomalies.
|
|
275
|
+
"""
|
|
276
|
+
anomalies: list[Anomaly] = []
|
|
277
|
+
|
|
278
|
+
if voltage_swing == 0 or len(data) < 100:
|
|
279
|
+
return anomalies
|
|
280
|
+
|
|
281
|
+
# Estimate expected period from transitions
|
|
282
|
+
threshold = (stats["max"] + stats["min"]) / 2
|
|
283
|
+
digital = data > threshold
|
|
284
|
+
transitions = np.where(np.diff(digital.astype(int)) != 0)[0]
|
|
285
|
+
|
|
286
|
+
if len(transitions) < 5:
|
|
287
|
+
return anomalies
|
|
288
|
+
|
|
289
|
+
# Calculate typical transition interval
|
|
290
|
+
intervals = np.diff(transitions)
|
|
291
|
+
expected_period = np.median(intervals)
|
|
292
|
+
|
|
293
|
+
# Find unusually long intervals (>2x expected)
|
|
294
|
+
for i, interval in enumerate(intervals):
|
|
295
|
+
if interval > expected_period * 2.0:
|
|
296
|
+
timestamp_us = (transitions[i] / sample_rate) * 1e6
|
|
297
|
+
duration_ns = (interval / sample_rate) * 1e9
|
|
298
|
+
multiplier = interval / expected_period
|
|
299
|
+
|
|
300
|
+
description = f"Missing transition, signal held for {multiplier:.1f}x expected duration"
|
|
301
|
+
|
|
302
|
+
# Severity based on how long the dropout is
|
|
303
|
+
if multiplier > 5.0:
|
|
304
|
+
severity: Severity = "CRITICAL"
|
|
305
|
+
elif multiplier > 3.0:
|
|
306
|
+
severity = "WARNING"
|
|
307
|
+
else:
|
|
308
|
+
severity = "INFO"
|
|
309
|
+
|
|
310
|
+
anomalies.append(
|
|
311
|
+
Anomaly(
|
|
312
|
+
timestamp_us=timestamp_us,
|
|
313
|
+
type="dropout",
|
|
314
|
+
severity=severity,
|
|
315
|
+
description=description,
|
|
316
|
+
duration_ns=duration_ns,
|
|
317
|
+
confidence=0.88,
|
|
318
|
+
metadata={"expected_period_ns": (expected_period / sample_rate) * 1e9},
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
return anomalies
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _detect_noise_spikes(
|
|
326
|
+
data: NDArray[np.floating[Any]],
|
|
327
|
+
sample_rate: float,
|
|
328
|
+
voltage_swing: float,
|
|
329
|
+
stats: dict[str, float],
|
|
330
|
+
) -> list[Anomaly]:
|
|
331
|
+
"""Detect noise spikes (>20% of signal swing).
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
data: Signal data array.
|
|
335
|
+
sample_rate: Sample rate in Hz.
|
|
336
|
+
voltage_swing: Peak-to-peak voltage.
|
|
337
|
+
stats: Basic statistics.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
List of detected noise spike anomalies.
|
|
341
|
+
"""
|
|
342
|
+
anomalies: list[Anomaly] = []
|
|
343
|
+
|
|
344
|
+
if voltage_swing == 0 or len(data) < 10:
|
|
345
|
+
return anomalies
|
|
346
|
+
|
|
347
|
+
# Use running window to detect local spikes
|
|
348
|
+
window = 10
|
|
349
|
+
spike_threshold = voltage_swing * 0.2
|
|
350
|
+
|
|
351
|
+
for i in range(window, len(data) - window):
|
|
352
|
+
local_mean = np.mean(data[i - window : i + window])
|
|
353
|
+
deviation = abs(data[i] - local_mean)
|
|
354
|
+
|
|
355
|
+
if deviation > spike_threshold:
|
|
356
|
+
timestamp_us = (i / sample_rate) * 1e6
|
|
357
|
+
percent = (deviation / voltage_swing) * 100
|
|
358
|
+
|
|
359
|
+
description = f"Noise spike {percent:.0f}% of signal swing"
|
|
360
|
+
|
|
361
|
+
# Severity based on spike magnitude
|
|
362
|
+
if percent > 50:
|
|
363
|
+
severity: Severity = "WARNING"
|
|
364
|
+
else:
|
|
365
|
+
severity = "INFO"
|
|
366
|
+
|
|
367
|
+
anomalies.append(
|
|
368
|
+
Anomaly(
|
|
369
|
+
timestamp_us=timestamp_us,
|
|
370
|
+
type="noise_spike",
|
|
371
|
+
severity=severity,
|
|
372
|
+
description=description,
|
|
373
|
+
duration_ns=(1 / sample_rate) * 1e9,
|
|
374
|
+
confidence=0.80,
|
|
375
|
+
metadata={"deviation_v": deviation},
|
|
376
|
+
)
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
# Skip ahead to avoid duplicate detections
|
|
380
|
+
i += window
|
|
381
|
+
|
|
382
|
+
# Limit number of noise spikes reported
|
|
383
|
+
return anomalies[:50]
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _detect_timing_violations(
|
|
387
|
+
data: NDArray[np.floating[Any]],
|
|
388
|
+
sample_rate: float,
|
|
389
|
+
stats: dict[str, float],
|
|
390
|
+
) -> list[Anomaly]:
|
|
391
|
+
"""Detect timing violations (±5% of expected timing).
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
data: Signal data array.
|
|
395
|
+
sample_rate: Sample rate in Hz.
|
|
396
|
+
stats: Basic statistics.
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
List of detected timing violation anomalies.
|
|
400
|
+
"""
|
|
401
|
+
anomalies: list[Anomaly] = []
|
|
402
|
+
|
|
403
|
+
if len(data) < 100:
|
|
404
|
+
return anomalies
|
|
405
|
+
|
|
406
|
+
# Find edges
|
|
407
|
+
threshold = stats["mean"]
|
|
408
|
+
digital = data > threshold
|
|
409
|
+
transitions = np.where(np.diff(digital.astype(int)) != 0)[0]
|
|
410
|
+
|
|
411
|
+
if len(transitions) < 10:
|
|
412
|
+
return anomalies
|
|
413
|
+
|
|
414
|
+
# Analyze timing consistency
|
|
415
|
+
intervals = np.diff(transitions)
|
|
416
|
+
expected_interval = np.median(intervals)
|
|
417
|
+
tolerance = expected_interval * 0.05 # 5% tolerance
|
|
418
|
+
|
|
419
|
+
# Find violations
|
|
420
|
+
for i, interval in enumerate(intervals):
|
|
421
|
+
deviation = abs(interval - expected_interval)
|
|
422
|
+
|
|
423
|
+
if deviation > tolerance:
|
|
424
|
+
timestamp_us = (transitions[i] / sample_rate) * 1e6
|
|
425
|
+
percent_dev = (deviation / expected_interval) * 100
|
|
426
|
+
|
|
427
|
+
description = f"Timing deviation {percent_dev:.1f}% from expected"
|
|
428
|
+
|
|
429
|
+
# Severity based on deviation magnitude
|
|
430
|
+
if percent_dev > 15:
|
|
431
|
+
severity: Severity = "WARNING"
|
|
432
|
+
else:
|
|
433
|
+
severity = "INFO"
|
|
434
|
+
|
|
435
|
+
anomalies.append(
|
|
436
|
+
Anomaly(
|
|
437
|
+
timestamp_us=timestamp_us,
|
|
438
|
+
type="timing_violation",
|
|
439
|
+
severity=severity,
|
|
440
|
+
description=description,
|
|
441
|
+
duration_ns=(interval / sample_rate) * 1e9,
|
|
442
|
+
confidence=0.75,
|
|
443
|
+
metadata={"deviation_percent": percent_dev},
|
|
444
|
+
)
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
# Limit violations reported
|
|
448
|
+
return anomalies[:20]
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _detect_ringing(
|
|
452
|
+
data: NDArray[np.floating[Any]],
|
|
453
|
+
sample_rate: float,
|
|
454
|
+
voltage_swing: float,
|
|
455
|
+
stats: dict[str, float],
|
|
456
|
+
) -> list[Anomaly]:
|
|
457
|
+
"""Detect ringing (≥3 oscillations).
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
data: Signal data array.
|
|
461
|
+
sample_rate: Sample rate in Hz.
|
|
462
|
+
voltage_swing: Peak-to-peak voltage.
|
|
463
|
+
stats: Basic statistics.
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
List of detected ringing anomalies.
|
|
467
|
+
"""
|
|
468
|
+
anomalies: list[Anomaly] = []
|
|
469
|
+
|
|
470
|
+
if voltage_swing == 0 or len(data) < 50:
|
|
471
|
+
return anomalies
|
|
472
|
+
|
|
473
|
+
# Look for oscillations after transitions
|
|
474
|
+
threshold = stats["mean"]
|
|
475
|
+
digital = data > threshold
|
|
476
|
+
transitions = np.where(np.diff(digital.astype(int)) != 0)[0]
|
|
477
|
+
|
|
478
|
+
for trans_idx in transitions:
|
|
479
|
+
# Check window after transition
|
|
480
|
+
window_size = min(50, len(data) - trans_idx - 1)
|
|
481
|
+
if window_size < 10:
|
|
482
|
+
continue
|
|
483
|
+
|
|
484
|
+
window = data[trans_idx + 1 : trans_idx + 1 + window_size]
|
|
485
|
+
|
|
486
|
+
# Count zero crossings (oscillations)
|
|
487
|
+
window_mean = np.mean(window)
|
|
488
|
+
crossings = np.sum(np.diff(np.sign(window - window_mean)) != 0)
|
|
489
|
+
|
|
490
|
+
# Ringing should have ≥3 oscillations
|
|
491
|
+
if crossings >= 6: # 6 crossings = 3 full oscillations
|
|
492
|
+
timestamp_us = (trans_idx / sample_rate) * 1e6
|
|
493
|
+
duration_ns = (window_size / sample_rate) * 1e9
|
|
494
|
+
num_oscillations = crossings // 2
|
|
495
|
+
|
|
496
|
+
description = f"Ringing with {num_oscillations} oscillations after edge"
|
|
497
|
+
|
|
498
|
+
severity: Severity = "INFO"
|
|
499
|
+
|
|
500
|
+
anomalies.append(
|
|
501
|
+
Anomaly(
|
|
502
|
+
timestamp_us=timestamp_us,
|
|
503
|
+
type="ringing",
|
|
504
|
+
severity=severity,
|
|
505
|
+
description=description,
|
|
506
|
+
duration_ns=duration_ns,
|
|
507
|
+
confidence=0.70,
|
|
508
|
+
metadata={"oscillations": num_oscillations},
|
|
509
|
+
)
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
return anomalies[:10]
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _detect_overshoot(
|
|
516
|
+
data: NDArray[np.floating[Any]],
|
|
517
|
+
sample_rate: float,
|
|
518
|
+
voltage_swing: float,
|
|
519
|
+
stats: dict[str, float],
|
|
520
|
+
) -> list[Anomaly]:
|
|
521
|
+
"""Detect overshoot (>10% beyond high rail).
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
data: Signal data array.
|
|
525
|
+
sample_rate: Sample rate in Hz.
|
|
526
|
+
voltage_swing: Peak-to-peak voltage.
|
|
527
|
+
stats: Basic statistics.
|
|
528
|
+
|
|
529
|
+
Returns:
|
|
530
|
+
List of detected overshoot anomalies.
|
|
531
|
+
"""
|
|
532
|
+
anomalies: list[Anomaly] = []
|
|
533
|
+
|
|
534
|
+
if voltage_swing == 0:
|
|
535
|
+
return anomalies
|
|
536
|
+
|
|
537
|
+
# Define expected high rail (based on histogram peaks)
|
|
538
|
+
high_rail = stats["max"] * 0.95 # Expected rail at 95th percentile
|
|
539
|
+
overshoot_threshold = high_rail * 1.1 # 10% above rail
|
|
540
|
+
|
|
541
|
+
# Find overshoot samples
|
|
542
|
+
overshoots = np.where(data > overshoot_threshold)[0]
|
|
543
|
+
|
|
544
|
+
if len(overshoots) == 0:
|
|
545
|
+
return anomalies
|
|
546
|
+
|
|
547
|
+
# Group consecutive samples
|
|
548
|
+
groups = []
|
|
549
|
+
current = [overshoots[0]]
|
|
550
|
+
|
|
551
|
+
for idx in overshoots[1:]:
|
|
552
|
+
if idx == current[-1] + 1:
|
|
553
|
+
current.append(idx)
|
|
554
|
+
else:
|
|
555
|
+
groups.append(current)
|
|
556
|
+
current = [idx]
|
|
557
|
+
|
|
558
|
+
groups.append(current)
|
|
559
|
+
|
|
560
|
+
# Report each overshoot event
|
|
561
|
+
for group in groups:
|
|
562
|
+
timestamp_us = (group[0] / sample_rate) * 1e6
|
|
563
|
+
peak_value = np.max(data[group])
|
|
564
|
+
percent_over = ((peak_value - high_rail) / high_rail) * 100
|
|
565
|
+
|
|
566
|
+
description = (
|
|
567
|
+
f"Signal exceeded expected high level by {percent_over:.0f}% (peak: {peak_value:.2f}V)"
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
# Severity based on overshoot magnitude
|
|
571
|
+
if percent_over > 20:
|
|
572
|
+
severity: Severity = "WARNING"
|
|
573
|
+
else:
|
|
574
|
+
severity = "INFO"
|
|
575
|
+
|
|
576
|
+
anomalies.append(
|
|
577
|
+
Anomaly(
|
|
578
|
+
timestamp_us=timestamp_us,
|
|
579
|
+
type="overshoot",
|
|
580
|
+
severity=severity,
|
|
581
|
+
description=description,
|
|
582
|
+
duration_ns=(len(group) / sample_rate) * 1e9,
|
|
583
|
+
confidence=0.82,
|
|
584
|
+
metadata={"peak_voltage": peak_value},
|
|
585
|
+
)
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
return anomalies[:10]
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def _detect_undershoot(
|
|
592
|
+
data: NDArray[np.floating[Any]],
|
|
593
|
+
sample_rate: float,
|
|
594
|
+
voltage_swing: float,
|
|
595
|
+
stats: dict[str, float],
|
|
596
|
+
) -> list[Anomaly]:
|
|
597
|
+
"""Detect undershoot (>10% beyond low rail).
|
|
598
|
+
|
|
599
|
+
Args:
|
|
600
|
+
data: Signal data array.
|
|
601
|
+
sample_rate: Sample rate in Hz.
|
|
602
|
+
voltage_swing: Peak-to-peak voltage.
|
|
603
|
+
stats: Basic statistics.
|
|
604
|
+
|
|
605
|
+
Returns:
|
|
606
|
+
List of detected undershoot anomalies.
|
|
607
|
+
"""
|
|
608
|
+
anomalies: list[Anomaly] = []
|
|
609
|
+
|
|
610
|
+
if voltage_swing == 0:
|
|
611
|
+
return anomalies
|
|
612
|
+
|
|
613
|
+
# Define expected low rail
|
|
614
|
+
low_rail = stats["min"] * 1.05 # Expected rail at 5th percentile
|
|
615
|
+
undershoot_threshold = low_rail * 0.9 # 10% below rail (more negative)
|
|
616
|
+
|
|
617
|
+
# Find undershoot samples
|
|
618
|
+
undershoots = np.where(data < undershoot_threshold)[0]
|
|
619
|
+
|
|
620
|
+
if len(undershoots) == 0:
|
|
621
|
+
return anomalies
|
|
622
|
+
|
|
623
|
+
# Group consecutive samples
|
|
624
|
+
groups = []
|
|
625
|
+
current = [undershoots[0]]
|
|
626
|
+
|
|
627
|
+
for idx in undershoots[1:]:
|
|
628
|
+
if idx == current[-1] + 1:
|
|
629
|
+
current.append(idx)
|
|
630
|
+
else:
|
|
631
|
+
groups.append(current)
|
|
632
|
+
current = [idx]
|
|
633
|
+
|
|
634
|
+
groups.append(current)
|
|
635
|
+
|
|
636
|
+
# Report each undershoot event
|
|
637
|
+
for group in groups:
|
|
638
|
+
timestamp_us = (group[0] / sample_rate) * 1e6
|
|
639
|
+
min_value = np.min(data[group])
|
|
640
|
+
percent_under = ((low_rail - min_value) / abs(low_rail)) * 100 if low_rail != 0 else 0
|
|
641
|
+
|
|
642
|
+
description = (
|
|
643
|
+
f"Signal fell below expected low level by {percent_under:.0f}% (min: {min_value:.2f}V)"
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
# Severity based on undershoot magnitude
|
|
647
|
+
if percent_under > 20:
|
|
648
|
+
severity: Severity = "WARNING"
|
|
649
|
+
else:
|
|
650
|
+
severity = "INFO"
|
|
651
|
+
|
|
652
|
+
anomalies.append(
|
|
653
|
+
Anomaly(
|
|
654
|
+
timestamp_us=timestamp_us,
|
|
655
|
+
type="undershoot",
|
|
656
|
+
severity=severity,
|
|
657
|
+
description=description,
|
|
658
|
+
duration_ns=(len(group) / sample_rate) * 1e9,
|
|
659
|
+
confidence=0.82,
|
|
660
|
+
metadata={"min_voltage": min_value},
|
|
661
|
+
)
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
return anomalies[:10]
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
__all__ = [
|
|
668
|
+
"Anomaly",
|
|
669
|
+
"AnomalyType",
|
|
670
|
+
"Severity",
|
|
671
|
+
"find_anomalies",
|
|
672
|
+
]
|