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,877 @@
|
|
|
1
|
+
"""Signal quality and integrity analysis.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive signal integrity analysis for digital signals,
|
|
4
|
+
including noise margin measurements, transition characterization, overshoot/
|
|
5
|
+
undershoot detection, and ringing analysis.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
>>> import numpy as np
|
|
10
|
+
>>> from oscura.analyzers.digital.signal_quality import SignalQualityAnalyzer
|
|
11
|
+
>>> # Generate test signal
|
|
12
|
+
>>> signal = np.concatenate([np.zeros(100), np.ones(100)])
|
|
13
|
+
>>> analyzer = SignalQualityAnalyzer(sample_rate=100e6, logic_family='TTL')
|
|
14
|
+
>>> report = analyzer.analyze(signal)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
from scipy import signal as scipy_signal
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from numpy.typing import NDArray
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Logic family thresholds (from existing extraction.py)
|
|
30
|
+
LOGIC_THRESHOLDS = {
|
|
31
|
+
"ttl": {"VIL": 0.8, "VIH": 2.0, "VOL": 0.4, "VOH": 2.4, "VCC": 5.0},
|
|
32
|
+
"cmos": {"VIL": 1.5, "VIH": 3.5, "VOL": 0.1, "VOH": 4.9, "VCC": 5.0},
|
|
33
|
+
"lvttl": {"VIL": 0.8, "VIH": 1.5, "VOL": 0.4, "VOH": 2.4, "VCC": 3.3},
|
|
34
|
+
"lvcmos": {"VIL": 0.99, "VIH": 2.31, "VOL": 0.1, "VOH": 3.2, "VCC": 3.3},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class NoiseMargins:
|
|
40
|
+
"""Noise margins for digital signal.
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
high_margin: Distance from threshold to logic high level (V).
|
|
44
|
+
low_margin: Distance from threshold to logic low level (V).
|
|
45
|
+
high_mean: Mean high level voltage.
|
|
46
|
+
low_mean: Mean low level voltage.
|
|
47
|
+
high_std: Standard deviation of high level (noise).
|
|
48
|
+
low_std: Standard deviation of low level (noise).
|
|
49
|
+
threshold: Detection threshold used.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
high_margin: float # Distance from threshold to logic high
|
|
53
|
+
low_margin: float # Distance from threshold to logic low
|
|
54
|
+
high_mean: float # Mean high level
|
|
55
|
+
low_mean: float # Mean low level
|
|
56
|
+
high_std: float # High level noise
|
|
57
|
+
low_std: float # Low level noise
|
|
58
|
+
threshold: float # Detection threshold
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class TransitionMetrics:
|
|
63
|
+
"""Metrics for signal transitions.
|
|
64
|
+
|
|
65
|
+
Attributes:
|
|
66
|
+
rise_time: 10-90% rise time in seconds.
|
|
67
|
+
fall_time: 90-10% fall time in seconds.
|
|
68
|
+
slew_rate_rising: Rising edge slew rate (V/s).
|
|
69
|
+
slew_rate_falling: Falling edge slew rate (V/s).
|
|
70
|
+
overshoot: Overshoot as percentage of signal swing.
|
|
71
|
+
undershoot: Undershoot as percentage of signal swing.
|
|
72
|
+
ringing_frequency: Ringing frequency in Hz (None if no ringing).
|
|
73
|
+
ringing_amplitude: Ringing amplitude in volts (None if no ringing).
|
|
74
|
+
settling_time: Time to settle within tolerance (None if not measured).
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
rise_time: float # 10-90% rise time
|
|
78
|
+
fall_time: float # 90-10% fall time
|
|
79
|
+
slew_rate_rising: float
|
|
80
|
+
slew_rate_falling: float
|
|
81
|
+
overshoot: float # Percentage overshoot
|
|
82
|
+
undershoot: float # Percentage undershoot
|
|
83
|
+
ringing_frequency: float | None = None
|
|
84
|
+
ringing_amplitude: float | None = None
|
|
85
|
+
settling_time: float | None = None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class SignalIntegrityReport:
|
|
90
|
+
"""Complete signal integrity report.
|
|
91
|
+
|
|
92
|
+
Attributes:
|
|
93
|
+
noise_margins: Noise margin measurements.
|
|
94
|
+
transitions: Transition quality metrics.
|
|
95
|
+
snr_db: Signal-to-noise ratio in dB.
|
|
96
|
+
signal_quality: Overall quality assessment.
|
|
97
|
+
issues: List of detected issues.
|
|
98
|
+
recommendations: List of recommendations for improvement.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
noise_margins: NoiseMargins
|
|
102
|
+
transitions: TransitionMetrics
|
|
103
|
+
snr_db: float
|
|
104
|
+
signal_quality: Literal["excellent", "good", "fair", "poor"]
|
|
105
|
+
issues: list[str]
|
|
106
|
+
recommendations: list[str]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class SimpleQualityMetrics:
|
|
111
|
+
"""Simplified quality metrics for test compatibility.
|
|
112
|
+
|
|
113
|
+
Provides a flat interface with direct attribute access for common metrics.
|
|
114
|
+
|
|
115
|
+
Attributes:
|
|
116
|
+
noise_margin_low: Low-side noise margin in volts.
|
|
117
|
+
noise_margin_high: High-side noise margin in volts.
|
|
118
|
+
rise_time: Rise time in samples (or seconds depending on context).
|
|
119
|
+
fall_time: Fall time in samples (or seconds depending on context).
|
|
120
|
+
has_overshoot: Whether overshoot was detected.
|
|
121
|
+
max_overshoot: Maximum overshoot value in volts.
|
|
122
|
+
duty_cycle: Signal duty cycle (0.0 to 1.0).
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
noise_margin_low: float
|
|
126
|
+
noise_margin_high: float
|
|
127
|
+
rise_time: float
|
|
128
|
+
fall_time: float
|
|
129
|
+
has_overshoot: bool
|
|
130
|
+
max_overshoot: float
|
|
131
|
+
duty_cycle: float
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class SignalQualityAnalyzer:
|
|
135
|
+
"""Analyze digital signal quality and integrity.
|
|
136
|
+
|
|
137
|
+
Provides comprehensive signal integrity analysis including noise margins,
|
|
138
|
+
transition metrics, overshoot/undershoot, and ringing detection.
|
|
139
|
+
|
|
140
|
+
Supports two initialization modes:
|
|
141
|
+
1. Full mode: SignalQualityAnalyzer(sample_rate=1e9, logic_family='TTL')
|
|
142
|
+
2. Simple mode: SignalQualityAnalyzer(v_il=0.8, v_ih=2.0) - for test compatibility
|
|
143
|
+
|
|
144
|
+
Attributes:
|
|
145
|
+
sample_rate: Sample rate of input signals in Hz.
|
|
146
|
+
logic_family: Logic family for threshold determination.
|
|
147
|
+
v_il: Input low threshold voltage.
|
|
148
|
+
v_ih: Input high threshold voltage.
|
|
149
|
+
vdd: Supply voltage for overshoot reference.
|
|
150
|
+
|
|
151
|
+
Example:
|
|
152
|
+
>>> analyzer = SignalQualityAnalyzer(sample_rate=1e9, logic_family='TTL')
|
|
153
|
+
>>> report = analyzer.analyze(signal_trace)
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
def __init__(
|
|
157
|
+
self,
|
|
158
|
+
sample_rate: float | None = None,
|
|
159
|
+
logic_family: str = "auto",
|
|
160
|
+
v_il: float | None = None,
|
|
161
|
+
v_ih: float | None = None,
|
|
162
|
+
vdd: float | None = None,
|
|
163
|
+
):
|
|
164
|
+
"""Initialize analyzer.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
sample_rate: Sample rate in Hz (optional for simple mode).
|
|
168
|
+
logic_family: Logic family ('TTL', 'CMOS', 'LVTTL', 'LVCMOS', 'auto').
|
|
169
|
+
v_il: Input low threshold voltage (for simple mode).
|
|
170
|
+
v_ih: Input high threshold voltage (for simple mode).
|
|
171
|
+
vdd: Supply voltage for overshoot reference (for simple mode).
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
ValueError: If sample rate is invalid (when provided).
|
|
175
|
+
"""
|
|
176
|
+
# Simple mode: thresholds provided directly
|
|
177
|
+
self.v_il = v_il
|
|
178
|
+
self.v_ih = v_ih
|
|
179
|
+
self.vdd = vdd
|
|
180
|
+
|
|
181
|
+
# Full mode: sample rate and logic family
|
|
182
|
+
if sample_rate is not None:
|
|
183
|
+
if sample_rate <= 0:
|
|
184
|
+
raise ValueError(f"Sample rate must be positive, got {sample_rate}")
|
|
185
|
+
self.sample_rate = sample_rate
|
|
186
|
+
self._time_base = 1.0 / sample_rate
|
|
187
|
+
else:
|
|
188
|
+
# Default sample rate for simple mode (samples per second = 1)
|
|
189
|
+
self.sample_rate = 1.0
|
|
190
|
+
self._time_base = 1.0
|
|
191
|
+
|
|
192
|
+
self.logic_family = logic_family.lower() if logic_family else "auto"
|
|
193
|
+
|
|
194
|
+
# If thresholds provided, use them to determine logic family settings
|
|
195
|
+
self._threshold: float | None
|
|
196
|
+
if v_il is not None and v_ih is not None:
|
|
197
|
+
self._threshold = (v_il + v_ih) / 2.0
|
|
198
|
+
else:
|
|
199
|
+
self._threshold = None
|
|
200
|
+
|
|
201
|
+
def analyze(
|
|
202
|
+
self, trace: NDArray[np.float64], clock_trace: NDArray[np.float64] | None = None
|
|
203
|
+
) -> Any:
|
|
204
|
+
"""Perform complete signal integrity analysis.
|
|
205
|
+
|
|
206
|
+
Returns SimpleQualityMetrics in simple mode (when v_il/v_ih provided),
|
|
207
|
+
or SignalIntegrityReport in full mode.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
trace: Input signal trace (analog voltage values).
|
|
211
|
+
clock_trace: Optional clock signal for synchronized analysis.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
SimpleQualityMetrics or SignalIntegrityReport with analysis results.
|
|
215
|
+
|
|
216
|
+
Example:
|
|
217
|
+
>>> report = analyzer.analyze(signal_trace)
|
|
218
|
+
>>> print(f"Signal quality: {report.signal_quality}")
|
|
219
|
+
"""
|
|
220
|
+
trace = np.asarray(trace, dtype=np.float64)
|
|
221
|
+
|
|
222
|
+
# Simple mode: return SimpleQualityMetrics
|
|
223
|
+
if self.v_il is not None or self.v_ih is not None or self.vdd is not None:
|
|
224
|
+
return self._analyze_simple(trace)
|
|
225
|
+
|
|
226
|
+
# Full mode: return SignalIntegrityReport
|
|
227
|
+
return self._analyze_full(trace, clock_trace)
|
|
228
|
+
|
|
229
|
+
def _analyze_simple(self, trace: NDArray[np.float64]) -> SimpleQualityMetrics:
|
|
230
|
+
"""Simple analysis mode returning flat metrics.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
trace: Input signal trace.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
SimpleQualityMetrics with measured values.
|
|
237
|
+
"""
|
|
238
|
+
# Determine threshold
|
|
239
|
+
threshold: float
|
|
240
|
+
if self._threshold is not None:
|
|
241
|
+
threshold = self._threshold
|
|
242
|
+
else:
|
|
243
|
+
threshold = float((np.max(trace) + np.min(trace)) / 2.0)
|
|
244
|
+
|
|
245
|
+
# Separate high and low samples
|
|
246
|
+
high_samples = trace[trace > threshold]
|
|
247
|
+
low_samples = trace[trace <= threshold]
|
|
248
|
+
|
|
249
|
+
# Calculate noise margins
|
|
250
|
+
if len(high_samples) > 0:
|
|
251
|
+
high_mean = np.mean(high_samples)
|
|
252
|
+
if self.v_ih is not None:
|
|
253
|
+
noise_margin_high = high_mean - self.v_ih
|
|
254
|
+
else:
|
|
255
|
+
noise_margin_high = high_mean - threshold
|
|
256
|
+
else:
|
|
257
|
+
noise_margin_high = 0.0
|
|
258
|
+
|
|
259
|
+
if len(low_samples) > 0:
|
|
260
|
+
low_mean = np.mean(low_samples)
|
|
261
|
+
if self.v_il is not None:
|
|
262
|
+
noise_margin_low = self.v_il - low_mean
|
|
263
|
+
else:
|
|
264
|
+
noise_margin_low = threshold - low_mean
|
|
265
|
+
else:
|
|
266
|
+
noise_margin_low = 0.0
|
|
267
|
+
|
|
268
|
+
# Measure rise/fall times in samples
|
|
269
|
+
rise_time, fall_time = self._measure_rise_fall_samples(trace, threshold)
|
|
270
|
+
|
|
271
|
+
# Detect overshoot
|
|
272
|
+
has_overshoot, max_overshoot = self._detect_overshoot_simple(trace)
|
|
273
|
+
|
|
274
|
+
# Calculate duty cycle
|
|
275
|
+
duty_cycle = self._calculate_duty_cycle(trace, threshold)
|
|
276
|
+
|
|
277
|
+
return SimpleQualityMetrics(
|
|
278
|
+
noise_margin_low=float(noise_margin_low),
|
|
279
|
+
noise_margin_high=float(noise_margin_high),
|
|
280
|
+
rise_time=float(rise_time),
|
|
281
|
+
fall_time=float(fall_time),
|
|
282
|
+
has_overshoot=has_overshoot,
|
|
283
|
+
max_overshoot=float(max_overshoot),
|
|
284
|
+
duty_cycle=float(duty_cycle),
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def _measure_rise_fall_samples(
|
|
288
|
+
self, trace: NDArray[np.float64], threshold: float
|
|
289
|
+
) -> tuple[float, float]:
|
|
290
|
+
"""Measure rise and fall times in samples.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
trace: Input signal trace.
|
|
294
|
+
threshold: Detection threshold.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Tuple of (rise_time_samples, fall_time_samples).
|
|
298
|
+
"""
|
|
299
|
+
# Detect edges
|
|
300
|
+
crossings = np.diff((trace > threshold).astype(int))
|
|
301
|
+
rising_edges = np.where(crossings > 0)[0]
|
|
302
|
+
falling_edges = np.where(crossings < 0)[0]
|
|
303
|
+
|
|
304
|
+
# Measure rise times
|
|
305
|
+
rise_times = []
|
|
306
|
+
for edge_idx in rising_edges:
|
|
307
|
+
window_size = min(10, edge_idx, len(trace) - edge_idx - 1)
|
|
308
|
+
if window_size < 2:
|
|
309
|
+
continue
|
|
310
|
+
|
|
311
|
+
window = trace[edge_idx - window_size : edge_idx + window_size + 1]
|
|
312
|
+
v_min = np.min(window)
|
|
313
|
+
v_max = np.max(window)
|
|
314
|
+
|
|
315
|
+
if v_max - v_min < 1e-6:
|
|
316
|
+
continue
|
|
317
|
+
|
|
318
|
+
# Find 10% and 90% points
|
|
319
|
+
v_10 = v_min + 0.1 * (v_max - v_min)
|
|
320
|
+
v_90 = v_min + 0.9 * (v_max - v_min)
|
|
321
|
+
|
|
322
|
+
idx_10 = np.where(window >= v_10)[0]
|
|
323
|
+
idx_90 = np.where(window >= v_90)[0]
|
|
324
|
+
|
|
325
|
+
if len(idx_10) > 0 and len(idx_90) > 0:
|
|
326
|
+
rise_time = idx_90[0] - idx_10[0]
|
|
327
|
+
if rise_time > 0:
|
|
328
|
+
rise_times.append(rise_time)
|
|
329
|
+
|
|
330
|
+
# Measure fall times
|
|
331
|
+
fall_times = []
|
|
332
|
+
for edge_idx in falling_edges:
|
|
333
|
+
window_size = min(10, edge_idx, len(trace) - edge_idx - 1)
|
|
334
|
+
if window_size < 2:
|
|
335
|
+
continue
|
|
336
|
+
|
|
337
|
+
window = trace[edge_idx - window_size : edge_idx + window_size + 1]
|
|
338
|
+
v_min = np.min(window)
|
|
339
|
+
v_max = np.max(window)
|
|
340
|
+
|
|
341
|
+
if v_max - v_min < 1e-6:
|
|
342
|
+
continue
|
|
343
|
+
|
|
344
|
+
v_90 = v_min + 0.9 * (v_max - v_min)
|
|
345
|
+
v_10 = v_min + 0.1 * (v_max - v_min)
|
|
346
|
+
|
|
347
|
+
idx_90 = np.where(window <= v_90)[0]
|
|
348
|
+
idx_10 = np.where(window <= v_10)[0]
|
|
349
|
+
|
|
350
|
+
if len(idx_90) > 0 and len(idx_10) > 0:
|
|
351
|
+
fall_time = idx_10[-1] - idx_90[0]
|
|
352
|
+
if fall_time > 0:
|
|
353
|
+
fall_times.append(fall_time)
|
|
354
|
+
|
|
355
|
+
rise_time = np.mean(rise_times) if rise_times else 0.0
|
|
356
|
+
fall_time = np.mean(fall_times) if fall_times else 0.0
|
|
357
|
+
|
|
358
|
+
return rise_time, fall_time
|
|
359
|
+
|
|
360
|
+
def _detect_overshoot_simple(self, trace: NDArray[np.float64]) -> tuple[bool, float]:
|
|
361
|
+
"""Detect overshoot in simple mode.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
trace: Input signal trace.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Tuple of (has_overshoot, max_overshoot_value).
|
|
368
|
+
"""
|
|
369
|
+
threshold = self._threshold or (np.max(trace) + np.min(trace)) / 2.0
|
|
370
|
+
high_samples = trace[trace > threshold]
|
|
371
|
+
|
|
372
|
+
if len(high_samples) == 0:
|
|
373
|
+
return False, 0.0
|
|
374
|
+
|
|
375
|
+
high_median = np.median(high_samples)
|
|
376
|
+
max_val = np.max(trace)
|
|
377
|
+
|
|
378
|
+
# Check if max exceeds expected high level
|
|
379
|
+
if self.vdd is not None:
|
|
380
|
+
# Check against VDD
|
|
381
|
+
overshoot = float(max_val - self.vdd)
|
|
382
|
+
has_overshoot = overshoot > 0.05 # 50mV threshold
|
|
383
|
+
else:
|
|
384
|
+
# Check against median high level
|
|
385
|
+
overshoot = float(max_val - high_median)
|
|
386
|
+
# Only count as overshoot if significantly above stable level
|
|
387
|
+
_high_level = high_median
|
|
388
|
+
signal_swing = high_median - np.min(trace)
|
|
389
|
+
has_overshoot = overshoot > float(signal_swing * 0.05) # 5% threshold
|
|
390
|
+
|
|
391
|
+
return bool(has_overshoot), max(0.0, overshoot)
|
|
392
|
+
|
|
393
|
+
def _calculate_duty_cycle(self, trace: NDArray[np.float64], threshold: float) -> float:
|
|
394
|
+
"""Calculate signal duty cycle.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
trace: Input signal trace.
|
|
398
|
+
threshold: Detection threshold.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Duty cycle as ratio (0.0 to 1.0).
|
|
402
|
+
"""
|
|
403
|
+
if len(trace) == 0:
|
|
404
|
+
return 0.0
|
|
405
|
+
|
|
406
|
+
# Handle boolean trace
|
|
407
|
+
if trace.dtype == np.bool_:
|
|
408
|
+
high_count = np.sum(trace)
|
|
409
|
+
else:
|
|
410
|
+
high_count = np.sum(trace > threshold)
|
|
411
|
+
|
|
412
|
+
return float(high_count) / float(len(trace))
|
|
413
|
+
|
|
414
|
+
def _analyze_full(
|
|
415
|
+
self, trace: NDArray[np.float64], clock_trace: NDArray[np.float64] | None = None
|
|
416
|
+
) -> SignalIntegrityReport:
|
|
417
|
+
"""Full analysis mode returning comprehensive report.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
trace: Input signal trace.
|
|
421
|
+
clock_trace: Optional clock signal.
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
SignalIntegrityReport with complete analysis.
|
|
425
|
+
"""
|
|
426
|
+
# Measure noise margins
|
|
427
|
+
logic_fam: Literal["ttl", "cmos", "lvttl", "lvcmos", "auto"]
|
|
428
|
+
if self.logic_family in ("ttl", "cmos", "lvttl", "lvcmos", "auto"):
|
|
429
|
+
logic_fam = self.logic_family # type: ignore[assignment]
|
|
430
|
+
else:
|
|
431
|
+
logic_fam = "auto"
|
|
432
|
+
noise_margins = self.measure_noise_margins(trace, logic_fam)
|
|
433
|
+
|
|
434
|
+
# Measure transitions
|
|
435
|
+
transitions = self.measure_transitions(trace)
|
|
436
|
+
|
|
437
|
+
# Calculate SNR
|
|
438
|
+
snr_db = self.calculate_snr(trace)
|
|
439
|
+
|
|
440
|
+
# Assess overall quality and identify issues
|
|
441
|
+
issues = []
|
|
442
|
+
recommendations = []
|
|
443
|
+
|
|
444
|
+
# Check noise margins
|
|
445
|
+
if noise_margins.high_margin < 0.4:
|
|
446
|
+
issues.append("Insufficient high-level noise margin")
|
|
447
|
+
recommendations.append("Increase signal high level or reduce noise")
|
|
448
|
+
|
|
449
|
+
if noise_margins.low_margin < 0.4:
|
|
450
|
+
issues.append("Insufficient low-level noise margin")
|
|
451
|
+
recommendations.append("Decrease signal low level or reduce noise")
|
|
452
|
+
|
|
453
|
+
# Check transitions
|
|
454
|
+
if transitions.overshoot > 20:
|
|
455
|
+
issues.append(f"Excessive overshoot: {transitions.overshoot:.1f}%")
|
|
456
|
+
recommendations.append("Add series termination or reduce capacitance")
|
|
457
|
+
|
|
458
|
+
if transitions.undershoot > 20:
|
|
459
|
+
issues.append(f"Excessive undershoot: {transitions.undershoot:.1f}%")
|
|
460
|
+
recommendations.append("Check ground connections and reduce inductance")
|
|
461
|
+
|
|
462
|
+
if transitions.ringing_amplitude and transitions.ringing_amplitude > 0.2:
|
|
463
|
+
issues.append("Significant ringing detected")
|
|
464
|
+
recommendations.append("Add damping resistor or improve impedance matching")
|
|
465
|
+
|
|
466
|
+
# Check SNR
|
|
467
|
+
if snr_db < 20:
|
|
468
|
+
issues.append(f"Low SNR: {snr_db:.1f} dB")
|
|
469
|
+
recommendations.append("Reduce noise sources or improve shielding")
|
|
470
|
+
|
|
471
|
+
# Determine overall quality
|
|
472
|
+
quality: Literal["excellent", "good", "fair", "poor"]
|
|
473
|
+
if len(issues) == 0 and snr_db > 40:
|
|
474
|
+
quality = "excellent"
|
|
475
|
+
elif len(issues) <= 1 and snr_db > 30:
|
|
476
|
+
quality = "good"
|
|
477
|
+
elif len(issues) <= 2 and snr_db > 20:
|
|
478
|
+
quality = "fair"
|
|
479
|
+
else:
|
|
480
|
+
quality = "poor"
|
|
481
|
+
|
|
482
|
+
return SignalIntegrityReport(
|
|
483
|
+
noise_margins=noise_margins,
|
|
484
|
+
transitions=transitions,
|
|
485
|
+
snr_db=snr_db,
|
|
486
|
+
signal_quality=quality,
|
|
487
|
+
issues=issues,
|
|
488
|
+
recommendations=recommendations,
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
def measure_noise_margins(
|
|
492
|
+
self,
|
|
493
|
+
trace: NDArray[np.float64],
|
|
494
|
+
logic_family: Literal["ttl", "cmos", "lvttl", "lvcmos", "auto"] = "auto",
|
|
495
|
+
) -> NoiseMargins:
|
|
496
|
+
"""Measure noise margins for high and low states.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
trace: Input signal trace (analog voltage values).
|
|
500
|
+
logic_family: Logic family for threshold determination.
|
|
501
|
+
|
|
502
|
+
Returns:
|
|
503
|
+
NoiseMargins object with measured margins.
|
|
504
|
+
|
|
505
|
+
Example:
|
|
506
|
+
>>> margins = analyzer.measure_noise_margins(trace, logic_family='TTL')
|
|
507
|
+
"""
|
|
508
|
+
trace = np.asarray(trace)
|
|
509
|
+
|
|
510
|
+
# Determine threshold
|
|
511
|
+
if logic_family == "auto":
|
|
512
|
+
# Auto-detect based on signal range
|
|
513
|
+
signal_range = np.max(trace) - np.min(trace)
|
|
514
|
+
if signal_range > 4.0:
|
|
515
|
+
logic_family = "ttl" # 5V logic
|
|
516
|
+
elif signal_range > 2.5:
|
|
517
|
+
logic_family = "lvttl" # 3.3V logic
|
|
518
|
+
else:
|
|
519
|
+
logic_family = "lvcmos" # Low voltage
|
|
520
|
+
|
|
521
|
+
# Get thresholds for logic family
|
|
522
|
+
thresholds = LOGIC_THRESHOLDS.get(logic_family, LOGIC_THRESHOLDS["ttl"])
|
|
523
|
+
threshold = (thresholds["VIL"] + thresholds["VIH"]) / 2.0
|
|
524
|
+
|
|
525
|
+
# Separate high and low samples
|
|
526
|
+
high_samples = trace[trace > threshold]
|
|
527
|
+
low_samples = trace[trace <= threshold]
|
|
528
|
+
|
|
529
|
+
# Calculate statistics
|
|
530
|
+
if len(high_samples) > 0:
|
|
531
|
+
high_mean = np.mean(high_samples)
|
|
532
|
+
high_std = np.std(high_samples)
|
|
533
|
+
high_margin = high_mean - threshold
|
|
534
|
+
else:
|
|
535
|
+
high_mean = 0.0
|
|
536
|
+
high_std = 0.0
|
|
537
|
+
high_margin = 0.0
|
|
538
|
+
|
|
539
|
+
if len(low_samples) > 0:
|
|
540
|
+
low_mean = np.mean(low_samples)
|
|
541
|
+
low_std = np.std(low_samples)
|
|
542
|
+
low_margin = threshold - low_mean
|
|
543
|
+
else:
|
|
544
|
+
low_mean = 0.0
|
|
545
|
+
low_std = 0.0
|
|
546
|
+
low_margin = 0.0
|
|
547
|
+
|
|
548
|
+
return NoiseMargins(
|
|
549
|
+
high_margin=float(high_margin),
|
|
550
|
+
low_margin=float(low_margin),
|
|
551
|
+
high_mean=float(high_mean),
|
|
552
|
+
low_mean=float(low_mean),
|
|
553
|
+
high_std=float(high_std),
|
|
554
|
+
low_std=float(low_std),
|
|
555
|
+
threshold=float(threshold),
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
def measure_transitions(self, trace: NDArray[np.float64]) -> TransitionMetrics:
|
|
559
|
+
"""Measure transition characteristics.
|
|
560
|
+
|
|
561
|
+
Analyzes rising and falling edges to measure rise/fall times,
|
|
562
|
+
slew rates, overshoot, undershoot, and ringing.
|
|
563
|
+
|
|
564
|
+
Args:
|
|
565
|
+
trace: Input signal trace (analog voltage values).
|
|
566
|
+
|
|
567
|
+
Returns:
|
|
568
|
+
TransitionMetrics object with transition measurements.
|
|
569
|
+
|
|
570
|
+
Example:
|
|
571
|
+
>>> metrics = analyzer.measure_transitions(trace)
|
|
572
|
+
"""
|
|
573
|
+
trace = np.asarray(trace)
|
|
574
|
+
|
|
575
|
+
# Find threshold crossings
|
|
576
|
+
threshold = (np.max(trace) + np.min(trace)) / 2.0
|
|
577
|
+
signal_range = np.max(trace) - np.min(trace)
|
|
578
|
+
|
|
579
|
+
# Detect edges (simple threshold crossing)
|
|
580
|
+
crossings = np.diff((trace > threshold).astype(int))
|
|
581
|
+
rising_edges = np.where(crossings > 0)[0]
|
|
582
|
+
falling_edges = np.where(crossings < 0)[0]
|
|
583
|
+
|
|
584
|
+
# Measure rise time (10-90%)
|
|
585
|
+
rise_times = []
|
|
586
|
+
for edge_idx in rising_edges:
|
|
587
|
+
if edge_idx > 10 and edge_idx < len(trace) - 10:
|
|
588
|
+
# Get window around edge
|
|
589
|
+
window = trace[edge_idx - 10 : edge_idx + 10]
|
|
590
|
+
v_min = np.min(window)
|
|
591
|
+
v_max = np.max(window)
|
|
592
|
+
|
|
593
|
+
# Find 10% and 90% points
|
|
594
|
+
v_10 = v_min + 0.1 * (v_max - v_min)
|
|
595
|
+
v_90 = v_min + 0.9 * (v_max - v_min)
|
|
596
|
+
|
|
597
|
+
# Find sample indices
|
|
598
|
+
idx_10 = np.where(window >= v_10)[0]
|
|
599
|
+
idx_90 = np.where(window >= v_90)[0]
|
|
600
|
+
|
|
601
|
+
if len(idx_10) > 0 and len(idx_90) > 0:
|
|
602
|
+
rise_time = (idx_90[0] - idx_10[0]) * self._time_base
|
|
603
|
+
rise_times.append(rise_time)
|
|
604
|
+
|
|
605
|
+
# Measure fall time (90-10%)
|
|
606
|
+
fall_times = []
|
|
607
|
+
for edge_idx in falling_edges:
|
|
608
|
+
if edge_idx > 10 and edge_idx < len(trace) - 10:
|
|
609
|
+
window = trace[edge_idx - 10 : edge_idx + 10]
|
|
610
|
+
v_min = np.min(window)
|
|
611
|
+
v_max = np.max(window)
|
|
612
|
+
|
|
613
|
+
v_90 = v_min + 0.9 * (v_max - v_min)
|
|
614
|
+
v_10 = v_min + 0.1 * (v_max - v_min)
|
|
615
|
+
|
|
616
|
+
idx_90 = np.where(window <= v_90)[0]
|
|
617
|
+
idx_10 = np.where(window <= v_10)[0]
|
|
618
|
+
|
|
619
|
+
if len(idx_90) > 0 and len(idx_10) > 0:
|
|
620
|
+
fall_time = (idx_10[-1] - idx_90[0]) * self._time_base
|
|
621
|
+
fall_times.append(fall_time)
|
|
622
|
+
|
|
623
|
+
# Calculate average times
|
|
624
|
+
rise_time = np.mean(rise_times) if rise_times else 0.0
|
|
625
|
+
fall_time = np.mean(fall_times) if fall_times else 0.0
|
|
626
|
+
|
|
627
|
+
# Calculate slew rates
|
|
628
|
+
slew_rate_rising = (0.8 * signal_range / rise_time) if rise_time > 0 else 0.0
|
|
629
|
+
slew_rate_falling = (0.8 * signal_range / fall_time) if fall_time > 0 else 0.0
|
|
630
|
+
|
|
631
|
+
# Detect overshoot and undershoot
|
|
632
|
+
overshoot_pct, undershoot_pct = self.detect_overshoot(trace)
|
|
633
|
+
|
|
634
|
+
# Detect ringing
|
|
635
|
+
ringing = self.detect_ringing(trace)
|
|
636
|
+
if ringing:
|
|
637
|
+
ringing_freq, ringing_amp = ringing
|
|
638
|
+
else:
|
|
639
|
+
ringing_freq, ringing_amp = None, None
|
|
640
|
+
|
|
641
|
+
return TransitionMetrics(
|
|
642
|
+
rise_time=float(rise_time),
|
|
643
|
+
fall_time=float(fall_time),
|
|
644
|
+
slew_rate_rising=float(slew_rate_rising),
|
|
645
|
+
slew_rate_falling=float(slew_rate_falling),
|
|
646
|
+
overshoot=float(overshoot_pct),
|
|
647
|
+
undershoot=float(undershoot_pct),
|
|
648
|
+
ringing_frequency=ringing_freq,
|
|
649
|
+
ringing_amplitude=ringing_amp,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
def detect_overshoot(
|
|
653
|
+
self, trace: NDArray[np.float64], edges: list[Any] | None = None
|
|
654
|
+
) -> tuple[float, float]:
|
|
655
|
+
"""Detect and measure overshoot and undershoot.
|
|
656
|
+
|
|
657
|
+
Args:
|
|
658
|
+
trace: Input signal trace.
|
|
659
|
+
edges: Optional list of edge objects (not used in this implementation).
|
|
660
|
+
|
|
661
|
+
Returns:
|
|
662
|
+
Tuple of (overshoot_percent, undershoot_percent).
|
|
663
|
+
|
|
664
|
+
Example:
|
|
665
|
+
>>> overshoot, undershoot = analyzer.detect_overshoot(trace)
|
|
666
|
+
"""
|
|
667
|
+
trace = np.asarray(trace)
|
|
668
|
+
|
|
669
|
+
# Determine signal levels
|
|
670
|
+
threshold = (np.max(trace) + np.min(trace)) / 2.0
|
|
671
|
+
high_samples = trace[trace > threshold]
|
|
672
|
+
low_samples = trace[trace <= threshold]
|
|
673
|
+
|
|
674
|
+
if len(high_samples) == 0 or len(low_samples) == 0:
|
|
675
|
+
return 0.0, 0.0
|
|
676
|
+
|
|
677
|
+
# Expected levels (mean of stable regions)
|
|
678
|
+
high_level = np.median(high_samples)
|
|
679
|
+
low_level = np.median(low_samples)
|
|
680
|
+
signal_swing = high_level - low_level
|
|
681
|
+
|
|
682
|
+
if signal_swing < 1e-6:
|
|
683
|
+
return 0.0, 0.0
|
|
684
|
+
|
|
685
|
+
# Overshoot: how much signal exceeds high level
|
|
686
|
+
max_val = np.max(trace)
|
|
687
|
+
overshoot = max_val - high_level
|
|
688
|
+
overshoot_pct = (overshoot / signal_swing) * 100.0
|
|
689
|
+
|
|
690
|
+
# Undershoot: how much signal goes below low level
|
|
691
|
+
min_val = np.min(trace)
|
|
692
|
+
undershoot = low_level - min_val
|
|
693
|
+
undershoot_pct = (undershoot / signal_swing) * 100.0
|
|
694
|
+
|
|
695
|
+
return max(0.0, overshoot_pct), max(0.0, undershoot_pct)
|
|
696
|
+
|
|
697
|
+
def detect_ringing(self, trace: NDArray[np.float64]) -> tuple[float, float] | None:
|
|
698
|
+
"""Detect and characterize ringing (frequency, amplitude).
|
|
699
|
+
|
|
700
|
+
Uses FFT analysis to detect oscillations after edges that indicate ringing.
|
|
701
|
+
|
|
702
|
+
Args:
|
|
703
|
+
trace: Input signal trace.
|
|
704
|
+
|
|
705
|
+
Returns:
|
|
706
|
+
Tuple of (frequency_hz, amplitude_volts) if ringing detected, None otherwise.
|
|
707
|
+
|
|
708
|
+
Example:
|
|
709
|
+
>>> ringing = analyzer.detect_ringing(trace)
|
|
710
|
+
>>> if ringing:
|
|
711
|
+
... freq, amp = ringing
|
|
712
|
+
"""
|
|
713
|
+
trace = np.asarray(trace)
|
|
714
|
+
|
|
715
|
+
if len(trace) < 32:
|
|
716
|
+
return None
|
|
717
|
+
|
|
718
|
+
# Detrend to remove DC offset
|
|
719
|
+
detrended = trace - np.mean(trace)
|
|
720
|
+
|
|
721
|
+
# Apply FFT to detect high-frequency oscillations
|
|
722
|
+
fft = np.fft.rfft(detrended)
|
|
723
|
+
freqs = np.fft.rfftfreq(len(trace), self._time_base)
|
|
724
|
+
power = np.abs(fft) ** 2
|
|
725
|
+
|
|
726
|
+
# Look for peaks in high-frequency range (above 1 MHz or 1% of sample rate)
|
|
727
|
+
min_freq = max(1e6, self.sample_rate * 0.01)
|
|
728
|
+
max_freq = self.sample_rate / 4.0 # Below Nyquist/2 for safety
|
|
729
|
+
|
|
730
|
+
freq_mask = (freqs > min_freq) & (freqs < max_freq)
|
|
731
|
+
|
|
732
|
+
if not np.any(freq_mask):
|
|
733
|
+
return None
|
|
734
|
+
|
|
735
|
+
# Find dominant frequency in ringing range
|
|
736
|
+
masked_power = power.copy()
|
|
737
|
+
masked_power[~freq_mask] = 0
|
|
738
|
+
|
|
739
|
+
if np.max(masked_power) < np.max(power) * 0.1:
|
|
740
|
+
# No significant high-frequency content
|
|
741
|
+
return None
|
|
742
|
+
|
|
743
|
+
peak_idx = np.argmax(masked_power)
|
|
744
|
+
ringing_freq = freqs[peak_idx]
|
|
745
|
+
|
|
746
|
+
# Estimate amplitude of ringing (very simplified)
|
|
747
|
+
# Band-pass filter around detected frequency
|
|
748
|
+
try:
|
|
749
|
+
# Design bandpass filter
|
|
750
|
+
bandwidth = ringing_freq * 0.2 # 20% bandwidth
|
|
751
|
+
low = max(ringing_freq - bandwidth, 1.0)
|
|
752
|
+
high = min(ringing_freq + bandwidth, self.sample_rate / 2.0 - 1.0)
|
|
753
|
+
|
|
754
|
+
if high > low:
|
|
755
|
+
sos = scipy_signal.butter(4, [low, high], "band", fs=self.sample_rate, output="sos")
|
|
756
|
+
filtered = scipy_signal.sosfilt(sos, detrended)
|
|
757
|
+
ringing_amp = np.std(filtered) * 2.0 # Peak-to-peak estimate
|
|
758
|
+
else:
|
|
759
|
+
ringing_amp = 0.0
|
|
760
|
+
except Exception:
|
|
761
|
+
# If filtering fails, use simple estimate
|
|
762
|
+
ringing_amp = np.std(detrended) * 0.5
|
|
763
|
+
|
|
764
|
+
# Only report if amplitude is significant
|
|
765
|
+
if ringing_amp < np.std(trace) * 0.1:
|
|
766
|
+
return None
|
|
767
|
+
|
|
768
|
+
return float(ringing_freq), float(ringing_amp)
|
|
769
|
+
|
|
770
|
+
def calculate_snr(self, trace: NDArray[np.float64]) -> float:
|
|
771
|
+
"""Calculate signal-to-noise ratio.
|
|
772
|
+
|
|
773
|
+
Computes SNR by separating signal from noise in stable regions.
|
|
774
|
+
|
|
775
|
+
Args:
|
|
776
|
+
trace: Input signal trace.
|
|
777
|
+
|
|
778
|
+
Returns:
|
|
779
|
+
SNR in decibels.
|
|
780
|
+
|
|
781
|
+
Example:
|
|
782
|
+
>>> snr = analyzer.calculate_snr(trace)
|
|
783
|
+
"""
|
|
784
|
+
trace = np.asarray(trace)
|
|
785
|
+
|
|
786
|
+
# Separate into high and low regions
|
|
787
|
+
threshold = (np.max(trace) + np.min(trace)) / 2.0
|
|
788
|
+
high_samples = trace[trace > threshold]
|
|
789
|
+
low_samples = trace[trace <= threshold]
|
|
790
|
+
|
|
791
|
+
if len(high_samples) == 0 or len(low_samples) == 0:
|
|
792
|
+
return 0.0
|
|
793
|
+
|
|
794
|
+
# Signal power: difference between high and low levels
|
|
795
|
+
signal_level = abs(np.mean(high_samples) - np.mean(low_samples))
|
|
796
|
+
|
|
797
|
+
# Noise power: standard deviation in stable regions
|
|
798
|
+
noise_high = np.std(high_samples)
|
|
799
|
+
noise_low = np.std(low_samples)
|
|
800
|
+
noise_level = (noise_high + noise_low) / 2.0
|
|
801
|
+
|
|
802
|
+
if noise_level < 1e-10:
|
|
803
|
+
return 100.0 # Very high SNR
|
|
804
|
+
|
|
805
|
+
# SNR in dB
|
|
806
|
+
snr = 20 * np.log10(signal_level / noise_level)
|
|
807
|
+
|
|
808
|
+
return float(snr)
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
# Convenience functions
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def measure_noise_margins(trace: NDArray[np.float64], logic_family: str = "auto") -> NoiseMargins:
|
|
815
|
+
"""Measure noise margins.
|
|
816
|
+
|
|
817
|
+
Convenience function for quick noise margin measurement.
|
|
818
|
+
|
|
819
|
+
Args:
|
|
820
|
+
trace: Input signal trace.
|
|
821
|
+
logic_family: Logic family ('TTL', 'CMOS', 'LVTTL', 'LVCMOS', 'auto').
|
|
822
|
+
|
|
823
|
+
Returns:
|
|
824
|
+
NoiseMargins object.
|
|
825
|
+
|
|
826
|
+
Example:
|
|
827
|
+
>>> margins = measure_noise_margins(trace, 'TTL')
|
|
828
|
+
"""
|
|
829
|
+
# Use a default sample rate for convenience
|
|
830
|
+
sample_rate = 1e9 # 1 GHz default
|
|
831
|
+
analyzer = SignalQualityAnalyzer(sample_rate, logic_family)
|
|
832
|
+
logic_fam: Literal["ttl", "cmos", "lvttl", "lvcmos", "auto"]
|
|
833
|
+
logic_family_lower = logic_family.lower()
|
|
834
|
+
if logic_family_lower in ("ttl", "cmos", "lvttl", "lvcmos", "auto"):
|
|
835
|
+
logic_fam = logic_family_lower # type: ignore[assignment]
|
|
836
|
+
else:
|
|
837
|
+
logic_fam = "auto"
|
|
838
|
+
return analyzer.measure_noise_margins(trace, logic_fam)
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def analyze_signal_integrity(
|
|
842
|
+
trace: NDArray[np.float64],
|
|
843
|
+
sample_rate: float,
|
|
844
|
+
clock_trace: NDArray[np.float64] | None = None,
|
|
845
|
+
) -> SignalIntegrityReport:
|
|
846
|
+
"""Complete signal integrity analysis.
|
|
847
|
+
|
|
848
|
+
Convenience function for complete signal integrity analysis.
|
|
849
|
+
|
|
850
|
+
Args:
|
|
851
|
+
trace: Input signal trace.
|
|
852
|
+
sample_rate: Sample rate in Hz.
|
|
853
|
+
clock_trace: Optional clock signal.
|
|
854
|
+
|
|
855
|
+
Returns:
|
|
856
|
+
SignalIntegrityReport with complete analysis.
|
|
857
|
+
|
|
858
|
+
Example:
|
|
859
|
+
>>> report = analyze_signal_integrity(trace, 100e6)
|
|
860
|
+
"""
|
|
861
|
+
analyzer = SignalQualityAnalyzer(sample_rate, logic_family="auto")
|
|
862
|
+
result = analyzer.analyze(trace, clock_trace)
|
|
863
|
+
# In full mode (no v_il/v_ih/vdd), this always returns SignalIntegrityReport
|
|
864
|
+
assert isinstance(result, SignalIntegrityReport)
|
|
865
|
+
return result
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
__all__ = [
|
|
869
|
+
"LOGIC_THRESHOLDS",
|
|
870
|
+
"NoiseMargins",
|
|
871
|
+
"SignalIntegrityReport",
|
|
872
|
+
"SignalQualityAnalyzer",
|
|
873
|
+
"SimpleQualityMetrics",
|
|
874
|
+
"TransitionMetrics",
|
|
875
|
+
"analyze_signal_integrity",
|
|
876
|
+
"measure_noise_margins",
|
|
877
|
+
]
|