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,298 @@
|
|
|
1
|
+
"""EMC compliance testing implementation.
|
|
2
|
+
|
|
3
|
+
This module provides compliance testing against regulatory limit masks.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from oscura.compliance import load_limit_mask, test_compliance
|
|
8
|
+
>>> mask = load_limit_mask('FCC_Part15_ClassB')
|
|
9
|
+
>>> result = test_compliance(trace, mask)
|
|
10
|
+
>>> print(f"Status: {result.status}")
|
|
11
|
+
|
|
12
|
+
References:
|
|
13
|
+
CISPR 16-1-1 (Measuring Apparatus)
|
|
14
|
+
ANSI C63.2 (Instrumentation)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from enum import Enum
|
|
21
|
+
from typing import TYPE_CHECKING, Any
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from numpy.typing import NDArray
|
|
27
|
+
|
|
28
|
+
from oscura.compliance.masks import LimitMask
|
|
29
|
+
from oscura.core.types import WaveformTrace
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DetectorType(Enum):
|
|
33
|
+
"""EMC measurement detector types."""
|
|
34
|
+
|
|
35
|
+
PEAK = "peak"
|
|
36
|
+
QUASI_PEAK = "quasi-peak"
|
|
37
|
+
AVERAGE = "average"
|
|
38
|
+
RMS = "rms"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class ComplianceViolation:
|
|
43
|
+
"""Single compliance violation record.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
frequency: Violation frequency in Hz
|
|
47
|
+
measured_level: Measured level in mask unit (dBuV, etc.)
|
|
48
|
+
limit_level: Limit level at this frequency
|
|
49
|
+
excess_db: Amount exceeding limit (positive = violation)
|
|
50
|
+
detector: Detector type used
|
|
51
|
+
severity: Severity classification
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
frequency: float
|
|
55
|
+
measured_level: float
|
|
56
|
+
limit_level: float
|
|
57
|
+
excess_db: float
|
|
58
|
+
detector: str = "peak"
|
|
59
|
+
severity: str = "FAIL"
|
|
60
|
+
|
|
61
|
+
def __str__(self) -> str:
|
|
62
|
+
"""Format violation as string."""
|
|
63
|
+
freq_mhz = self.frequency / 1e6
|
|
64
|
+
return (
|
|
65
|
+
f"{freq_mhz:.3f} MHz: {self.measured_level:.1f} dB "
|
|
66
|
+
f"(limit: {self.limit_level:.1f} dB, excess: {self.excess_db:.1f} dB)"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class ComplianceResult:
|
|
72
|
+
"""Compliance test result.
|
|
73
|
+
|
|
74
|
+
Attributes:
|
|
75
|
+
status: Overall status ('PASS' or 'FAIL')
|
|
76
|
+
mask_name: Name of limit mask used
|
|
77
|
+
violations: List of violations
|
|
78
|
+
margin_to_limit: Minimum margin in dB (negative = failing)
|
|
79
|
+
worst_frequency: Frequency with worst margin
|
|
80
|
+
worst_margin: Worst margin value in dB
|
|
81
|
+
spectrum_freq: Tested frequency array
|
|
82
|
+
spectrum_level: Measured level array
|
|
83
|
+
limit_level: Limit level array (interpolated to spectrum frequencies)
|
|
84
|
+
detector: Detector type used
|
|
85
|
+
metadata: Additional result metadata
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
status: str
|
|
89
|
+
mask_name: str
|
|
90
|
+
violations: list[ComplianceViolation]
|
|
91
|
+
margin_to_limit: float
|
|
92
|
+
worst_frequency: float
|
|
93
|
+
worst_margin: float
|
|
94
|
+
spectrum_freq: NDArray[np.float64]
|
|
95
|
+
spectrum_level: NDArray[np.float64]
|
|
96
|
+
limit_level: NDArray[np.float64]
|
|
97
|
+
detector: str = "peak"
|
|
98
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def passed(self) -> bool:
|
|
102
|
+
"""Return True if compliance test passed."""
|
|
103
|
+
return self.status == "PASS"
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def violation_count(self) -> int:
|
|
107
|
+
"""Return number of violations."""
|
|
108
|
+
return len(self.violations)
|
|
109
|
+
|
|
110
|
+
def summary(self) -> str:
|
|
111
|
+
"""Generate text summary of result."""
|
|
112
|
+
lines = [
|
|
113
|
+
f"EMC Compliance Test: {self.mask_name}",
|
|
114
|
+
f"Status: {self.status}",
|
|
115
|
+
f"Margin to limit: {self.margin_to_limit:.1f} dB",
|
|
116
|
+
f"Worst frequency: {self.worst_frequency / 1e6:.3f} MHz",
|
|
117
|
+
f"Worst margin: {self.worst_margin:.1f} dB",
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
if self.violations:
|
|
121
|
+
lines.append(f"\nViolations ({len(self.violations)}):")
|
|
122
|
+
for v in self.violations[:10]: # Limit to first 10
|
|
123
|
+
lines.append(f" - {v}")
|
|
124
|
+
if len(self.violations) > 10:
|
|
125
|
+
lines.append(f" ... and {len(self.violations) - 10} more")
|
|
126
|
+
|
|
127
|
+
return "\n".join(lines)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def check_compliance(
|
|
131
|
+
trace_or_spectrum: WaveformTrace | tuple[NDArray[np.float64], NDArray[np.float64]],
|
|
132
|
+
mask: LimitMask,
|
|
133
|
+
*,
|
|
134
|
+
detector: DetectorType | str = DetectorType.PEAK,
|
|
135
|
+
frequency_range: tuple[float, float] | None = None,
|
|
136
|
+
unit_conversion: str | None = None,
|
|
137
|
+
) -> ComplianceResult:
|
|
138
|
+
"""Check signal against EMC limit mask.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
trace_or_spectrum: Either a WaveformTrace to analyze, or a tuple of
|
|
142
|
+
(frequency_array, magnitude_array) if spectrum already computed.
|
|
143
|
+
mask: LimitMask to test against.
|
|
144
|
+
detector: Detector type to use ('peak', 'quasi-peak', 'average', 'rms').
|
|
145
|
+
frequency_range: Optional (min, max) frequency range to test.
|
|
146
|
+
unit_conversion: Optional unit conversion ('V_to_dBuV', 'W_to_dBm', etc.)
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
ComplianceResult with pass/fail status and violation details.
|
|
150
|
+
|
|
151
|
+
Example:
|
|
152
|
+
>>> mask = load_limit_mask('FCC_Part15_ClassB')
|
|
153
|
+
>>> result = check_compliance(trace, mask)
|
|
154
|
+
>>> print(result.summary())
|
|
155
|
+
"""
|
|
156
|
+
from oscura.core.types import WaveformTrace
|
|
157
|
+
|
|
158
|
+
# Handle detector type
|
|
159
|
+
if isinstance(detector, str):
|
|
160
|
+
detector = DetectorType(detector.lower().replace("-", "_").replace(" ", "_"))
|
|
161
|
+
|
|
162
|
+
# Get spectrum
|
|
163
|
+
if isinstance(trace_or_spectrum, WaveformTrace):
|
|
164
|
+
freq, mag = _compute_spectrum(trace_or_spectrum, detector)
|
|
165
|
+
else:
|
|
166
|
+
freq, mag = trace_or_spectrum
|
|
167
|
+
|
|
168
|
+
# Convert to dB if needed
|
|
169
|
+
if unit_conversion == "V_to_dBuV":
|
|
170
|
+
# dBuV = 20*log10(V * 1e6)
|
|
171
|
+
spectrum_level = 20 * np.log10(np.abs(mag) * 1e6 + 1e-12)
|
|
172
|
+
elif unit_conversion == "W_to_dBm":
|
|
173
|
+
# dBm = 10*log10(W * 1000)
|
|
174
|
+
spectrum_level = 10 * np.log10(np.abs(mag) * 1000 + 1e-12)
|
|
175
|
+
elif mag.max() > 0 and mag.max() < 10:
|
|
176
|
+
# Assume linear voltage, convert to dBuV
|
|
177
|
+
spectrum_level = 20 * np.log10(np.abs(mag) * 1e6 + 1e-12)
|
|
178
|
+
else:
|
|
179
|
+
# Assume already in dB
|
|
180
|
+
spectrum_level = mag
|
|
181
|
+
|
|
182
|
+
# Apply frequency range filter
|
|
183
|
+
if frequency_range is not None:
|
|
184
|
+
f_min, f_max = frequency_range
|
|
185
|
+
mask_filter = (freq >= f_min) & (freq <= f_max)
|
|
186
|
+
freq = freq[mask_filter]
|
|
187
|
+
spectrum_level = spectrum_level[mask_filter]
|
|
188
|
+
|
|
189
|
+
# Limit to mask frequency range
|
|
190
|
+
mask_f_min, mask_f_max = mask.frequency_range
|
|
191
|
+
in_range = (freq >= mask_f_min) & (freq <= mask_f_max)
|
|
192
|
+
freq = freq[in_range]
|
|
193
|
+
spectrum_level = spectrum_level[in_range]
|
|
194
|
+
|
|
195
|
+
if len(freq) == 0:
|
|
196
|
+
# No data in mask range
|
|
197
|
+
return ComplianceResult(
|
|
198
|
+
status="PASS",
|
|
199
|
+
mask_name=mask.name,
|
|
200
|
+
violations=[],
|
|
201
|
+
margin_to_limit=np.inf,
|
|
202
|
+
worst_frequency=0.0,
|
|
203
|
+
worst_margin=np.inf,
|
|
204
|
+
spectrum_freq=np.array([]),
|
|
205
|
+
spectrum_level=np.array([]),
|
|
206
|
+
limit_level=np.array([]),
|
|
207
|
+
detector=detector.value,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Interpolate limit to spectrum frequencies
|
|
211
|
+
limit_level = mask.interpolate(freq)
|
|
212
|
+
|
|
213
|
+
# Calculate margin (positive = passing)
|
|
214
|
+
margin = limit_level - spectrum_level
|
|
215
|
+
|
|
216
|
+
# Find violations
|
|
217
|
+
violations: list[ComplianceViolation] = []
|
|
218
|
+
violation_mask = margin < 0
|
|
219
|
+
if np.any(violation_mask):
|
|
220
|
+
violation_indices = np.where(violation_mask)[0]
|
|
221
|
+
for idx in violation_indices:
|
|
222
|
+
violations.append(
|
|
223
|
+
ComplianceViolation(
|
|
224
|
+
frequency=float(freq[idx]),
|
|
225
|
+
measured_level=float(spectrum_level[idx]),
|
|
226
|
+
limit_level=float(limit_level[idx]),
|
|
227
|
+
excess_db=float(-margin[idx]),
|
|
228
|
+
detector=detector.value,
|
|
229
|
+
severity="FAIL",
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Overall results
|
|
234
|
+
status = "FAIL" if violations else "PASS"
|
|
235
|
+
margin_to_limit = float(np.min(margin))
|
|
236
|
+
worst_idx = int(np.argmin(margin))
|
|
237
|
+
worst_frequency = float(freq[worst_idx])
|
|
238
|
+
worst_margin = float(margin[worst_idx])
|
|
239
|
+
|
|
240
|
+
return ComplianceResult(
|
|
241
|
+
status=status,
|
|
242
|
+
mask_name=mask.name,
|
|
243
|
+
violations=violations,
|
|
244
|
+
margin_to_limit=margin_to_limit,
|
|
245
|
+
worst_frequency=worst_frequency,
|
|
246
|
+
worst_margin=worst_margin,
|
|
247
|
+
spectrum_freq=freq,
|
|
248
|
+
spectrum_level=spectrum_level,
|
|
249
|
+
limit_level=limit_level,
|
|
250
|
+
detector=detector.value,
|
|
251
|
+
metadata={
|
|
252
|
+
"unit": mask.unit,
|
|
253
|
+
"distance": mask.distance,
|
|
254
|
+
"regulatory_body": mask.regulatory_body,
|
|
255
|
+
},
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _compute_spectrum(
|
|
260
|
+
trace: WaveformTrace,
|
|
261
|
+
detector: DetectorType,
|
|
262
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
263
|
+
"""Compute spectrum from trace with specified detector.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
trace: Input waveform trace.
|
|
267
|
+
detector: Detector type.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
(frequency, magnitude) arrays.
|
|
271
|
+
"""
|
|
272
|
+
from oscura.analyzers.waveform.spectral import fft, psd
|
|
273
|
+
|
|
274
|
+
if detector == DetectorType.PEAK:
|
|
275
|
+
# Use FFT for peak detection
|
|
276
|
+
freq, mag = fft(trace) # type: ignore[misc]
|
|
277
|
+
return freq, np.abs(mag)
|
|
278
|
+
elif detector == DetectorType.AVERAGE:
|
|
279
|
+
# Use Welch PSD for averaging
|
|
280
|
+
freq, mag = psd(trace, method="welch") # type: ignore[call-arg]
|
|
281
|
+
return freq, np.sqrt(mag) # Convert PSD to magnitude
|
|
282
|
+
elif detector == DetectorType.QUASI_PEAK:
|
|
283
|
+
# Quasi-peak requires special weighting (simplified here)
|
|
284
|
+
# Real implementation would use CISPR 16 weighting network
|
|
285
|
+
freq, mag = fft(trace) # type: ignore[misc]
|
|
286
|
+
# Apply simplified quasi-peak envelope
|
|
287
|
+
return freq, np.abs(mag) * 0.8 # Approximate QP < peak
|
|
288
|
+
else: # RMS
|
|
289
|
+
freq, mag = psd(trace, method="welch") # type: ignore[call-arg]
|
|
290
|
+
return freq, np.sqrt(mag)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
__all__ = [
|
|
294
|
+
"ComplianceResult",
|
|
295
|
+
"ComplianceViolation",
|
|
296
|
+
"DetectorType",
|
|
297
|
+
"check_compliance",
|
|
298
|
+
]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Component analysis module for Oscura.
|
|
2
|
+
|
|
3
|
+
This module provides TDR-based impedance extraction, capacitance/inductance
|
|
4
|
+
measurement, parasitic extraction, and transmission line analysis.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from oscura.component.impedance import (
|
|
8
|
+
discontinuity_analysis,
|
|
9
|
+
extract_impedance,
|
|
10
|
+
impedance_profile,
|
|
11
|
+
)
|
|
12
|
+
from oscura.component.reactive import (
|
|
13
|
+
extract_parasitics,
|
|
14
|
+
measure_capacitance,
|
|
15
|
+
measure_inductance,
|
|
16
|
+
)
|
|
17
|
+
from oscura.component.transmission_line import (
|
|
18
|
+
characteristic_impedance,
|
|
19
|
+
propagation_delay,
|
|
20
|
+
transmission_line_analysis,
|
|
21
|
+
velocity_factor,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"characteristic_impedance",
|
|
26
|
+
"discontinuity_analysis",
|
|
27
|
+
# Impedance
|
|
28
|
+
"extract_impedance",
|
|
29
|
+
"extract_parasitics",
|
|
30
|
+
"impedance_profile",
|
|
31
|
+
# Reactive
|
|
32
|
+
"measure_capacitance",
|
|
33
|
+
"measure_inductance",
|
|
34
|
+
"propagation_delay",
|
|
35
|
+
# Transmission line
|
|
36
|
+
"transmission_line_analysis",
|
|
37
|
+
"velocity_factor",
|
|
38
|
+
]
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""TDR impedance extraction for Oscura.
|
|
2
|
+
|
|
3
|
+
This module provides impedance extraction from Time Domain Reflectometry
|
|
4
|
+
(TDR) measurements, including impedance profiling and discontinuity analysis.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.component import extract_impedance
|
|
9
|
+
>>> z0, z_profile = extract_impedance(tdr_trace)
|
|
10
|
+
|
|
11
|
+
References:
|
|
12
|
+
IPC-TM-650 2.5.5.7: Characteristic Impedance of Lines on PCBs
|
|
13
|
+
IEEE 370-2020: Electrical Characterization of Interconnects
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import TYPE_CHECKING, Literal
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
from scipy import signal as sp_signal
|
|
23
|
+
|
|
24
|
+
from oscura.core.exceptions import InsufficientDataError
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from numpy.typing import NDArray
|
|
28
|
+
|
|
29
|
+
from oscura.core.types import WaveformTrace
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ImpedanceProfile:
|
|
34
|
+
"""Impedance profile from TDR measurement.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
distance: Distance axis in meters.
|
|
38
|
+
time: Time axis in seconds.
|
|
39
|
+
impedance: Impedance values in ohms.
|
|
40
|
+
z0_source: Source impedance (reference).
|
|
41
|
+
velocity: Propagation velocity used (m/s).
|
|
42
|
+
statistics: Additional statistics.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
distance: NDArray[np.float64]
|
|
46
|
+
time: NDArray[np.float64]
|
|
47
|
+
impedance: NDArray[np.float64]
|
|
48
|
+
z0_source: float
|
|
49
|
+
velocity: float
|
|
50
|
+
statistics: dict = field(default_factory=dict) # type: ignore[type-arg]
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def mean_impedance(self) -> float:
|
|
54
|
+
"""Mean impedance value."""
|
|
55
|
+
return float(np.mean(self.impedance))
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def max_impedance(self) -> float:
|
|
59
|
+
"""Maximum impedance value."""
|
|
60
|
+
return float(np.max(self.impedance))
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def min_impedance(self) -> float:
|
|
64
|
+
"""Minimum impedance value."""
|
|
65
|
+
return float(np.min(self.impedance))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class Discontinuity:
|
|
70
|
+
"""A detected impedance discontinuity.
|
|
71
|
+
|
|
72
|
+
Attributes:
|
|
73
|
+
position: Position in meters.
|
|
74
|
+
time: Time position in seconds.
|
|
75
|
+
impedance_before: Impedance before discontinuity.
|
|
76
|
+
impedance_after: Impedance after discontinuity.
|
|
77
|
+
magnitude: Magnitude of change (ohms).
|
|
78
|
+
reflection_coeff: Reflection coefficient (rho).
|
|
79
|
+
discontinuity_type: Type of discontinuity.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
position: float
|
|
83
|
+
time: float
|
|
84
|
+
impedance_before: float
|
|
85
|
+
impedance_after: float
|
|
86
|
+
magnitude: float
|
|
87
|
+
reflection_coeff: float
|
|
88
|
+
discontinuity_type: Literal["capacitive", "inductive", "resistive", "unknown"]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def extract_impedance(
|
|
92
|
+
trace: WaveformTrace,
|
|
93
|
+
*,
|
|
94
|
+
z0_source: float = 50.0,
|
|
95
|
+
velocity: float | None = None,
|
|
96
|
+
velocity_factor: float = 0.66,
|
|
97
|
+
start_time: float | None = None,
|
|
98
|
+
end_time: float | None = None,
|
|
99
|
+
) -> tuple[float, ImpedanceProfile]:
|
|
100
|
+
"""Extract impedance profile from TDR waveform.
|
|
101
|
+
|
|
102
|
+
Calculates the impedance profile from a TDR reflection measurement
|
|
103
|
+
using the relationship between incident and reflected waves.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
trace: TDR reflection waveform.
|
|
107
|
+
z0_source: Source/reference impedance (default 50 ohms).
|
|
108
|
+
velocity: Propagation velocity in m/s. If None, calculated from
|
|
109
|
+
velocity_factor.
|
|
110
|
+
velocity_factor: Fraction of speed of light (default 0.66 for FR4).
|
|
111
|
+
start_time: Start time for analysis window (seconds).
|
|
112
|
+
end_time: End time for analysis window (seconds).
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Tuple of (characteristic_impedance, impedance_profile).
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
InsufficientDataError: If trace has fewer than 10 samples.
|
|
119
|
+
|
|
120
|
+
Example:
|
|
121
|
+
>>> z0, profile = extract_impedance(tdr_trace, z0_source=50)
|
|
122
|
+
>>> print(f"Z0 = {z0:.1f} ohms")
|
|
123
|
+
|
|
124
|
+
References:
|
|
125
|
+
IPC-TM-650 2.5.5.7
|
|
126
|
+
"""
|
|
127
|
+
data = trace.data.astype(np.float64)
|
|
128
|
+
sample_rate = trace.metadata.sample_rate
|
|
129
|
+
dt = 1.0 / sample_rate
|
|
130
|
+
|
|
131
|
+
if len(data) < 10:
|
|
132
|
+
raise InsufficientDataError(
|
|
133
|
+
"TDR analysis requires at least 10 samples",
|
|
134
|
+
required=10,
|
|
135
|
+
available=len(data),
|
|
136
|
+
analysis_type="tdr_impedance",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Calculate propagation velocity
|
|
140
|
+
c = 299792458.0 # Speed of light in m/s
|
|
141
|
+
if velocity is None:
|
|
142
|
+
velocity = c * velocity_factor
|
|
143
|
+
|
|
144
|
+
# Create time and distance axes
|
|
145
|
+
time_axis = np.arange(len(data)) * dt
|
|
146
|
+
# TDR: distance is velocity * time / 2 (round trip)
|
|
147
|
+
distance_axis = velocity * time_axis / 2.0
|
|
148
|
+
|
|
149
|
+
# Apply time window if specified
|
|
150
|
+
start_idx = 0
|
|
151
|
+
end_idx = len(data)
|
|
152
|
+
if start_time is not None:
|
|
153
|
+
start_idx = int(start_time * sample_rate)
|
|
154
|
+
if end_time is not None:
|
|
155
|
+
end_idx = int(end_time * sample_rate)
|
|
156
|
+
|
|
157
|
+
start_idx = max(0, min(start_idx, len(data) - 1))
|
|
158
|
+
end_idx = max(start_idx + 1, min(end_idx, len(data)))
|
|
159
|
+
|
|
160
|
+
# Find the incident step level in TDR data
|
|
161
|
+
# For TDR with a matched load (Z = Z0), the steady-state voltage is V_source/2
|
|
162
|
+
incident_level = _find_incident_level(data)
|
|
163
|
+
|
|
164
|
+
# Calculate reflection coefficient from TDR waveform
|
|
165
|
+
# For TDR: V_measured = V_incident * (1 + rho)
|
|
166
|
+
# where rho is the reflection coefficient
|
|
167
|
+
# So: rho = (V_measured / V_incident) - 1
|
|
168
|
+
|
|
169
|
+
if incident_level > 0:
|
|
170
|
+
rho = (data / incident_level) - 1.0
|
|
171
|
+
else:
|
|
172
|
+
# Fallback: assume data is already normalized
|
|
173
|
+
rho = data - 1.0
|
|
174
|
+
|
|
175
|
+
# Calculate impedance from reflection coefficient
|
|
176
|
+
# Z = Z0 * (1 + rho) / (1 - rho)
|
|
177
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
178
|
+
impedance = z0_source * (1 + rho) / (1 - rho)
|
|
179
|
+
# Clip unreasonable values
|
|
180
|
+
impedance = np.clip(impedance, 1.0, 10000.0)
|
|
181
|
+
|
|
182
|
+
# Extract characteristic impedance from stable region
|
|
183
|
+
stable_region = impedance[start_idx:end_idx]
|
|
184
|
+
z0 = float(np.median(stable_region))
|
|
185
|
+
|
|
186
|
+
# Create profile
|
|
187
|
+
profile = ImpedanceProfile(
|
|
188
|
+
distance=distance_axis,
|
|
189
|
+
time=time_axis,
|
|
190
|
+
impedance=impedance,
|
|
191
|
+
z0_source=z0_source,
|
|
192
|
+
velocity=velocity,
|
|
193
|
+
statistics={
|
|
194
|
+
"z0_measured": z0,
|
|
195
|
+
"z0_std": float(np.std(stable_region)),
|
|
196
|
+
"z0_min": float(np.min(stable_region)),
|
|
197
|
+
"z0_max": float(np.max(stable_region)),
|
|
198
|
+
"analysis_start_m": float(distance_axis[start_idx]),
|
|
199
|
+
"analysis_end_m": float(distance_axis[end_idx - 1]),
|
|
200
|
+
},
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return z0, profile
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def impedance_profile(
|
|
207
|
+
trace: WaveformTrace,
|
|
208
|
+
*,
|
|
209
|
+
z0_source: float = 50.0,
|
|
210
|
+
velocity_factor: float = 0.66,
|
|
211
|
+
smooth_window: int = 0,
|
|
212
|
+
) -> ImpedanceProfile:
|
|
213
|
+
"""Get impedance profile from TDR waveform.
|
|
214
|
+
|
|
215
|
+
Convenience function that returns just the impedance profile.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
trace: TDR reflection waveform.
|
|
219
|
+
z0_source: Source/reference impedance.
|
|
220
|
+
velocity_factor: Fraction of speed of light.
|
|
221
|
+
smooth_window: Smoothing window size (0 = no smoothing).
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
ImpedanceProfile object.
|
|
225
|
+
"""
|
|
226
|
+
_, profile = extract_impedance(
|
|
227
|
+
trace,
|
|
228
|
+
z0_source=z0_source,
|
|
229
|
+
velocity_factor=velocity_factor,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
if smooth_window > 0:
|
|
233
|
+
# Apply smoothing
|
|
234
|
+
kernel = np.ones(smooth_window) / smooth_window
|
|
235
|
+
profile.impedance = np.convolve(profile.impedance, kernel, mode="same")
|
|
236
|
+
|
|
237
|
+
return profile
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def discontinuity_analysis(
|
|
241
|
+
trace: WaveformTrace,
|
|
242
|
+
*,
|
|
243
|
+
z0_source: float = 50.0,
|
|
244
|
+
velocity_factor: float = 0.66,
|
|
245
|
+
threshold: float = 5.0,
|
|
246
|
+
min_separation: float = 1e-12,
|
|
247
|
+
) -> list[Discontinuity]:
|
|
248
|
+
"""Analyze impedance discontinuities in TDR waveform.
|
|
249
|
+
|
|
250
|
+
Detects and characterizes impedance discontinuities along a
|
|
251
|
+
transmission line from TDR measurements.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
trace: TDR reflection waveform.
|
|
255
|
+
z0_source: Source/reference impedance.
|
|
256
|
+
velocity_factor: Fraction of speed of light.
|
|
257
|
+
threshold: Minimum impedance change to detect (ohms).
|
|
258
|
+
min_separation: Minimum time between discontinuities (seconds).
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
List of detected Discontinuity objects.
|
|
262
|
+
|
|
263
|
+
Example:
|
|
264
|
+
>>> disconts = discontinuity_analysis(tdr_trace)
|
|
265
|
+
>>> for d in disconts:
|
|
266
|
+
... print(f"{d.position*1000:.1f}mm: {d.magnitude:.1f} ohms")
|
|
267
|
+
"""
|
|
268
|
+
# Get impedance profile
|
|
269
|
+
_, profile = extract_impedance(
|
|
270
|
+
trace,
|
|
271
|
+
z0_source=z0_source,
|
|
272
|
+
velocity_factor=velocity_factor,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
impedance = profile.impedance
|
|
276
|
+
time_axis = profile.time
|
|
277
|
+
distance_axis = profile.distance
|
|
278
|
+
|
|
279
|
+
# Find discontinuities using derivative
|
|
280
|
+
derivative = np.abs(np.diff(impedance))
|
|
281
|
+
|
|
282
|
+
# Smooth derivative
|
|
283
|
+
if len(derivative) > 5:
|
|
284
|
+
kernel = np.ones(5) / 5
|
|
285
|
+
derivative = np.convolve(derivative, kernel, mode="same")
|
|
286
|
+
|
|
287
|
+
# Find peaks in derivative (discontinuities)
|
|
288
|
+
sample_rate = trace.metadata.sample_rate
|
|
289
|
+
min_samples = int(min_separation * sample_rate)
|
|
290
|
+
|
|
291
|
+
peaks, _properties = sp_signal.find_peaks(
|
|
292
|
+
derivative,
|
|
293
|
+
height=threshold,
|
|
294
|
+
distance=max(1, min_samples),
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# Analyze each discontinuity
|
|
298
|
+
discontinuities = []
|
|
299
|
+
for peak_idx in peaks:
|
|
300
|
+
if peak_idx < 1 or peak_idx >= len(impedance) - 1:
|
|
301
|
+
continue
|
|
302
|
+
|
|
303
|
+
z_before = float(np.mean(impedance[max(0, peak_idx - 5) : peak_idx]))
|
|
304
|
+
z_after = float(np.mean(impedance[peak_idx + 1 : min(len(impedance), peak_idx + 6)]))
|
|
305
|
+
|
|
306
|
+
magnitude = z_after - z_before
|
|
307
|
+
position = float(distance_axis[peak_idx])
|
|
308
|
+
time_pos = float(time_axis[peak_idx])
|
|
309
|
+
|
|
310
|
+
# Calculate reflection coefficient
|
|
311
|
+
rho = (z_after - z_before) / (z_after + z_before) if z_before + z_after > 0 else 0.0
|
|
312
|
+
|
|
313
|
+
# Determine discontinuity type
|
|
314
|
+
if magnitude > 0:
|
|
315
|
+
# Increasing impedance
|
|
316
|
+
if abs(magnitude) > 20:
|
|
317
|
+
disc_type: Literal["capacitive", "inductive", "resistive", "unknown"] = "inductive"
|
|
318
|
+
else:
|
|
319
|
+
disc_type = "resistive"
|
|
320
|
+
# Decreasing impedance
|
|
321
|
+
elif abs(magnitude) > 20:
|
|
322
|
+
disc_type = "capacitive"
|
|
323
|
+
else:
|
|
324
|
+
disc_type = "resistive"
|
|
325
|
+
|
|
326
|
+
discontinuities.append(
|
|
327
|
+
Discontinuity(
|
|
328
|
+
position=position,
|
|
329
|
+
time=time_pos,
|
|
330
|
+
impedance_before=z_before,
|
|
331
|
+
impedance_after=z_after,
|
|
332
|
+
magnitude=magnitude,
|
|
333
|
+
reflection_coeff=float(rho),
|
|
334
|
+
discontinuity_type=disc_type,
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
return discontinuities
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _find_incident_level(data: NDArray[np.float64]) -> float:
|
|
342
|
+
"""Find the incident step level in TDR data.
|
|
343
|
+
|
|
344
|
+
Looks for the stable level after the initial edge and before
|
|
345
|
+
any reflections return.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
data: TDR waveform data array.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Median voltage level in the incident region.
|
|
352
|
+
"""
|
|
353
|
+
if len(data) < 10:
|
|
354
|
+
return float(np.max(data))
|
|
355
|
+
|
|
356
|
+
# Look at first 10-20% of data for incident level
|
|
357
|
+
search_end = len(data) // 5
|
|
358
|
+
search_start = len(data) // 20
|
|
359
|
+
|
|
360
|
+
if search_end <= search_start:
|
|
361
|
+
return float(np.max(data[:search_end]))
|
|
362
|
+
|
|
363
|
+
# Find stable region using variance
|
|
364
|
+
stable_data = data[search_start:search_end]
|
|
365
|
+
return float(np.median(stable_data))
|