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,297 @@
|
|
|
1
|
+
"""Power efficiency calculations for TraceKit.
|
|
2
|
+
|
|
3
|
+
Provides efficiency calculations for power converters and systems.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from oscura.analyzers.power.efficiency import efficiency
|
|
8
|
+
>>> eta = efficiency(v_in, i_in, v_out, i_out)
|
|
9
|
+
>>> print(f"Efficiency: {eta*100:.1f}%")
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
from oscura.analyzers.power.basic import average_power, instantaneous_power
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from numpy.typing import NDArray
|
|
22
|
+
|
|
23
|
+
from oscura.core.types import WaveformTrace
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def efficiency(
|
|
27
|
+
v_in: WaveformTrace,
|
|
28
|
+
i_in: WaveformTrace,
|
|
29
|
+
v_out: WaveformTrace,
|
|
30
|
+
i_out: WaveformTrace,
|
|
31
|
+
) -> float:
|
|
32
|
+
"""Calculate power conversion efficiency.
|
|
33
|
+
|
|
34
|
+
eta = P_out / P_in * 100%
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
v_in: Input voltage trace.
|
|
38
|
+
i_in: Input current trace.
|
|
39
|
+
v_out: Output voltage trace.
|
|
40
|
+
i_out: Output current trace.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Efficiency as a ratio (0 to 1).
|
|
44
|
+
|
|
45
|
+
Example:
|
|
46
|
+
>>> eta = efficiency(v_in, i_in, v_out, i_out)
|
|
47
|
+
>>> print(f"Efficiency: {eta*100:.1f}%")
|
|
48
|
+
"""
|
|
49
|
+
p_in = average_power(voltage=v_in, current=i_in)
|
|
50
|
+
p_out = average_power(voltage=v_out, current=i_out)
|
|
51
|
+
|
|
52
|
+
if p_in <= 0:
|
|
53
|
+
return 0.0
|
|
54
|
+
|
|
55
|
+
return p_out / p_in
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def power_conversion_efficiency(
|
|
59
|
+
p_in: float,
|
|
60
|
+
p_out: float,
|
|
61
|
+
) -> float:
|
|
62
|
+
"""Calculate efficiency from power values.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
p_in: Input power in Watts.
|
|
66
|
+
p_out: Output power in Watts.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Efficiency as a ratio (0 to 1).
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
>>> eta = power_conversion_efficiency(p_in=100, p_out=90)
|
|
73
|
+
>>> print(f"Efficiency: {eta*100:.1f}%")
|
|
74
|
+
"""
|
|
75
|
+
if p_in <= 0:
|
|
76
|
+
return 0.0
|
|
77
|
+
return p_out / p_in
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def multi_output_efficiency(
|
|
81
|
+
v_in: WaveformTrace,
|
|
82
|
+
i_in: WaveformTrace,
|
|
83
|
+
outputs: list[tuple[WaveformTrace, WaveformTrace]],
|
|
84
|
+
) -> dict[str, float]:
|
|
85
|
+
"""Calculate efficiency for multi-output power supply.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
v_in: Input voltage trace.
|
|
89
|
+
i_in: Input current trace.
|
|
90
|
+
outputs: List of (v_out, i_out) trace tuples for each output.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Dictionary with:
|
|
94
|
+
- total_efficiency: Overall efficiency
|
|
95
|
+
- output_N_efficiency: Per-output efficiency (contribution)
|
|
96
|
+
- output_N_power: Per-output power
|
|
97
|
+
- total_output_power: Sum of all output powers
|
|
98
|
+
- input_power: Input power
|
|
99
|
+
- losses: Power losses (P_in - P_out_total)
|
|
100
|
+
|
|
101
|
+
Example:
|
|
102
|
+
>>> outputs = [(v1, i1), (v2, i2), (v3, i3)]
|
|
103
|
+
>>> result = multi_output_efficiency(v_in, i_in, outputs)
|
|
104
|
+
>>> print(f"Total efficiency: {result['total_efficiency']*100:.1f}%")
|
|
105
|
+
"""
|
|
106
|
+
p_in = average_power(voltage=v_in, current=i_in)
|
|
107
|
+
|
|
108
|
+
result = {
|
|
109
|
+
"input_power": p_in,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
total_output = 0.0
|
|
113
|
+
for idx, (v_out, i_out) in enumerate(outputs):
|
|
114
|
+
p_out = average_power(voltage=v_out, current=i_out)
|
|
115
|
+
result[f"output_{idx + 1}_power"] = p_out
|
|
116
|
+
result[f"output_{idx + 1}_efficiency"] = p_out / p_in if p_in > 0 else 0.0
|
|
117
|
+
total_output += p_out
|
|
118
|
+
|
|
119
|
+
result["total_output_power"] = total_output
|
|
120
|
+
result["total_efficiency"] = total_output / p_in if p_in > 0 else 0.0
|
|
121
|
+
result["losses"] = p_in - total_output
|
|
122
|
+
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def efficiency_vs_load(
|
|
127
|
+
v_in: WaveformTrace,
|
|
128
|
+
i_in: WaveformTrace,
|
|
129
|
+
v_out: WaveformTrace,
|
|
130
|
+
i_out: WaveformTrace,
|
|
131
|
+
*,
|
|
132
|
+
n_points: int = 100,
|
|
133
|
+
) -> dict[str, NDArray[np.float64]]:
|
|
134
|
+
"""Calculate efficiency across the load range.
|
|
135
|
+
|
|
136
|
+
Segments the waveforms and calculates efficiency at each load level.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
v_in: Input voltage trace.
|
|
140
|
+
i_in: Input current trace.
|
|
141
|
+
v_out: Output voltage trace.
|
|
142
|
+
i_out: Output current trace.
|
|
143
|
+
n_points: Number of load points to evaluate.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Dictionary with:
|
|
147
|
+
- load_percent: Load levels as percentage of max
|
|
148
|
+
- efficiency: Efficiency at each load level
|
|
149
|
+
- output_power: Output power at each load level
|
|
150
|
+
- input_power: Input power at each load level
|
|
151
|
+
|
|
152
|
+
Example:
|
|
153
|
+
>>> result = efficiency_vs_load(v_in, i_in, v_out, i_out)
|
|
154
|
+
>>> plt.plot(result['load_percent'], result['efficiency'] * 100)
|
|
155
|
+
"""
|
|
156
|
+
# Calculate instantaneous power
|
|
157
|
+
p_out_trace = instantaneous_power(v_out, i_out)
|
|
158
|
+
p_in_trace = instantaneous_power(v_in, i_in)
|
|
159
|
+
|
|
160
|
+
p_out_data = p_out_trace.data
|
|
161
|
+
p_in_data = p_in_trace.data[: len(p_out_data)]
|
|
162
|
+
|
|
163
|
+
# Sort by output power to get load curve
|
|
164
|
+
sort_idx = np.argsort(p_out_data)
|
|
165
|
+
p_out_sorted = p_out_data[sort_idx]
|
|
166
|
+
p_in_sorted = p_in_data[sort_idx]
|
|
167
|
+
|
|
168
|
+
# Divide into bins
|
|
169
|
+
bin_size = len(p_out_sorted) // n_points
|
|
170
|
+
bin_size = max(bin_size, 1)
|
|
171
|
+
|
|
172
|
+
load_pct = []
|
|
173
|
+
efficiency_vals = []
|
|
174
|
+
p_out_vals = []
|
|
175
|
+
p_in_vals = []
|
|
176
|
+
|
|
177
|
+
max_p_out = np.max(p_out_data)
|
|
178
|
+
|
|
179
|
+
for i in range(0, len(p_out_sorted), bin_size):
|
|
180
|
+
bin_p_out = np.mean(p_out_sorted[i : i + bin_size])
|
|
181
|
+
bin_p_in = np.mean(p_in_sorted[i : i + bin_size])
|
|
182
|
+
|
|
183
|
+
load_pct.append(bin_p_out / max_p_out * 100 if max_p_out > 0 else 0)
|
|
184
|
+
p_out_vals.append(bin_p_out)
|
|
185
|
+
p_in_vals.append(bin_p_in)
|
|
186
|
+
efficiency_vals.append(bin_p_out / bin_p_in if bin_p_in > 0 else 0)
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
"load_percent": np.array(load_pct),
|
|
190
|
+
"efficiency": np.array(efficiency_vals),
|
|
191
|
+
"output_power": np.array(p_out_vals),
|
|
192
|
+
"input_power": np.array(p_in_vals),
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def loss_breakdown(
|
|
197
|
+
v_in: WaveformTrace,
|
|
198
|
+
i_in: WaveformTrace,
|
|
199
|
+
v_out: WaveformTrace,
|
|
200
|
+
i_out: WaveformTrace,
|
|
201
|
+
*,
|
|
202
|
+
switching_loss: float = 0.0,
|
|
203
|
+
conduction_loss: float = 0.0,
|
|
204
|
+
magnetic_loss: float = 0.0,
|
|
205
|
+
gate_drive_loss: float = 0.0,
|
|
206
|
+
) -> dict[str, float]:
|
|
207
|
+
"""Break down power losses by category.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
v_in: Input voltage trace.
|
|
211
|
+
i_in: Input current trace.
|
|
212
|
+
v_out: Output voltage trace.
|
|
213
|
+
i_out: Output current trace.
|
|
214
|
+
switching_loss: Known switching losses in Watts.
|
|
215
|
+
conduction_loss: Known conduction losses in Watts.
|
|
216
|
+
magnetic_loss: Known magnetic (core/copper) losses in Watts.
|
|
217
|
+
gate_drive_loss: Known gate drive losses in Watts.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Dictionary with loss breakdown.
|
|
221
|
+
|
|
222
|
+
Example:
|
|
223
|
+
>>> result = loss_breakdown(v_in, i_in, v_out, i_out,
|
|
224
|
+
... switching_loss=2.0, conduction_loss=1.5, magnetic_loss=0.5)
|
|
225
|
+
>>> print(f"Other losses: {result['other_loss']:.2f} W")
|
|
226
|
+
"""
|
|
227
|
+
p_in = average_power(voltage=v_in, current=i_in)
|
|
228
|
+
p_out = average_power(voltage=v_out, current=i_out)
|
|
229
|
+
total_loss = p_in - p_out
|
|
230
|
+
|
|
231
|
+
known_losses = switching_loss + conduction_loss + magnetic_loss + gate_drive_loss
|
|
232
|
+
other_loss = total_loss - known_losses
|
|
233
|
+
|
|
234
|
+
eta = p_out / p_in if p_in > 0 else 0.0
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
"input_power": p_in,
|
|
238
|
+
"output_power": p_out,
|
|
239
|
+
"efficiency": eta,
|
|
240
|
+
"total_loss": total_loss,
|
|
241
|
+
"switching_loss": switching_loss,
|
|
242
|
+
"conduction_loss": conduction_loss,
|
|
243
|
+
"magnetic_loss": magnetic_loss,
|
|
244
|
+
"gate_drive_loss": gate_drive_loss,
|
|
245
|
+
"other_loss": max(0, other_loss), # Clamp to non-negative
|
|
246
|
+
"switching_loss_percent": switching_loss / total_loss * 100 if total_loss > 0 else 0,
|
|
247
|
+
"conduction_loss_percent": conduction_loss / total_loss * 100 if total_loss > 0 else 0,
|
|
248
|
+
"magnetic_loss_percent": magnetic_loss / total_loss * 100 if total_loss > 0 else 0,
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def thermal_efficiency(
|
|
253
|
+
p_in: float,
|
|
254
|
+
p_out: float,
|
|
255
|
+
ambient_temp: float,
|
|
256
|
+
case_temp: float,
|
|
257
|
+
thermal_resistance: float,
|
|
258
|
+
) -> dict[str, float]:
|
|
259
|
+
"""Calculate thermal-related efficiency metrics.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
p_in: Input power in Watts.
|
|
263
|
+
p_out: Output power in Watts.
|
|
264
|
+
ambient_temp: Ambient temperature in Celsius.
|
|
265
|
+
case_temp: Case/junction temperature in Celsius.
|
|
266
|
+
thermal_resistance: Thermal resistance (Rth_j-a) in C/W.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Dictionary with thermal analysis.
|
|
270
|
+
|
|
271
|
+
Example:
|
|
272
|
+
>>> result = thermal_efficiency(100, 90, 25, 65, 2.5)
|
|
273
|
+
>>> print(f"Estimated losses: {result['estimated_losses']:.1f} W")
|
|
274
|
+
"""
|
|
275
|
+
losses = p_in - p_out
|
|
276
|
+
eta = p_out / p_in if p_in > 0 else 0.0
|
|
277
|
+
|
|
278
|
+
# Estimate losses from thermal measurement
|
|
279
|
+
thermal_losses = (case_temp - ambient_temp) / thermal_resistance
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
"efficiency": eta,
|
|
283
|
+
"electrical_losses": losses,
|
|
284
|
+
"thermal_estimated_losses": thermal_losses,
|
|
285
|
+
"temperature_rise": case_temp - ambient_temp,
|
|
286
|
+
"loss_discrepancy": abs(losses - thermal_losses),
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
__all__ = [
|
|
291
|
+
"efficiency",
|
|
292
|
+
"efficiency_vs_load",
|
|
293
|
+
"loss_breakdown",
|
|
294
|
+
"multi_output_efficiency",
|
|
295
|
+
"power_conversion_efficiency",
|
|
296
|
+
"thermal_efficiency",
|
|
297
|
+
]
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""Ripple measurement for TraceKit.
|
|
2
|
+
|
|
3
|
+
Provides AC ripple analysis for DC power supply outputs.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from oscura.analyzers.power.ripple import ripple, ripple_statistics
|
|
8
|
+
>>> r_pp, r_rms = ripple(dc_output_trace)
|
|
9
|
+
>>> print(f"Ripple: {r_pp*1e3:.2f} mV pp, {r_rms*1e3:.2f} mV rms")
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
from scipy import signal
|
|
16
|
+
|
|
17
|
+
from oscura.core.exceptions import AnalysisError
|
|
18
|
+
from oscura.core.types import WaveformTrace
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def ripple(
|
|
22
|
+
trace: WaveformTrace,
|
|
23
|
+
*,
|
|
24
|
+
dc_coupling: bool = False,
|
|
25
|
+
) -> tuple[float, float]:
|
|
26
|
+
"""Measure AC ripple on a DC signal.
|
|
27
|
+
|
|
28
|
+
Isolates the AC component from the DC offset and measures
|
|
29
|
+
peak-to-peak and RMS ripple.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
trace: DC voltage/current waveform with AC ripple.
|
|
33
|
+
dc_coupling: If True, include DC component in measurement.
|
|
34
|
+
If False (default), remove DC for pure AC ripple.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Tuple of (ripple_pp, ripple_rms) in signal units.
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
>>> vpp, vrms = ripple(output_voltage)
|
|
41
|
+
>>> print(f"Ripple: {vpp*1e3:.2f} mV pp, {vrms*1e3:.2f} mV rms")
|
|
42
|
+
|
|
43
|
+
References:
|
|
44
|
+
IEC 61000-4-7 (power quality)
|
|
45
|
+
"""
|
|
46
|
+
data = trace.data
|
|
47
|
+
|
|
48
|
+
if dc_coupling:
|
|
49
|
+
ac_component = data
|
|
50
|
+
else:
|
|
51
|
+
# Remove DC (mean)
|
|
52
|
+
ac_component = data - np.mean(data)
|
|
53
|
+
|
|
54
|
+
ripple_pp = float(np.max(ac_component) - np.min(ac_component))
|
|
55
|
+
ripple_rms = float(np.sqrt(np.mean(ac_component**2)))
|
|
56
|
+
|
|
57
|
+
return ripple_pp, ripple_rms
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def ripple_percentage(
|
|
61
|
+
trace: WaveformTrace,
|
|
62
|
+
) -> tuple[float, float]:
|
|
63
|
+
"""Measure ripple as percentage of DC level.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
trace: DC voltage/current waveform with AC ripple.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Tuple of (ripple_pp_percent, ripple_rms_percent).
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
>>> pp_pct, rms_pct = ripple_percentage(output_voltage)
|
|
73
|
+
>>> print(f"Ripple: {pp_pct:.2f}% pp, {rms_pct:.2f}% rms")
|
|
74
|
+
"""
|
|
75
|
+
dc_level = float(np.mean(trace.data))
|
|
76
|
+
|
|
77
|
+
if dc_level == 0:
|
|
78
|
+
return np.nan, np.nan
|
|
79
|
+
|
|
80
|
+
r_pp, r_rms = ripple(trace)
|
|
81
|
+
|
|
82
|
+
return (r_pp / dc_level * 100, r_rms / dc_level * 100)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def ripple_frequency(
|
|
86
|
+
trace: WaveformTrace,
|
|
87
|
+
*,
|
|
88
|
+
min_frequency: float | None = None,
|
|
89
|
+
max_frequency: float | None = None,
|
|
90
|
+
) -> float:
|
|
91
|
+
"""Find dominant ripple frequency.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
trace: DC voltage waveform with AC ripple.
|
|
95
|
+
min_frequency: Minimum frequency to consider (Hz).
|
|
96
|
+
max_frequency: Maximum frequency to consider (Hz).
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Dominant ripple frequency in Hz.
|
|
100
|
+
|
|
101
|
+
Example:
|
|
102
|
+
>>> f_ripple = ripple_frequency(output_voltage)
|
|
103
|
+
>>> print(f"Ripple frequency: {f_ripple/1e3:.2f} kHz")
|
|
104
|
+
"""
|
|
105
|
+
data = trace.data
|
|
106
|
+
sample_rate = trace.metadata.sample_rate
|
|
107
|
+
|
|
108
|
+
# Remove DC
|
|
109
|
+
ac_data = data - np.mean(data)
|
|
110
|
+
|
|
111
|
+
# FFT
|
|
112
|
+
n = len(ac_data)
|
|
113
|
+
fft_result = np.abs(np.fft.rfft(ac_data))
|
|
114
|
+
freqs = np.fft.rfftfreq(n, 1 / sample_rate)
|
|
115
|
+
|
|
116
|
+
# Apply frequency limits
|
|
117
|
+
freq_mask = np.ones(len(freqs), dtype=bool)
|
|
118
|
+
if min_frequency is not None:
|
|
119
|
+
freq_mask &= freqs >= min_frequency
|
|
120
|
+
if max_frequency is not None:
|
|
121
|
+
freq_mask &= freqs <= max_frequency
|
|
122
|
+
|
|
123
|
+
# Exclude DC
|
|
124
|
+
freq_mask[0] = False
|
|
125
|
+
|
|
126
|
+
if not np.any(freq_mask):
|
|
127
|
+
return 0.0
|
|
128
|
+
|
|
129
|
+
# Find peak
|
|
130
|
+
masked_fft = fft_result.copy()
|
|
131
|
+
masked_fft[~freq_mask] = 0
|
|
132
|
+
peak_idx = np.argmax(masked_fft)
|
|
133
|
+
|
|
134
|
+
return float(freqs[peak_idx])
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def ripple_harmonics(
|
|
138
|
+
trace: WaveformTrace,
|
|
139
|
+
fundamental_freq: float | None = None,
|
|
140
|
+
n_harmonics: int = 10,
|
|
141
|
+
) -> dict[int, float]:
|
|
142
|
+
"""Analyze ripple harmonics.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
trace: DC voltage waveform with AC ripple.
|
|
146
|
+
fundamental_freq: Fundamental ripple frequency. If None, auto-detect.
|
|
147
|
+
n_harmonics: Number of harmonics to analyze.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Dictionary mapping harmonic number to amplitude.
|
|
151
|
+
|
|
152
|
+
Example:
|
|
153
|
+
>>> harmonics = ripple_harmonics(output_voltage)
|
|
154
|
+
>>> for h, amp in harmonics.items():
|
|
155
|
+
... print(f"H{h}: {amp*1e3:.2f} mV")
|
|
156
|
+
"""
|
|
157
|
+
data = trace.data
|
|
158
|
+
sample_rate = trace.metadata.sample_rate
|
|
159
|
+
|
|
160
|
+
# Remove DC
|
|
161
|
+
ac_data = data - np.mean(data)
|
|
162
|
+
|
|
163
|
+
# Find fundamental if not provided
|
|
164
|
+
if fundamental_freq is None:
|
|
165
|
+
fundamental_freq = ripple_frequency(trace)
|
|
166
|
+
|
|
167
|
+
if fundamental_freq <= 0:
|
|
168
|
+
return {}
|
|
169
|
+
|
|
170
|
+
# FFT
|
|
171
|
+
n = len(ac_data)
|
|
172
|
+
fft_result = np.abs(np.fft.rfft(ac_data)) * 2 / n # Scale for amplitude
|
|
173
|
+
freqs = np.fft.rfftfreq(n, 1 / sample_rate)
|
|
174
|
+
|
|
175
|
+
harmonics = {}
|
|
176
|
+
for h in range(1, n_harmonics + 1):
|
|
177
|
+
target_freq = h * fundamental_freq
|
|
178
|
+
# Find closest bin
|
|
179
|
+
idx = np.argmin(np.abs(freqs - target_freq))
|
|
180
|
+
if idx < len(fft_result):
|
|
181
|
+
harmonics[h] = float(fft_result[idx])
|
|
182
|
+
|
|
183
|
+
return harmonics
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def ripple_statistics(
|
|
187
|
+
trace: WaveformTrace,
|
|
188
|
+
) -> dict[str, float]:
|
|
189
|
+
"""Calculate comprehensive ripple statistics.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
trace: DC voltage waveform with AC ripple.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Dictionary with:
|
|
196
|
+
- dc_level: DC (mean) level
|
|
197
|
+
- ripple_pp: Peak-to-peak ripple
|
|
198
|
+
- ripple_rms: RMS ripple
|
|
199
|
+
- ripple_pp_percent: Peak-to-peak as % of DC
|
|
200
|
+
- ripple_rms_percent: RMS as % of DC
|
|
201
|
+
- ripple_frequency: Dominant ripple frequency
|
|
202
|
+
- crest_factor: Ripple peak / ripple RMS
|
|
203
|
+
|
|
204
|
+
Example:
|
|
205
|
+
>>> stats = ripple_statistics(output_voltage)
|
|
206
|
+
>>> print(f"DC: {stats['dc_level']:.2f} V")
|
|
207
|
+
>>> print(f"Ripple: {stats['ripple_pp']*1e3:.2f} mV pp")
|
|
208
|
+
"""
|
|
209
|
+
data = trace.data
|
|
210
|
+
dc_level = float(np.mean(data))
|
|
211
|
+
ac_data = data - dc_level
|
|
212
|
+
|
|
213
|
+
r_pp = float(np.max(ac_data) - np.min(ac_data))
|
|
214
|
+
r_rms = float(np.sqrt(np.mean(ac_data**2)))
|
|
215
|
+
r_peak = float(np.max(np.abs(ac_data)))
|
|
216
|
+
|
|
217
|
+
crest_factor = r_peak / r_rms if r_rms > 0 else 0.0
|
|
218
|
+
|
|
219
|
+
if dc_level != 0:
|
|
220
|
+
r_pp_pct = r_pp / dc_level * 100
|
|
221
|
+
r_rms_pct = r_rms / dc_level * 100
|
|
222
|
+
else:
|
|
223
|
+
r_pp_pct = np.nan
|
|
224
|
+
r_rms_pct = np.nan
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
"dc_level": dc_level,
|
|
228
|
+
"ripple_pp": r_pp,
|
|
229
|
+
"ripple_rms": r_rms,
|
|
230
|
+
"ripple_pp_percent": r_pp_pct,
|
|
231
|
+
"ripple_rms_percent": r_rms_pct,
|
|
232
|
+
"ripple_frequency": ripple_frequency(trace),
|
|
233
|
+
"crest_factor": crest_factor,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def extract_ripple(
|
|
238
|
+
trace: WaveformTrace,
|
|
239
|
+
*,
|
|
240
|
+
high_pass_freq: float | None = None,
|
|
241
|
+
filter_order: int = 4,
|
|
242
|
+
) -> WaveformTrace:
|
|
243
|
+
"""Extract AC ripple component from DC signal.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
trace: DC voltage waveform with AC ripple.
|
|
247
|
+
high_pass_freq: High-pass filter cutoff. If None, uses simple DC removal.
|
|
248
|
+
filter_order: Order of the Butterworth high-pass filter (default: 4).
|
|
249
|
+
Higher orders give sharper cutoff but more phase distortion.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Waveform trace containing only the AC ripple component.
|
|
253
|
+
|
|
254
|
+
Raises:
|
|
255
|
+
AnalysisError: If high_pass_freq exceeds Nyquist frequency
|
|
256
|
+
|
|
257
|
+
Example:
|
|
258
|
+
>>> ac_ripple = extract_ripple(output_voltage)
|
|
259
|
+
>>> # Now analyze or plot just the ripple
|
|
260
|
+
>>> # With custom filter order for sharper cutoff:
|
|
261
|
+
>>> ac_ripple = extract_ripple(output_voltage, high_pass_freq=10, filter_order=6)
|
|
262
|
+
"""
|
|
263
|
+
data = trace.data
|
|
264
|
+
sample_rate = trace.metadata.sample_rate
|
|
265
|
+
|
|
266
|
+
if high_pass_freq is not None:
|
|
267
|
+
# Use high-pass filter
|
|
268
|
+
nyquist = sample_rate / 2
|
|
269
|
+
if high_pass_freq >= nyquist:
|
|
270
|
+
raise AnalysisError(
|
|
271
|
+
f"High-pass frequency {high_pass_freq} Hz must be less than "
|
|
272
|
+
f"Nyquist frequency {nyquist} Hz"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
sos = signal.butter(filter_order, high_pass_freq / nyquist, btype="high", output="sos")
|
|
276
|
+
ac_data = signal.sosfiltfilt(sos, data)
|
|
277
|
+
else:
|
|
278
|
+
# Simple DC removal
|
|
279
|
+
ac_data = data - np.mean(data)
|
|
280
|
+
|
|
281
|
+
return WaveformTrace(
|
|
282
|
+
data=ac_data.astype(np.float64),
|
|
283
|
+
metadata=trace.metadata,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def ripple_envelope(
|
|
288
|
+
trace: WaveformTrace,
|
|
289
|
+
*,
|
|
290
|
+
method: str = "hilbert",
|
|
291
|
+
peak_window_size: int | None = None,
|
|
292
|
+
) -> WaveformTrace:
|
|
293
|
+
"""Extract ripple envelope (for amplitude modulation analysis).
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
trace: DC voltage waveform with AC ripple.
|
|
297
|
+
method: Envelope detection method ("hilbert" or "peak").
|
|
298
|
+
peak_window_size: Window size for peak envelope detection (samples).
|
|
299
|
+
If None, defaults to a size that covers approximately one ripple period.
|
|
300
|
+
Only used when method="peak".
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Waveform trace containing the ripple envelope.
|
|
304
|
+
|
|
305
|
+
Raises:
|
|
306
|
+
AnalysisError: If unknown envelope method specified
|
|
307
|
+
|
|
308
|
+
Example:
|
|
309
|
+
>>> envelope = ripple_envelope(output_voltage)
|
|
310
|
+
>>> # Analyze envelope for beat frequencies, etc.
|
|
311
|
+
>>> # With custom peak window size:
|
|
312
|
+
>>> envelope = ripple_envelope(output_voltage, method="peak", peak_window_size=200)
|
|
313
|
+
"""
|
|
314
|
+
# First extract AC component
|
|
315
|
+
ac_trace = extract_ripple(trace)
|
|
316
|
+
ac_data = ac_trace.data
|
|
317
|
+
|
|
318
|
+
if method == "hilbert":
|
|
319
|
+
analytic_signal = signal.hilbert(ac_data)
|
|
320
|
+
envelope = np.abs(analytic_signal)
|
|
321
|
+
elif method == "peak":
|
|
322
|
+
# Simple peak detection
|
|
323
|
+
from scipy.ndimage import maximum_filter1d
|
|
324
|
+
|
|
325
|
+
# Determine window size
|
|
326
|
+
if peak_window_size is None:
|
|
327
|
+
# Default: scale to signal frequency if possible
|
|
328
|
+
# Try to detect ripple frequency and use ~1 period
|
|
329
|
+
ripple_freq = ripple_frequency(trace)
|
|
330
|
+
sample_rate = trace.metadata.sample_rate
|
|
331
|
+
if ripple_freq > 0:
|
|
332
|
+
# Use approximately one period of the ripple
|
|
333
|
+
peak_window_size = max(10, int(sample_rate / ripple_freq))
|
|
334
|
+
else:
|
|
335
|
+
# Fallback: use 1% of signal length, min 10, max 1000
|
|
336
|
+
peak_window_size = max(10, min(1000, len(ac_data) // 100))
|
|
337
|
+
|
|
338
|
+
envelope = maximum_filter1d(np.abs(ac_data), size=peak_window_size)
|
|
339
|
+
else:
|
|
340
|
+
raise AnalysisError(f"Unknown envelope method: {method}")
|
|
341
|
+
|
|
342
|
+
return WaveformTrace(
|
|
343
|
+
data=envelope.astype(np.float64),
|
|
344
|
+
metadata=trace.metadata,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
__all__ = [
|
|
349
|
+
"extract_ripple",
|
|
350
|
+
"ripple",
|
|
351
|
+
"ripple_envelope",
|
|
352
|
+
"ripple_frequency",
|
|
353
|
+
"ripple_harmonics",
|
|
354
|
+
"ripple_percentage",
|
|
355
|
+
"ripple_statistics",
|
|
356
|
+
]
|