oscura 0.0.1__py3-none-any.whl → 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oscura/__init__.py +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.0.dist-info/METADATA +300 -0
- oscura-0.1.0.dist-info/RECORD +463 -0
- oscura-0.1.0.dist-info/entry_points.txt +2 -0
- {oscura-0.0.1.dist-info → oscura-0.1.0.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.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""Channel embedding and de-embedding functions.
|
|
2
|
+
|
|
3
|
+
This module provides time-domain de-embedding to remove fixture
|
|
4
|
+
effects and embedding to simulate channel effects.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.analyzers.signal_integrity.embedding import deembed
|
|
9
|
+
>>> clean_trace = deembed(trace, s_params)
|
|
10
|
+
|
|
11
|
+
References:
|
|
12
|
+
IEEE 370-2020: Standard for Electrical Characterization of PCBs
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
from oscura.analyzers.signal_integrity.sparams import (
|
|
20
|
+
SParameterData,
|
|
21
|
+
abcd_to_s,
|
|
22
|
+
s_to_abcd,
|
|
23
|
+
)
|
|
24
|
+
from oscura.core.exceptions import AnalysisError
|
|
25
|
+
from oscura.core.types import WaveformTrace
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def deembed(
|
|
29
|
+
trace: WaveformTrace,
|
|
30
|
+
s_params: SParameterData,
|
|
31
|
+
*,
|
|
32
|
+
method: str = "frequency_domain",
|
|
33
|
+
regularization: float = 1e-6,
|
|
34
|
+
) -> WaveformTrace:
|
|
35
|
+
"""Remove fixture effects from waveform using S-parameters.
|
|
36
|
+
|
|
37
|
+
Applies the inverse of the fixture transfer function in the
|
|
38
|
+
frequency domain to recover the signal at the DUT reference plane.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
trace: Input waveform trace.
|
|
42
|
+
s_params: S-parameters of fixture to remove.
|
|
43
|
+
method: De-embedding method ("frequency_domain" or "time_domain").
|
|
44
|
+
regularization: Regularization for matrix inversion.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
De-embedded waveform trace.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ValueError: If method is unknown.
|
|
51
|
+
AnalysisError: If de-embedding fails.
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
>>> clean = deembed(measured_trace, fixture_sparams)
|
|
55
|
+
>>> # clean now has fixture effects removed
|
|
56
|
+
|
|
57
|
+
References:
|
|
58
|
+
IEEE 370-2020 Section 7
|
|
59
|
+
"""
|
|
60
|
+
if s_params.n_ports != 2:
|
|
61
|
+
raise AnalysisError(
|
|
62
|
+
f"De-embedding requires 2-port S-parameters, got {s_params.n_ports}-port"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if method == "frequency_domain":
|
|
66
|
+
return _deembed_frequency_domain(trace, s_params, regularization)
|
|
67
|
+
elif method == "time_domain":
|
|
68
|
+
return _deembed_time_domain(trace, s_params)
|
|
69
|
+
else:
|
|
70
|
+
raise ValueError(f"Unknown method: {method}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _deembed_frequency_domain(
|
|
74
|
+
trace: WaveformTrace,
|
|
75
|
+
s_params: SParameterData,
|
|
76
|
+
regularization: float,
|
|
77
|
+
) -> WaveformTrace:
|
|
78
|
+
"""De-embed using frequency domain approach."""
|
|
79
|
+
data = trace.data
|
|
80
|
+
sample_rate = trace.metadata.sample_rate
|
|
81
|
+
n = len(data)
|
|
82
|
+
|
|
83
|
+
# Compute FFT of input signal
|
|
84
|
+
signal_fft = np.fft.rfft(data)
|
|
85
|
+
frequencies = np.fft.rfftfreq(n, d=1.0 / sample_rate)
|
|
86
|
+
|
|
87
|
+
# Interpolate S21 to signal frequencies
|
|
88
|
+
s21 = np.interp(
|
|
89
|
+
frequencies,
|
|
90
|
+
s_params.frequencies,
|
|
91
|
+
s_params.s_matrix[:, 1, 0],
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Apply inverse transfer function with regularization
|
|
95
|
+
# H_inv = 1 / S21, but regularized
|
|
96
|
+
magnitude = np.abs(s21)
|
|
97
|
+
magnitude_reg = np.maximum(magnitude, regularization)
|
|
98
|
+
h_inv = np.conj(s21) / (magnitude_reg**2 + regularization)
|
|
99
|
+
|
|
100
|
+
# Apply inverse filter
|
|
101
|
+
deembedded_fft = signal_fft * h_inv
|
|
102
|
+
|
|
103
|
+
# Inverse FFT
|
|
104
|
+
deembedded_data = np.fft.irfft(deembedded_fft, n=n)
|
|
105
|
+
|
|
106
|
+
return WaveformTrace(
|
|
107
|
+
data=deembedded_data.astype(np.float64),
|
|
108
|
+
metadata=trace.metadata,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _deembed_time_domain(
|
|
113
|
+
trace: WaveformTrace,
|
|
114
|
+
s_params: SParameterData,
|
|
115
|
+
) -> WaveformTrace:
|
|
116
|
+
"""De-embed using time domain impulse response."""
|
|
117
|
+
# Convert S-parameters to impulse response
|
|
118
|
+
s21 = s_params.s_matrix[:, 1, 0]
|
|
119
|
+
frequencies = s_params.frequencies
|
|
120
|
+
|
|
121
|
+
# Create symmetric frequency axis for IFFT
|
|
122
|
+
n_freq = len(frequencies)
|
|
123
|
+
2 * (n_freq - 1)
|
|
124
|
+
|
|
125
|
+
# Pad with conjugate symmetric extension
|
|
126
|
+
s21_symmetric = np.concatenate([s21, np.conj(s21[-2:0:-1])])
|
|
127
|
+
|
|
128
|
+
# IFFT to get impulse response
|
|
129
|
+
impulse_response = np.fft.ifft(s21_symmetric).real
|
|
130
|
+
|
|
131
|
+
# Create inverse filter (approximate)
|
|
132
|
+
# Use Wiener deconvolution approach
|
|
133
|
+
data = trace.data
|
|
134
|
+
n = len(data)
|
|
135
|
+
|
|
136
|
+
# Pad impulse response
|
|
137
|
+
ir_padded = np.zeros(n)
|
|
138
|
+
ir_len = min(len(impulse_response), n)
|
|
139
|
+
ir_padded[:ir_len] = impulse_response[:ir_len]
|
|
140
|
+
|
|
141
|
+
# FFT-based deconvolution
|
|
142
|
+
data_fft = np.fft.fft(data)
|
|
143
|
+
ir_fft = np.fft.fft(ir_padded)
|
|
144
|
+
|
|
145
|
+
# Wiener filter
|
|
146
|
+
noise_power = 0.01 # Assumed noise level
|
|
147
|
+
ir_power = np.abs(ir_fft) ** 2
|
|
148
|
+
wiener = np.conj(ir_fft) / (ir_power + noise_power)
|
|
149
|
+
|
|
150
|
+
deembedded_fft = data_fft * wiener
|
|
151
|
+
deembedded_data = np.fft.ifft(deembedded_fft).real
|
|
152
|
+
|
|
153
|
+
return WaveformTrace(
|
|
154
|
+
data=deembedded_data.astype(np.float64),
|
|
155
|
+
metadata=trace.metadata,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def embed(
|
|
160
|
+
trace: WaveformTrace,
|
|
161
|
+
s_params: SParameterData,
|
|
162
|
+
) -> WaveformTrace:
|
|
163
|
+
"""Apply channel effects to waveform using S-parameters.
|
|
164
|
+
|
|
165
|
+
Convolves the signal with the channel impulse response
|
|
166
|
+
derived from S21 to simulate channel effects.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
trace: Input (ideal) waveform trace.
|
|
170
|
+
s_params: S-parameters of channel to apply.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Waveform with channel effects applied.
|
|
174
|
+
|
|
175
|
+
Raises:
|
|
176
|
+
AnalysisError: If embedding fails.
|
|
177
|
+
|
|
178
|
+
Example:
|
|
179
|
+
>>> degraded = embed(ideal_trace, channel_sparams)
|
|
180
|
+
>>> # degraded now has channel ISI/loss
|
|
181
|
+
|
|
182
|
+
References:
|
|
183
|
+
IEEE 370-2020 Section 7
|
|
184
|
+
"""
|
|
185
|
+
if s_params.n_ports != 2:
|
|
186
|
+
raise AnalysisError(f"Embedding requires 2-port S-parameters, got {s_params.n_ports}-port")
|
|
187
|
+
|
|
188
|
+
data = trace.data
|
|
189
|
+
sample_rate = trace.metadata.sample_rate
|
|
190
|
+
n = len(data)
|
|
191
|
+
|
|
192
|
+
# Compute FFT
|
|
193
|
+
signal_fft = np.fft.rfft(data)
|
|
194
|
+
frequencies = np.fft.rfftfreq(n, d=1.0 / sample_rate)
|
|
195
|
+
|
|
196
|
+
# Interpolate S21 to signal frequencies
|
|
197
|
+
s21 = np.interp(
|
|
198
|
+
frequencies,
|
|
199
|
+
s_params.frequencies,
|
|
200
|
+
s_params.s_matrix[:, 1, 0],
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Apply transfer function
|
|
204
|
+
embedded_fft = signal_fft * s21
|
|
205
|
+
|
|
206
|
+
# Inverse FFT
|
|
207
|
+
embedded_data = np.fft.irfft(embedded_fft, n=n)
|
|
208
|
+
|
|
209
|
+
return WaveformTrace(
|
|
210
|
+
data=embedded_data.astype(np.float64),
|
|
211
|
+
metadata=trace.metadata,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def cascade_deembed(
|
|
216
|
+
trace: WaveformTrace,
|
|
217
|
+
fixtures: list[SParameterData],
|
|
218
|
+
*,
|
|
219
|
+
regularization: float = 1e-6,
|
|
220
|
+
) -> WaveformTrace:
|
|
221
|
+
"""Remove multiple fixture effects from waveform.
|
|
222
|
+
|
|
223
|
+
Cascades multiple fixtures and removes their combined effect
|
|
224
|
+
using ABCD matrix multiplication.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
trace: Input waveform trace.
|
|
228
|
+
fixtures: List of S-parameter fixtures to remove.
|
|
229
|
+
regularization: Regularization for matrix inversion.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
De-embedded waveform trace.
|
|
233
|
+
|
|
234
|
+
Example:
|
|
235
|
+
>>> clean = cascade_deembed(trace, [fixture1, fixture2])
|
|
236
|
+
|
|
237
|
+
References:
|
|
238
|
+
IEEE 370-2020 Section 7.3
|
|
239
|
+
"""
|
|
240
|
+
if len(fixtures) == 0:
|
|
241
|
+
return trace
|
|
242
|
+
|
|
243
|
+
if len(fixtures) == 1:
|
|
244
|
+
return deembed(trace, fixtures[0], regularization=regularization)
|
|
245
|
+
|
|
246
|
+
# Find common frequency points
|
|
247
|
+
all_freqs = [f.frequencies for f in fixtures]
|
|
248
|
+
min_freq = max(f.min() for f in all_freqs)
|
|
249
|
+
max_freq = min(f.max() for f in all_freqs)
|
|
250
|
+
n_freq = min(len(f) for f in all_freqs)
|
|
251
|
+
|
|
252
|
+
common_freqs = np.linspace(min_freq, max_freq, n_freq)
|
|
253
|
+
|
|
254
|
+
# Convert each fixture to ABCD and cascade
|
|
255
|
+
abcd_cascade = np.zeros((n_freq, 2, 2), dtype=np.complex128)
|
|
256
|
+
abcd_cascade[:, 0, 0] = 1
|
|
257
|
+
abcd_cascade[:, 1, 1] = 1 # Identity matrix
|
|
258
|
+
|
|
259
|
+
for fixture in fixtures:
|
|
260
|
+
abcd = s_to_abcd(fixture)
|
|
261
|
+
|
|
262
|
+
# Interpolate to common frequencies
|
|
263
|
+
abcd_interp = np.zeros((n_freq, 2, 2), dtype=np.complex128)
|
|
264
|
+
for i in range(2):
|
|
265
|
+
for j in range(2):
|
|
266
|
+
abcd_interp[:, i, j] = np.interp(
|
|
267
|
+
common_freqs,
|
|
268
|
+
fixture.frequencies,
|
|
269
|
+
abcd[:, i, j],
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Matrix multiply for each frequency
|
|
273
|
+
for f_idx in range(n_freq):
|
|
274
|
+
abcd_cascade[f_idx] = abcd_cascade[f_idx] @ abcd_interp[f_idx]
|
|
275
|
+
|
|
276
|
+
# Convert cascaded ABCD back to S-parameters
|
|
277
|
+
s_cascade = abcd_to_s(abcd_cascade, z0=fixtures[0].z0)
|
|
278
|
+
|
|
279
|
+
# Create combined S-parameter object
|
|
280
|
+
combined = SParameterData(
|
|
281
|
+
frequencies=common_freqs,
|
|
282
|
+
s_matrix=s_cascade,
|
|
283
|
+
n_ports=2,
|
|
284
|
+
z0=fixtures[0].z0,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
return deembed(trace, combined, regularization=regularization)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
__all__ = [
|
|
291
|
+
"cascade_deembed",
|
|
292
|
+
"deembed",
|
|
293
|
+
"embed",
|
|
294
|
+
]
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""Equalization algorithms for signal integrity.
|
|
2
|
+
|
|
3
|
+
This module provides FFE, DFE, and CTLE equalization to
|
|
4
|
+
compensate for channel loss and ISI.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.analyzers.signal_integrity.equalization import ffe_equalize
|
|
9
|
+
>>> equalized = ffe_equalize(trace, taps=[-0.1, 1.0, -0.1])
|
|
10
|
+
|
|
11
|
+
References:
|
|
12
|
+
IEEE 802.3: Ethernet PHY Equalization Requirements
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
from scipy import optimize
|
|
22
|
+
from scipy import signal as scipy_signal
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from numpy.typing import NDArray
|
|
26
|
+
|
|
27
|
+
from oscura.core.types import WaveformTrace
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class FFEResult:
|
|
32
|
+
"""Result of FFE equalization.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
equalized_data: Equalized waveform data.
|
|
36
|
+
taps: FFE tap coefficients used.
|
|
37
|
+
n_precursor: Number of precursor taps.
|
|
38
|
+
n_postcursor: Number of postcursor taps.
|
|
39
|
+
mse: Mean squared error (if optimized).
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
equalized_data: NDArray[np.float64]
|
|
43
|
+
taps: NDArray[np.float64]
|
|
44
|
+
n_precursor: int
|
|
45
|
+
n_postcursor: int
|
|
46
|
+
mse: float | None = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class DFEResult:
|
|
51
|
+
"""Result of DFE equalization.
|
|
52
|
+
|
|
53
|
+
Attributes:
|
|
54
|
+
equalized_data: Equalized waveform data.
|
|
55
|
+
taps: DFE tap coefficients.
|
|
56
|
+
decisions: Decoded bit decisions.
|
|
57
|
+
n_taps: Number of DFE taps.
|
|
58
|
+
error_count: Number of decision errors (if reference known).
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
equalized_data: NDArray[np.float64]
|
|
62
|
+
taps: NDArray[np.float64]
|
|
63
|
+
decisions: NDArray[np.int_] | None
|
|
64
|
+
n_taps: int
|
|
65
|
+
error_count: int | None = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class CTLEResult:
|
|
70
|
+
"""Result of CTLE equalization.
|
|
71
|
+
|
|
72
|
+
Attributes:
|
|
73
|
+
equalized_data: Equalized waveform data.
|
|
74
|
+
dc_gain: DC gain in dB.
|
|
75
|
+
ac_gain: AC (peaking) gain in dB.
|
|
76
|
+
pole_frequency: Pole frequency in Hz.
|
|
77
|
+
zero_frequency: Zero frequency in Hz.
|
|
78
|
+
boost: High-frequency boost in dB.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
equalized_data: NDArray[np.float64]
|
|
82
|
+
dc_gain: float
|
|
83
|
+
ac_gain: float
|
|
84
|
+
pole_frequency: float
|
|
85
|
+
zero_frequency: float | None
|
|
86
|
+
boost: float
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def ffe_equalize(
|
|
90
|
+
trace: WaveformTrace,
|
|
91
|
+
taps: list[float] | NDArray[np.float64],
|
|
92
|
+
*,
|
|
93
|
+
samples_per_symbol: int | None = None,
|
|
94
|
+
) -> FFEResult:
|
|
95
|
+
"""Apply Feed-Forward Equalization to waveform.
|
|
96
|
+
|
|
97
|
+
FFE uses a linear FIR filter to compensate for channel ISI.
|
|
98
|
+
The main cursor (largest tap) should be 1.0 for unity gain.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
trace: Input waveform trace.
|
|
102
|
+
taps: FFE tap coefficients (main cursor should be 1.0).
|
|
103
|
+
samples_per_symbol: Samples per UI (auto-detected if None).
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
FFEResult with equalized data.
|
|
107
|
+
|
|
108
|
+
Example:
|
|
109
|
+
>>> result = ffe_equalize(trace, taps=[-0.1, 1.0, -0.1])
|
|
110
|
+
>>> # 3-tap equalizer: 1 precursor, 1 main, 1 postcursor
|
|
111
|
+
|
|
112
|
+
References:
|
|
113
|
+
IEEE 802.3 Clause 93
|
|
114
|
+
"""
|
|
115
|
+
taps = np.array(taps, dtype=np.float64)
|
|
116
|
+
|
|
117
|
+
# Find main cursor position
|
|
118
|
+
main_idx = int(np.argmax(np.abs(taps)))
|
|
119
|
+
n_precursor = main_idx
|
|
120
|
+
n_postcursor = len(taps) - main_idx - 1
|
|
121
|
+
|
|
122
|
+
# Apply FIR filter
|
|
123
|
+
data = trace.data
|
|
124
|
+
equalized = np.convolve(data, taps, mode="same")
|
|
125
|
+
|
|
126
|
+
return FFEResult(
|
|
127
|
+
equalized_data=equalized,
|
|
128
|
+
taps=taps,
|
|
129
|
+
n_precursor=n_precursor,
|
|
130
|
+
n_postcursor=n_postcursor,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def optimize_ffe(
|
|
135
|
+
trace: WaveformTrace,
|
|
136
|
+
n_taps: int = 5,
|
|
137
|
+
*,
|
|
138
|
+
n_precursor: int = 1,
|
|
139
|
+
samples_per_symbol: int | None = None,
|
|
140
|
+
target: NDArray[np.float64] | None = None,
|
|
141
|
+
) -> FFEResult:
|
|
142
|
+
"""Find optimal FFE tap coefficients.
|
|
143
|
+
|
|
144
|
+
Uses least-squares optimization to find taps that minimize
|
|
145
|
+
ISI and maximize eye opening.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
trace: Input waveform trace.
|
|
149
|
+
n_taps: Total number of FFE taps.
|
|
150
|
+
n_precursor: Number of precursor taps.
|
|
151
|
+
samples_per_symbol: Samples per UI.
|
|
152
|
+
target: Target (ideal) waveform for optimization.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
FFEResult with optimized taps.
|
|
156
|
+
|
|
157
|
+
Example:
|
|
158
|
+
>>> result = optimize_ffe(trace, n_taps=5, n_precursor=1)
|
|
159
|
+
>>> print(f"Optimal taps: {result.taps}")
|
|
160
|
+
"""
|
|
161
|
+
data = trace.data
|
|
162
|
+
len(data)
|
|
163
|
+
|
|
164
|
+
if target is None:
|
|
165
|
+
# Create target from sliced data (simplified)
|
|
166
|
+
# Use a decision slicer approach
|
|
167
|
+
threshold = np.median(data)
|
|
168
|
+
target = np.where(data > threshold, 1.0, -1.0)
|
|
169
|
+
|
|
170
|
+
def objective(taps): # type: ignore[no-untyped-def]
|
|
171
|
+
"""Minimize MSE between equalized and target."""
|
|
172
|
+
equalized = np.convolve(data, taps, mode="same")
|
|
173
|
+
mse = np.mean((equalized - target) ** 2)
|
|
174
|
+
return mse
|
|
175
|
+
|
|
176
|
+
# Initial guess: main cursor at 1.0, others small
|
|
177
|
+
n_postcursor = n_taps - n_precursor - 1
|
|
178
|
+
x0 = np.zeros(n_taps)
|
|
179
|
+
x0[n_precursor] = 1.0
|
|
180
|
+
|
|
181
|
+
# Constraints: limit tap magnitude
|
|
182
|
+
bounds = [(-2.0, 2.0)] * n_taps
|
|
183
|
+
bounds[n_precursor] = (0.5, 1.5) # Main cursor near 1.0
|
|
184
|
+
|
|
185
|
+
result = optimize.minimize(
|
|
186
|
+
objective,
|
|
187
|
+
x0,
|
|
188
|
+
method="L-BFGS-B",
|
|
189
|
+
bounds=bounds,
|
|
190
|
+
options={"maxiter": 100},
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
optimal_taps = result.x
|
|
194
|
+
|
|
195
|
+
# Normalize so main cursor is 1.0
|
|
196
|
+
main_val = optimal_taps[n_precursor]
|
|
197
|
+
if abs(main_val) > 1e-6:
|
|
198
|
+
optimal_taps = optimal_taps / main_val
|
|
199
|
+
|
|
200
|
+
equalized = np.convolve(data, optimal_taps, mode="same")
|
|
201
|
+
mse = float(np.mean((equalized - target) ** 2))
|
|
202
|
+
|
|
203
|
+
return FFEResult(
|
|
204
|
+
equalized_data=equalized,
|
|
205
|
+
taps=optimal_taps,
|
|
206
|
+
n_precursor=n_precursor,
|
|
207
|
+
n_postcursor=n_postcursor,
|
|
208
|
+
mse=mse,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def dfe_equalize(
|
|
213
|
+
trace: WaveformTrace,
|
|
214
|
+
taps: list[float] | NDArray[np.float64],
|
|
215
|
+
*,
|
|
216
|
+
threshold: float | None = None,
|
|
217
|
+
samples_per_symbol: int = 1,
|
|
218
|
+
) -> DFEResult:
|
|
219
|
+
"""Apply Decision Feedback Equalization.
|
|
220
|
+
|
|
221
|
+
DFE cancels post-cursor ISI using feedback from previous
|
|
222
|
+
bit decisions. Unlike FFE, DFE does not amplify noise.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
trace: Input waveform trace.
|
|
226
|
+
taps: DFE tap coefficients for post-cursor cancellation.
|
|
227
|
+
threshold: Decision threshold (auto-detected if None).
|
|
228
|
+
samples_per_symbol: Samples per UI (default 1 for symbol-rate).
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
DFEResult with equalized data and decisions.
|
|
232
|
+
|
|
233
|
+
Example:
|
|
234
|
+
>>> result = dfe_equalize(trace, taps=[0.2, 0.1])
|
|
235
|
+
>>> # 2-tap DFE canceling h1 and h2
|
|
236
|
+
|
|
237
|
+
References:
|
|
238
|
+
IEEE 802.3 Clause 93
|
|
239
|
+
"""
|
|
240
|
+
taps = np.array(taps, dtype=np.float64)
|
|
241
|
+
n_taps = len(taps)
|
|
242
|
+
data = trace.data
|
|
243
|
+
n = len(data)
|
|
244
|
+
|
|
245
|
+
# Auto-detect threshold
|
|
246
|
+
if threshold is None:
|
|
247
|
+
threshold = float(np.median(data))
|
|
248
|
+
|
|
249
|
+
# Output arrays
|
|
250
|
+
equalized = np.zeros(n, dtype=np.float64)
|
|
251
|
+
decisions = np.zeros(n // samples_per_symbol, dtype=np.int_)
|
|
252
|
+
|
|
253
|
+
# Previous decisions buffer (for feedback)
|
|
254
|
+
prev_decisions = np.zeros(n_taps, dtype=np.float64)
|
|
255
|
+
|
|
256
|
+
# Process symbol-by-symbol
|
|
257
|
+
decision_idx = 0
|
|
258
|
+
|
|
259
|
+
for i in range(0, n, samples_per_symbol):
|
|
260
|
+
# Get input sample
|
|
261
|
+
input_val = data[i]
|
|
262
|
+
|
|
263
|
+
# Subtract DFE feedback
|
|
264
|
+
dfe_correction = np.dot(taps, prev_decisions)
|
|
265
|
+
corrected = input_val - dfe_correction
|
|
266
|
+
|
|
267
|
+
# Make decision
|
|
268
|
+
decision = 1.0 if corrected > threshold else -1.0
|
|
269
|
+
|
|
270
|
+
# Store
|
|
271
|
+
equalized[i : i + samples_per_symbol] = corrected
|
|
272
|
+
if decision_idx < len(decisions):
|
|
273
|
+
decisions[decision_idx] = int((decision + 1) / 2) # 0 or 1
|
|
274
|
+
decision_idx += 1
|
|
275
|
+
|
|
276
|
+
# Shift feedback register
|
|
277
|
+
prev_decisions = np.roll(prev_decisions, 1)
|
|
278
|
+
prev_decisions[0] = decision
|
|
279
|
+
|
|
280
|
+
return DFEResult(
|
|
281
|
+
equalized_data=equalized,
|
|
282
|
+
taps=taps,
|
|
283
|
+
decisions=decisions[:decision_idx],
|
|
284
|
+
n_taps=n_taps,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def ctle_equalize(
|
|
289
|
+
trace: WaveformTrace,
|
|
290
|
+
dc_gain: float = 0.0,
|
|
291
|
+
ac_gain: float = 6.0,
|
|
292
|
+
pole_frequency: float = 5e9,
|
|
293
|
+
*,
|
|
294
|
+
zero_frequency: float | None = None,
|
|
295
|
+
) -> CTLEResult:
|
|
296
|
+
"""Apply Continuous Time Linear Equalization.
|
|
297
|
+
|
|
298
|
+
CTLE provides high-frequency boost to compensate for
|
|
299
|
+
channel loss. It uses an analog-style transfer function.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
trace: Input waveform trace.
|
|
303
|
+
dc_gain: DC gain in dB.
|
|
304
|
+
ac_gain: AC (peaking) gain in dB.
|
|
305
|
+
pole_frequency: Pole frequency in Hz.
|
|
306
|
+
zero_frequency: Zero frequency in Hz (computed if None).
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
CTLEResult with equalized data.
|
|
310
|
+
|
|
311
|
+
Example:
|
|
312
|
+
>>> result = ctle_equalize(trace, ac_gain=6, pole_frequency=5e9)
|
|
313
|
+
>>> # 6 dB of high-frequency boost
|
|
314
|
+
|
|
315
|
+
References:
|
|
316
|
+
IEEE 802.3 Clause 93
|
|
317
|
+
"""
|
|
318
|
+
data = trace.data
|
|
319
|
+
sample_rate = trace.metadata.sample_rate
|
|
320
|
+
len(data)
|
|
321
|
+
|
|
322
|
+
# Convert gains from dB to linear
|
|
323
|
+
dc_linear = 10 ** (dc_gain / 20)
|
|
324
|
+
ac_linear = 10 ** (ac_gain / 20)
|
|
325
|
+
|
|
326
|
+
# Calculate zero frequency to achieve desired boost
|
|
327
|
+
if zero_frequency is None:
|
|
328
|
+
# Zero at lower frequency than pole to create peaking
|
|
329
|
+
zero_frequency = pole_frequency / (ac_linear / dc_linear)
|
|
330
|
+
|
|
331
|
+
# Compute boost
|
|
332
|
+
boost = ac_gain - dc_gain
|
|
333
|
+
|
|
334
|
+
# Create CTLE transfer function
|
|
335
|
+
# H(s) = (1 + s/wz) / (1 + s/wp) * gain
|
|
336
|
+
wz = 2 * np.pi * zero_frequency
|
|
337
|
+
wp = 2 * np.pi * pole_frequency
|
|
338
|
+
|
|
339
|
+
# Convert to digital filter using bilinear transform
|
|
340
|
+
b_analog = [1 / wz, 1] # numerator coefficients
|
|
341
|
+
a_analog = [1 / wp, 1] # denominator coefficients
|
|
342
|
+
|
|
343
|
+
# Bilinear transform
|
|
344
|
+
b_digital, a_digital = scipy_signal.bilinear(b_analog, a_analog, fs=sample_rate)
|
|
345
|
+
|
|
346
|
+
# Scale for DC gain
|
|
347
|
+
b_digital = b_digital * dc_linear
|
|
348
|
+
|
|
349
|
+
# Apply filter
|
|
350
|
+
equalized = scipy_signal.lfilter(b_digital, a_digital, data)
|
|
351
|
+
|
|
352
|
+
return CTLEResult(
|
|
353
|
+
equalized_data=equalized.astype(np.float64),
|
|
354
|
+
dc_gain=dc_gain,
|
|
355
|
+
ac_gain=ac_gain,
|
|
356
|
+
pole_frequency=pole_frequency,
|
|
357
|
+
zero_frequency=zero_frequency,
|
|
358
|
+
boost=boost,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
__all__ = [
|
|
363
|
+
"CTLEResult",
|
|
364
|
+
"DFEResult",
|
|
365
|
+
"FFEResult",
|
|
366
|
+
"ctle_equalize",
|
|
367
|
+
"dfe_equalize",
|
|
368
|
+
"ffe_equalize",
|
|
369
|
+
"optimize_ffe",
|
|
370
|
+
]
|