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,756 @@
|
|
|
1
|
+
"""Advanced EMC compliance features.
|
|
2
|
+
|
|
3
|
+
This module provides advanced compliance testing capabilities including
|
|
4
|
+
limit interpolation, compliance test execution, and quasi-peak detection.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
References:
|
|
8
|
+
CISPR 16-1-1: Measuring Apparatus
|
|
9
|
+
FCC Part 15: Unintentional Radiators
|
|
10
|
+
EN 55032: EMC Standard for Multimedia Equipment
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from typing import TYPE_CHECKING, Any
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from numpy.typing import NDArray
|
|
24
|
+
|
|
25
|
+
from oscura.compliance.masks import LimitMask
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"ComplianceTestConfig",
|
|
31
|
+
"ComplianceTestRunner",
|
|
32
|
+
"ComplianceTestSuite",
|
|
33
|
+
"InterpolationMethod",
|
|
34
|
+
"LimitInterpolator",
|
|
35
|
+
"QPDetectorBand",
|
|
36
|
+
"QuasiPeakDetector",
|
|
37
|
+
"interpolate_limit",
|
|
38
|
+
"run_compliance_suite",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# =============================================================================
|
|
43
|
+
# =============================================================================
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class InterpolationMethod(Enum):
|
|
47
|
+
"""Interpolation methods for limit masks.
|
|
48
|
+
|
|
49
|
+
References:
|
|
50
|
+
COMP-005: Limit Interpolation
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
LINEAR = "linear" # Linear interpolation
|
|
54
|
+
LOG_LINEAR = "log-linear" # Log-linear (dB) interpolation
|
|
55
|
+
CUBIC = "cubic" # Cubic spline
|
|
56
|
+
STEP = "step" # Step function (no interpolation)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class LimitInterpolator:
|
|
60
|
+
"""Limit mask interpolator.
|
|
61
|
+
|
|
62
|
+
Provides accurate interpolation of EMC limits between
|
|
63
|
+
defined frequency points.
|
|
64
|
+
|
|
65
|
+
Example:
|
|
66
|
+
>>> from oscura.compliance import load_limit_mask
|
|
67
|
+
>>> mask = load_limit_mask('FCC_Part15_ClassB')
|
|
68
|
+
>>> interp = LimitInterpolator(mask)
|
|
69
|
+
>>> limit_at_100mhz = interp.interpolate(100e6)
|
|
70
|
+
|
|
71
|
+
References:
|
|
72
|
+
COMP-005: Limit Interpolation
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
mask: LimitMask,
|
|
78
|
+
method: InterpolationMethod = InterpolationMethod.LOG_LINEAR,
|
|
79
|
+
extrapolate: bool = False,
|
|
80
|
+
) -> None:
|
|
81
|
+
"""Initialize interpolator.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
mask: Limit mask to interpolate
|
|
85
|
+
method: Interpolation method
|
|
86
|
+
extrapolate: Allow extrapolation beyond mask range
|
|
87
|
+
"""
|
|
88
|
+
self._mask = mask
|
|
89
|
+
self._method = method
|
|
90
|
+
self._extrapolate = extrapolate
|
|
91
|
+
|
|
92
|
+
# Precompute log frequencies for log-linear interpolation
|
|
93
|
+
self._log_freq = np.log10(mask.frequency)
|
|
94
|
+
self._log_limit = mask.limit # Already in dB
|
|
95
|
+
|
|
96
|
+
def interpolate(
|
|
97
|
+
self,
|
|
98
|
+
frequency: float | NDArray[np.float64],
|
|
99
|
+
) -> NDArray[np.float64]:
|
|
100
|
+
"""Interpolate limit at given frequency/frequencies.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
frequency: Frequency or array of frequencies in Hz
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Interpolated limit value(s)
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
ValueError: If frequency outside range and extrapolation disabled
|
|
110
|
+
"""
|
|
111
|
+
freq_array = np.atleast_1d(np.asarray(frequency, dtype=np.float64))
|
|
112
|
+
|
|
113
|
+
# Validate positive frequencies first
|
|
114
|
+
if np.any(freq_array <= 0):
|
|
115
|
+
raise ValueError("Frequency must be positive")
|
|
116
|
+
|
|
117
|
+
# Check range
|
|
118
|
+
f_min, f_max = self._mask.frequency_range
|
|
119
|
+
if not self._extrapolate:
|
|
120
|
+
if np.any(freq_array < f_min) or np.any(freq_array > f_max):
|
|
121
|
+
out_of_range = freq_array[(freq_array < f_min) | (freq_array > f_max)]
|
|
122
|
+
raise ValueError(
|
|
123
|
+
f"Frequency {out_of_range[0]:.2e} Hz outside mask range "
|
|
124
|
+
f"[{f_min:.2e}, {f_max:.2e}] Hz. "
|
|
125
|
+
f"Set extrapolate=True to allow extrapolation."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if self._method == InterpolationMethod.LINEAR:
|
|
129
|
+
return self._interp_linear(freq_array)
|
|
130
|
+
elif self._method == InterpolationMethod.LOG_LINEAR:
|
|
131
|
+
return self._interp_log_linear(freq_array)
|
|
132
|
+
elif self._method == InterpolationMethod.CUBIC:
|
|
133
|
+
return self._interp_cubic(freq_array)
|
|
134
|
+
else: # STEP
|
|
135
|
+
return self._interp_step(freq_array)
|
|
136
|
+
|
|
137
|
+
def _interp_linear(self, freq: NDArray[np.float64]) -> NDArray[np.float64]:
|
|
138
|
+
"""Linear interpolation."""
|
|
139
|
+
return np.interp(freq, self._mask.frequency, self._mask.limit)
|
|
140
|
+
|
|
141
|
+
def _interp_log_linear(self, freq: NDArray[np.float64]) -> NDArray[np.float64]:
|
|
142
|
+
"""Log-linear interpolation (linear in log-frequency space)."""
|
|
143
|
+
log_freq = np.log10(freq)
|
|
144
|
+
return np.interp(log_freq, self._log_freq, self._log_limit)
|
|
145
|
+
|
|
146
|
+
def _interp_cubic(self, freq: NDArray[np.float64]) -> NDArray[np.float64]:
|
|
147
|
+
"""Cubic spline interpolation."""
|
|
148
|
+
from scipy.interpolate import CubicSpline
|
|
149
|
+
|
|
150
|
+
# Use log-frequency for better behavior
|
|
151
|
+
log_freq = np.log10(freq)
|
|
152
|
+
spline = CubicSpline(self._log_freq, self._log_limit, extrapolate=self._extrapolate)
|
|
153
|
+
result: NDArray[np.float64] = spline(log_freq)
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
def _interp_step(self, freq: NDArray[np.float64]) -> NDArray[np.float64]:
|
|
157
|
+
"""Step function (nearest lower point)."""
|
|
158
|
+
result = np.zeros_like(freq)
|
|
159
|
+
for i, f in enumerate(freq):
|
|
160
|
+
idx = np.searchsorted(self._mask.frequency, f, side="right") - 1
|
|
161
|
+
idx = max(0, min(idx, len(self._mask.limit) - 1))
|
|
162
|
+
result[i] = self._mask.limit[idx]
|
|
163
|
+
return result
|
|
164
|
+
|
|
165
|
+
def get_limit_at(
|
|
166
|
+
self,
|
|
167
|
+
frequency: float,
|
|
168
|
+
warn_on_extrapolation: bool = True,
|
|
169
|
+
) -> tuple[float, dict[str, Any]]:
|
|
170
|
+
"""Get limit at specific frequency with metadata.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
frequency: Frequency in Hz
|
|
174
|
+
warn_on_extrapolation: Emit warning if extrapolating
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
(limit_value, metadata) tuple
|
|
178
|
+
"""
|
|
179
|
+
f_min, f_max = self._mask.frequency_range
|
|
180
|
+
is_extrapolated = frequency < f_min or frequency > f_max
|
|
181
|
+
|
|
182
|
+
if is_extrapolated and warn_on_extrapolation:
|
|
183
|
+
logger.warning(
|
|
184
|
+
f"Extrapolating limit at {frequency:.2e} Hz "
|
|
185
|
+
f"(mask range: {f_min:.2e} to {f_max:.2e} Hz)"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
limit = (
|
|
189
|
+
float(self.interpolate(frequency)[0])
|
|
190
|
+
if not is_extrapolated or self._extrapolate
|
|
191
|
+
else np.nan
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Find nearest defined points
|
|
195
|
+
idx = np.searchsorted(self._mask.frequency, frequency)
|
|
196
|
+
if idx == 0:
|
|
197
|
+
lower_freq = None
|
|
198
|
+
upper_freq = self._mask.frequency[0]
|
|
199
|
+
elif idx >= len(self._mask.frequency):
|
|
200
|
+
lower_freq = self._mask.frequency[-1]
|
|
201
|
+
upper_freq = None
|
|
202
|
+
else:
|
|
203
|
+
lower_freq = self._mask.frequency[idx - 1]
|
|
204
|
+
upper_freq = self._mask.frequency[idx]
|
|
205
|
+
|
|
206
|
+
return limit, {
|
|
207
|
+
"frequency": frequency,
|
|
208
|
+
"method": self._method.value,
|
|
209
|
+
"is_extrapolated": is_extrapolated,
|
|
210
|
+
"is_at_defined_point": frequency in self._mask.frequency,
|
|
211
|
+
"lower_defined_freq": float(lower_freq) if lower_freq is not None else None,
|
|
212
|
+
"upper_defined_freq": float(upper_freq) if upper_freq is not None else None,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def interpolate_limit(
|
|
217
|
+
mask: LimitMask,
|
|
218
|
+
frequency: float | NDArray[np.float64],
|
|
219
|
+
method: str = "log-linear",
|
|
220
|
+
) -> NDArray[np.float64]:
|
|
221
|
+
"""Convenience function for limit interpolation.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
mask: Limit mask
|
|
225
|
+
frequency: Frequency or frequencies in Hz
|
|
226
|
+
method: Interpolation method
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Interpolated limit value(s)
|
|
230
|
+
|
|
231
|
+
Example:
|
|
232
|
+
>>> limit = interpolate_limit(mask, 100e6)
|
|
233
|
+
"""
|
|
234
|
+
interp = LimitInterpolator(
|
|
235
|
+
mask,
|
|
236
|
+
method=InterpolationMethod(method),
|
|
237
|
+
extrapolate=True,
|
|
238
|
+
)
|
|
239
|
+
return interp.interpolate(frequency)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# =============================================================================
|
|
243
|
+
# =============================================================================
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@dataclass
|
|
247
|
+
class ComplianceTestConfig:
|
|
248
|
+
"""Configuration for compliance test.
|
|
249
|
+
|
|
250
|
+
Attributes:
|
|
251
|
+
mask_names: List of mask names to test against
|
|
252
|
+
detector_type: Detector type to use
|
|
253
|
+
frequency_range: Frequency range to test
|
|
254
|
+
margin_required_db: Required margin to limit
|
|
255
|
+
include_quasi_peak: Include QP detection
|
|
256
|
+
generate_report: Generate detailed report
|
|
257
|
+
|
|
258
|
+
References:
|
|
259
|
+
COMP-006: Compliance Test Execution
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
mask_names: list[str] = field(default_factory=lambda: ["FCC_Part15_ClassB"])
|
|
263
|
+
detector_type: str = "peak"
|
|
264
|
+
frequency_range: tuple[float, float] | None = None
|
|
265
|
+
margin_required_db: float = 0.0
|
|
266
|
+
include_quasi_peak: bool = True
|
|
267
|
+
generate_report: bool = True
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@dataclass
|
|
271
|
+
class ComplianceTestResult:
|
|
272
|
+
"""Result of a single compliance test.
|
|
273
|
+
|
|
274
|
+
Attributes:
|
|
275
|
+
mask_name: Mask tested against
|
|
276
|
+
passed: Whether test passed
|
|
277
|
+
margin_db: Margin to limit (negative = fail)
|
|
278
|
+
worst_frequency: Worst-case frequency
|
|
279
|
+
violations: List of violations
|
|
280
|
+
detector_used: Detector type used
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
mask_name: str
|
|
284
|
+
passed: bool
|
|
285
|
+
margin_db: float
|
|
286
|
+
worst_frequency: float
|
|
287
|
+
violations: list[dict[str, Any]]
|
|
288
|
+
detector_used: str
|
|
289
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@dataclass
|
|
293
|
+
class ComplianceTestSuiteResult:
|
|
294
|
+
"""Result of compliance test suite.
|
|
295
|
+
|
|
296
|
+
Attributes:
|
|
297
|
+
overall_passed: True if all tests passed
|
|
298
|
+
results: Individual test results
|
|
299
|
+
summary: Test summary
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
overall_passed: bool
|
|
303
|
+
results: list[ComplianceTestResult]
|
|
304
|
+
summary: dict[str, Any]
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class ComplianceTestRunner:
|
|
308
|
+
"""Compliance test execution engine.
|
|
309
|
+
|
|
310
|
+
Executes compliance tests against multiple masks with
|
|
311
|
+
configurable detection methods.
|
|
312
|
+
|
|
313
|
+
Example:
|
|
314
|
+
>>> runner = ComplianceTestRunner()
|
|
315
|
+
>>> runner.add_mask('FCC_Part15_ClassB')
|
|
316
|
+
>>> runner.add_mask('CE_CISPR32_ClassB')
|
|
317
|
+
>>> result = runner.run(spectrum_freq, spectrum_level)
|
|
318
|
+
|
|
319
|
+
References:
|
|
320
|
+
COMP-006: Compliance Test Execution
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
def __init__(self, config: ComplianceTestConfig | None = None) -> None:
|
|
324
|
+
"""Initialize test runner.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
config: Test configuration
|
|
328
|
+
"""
|
|
329
|
+
self._config = config or ComplianceTestConfig()
|
|
330
|
+
self._masks: list[tuple[str, Any]] = []
|
|
331
|
+
self._qp_detector = QuasiPeakDetector()
|
|
332
|
+
|
|
333
|
+
def add_mask(self, mask_name: str) -> ComplianceTestRunner:
|
|
334
|
+
"""Add mask to test suite.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
mask_name: Mask name to add
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
Self for chaining
|
|
341
|
+
"""
|
|
342
|
+
from oscura.compliance.masks import load_limit_mask
|
|
343
|
+
|
|
344
|
+
mask = load_limit_mask(mask_name)
|
|
345
|
+
self._masks.append((mask_name, mask))
|
|
346
|
+
return self
|
|
347
|
+
|
|
348
|
+
def run(
|
|
349
|
+
self,
|
|
350
|
+
frequencies: NDArray[np.float64],
|
|
351
|
+
levels: NDArray[np.float64],
|
|
352
|
+
unit: str = "dBuV",
|
|
353
|
+
) -> ComplianceTestSuiteResult:
|
|
354
|
+
"""Run compliance test suite.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
frequencies: Frequency array in Hz
|
|
358
|
+
levels: Level array in specified unit
|
|
359
|
+
unit: Unit of level measurements
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
Test suite result
|
|
363
|
+
"""
|
|
364
|
+
results: list[ComplianceTestResult] = []
|
|
365
|
+
|
|
366
|
+
for _mask_name, mask in self._masks:
|
|
367
|
+
result = self._test_against_mask(frequencies, levels, mask, unit)
|
|
368
|
+
results.append(result)
|
|
369
|
+
|
|
370
|
+
overall_passed = all(r.passed for r in results)
|
|
371
|
+
|
|
372
|
+
summary = {
|
|
373
|
+
"total_tests": len(results),
|
|
374
|
+
"passed": sum(1 for r in results if r.passed),
|
|
375
|
+
"failed": sum(1 for r in results if not r.passed),
|
|
376
|
+
"worst_margin_db": min(r.margin_db for r in results) if results else 0,
|
|
377
|
+
"masks_tested": [r.mask_name for r in results],
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return ComplianceTestSuiteResult(
|
|
381
|
+
overall_passed=overall_passed,
|
|
382
|
+
results=results,
|
|
383
|
+
summary=summary,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
def _test_against_mask(
|
|
387
|
+
self,
|
|
388
|
+
frequencies: NDArray[np.float64],
|
|
389
|
+
levels: NDArray[np.float64],
|
|
390
|
+
mask: Any,
|
|
391
|
+
unit: str,
|
|
392
|
+
) -> ComplianceTestResult:
|
|
393
|
+
"""Test against single mask."""
|
|
394
|
+
# Apply frequency range filter
|
|
395
|
+
if self._config.frequency_range:
|
|
396
|
+
f_min, f_max = self._config.frequency_range
|
|
397
|
+
in_range = (frequencies >= f_min) & (frequencies <= f_max)
|
|
398
|
+
frequencies = frequencies[in_range]
|
|
399
|
+
levels = levels[in_range]
|
|
400
|
+
|
|
401
|
+
# Limit to mask range
|
|
402
|
+
mask_f_min, mask_f_max = mask.frequency_range
|
|
403
|
+
in_mask = (frequencies >= mask_f_min) & (frequencies <= mask_f_max)
|
|
404
|
+
frequencies = frequencies[in_mask]
|
|
405
|
+
levels = levels[in_mask]
|
|
406
|
+
|
|
407
|
+
if len(frequencies) == 0:
|
|
408
|
+
return ComplianceTestResult(
|
|
409
|
+
mask_name=mask.name,
|
|
410
|
+
passed=True,
|
|
411
|
+
margin_db=np.inf,
|
|
412
|
+
worst_frequency=0.0,
|
|
413
|
+
violations=[],
|
|
414
|
+
detector_used=self._config.detector_type,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
# Interpolate limits
|
|
418
|
+
interp = LimitInterpolator(mask)
|
|
419
|
+
limits = interp.interpolate(frequencies)
|
|
420
|
+
|
|
421
|
+
# Apply quasi-peak if requested
|
|
422
|
+
if self._config.include_quasi_peak and mask.detector == "quasi-peak":
|
|
423
|
+
levels = self._qp_detector.apply(levels, frequencies)
|
|
424
|
+
|
|
425
|
+
# Calculate margin
|
|
426
|
+
margin = limits - levels
|
|
427
|
+
min_margin = float(np.min(margin))
|
|
428
|
+
worst_idx = int(np.argmin(margin))
|
|
429
|
+
|
|
430
|
+
# Find violations (considering required margin)
|
|
431
|
+
violations = []
|
|
432
|
+
violation_mask = margin < self._config.margin_required_db
|
|
433
|
+
if np.any(violation_mask):
|
|
434
|
+
for idx in np.where(violation_mask)[0]:
|
|
435
|
+
violations.append(
|
|
436
|
+
{
|
|
437
|
+
"frequency": float(frequencies[idx]),
|
|
438
|
+
"measured": float(levels[idx]),
|
|
439
|
+
"limit": float(limits[idx]),
|
|
440
|
+
"excess_db": float(-margin[idx]),
|
|
441
|
+
}
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
passed = len(violations) == 0
|
|
445
|
+
|
|
446
|
+
return ComplianceTestResult(
|
|
447
|
+
mask_name=mask.name,
|
|
448
|
+
passed=passed,
|
|
449
|
+
margin_db=min_margin,
|
|
450
|
+
worst_frequency=float(frequencies[worst_idx]),
|
|
451
|
+
violations=violations,
|
|
452
|
+
detector_used=self._config.detector_type,
|
|
453
|
+
metadata={"unit": unit},
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
class ComplianceTestSuite:
|
|
458
|
+
"""Pre-configured compliance test suites.
|
|
459
|
+
|
|
460
|
+
Provides standard test configurations for common scenarios.
|
|
461
|
+
|
|
462
|
+
References:
|
|
463
|
+
COMP-006: Compliance Test Execution
|
|
464
|
+
"""
|
|
465
|
+
|
|
466
|
+
@staticmethod
|
|
467
|
+
def residential() -> ComplianceTestRunner:
|
|
468
|
+
"""Get residential (Class B) test suite."""
|
|
469
|
+
runner = ComplianceTestRunner(ComplianceTestConfig(include_quasi_peak=True))
|
|
470
|
+
runner.add_mask("FCC_Part15_ClassB")
|
|
471
|
+
runner.add_mask("CE_CISPR32_ClassB")
|
|
472
|
+
return runner
|
|
473
|
+
|
|
474
|
+
@staticmethod
|
|
475
|
+
def commercial() -> ComplianceTestRunner:
|
|
476
|
+
"""Get commercial (Class A) test suite."""
|
|
477
|
+
runner = ComplianceTestRunner(ComplianceTestConfig(include_quasi_peak=True))
|
|
478
|
+
runner.add_mask("FCC_Part15_ClassA")
|
|
479
|
+
runner.add_mask("CE_CISPR32_ClassA")
|
|
480
|
+
return runner
|
|
481
|
+
|
|
482
|
+
@staticmethod
|
|
483
|
+
def military() -> ComplianceTestRunner:
|
|
484
|
+
"""Get military (MIL-STD) test suite."""
|
|
485
|
+
runner = ComplianceTestRunner(ComplianceTestConfig(include_quasi_peak=False))
|
|
486
|
+
runner.add_mask("MIL_STD_461G_RE102")
|
|
487
|
+
runner.add_mask("MIL_STD_461G_CE102")
|
|
488
|
+
return runner
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def run_compliance_suite(
|
|
492
|
+
frequencies: NDArray[np.float64],
|
|
493
|
+
levels: NDArray[np.float64],
|
|
494
|
+
suite: str = "residential",
|
|
495
|
+
) -> ComplianceTestSuiteResult:
|
|
496
|
+
"""Run standard compliance test suite.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
frequencies: Frequency array in Hz
|
|
500
|
+
levels: Level array in dB
|
|
501
|
+
suite: Suite name ('residential', 'commercial', 'military')
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
Test suite result
|
|
505
|
+
|
|
506
|
+
Raises:
|
|
507
|
+
ValueError: If suite name is unknown.
|
|
508
|
+
|
|
509
|
+
Example:
|
|
510
|
+
>>> result = run_compliance_suite(freq, levels, suite='residential')
|
|
511
|
+
>>> print(f"Passed: {result.overall_passed}")
|
|
512
|
+
"""
|
|
513
|
+
if suite == "residential":
|
|
514
|
+
runner = ComplianceTestSuite.residential()
|
|
515
|
+
elif suite == "commercial":
|
|
516
|
+
runner = ComplianceTestSuite.commercial()
|
|
517
|
+
elif suite == "military":
|
|
518
|
+
runner = ComplianceTestSuite.military()
|
|
519
|
+
else:
|
|
520
|
+
raise ValueError(f"Unknown suite: {suite}")
|
|
521
|
+
|
|
522
|
+
return runner.run(frequencies, levels)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
# =============================================================================
|
|
526
|
+
# =============================================================================
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
class QPDetectorBand(Enum):
|
|
530
|
+
"""CISPR 16-1-1 quasi-peak detector bands.
|
|
531
|
+
|
|
532
|
+
References:
|
|
533
|
+
CISPR 16-1-1 Table 1
|
|
534
|
+
COMP-007: Quasi-Peak Detection
|
|
535
|
+
"""
|
|
536
|
+
|
|
537
|
+
BAND_A = "A" # 9 kHz - 150 kHz
|
|
538
|
+
BAND_B = "B" # 150 kHz - 30 MHz
|
|
539
|
+
BAND_C = "C" # 30 MHz - 300 MHz
|
|
540
|
+
BAND_D = "D" # 300 MHz - 1 GHz
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
@dataclass
|
|
544
|
+
class QPDetectorParams:
|
|
545
|
+
"""Quasi-peak detector parameters per CISPR 16-1-1.
|
|
546
|
+
|
|
547
|
+
Attributes:
|
|
548
|
+
bandwidth: Measurement bandwidth in Hz
|
|
549
|
+
charge_time: Charge time constant in ms
|
|
550
|
+
discharge_time: Discharge time constant in ms
|
|
551
|
+
mechanical_time: Meter mechanical time constant in ms
|
|
552
|
+
"""
|
|
553
|
+
|
|
554
|
+
bandwidth: float
|
|
555
|
+
charge_time: float
|
|
556
|
+
discharge_time: float
|
|
557
|
+
mechanical_time: float
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
class QuasiPeakDetector:
|
|
561
|
+
"""CISPR 16-1-1 quasi-peak detector.
|
|
562
|
+
|
|
563
|
+
Implements quasi-peak detection per CISPR 16-1-1 standard for
|
|
564
|
+
EMC compliance measurements.
|
|
565
|
+
|
|
566
|
+
Example:
|
|
567
|
+
>>> detector = QuasiPeakDetector()
|
|
568
|
+
>>> qp_levels = detector.apply(peak_levels, frequencies)
|
|
569
|
+
|
|
570
|
+
References:
|
|
571
|
+
CISPR 16-1-1: Measuring Apparatus
|
|
572
|
+
COMP-007: Quasi-Peak Detection
|
|
573
|
+
"""
|
|
574
|
+
|
|
575
|
+
# CISPR 16-1-1 detector parameters by band
|
|
576
|
+
BAND_PARAMS = { # noqa: RUF012
|
|
577
|
+
QPDetectorBand.BAND_A: QPDetectorParams(
|
|
578
|
+
bandwidth=200, # 200 Hz
|
|
579
|
+
charge_time=45, # ms
|
|
580
|
+
discharge_time=500, # ms
|
|
581
|
+
mechanical_time=160, # ms
|
|
582
|
+
),
|
|
583
|
+
QPDetectorBand.BAND_B: QPDetectorParams(
|
|
584
|
+
bandwidth=9000, # 9 kHz
|
|
585
|
+
charge_time=1, # ms
|
|
586
|
+
discharge_time=160, # ms
|
|
587
|
+
mechanical_time=160, # ms
|
|
588
|
+
),
|
|
589
|
+
QPDetectorBand.BAND_C: QPDetectorParams(
|
|
590
|
+
bandwidth=120000, # 120 kHz
|
|
591
|
+
charge_time=1, # ms
|
|
592
|
+
discharge_time=550, # ms
|
|
593
|
+
mechanical_time=100, # ms
|
|
594
|
+
),
|
|
595
|
+
QPDetectorBand.BAND_D: QPDetectorParams(
|
|
596
|
+
bandwidth=1000000, # 1 MHz
|
|
597
|
+
charge_time=1, # ms
|
|
598
|
+
discharge_time=550, # ms
|
|
599
|
+
mechanical_time=100, # ms
|
|
600
|
+
),
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
# Frequency ranges for bands (Hz)
|
|
604
|
+
BAND_RANGES = { # noqa: RUF012
|
|
605
|
+
QPDetectorBand.BAND_A: (9e3, 150e3),
|
|
606
|
+
QPDetectorBand.BAND_B: (150e3, 30e6),
|
|
607
|
+
QPDetectorBand.BAND_C: (30e6, 300e6),
|
|
608
|
+
QPDetectorBand.BAND_D: (300e6, 1e9),
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
def __init__(self) -> None:
|
|
612
|
+
"""Initialize quasi-peak detector."""
|
|
613
|
+
self._lookup_table: dict[str, NDArray[np.float64]] = {}
|
|
614
|
+
|
|
615
|
+
def get_band(self, frequency: float) -> QPDetectorBand | None:
|
|
616
|
+
"""Get CISPR band for frequency.
|
|
617
|
+
|
|
618
|
+
Args:
|
|
619
|
+
frequency: Frequency in Hz
|
|
620
|
+
|
|
621
|
+
Returns:
|
|
622
|
+
Band or None if outside all bands
|
|
623
|
+
"""
|
|
624
|
+
for band, (f_min, f_max) in self.BAND_RANGES.items():
|
|
625
|
+
if f_min <= frequency <= f_max:
|
|
626
|
+
return band
|
|
627
|
+
return None
|
|
628
|
+
|
|
629
|
+
def get_params(self, frequency: float) -> QPDetectorParams | None:
|
|
630
|
+
"""Get detector parameters for frequency.
|
|
631
|
+
|
|
632
|
+
Args:
|
|
633
|
+
frequency: Frequency in Hz
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
Detector parameters or None
|
|
637
|
+
"""
|
|
638
|
+
band = self.get_band(frequency)
|
|
639
|
+
if band is None:
|
|
640
|
+
return None
|
|
641
|
+
return self.BAND_PARAMS[band]
|
|
642
|
+
|
|
643
|
+
def apply(
|
|
644
|
+
self,
|
|
645
|
+
peak_levels: NDArray[np.float64],
|
|
646
|
+
frequencies: NDArray[np.float64],
|
|
647
|
+
) -> NDArray[np.float64]:
|
|
648
|
+
"""Apply quasi-peak detection to peak levels.
|
|
649
|
+
|
|
650
|
+
Args:
|
|
651
|
+
peak_levels: Peak detector levels in dB
|
|
652
|
+
frequencies: Corresponding frequencies in Hz
|
|
653
|
+
|
|
654
|
+
Returns:
|
|
655
|
+
Quasi-peak levels in dB
|
|
656
|
+
|
|
657
|
+
Note:
|
|
658
|
+
Quasi-peak is always <= peak for repetitive signals.
|
|
659
|
+
The correction factor depends on pulse repetition rate.
|
|
660
|
+
"""
|
|
661
|
+
qp_levels = np.copy(peak_levels)
|
|
662
|
+
|
|
663
|
+
for i, (level, freq) in enumerate(zip(peak_levels, frequencies, strict=False)):
|
|
664
|
+
band = self.get_band(freq)
|
|
665
|
+
if band is not None:
|
|
666
|
+
# Apply approximate QP correction
|
|
667
|
+
# Real implementation would need actual signal for time-domain processing
|
|
668
|
+
correction = self._get_qp_correction(band)
|
|
669
|
+
qp_levels[i] = level - correction
|
|
670
|
+
|
|
671
|
+
return qp_levels
|
|
672
|
+
|
|
673
|
+
def _get_qp_correction(self, band: QPDetectorBand) -> float:
|
|
674
|
+
"""Get approximate QP correction factor.
|
|
675
|
+
|
|
676
|
+
This is a simplified model. Real QP detection requires
|
|
677
|
+
time-domain processing of the actual signal.
|
|
678
|
+
|
|
679
|
+
Args:
|
|
680
|
+
band: CISPR band
|
|
681
|
+
|
|
682
|
+
Returns:
|
|
683
|
+
Correction factor in dB
|
|
684
|
+
"""
|
|
685
|
+
# Approximate corrections for periodic signals
|
|
686
|
+
# Actual correction depends on pulse rate and duty cycle
|
|
687
|
+
corrections = {
|
|
688
|
+
QPDetectorBand.BAND_A: 3.0,
|
|
689
|
+
QPDetectorBand.BAND_B: 6.0,
|
|
690
|
+
QPDetectorBand.BAND_C: 4.0,
|
|
691
|
+
QPDetectorBand.BAND_D: 4.0,
|
|
692
|
+
}
|
|
693
|
+
return corrections.get(band, 0.0)
|
|
694
|
+
|
|
695
|
+
def compare_peak_qp(
|
|
696
|
+
self,
|
|
697
|
+
peak_levels: NDArray[np.float64],
|
|
698
|
+
frequencies: NDArray[np.float64],
|
|
699
|
+
) -> dict[str, Any]:
|
|
700
|
+
"""Compare peak and quasi-peak readings.
|
|
701
|
+
|
|
702
|
+
Args:
|
|
703
|
+
peak_levels: Peak detector levels
|
|
704
|
+
frequencies: Frequencies
|
|
705
|
+
|
|
706
|
+
Returns:
|
|
707
|
+
Comparison results
|
|
708
|
+
"""
|
|
709
|
+
qp_levels = self.apply(peak_levels, frequencies)
|
|
710
|
+
difference = peak_levels - qp_levels
|
|
711
|
+
|
|
712
|
+
return {
|
|
713
|
+
"peak_levels": peak_levels,
|
|
714
|
+
"qp_levels": qp_levels,
|
|
715
|
+
"difference_db": difference,
|
|
716
|
+
"max_difference_db": float(np.max(difference)),
|
|
717
|
+
"avg_difference_db": float(np.mean(difference)),
|
|
718
|
+
"description": (
|
|
719
|
+
"Quasi-peak is lower than peak for pulsed/repetitive signals. "
|
|
720
|
+
"For CW signals, QP equals peak."
|
|
721
|
+
),
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
def get_bandwidth(self, frequency: float) -> float:
|
|
725
|
+
"""Get measurement bandwidth for frequency.
|
|
726
|
+
|
|
727
|
+
Args:
|
|
728
|
+
frequency: Frequency in Hz
|
|
729
|
+
|
|
730
|
+
Returns:
|
|
731
|
+
Bandwidth in Hz
|
|
732
|
+
"""
|
|
733
|
+
params = self.get_params(frequency)
|
|
734
|
+
if params is None:
|
|
735
|
+
# Default to Band B
|
|
736
|
+
return 9000
|
|
737
|
+
return params.bandwidth
|
|
738
|
+
|
|
739
|
+
def validate_bandwidth(self, bandwidth: float) -> None:
|
|
740
|
+
"""Validate measurement bandwidth.
|
|
741
|
+
|
|
742
|
+
Args:
|
|
743
|
+
bandwidth: Bandwidth to validate
|
|
744
|
+
|
|
745
|
+
Raises:
|
|
746
|
+
ValueError: If bandwidth is invalid
|
|
747
|
+
"""
|
|
748
|
+
if bandwidth <= 0:
|
|
749
|
+
raise ValueError("Bandwidth must be positive")
|
|
750
|
+
|
|
751
|
+
valid_bandwidths = [p.bandwidth for p in self.BAND_PARAMS.values()]
|
|
752
|
+
if bandwidth not in valid_bandwidths:
|
|
753
|
+
logger.warning(
|
|
754
|
+
f"Non-standard bandwidth {bandwidth} Hz. "
|
|
755
|
+
f"Standard CISPR bandwidths: {valid_bandwidths}"
|
|
756
|
+
)
|