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,502 @@
|
|
|
1
|
+
"""Multi-Trace Workflow Support.
|
|
2
|
+
|
|
3
|
+
Provides workflows for processing and analyzing multiple traces together.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import concurrent.futures
|
|
7
|
+
from collections.abc import Iterator
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from glob import glob as glob_func
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from oscura.core.exceptions import OscuraError
|
|
16
|
+
from oscura.core.progress import create_progress_tracker
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AlignmentMethod:
|
|
20
|
+
"""Alignment method constants."""
|
|
21
|
+
|
|
22
|
+
TRIGGER = "trigger"
|
|
23
|
+
TIME_SYNC = "time"
|
|
24
|
+
CROSS_CORRELATION = "correlation"
|
|
25
|
+
MANUAL = "manual"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class TraceStatistics:
|
|
30
|
+
"""Statistics for a measurement across traces.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
mean: Mean value
|
|
34
|
+
std: Standard deviation
|
|
35
|
+
min: Minimum value
|
|
36
|
+
max: Maximum value
|
|
37
|
+
median: Median value
|
|
38
|
+
count: Number of traces
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
mean: float
|
|
42
|
+
std: float
|
|
43
|
+
min: float
|
|
44
|
+
max: float
|
|
45
|
+
median: float
|
|
46
|
+
count: int
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class MultiTraceResults:
|
|
51
|
+
"""Results from multi-trace workflow.
|
|
52
|
+
|
|
53
|
+
Attributes:
|
|
54
|
+
trace_ids: List of trace identifiers
|
|
55
|
+
measurements: Dict mapping trace_id -> measurement results
|
|
56
|
+
statistics: Dict mapping measurement_name -> TraceStatistics
|
|
57
|
+
metadata: Additional workflow metadata
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
trace_ids: list[str] = field(default_factory=list)
|
|
61
|
+
measurements: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
62
|
+
statistics: dict[str, TraceStatistics] = field(default_factory=dict)
|
|
63
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class MultiTraceWorkflow:
|
|
67
|
+
"""Workflow manager for multi-trace processing.
|
|
68
|
+
|
|
69
|
+
Provides methods to load, align, process, and analyze multiple traces
|
|
70
|
+
with memory-efficient streaming and optional parallelization.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
pattern: str | None = None,
|
|
76
|
+
traces: list[Any] | None = None,
|
|
77
|
+
lazy: bool = False,
|
|
78
|
+
):
|
|
79
|
+
"""Initialize multi-trace workflow.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
pattern: Glob pattern for trace files (e.g., "*.csv")
|
|
83
|
+
traces: Pre-loaded trace objects
|
|
84
|
+
lazy: If True, load traces on demand
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
OscuraError: If neither pattern nor traces provided
|
|
88
|
+
"""
|
|
89
|
+
self.pattern = pattern
|
|
90
|
+
self._traces = traces or []
|
|
91
|
+
self._lazy = lazy
|
|
92
|
+
self._file_paths: list[Path] = []
|
|
93
|
+
self._aligned = False
|
|
94
|
+
self._alignment_offset: dict[str, int] = {}
|
|
95
|
+
self.results = MultiTraceResults()
|
|
96
|
+
|
|
97
|
+
# Discover files if pattern provided
|
|
98
|
+
if pattern:
|
|
99
|
+
self._discover_files()
|
|
100
|
+
elif not traces:
|
|
101
|
+
raise OscuraError("Must provide either pattern or traces")
|
|
102
|
+
|
|
103
|
+
def _discover_files(self) -> None:
|
|
104
|
+
"""Discover trace files matching pattern."""
|
|
105
|
+
if not self.pattern:
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
paths = glob_func(self.pattern) # noqa: PTH207
|
|
109
|
+
if not paths:
|
|
110
|
+
raise OscuraError(f"No files match pattern: {self.pattern}")
|
|
111
|
+
|
|
112
|
+
self._file_paths = [Path(p) for p in sorted(paths)]
|
|
113
|
+
self.results.trace_ids = [p.name for p in self._file_paths]
|
|
114
|
+
|
|
115
|
+
def _load_trace(self, path: Path) -> Any:
|
|
116
|
+
"""Load a single trace file.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
path: Path to trace file
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Loaded trace object
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
OscuraError: If trace cannot be loaded
|
|
126
|
+
"""
|
|
127
|
+
# Determine loader based on extension
|
|
128
|
+
ext = path.suffix.lower()
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
if ext == ".csv":
|
|
132
|
+
from oscura.loaders.csv import ( # type: ignore[import-not-found]
|
|
133
|
+
load_csv, # type: ignore[import-not-found]
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return load_csv(str(path))
|
|
137
|
+
elif ext == ".bin":
|
|
138
|
+
from oscura.loaders.binary import ( # type: ignore[import-not-found]
|
|
139
|
+
load_binary, # type: ignore[import-not-found]
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return load_binary(str(path))
|
|
143
|
+
elif ext in (".h5", ".hdf5"):
|
|
144
|
+
from oscura.loaders.hdf5 import ( # type: ignore[import-not-found]
|
|
145
|
+
load_hdf5, # type: ignore[import-not-found]
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return load_hdf5(str(path))
|
|
149
|
+
else:
|
|
150
|
+
raise OscuraError(f"Unsupported format: {ext}")
|
|
151
|
+
|
|
152
|
+
except ImportError as e:
|
|
153
|
+
raise OscuraError(f"Loader not available for {ext}: {e}") # noqa: B904
|
|
154
|
+
|
|
155
|
+
def _iter_traces(self, lazy: bool = False) -> Iterator[tuple[str, Any]]:
|
|
156
|
+
"""Iterate over traces.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
lazy: If True, load on demand; if False, load all first
|
|
160
|
+
|
|
161
|
+
Yields:
|
|
162
|
+
Tuple of (trace_id, trace)
|
|
163
|
+
"""
|
|
164
|
+
# Use pre-loaded traces if available
|
|
165
|
+
if self._traces:
|
|
166
|
+
for i, trace in enumerate(self._traces):
|
|
167
|
+
trace_id = (
|
|
168
|
+
self.results.trace_ids[i] if i < len(self.results.trace_ids) else f"trace_{i}"
|
|
169
|
+
)
|
|
170
|
+
yield trace_id, trace
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
# Load from files
|
|
174
|
+
for path in self._file_paths:
|
|
175
|
+
trace_id = path.name
|
|
176
|
+
if lazy or self._lazy:
|
|
177
|
+
# Load on demand
|
|
178
|
+
trace = self._load_trace(path)
|
|
179
|
+
else:
|
|
180
|
+
# Would load all at once (not implemented here)
|
|
181
|
+
trace = self._load_trace(path)
|
|
182
|
+
yield trace_id, trace
|
|
183
|
+
|
|
184
|
+
def align(
|
|
185
|
+
self,
|
|
186
|
+
method: str = AlignmentMethod.TRIGGER,
|
|
187
|
+
channel: int = 0,
|
|
188
|
+
threshold: float | None = None,
|
|
189
|
+
**kwargs: Any,
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Align traces using specified method.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
method: Alignment method ('trigger', 'time', 'correlation', 'manual')
|
|
195
|
+
channel: Channel to use for alignment (for multi-channel traces)
|
|
196
|
+
threshold: Trigger threshold (for trigger alignment)
|
|
197
|
+
**kwargs: Additional method-specific parameters
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
OscuraError: If alignment fails
|
|
201
|
+
"""
|
|
202
|
+
if method == AlignmentMethod.TRIGGER:
|
|
203
|
+
self._align_by_trigger(channel, threshold, **kwargs)
|
|
204
|
+
elif method == AlignmentMethod.TIME_SYNC:
|
|
205
|
+
self._align_by_time(**kwargs)
|
|
206
|
+
elif method == AlignmentMethod.CROSS_CORRELATION:
|
|
207
|
+
self._align_by_correlation(channel, **kwargs)
|
|
208
|
+
elif method == AlignmentMethod.MANUAL:
|
|
209
|
+
self._align_manual(**kwargs)
|
|
210
|
+
else:
|
|
211
|
+
raise OscuraError(f"Unknown alignment method: {method}")
|
|
212
|
+
|
|
213
|
+
self._aligned = True
|
|
214
|
+
|
|
215
|
+
def _align_by_trigger(
|
|
216
|
+
self,
|
|
217
|
+
channel: int,
|
|
218
|
+
threshold: float | None,
|
|
219
|
+
**kwargs: Any,
|
|
220
|
+
) -> None:
|
|
221
|
+
"""Align traces by trigger point.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
channel: Channel index
|
|
225
|
+
threshold: Trigger threshold
|
|
226
|
+
**kwargs: Additional parameters
|
|
227
|
+
"""
|
|
228
|
+
# Find trigger point in each trace
|
|
229
|
+
for trace_id, trace in self._iter_traces(lazy=True):
|
|
230
|
+
# Find first crossing of threshold
|
|
231
|
+
if hasattr(trace, "data"):
|
|
232
|
+
data = trace.data
|
|
233
|
+
if threshold is None:
|
|
234
|
+
# Auto threshold: 50% of max
|
|
235
|
+
threshold = 0.5 * (np.max(data) + np.min(data))
|
|
236
|
+
|
|
237
|
+
# Find first rising edge
|
|
238
|
+
above = data > threshold
|
|
239
|
+
edges = np.diff(above.astype(int))
|
|
240
|
+
rising = np.where(edges > 0)[0]
|
|
241
|
+
|
|
242
|
+
if len(rising) > 0:
|
|
243
|
+
self._alignment_offset[trace_id] = int(rising[0])
|
|
244
|
+
else:
|
|
245
|
+
self._alignment_offset[trace_id] = 0
|
|
246
|
+
else:
|
|
247
|
+
self._alignment_offset[trace_id] = 0
|
|
248
|
+
|
|
249
|
+
def _align_by_time(self, **kwargs: Any) -> None:
|
|
250
|
+
"""Align traces by timestamp.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
**kwargs: Additional parameters
|
|
254
|
+
"""
|
|
255
|
+
# Align based on trace timestamps
|
|
256
|
+
# Placeholder implementation
|
|
257
|
+
for trace_id, _trace in self._iter_traces(lazy=True):
|
|
258
|
+
self._alignment_offset[trace_id] = 0
|
|
259
|
+
|
|
260
|
+
def _align_by_correlation(self, channel: int, **kwargs: Any) -> None:
|
|
261
|
+
"""Align traces by cross-correlation.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
channel: Channel index
|
|
265
|
+
**kwargs: Additional parameters
|
|
266
|
+
"""
|
|
267
|
+
# Use cross-correlation to find alignment
|
|
268
|
+
# Placeholder implementation
|
|
269
|
+
for trace_id, _trace in self._iter_traces(lazy=True):
|
|
270
|
+
self._alignment_offset[trace_id] = 0
|
|
271
|
+
|
|
272
|
+
def _align_manual(self, **kwargs: Any) -> None:
|
|
273
|
+
"""Manual alignment with specified offsets.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
**kwargs: Must include 'offsets' dict mapping trace_id -> offset
|
|
277
|
+
|
|
278
|
+
Raises:
|
|
279
|
+
OscuraError: If offsets parameter not provided.
|
|
280
|
+
"""
|
|
281
|
+
offsets = kwargs.get("offsets", {})
|
|
282
|
+
if not offsets:
|
|
283
|
+
raise OscuraError("Manual alignment requires 'offsets' parameter")
|
|
284
|
+
|
|
285
|
+
self._alignment_offset.update(offsets)
|
|
286
|
+
|
|
287
|
+
def measure(
|
|
288
|
+
self, *measurements: str, parallel: bool = False, max_workers: int | None = None
|
|
289
|
+
) -> None:
|
|
290
|
+
"""Measure properties across all traces.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
*measurements: Measurement names (rise_time, fall_time, etc.)
|
|
294
|
+
parallel: If True, process traces in parallel
|
|
295
|
+
max_workers: Maximum parallel workers (None = CPU count)
|
|
296
|
+
|
|
297
|
+
Raises:
|
|
298
|
+
OscuraError: If measurement fails
|
|
299
|
+
"""
|
|
300
|
+
if not measurements:
|
|
301
|
+
raise OscuraError("At least one measurement required")
|
|
302
|
+
|
|
303
|
+
if parallel:
|
|
304
|
+
self._measure_parallel(measurements, max_workers)
|
|
305
|
+
else:
|
|
306
|
+
self._measure_sequential(measurements)
|
|
307
|
+
|
|
308
|
+
def _measure_sequential(self, measurements: tuple[str, ...]) -> None:
|
|
309
|
+
"""Measure sequentially."""
|
|
310
|
+
# Progress tracking
|
|
311
|
+
progress = create_progress_tracker( # type: ignore[call-arg]
|
|
312
|
+
total=len(self.results.trace_ids),
|
|
313
|
+
description="Measuring traces",
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
for trace_id, trace in self._iter_traces(lazy=True):
|
|
317
|
+
results = {}
|
|
318
|
+
for meas_name in measurements:
|
|
319
|
+
try:
|
|
320
|
+
# Call measurement function
|
|
321
|
+
# Placeholder - would call actual measurement
|
|
322
|
+
results[meas_name] = self._perform_measurement(trace, meas_name)
|
|
323
|
+
except Exception as e:
|
|
324
|
+
results[meas_name] = None
|
|
325
|
+
print(f"Warning: {meas_name} failed for {trace_id}: {e}")
|
|
326
|
+
|
|
327
|
+
self.results.measurements[trace_id] = results
|
|
328
|
+
progress.update(1)
|
|
329
|
+
|
|
330
|
+
def _measure_parallel(self, measurements: tuple[str, ...], max_workers: int | None) -> None:
|
|
331
|
+
"""Measure in parallel."""
|
|
332
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
333
|
+
futures = {}
|
|
334
|
+
|
|
335
|
+
for trace_id, trace in self._iter_traces(lazy=False):
|
|
336
|
+
future = executor.submit(self._measure_trace, trace, measurements)
|
|
337
|
+
futures[future] = trace_id
|
|
338
|
+
|
|
339
|
+
for future in concurrent.futures.as_completed(futures):
|
|
340
|
+
trace_id = futures[future]
|
|
341
|
+
try:
|
|
342
|
+
results = future.result()
|
|
343
|
+
self.results.measurements[trace_id] = results
|
|
344
|
+
except Exception as e:
|
|
345
|
+
print(f"Error measuring {trace_id}: {e}")
|
|
346
|
+
|
|
347
|
+
def _measure_trace(self, trace: Any, measurements: tuple[str, ...]) -> dict[str, Any]:
|
|
348
|
+
"""Measure a single trace.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
trace: Trace object
|
|
352
|
+
measurements: Measurement names
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
Dict mapping measurement_name -> value
|
|
356
|
+
"""
|
|
357
|
+
results = {}
|
|
358
|
+
for meas_name in measurements:
|
|
359
|
+
try:
|
|
360
|
+
results[meas_name] = self._perform_measurement(trace, meas_name)
|
|
361
|
+
except Exception:
|
|
362
|
+
results[meas_name] = None
|
|
363
|
+
return results
|
|
364
|
+
|
|
365
|
+
def _perform_measurement(self, trace: Any, measurement: str) -> Any:
|
|
366
|
+
"""Perform a single measurement.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
trace: Trace object
|
|
370
|
+
measurement: Measurement name
|
|
371
|
+
|
|
372
|
+
Raises:
|
|
373
|
+
OscuraError: If measurement not available
|
|
374
|
+
"""
|
|
375
|
+
# Placeholder - would call actual measurement functions
|
|
376
|
+
# from oscura.analyzers.measurements
|
|
377
|
+
raise OscuraError(
|
|
378
|
+
f"Measurement '{measurement}' not yet implemented in multi-trace workflow"
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
def aggregate(self) -> MultiTraceResults:
|
|
382
|
+
"""Compute aggregate statistics across traces.
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
Results with statistics
|
|
386
|
+
|
|
387
|
+
Raises:
|
|
388
|
+
OscuraError: If no measurements available
|
|
389
|
+
"""
|
|
390
|
+
if not self.results.measurements:
|
|
391
|
+
raise OscuraError("No measurements available. Call measure() first.")
|
|
392
|
+
|
|
393
|
+
# Compute statistics for each measurement type
|
|
394
|
+
all_measurements = set() # type: ignore[var-annotated]
|
|
395
|
+
for trace_results in self.results.measurements.values():
|
|
396
|
+
all_measurements.update(trace_results.keys())
|
|
397
|
+
|
|
398
|
+
for meas_name in all_measurements:
|
|
399
|
+
values = []
|
|
400
|
+
for trace_results in self.results.measurements.values():
|
|
401
|
+
val = trace_results.get(meas_name)
|
|
402
|
+
if val is not None and not (isinstance(val, float) and np.isnan(val)):
|
|
403
|
+
values.append(float(val))
|
|
404
|
+
|
|
405
|
+
if values:
|
|
406
|
+
self.results.statistics[meas_name] = TraceStatistics(
|
|
407
|
+
mean=float(np.mean(values)),
|
|
408
|
+
std=float(np.std(values)),
|
|
409
|
+
min=float(np.min(values)),
|
|
410
|
+
max=float(np.max(values)),
|
|
411
|
+
median=float(np.median(values)),
|
|
412
|
+
count=len(values),
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
return self.results
|
|
416
|
+
|
|
417
|
+
def export_report(self, filename: str, format: str = "pdf") -> None:
|
|
418
|
+
"""Export combined report.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
filename: Output filename
|
|
422
|
+
format: Report format ('pdf', 'html', 'json')
|
|
423
|
+
|
|
424
|
+
Raises:
|
|
425
|
+
OscuraError: If export fails
|
|
426
|
+
"""
|
|
427
|
+
if format == "json":
|
|
428
|
+
self._export_json(filename)
|
|
429
|
+
elif format == "pdf":
|
|
430
|
+
self._export_pdf(filename)
|
|
431
|
+
elif format == "html":
|
|
432
|
+
self._export_html(filename)
|
|
433
|
+
else:
|
|
434
|
+
raise OscuraError(f"Unsupported report format: {format}")
|
|
435
|
+
|
|
436
|
+
def _export_json(self, filename: str) -> None:
|
|
437
|
+
"""Export results to JSON."""
|
|
438
|
+
import json
|
|
439
|
+
|
|
440
|
+
data = {
|
|
441
|
+
"trace_ids": self.results.trace_ids,
|
|
442
|
+
"measurements": self.results.measurements,
|
|
443
|
+
"statistics": {
|
|
444
|
+
name: {
|
|
445
|
+
"mean": stats.mean,
|
|
446
|
+
"std": stats.std,
|
|
447
|
+
"min": stats.min,
|
|
448
|
+
"max": stats.max,
|
|
449
|
+
"median": stats.median,
|
|
450
|
+
"count": stats.count,
|
|
451
|
+
}
|
|
452
|
+
for name, stats in self.results.statistics.items()
|
|
453
|
+
},
|
|
454
|
+
"metadata": self.results.metadata,
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
with open(filename, "w") as f:
|
|
458
|
+
json.dump(data, f, indent=2)
|
|
459
|
+
|
|
460
|
+
def _export_pdf(self, filename: str) -> None:
|
|
461
|
+
"""Export results to PDF.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
filename: Output filename
|
|
465
|
+
|
|
466
|
+
Raises:
|
|
467
|
+
OscuraError: PDF export not yet implemented
|
|
468
|
+
"""
|
|
469
|
+
raise OscuraError("PDF export not yet implemented")
|
|
470
|
+
|
|
471
|
+
def _export_html(self, filename: str) -> None:
|
|
472
|
+
"""Export results to HTML.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
filename: Output filename
|
|
476
|
+
|
|
477
|
+
Raises:
|
|
478
|
+
OscuraError: HTML export not yet implemented
|
|
479
|
+
"""
|
|
480
|
+
raise OscuraError("HTML export not yet implemented")
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def load_all(pattern: str, lazy: bool = True) -> list[Any]:
|
|
484
|
+
"""Load all traces matching pattern.
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
pattern: Glob pattern
|
|
488
|
+
lazy: If True, return lazy-loading proxy objects
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
List of trace objects
|
|
492
|
+
|
|
493
|
+
Raises:
|
|
494
|
+
OscuraError: If no traces found
|
|
495
|
+
"""
|
|
496
|
+
paths = glob_func(pattern) # noqa: PTH207
|
|
497
|
+
if not paths:
|
|
498
|
+
raise OscuraError(f"No files match pattern: {pattern}")
|
|
499
|
+
|
|
500
|
+
# For now, just return file paths
|
|
501
|
+
# Would implement lazy loading proxy
|
|
502
|
+
return [Path(p) for p in paths]
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Power analysis workflow.
|
|
2
|
+
|
|
3
|
+
This module implements comprehensive power consumption analysis from
|
|
4
|
+
voltage and current traces.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> import oscura as osc
|
|
9
|
+
>>> voltage = osc.load('vdd.wfm')
|
|
10
|
+
>>> current = osc.load('idd.wfm')
|
|
11
|
+
>>> result = osc.power_analysis(voltage, current)
|
|
12
|
+
>>> print(f"Average Power: {result['average_power']*1e3:.2f} mW")
|
|
13
|
+
|
|
14
|
+
References:
|
|
15
|
+
IEC 61000: Electromagnetic compatibility
|
|
16
|
+
IEEE 1241-2010: ADC terminology and test methods
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import TYPE_CHECKING, Any
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
from oscura.core.exceptions import AnalysisError
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from oscura.core.types import WaveformTrace
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def power_analysis(
|
|
32
|
+
voltage: WaveformTrace,
|
|
33
|
+
current: WaveformTrace,
|
|
34
|
+
*,
|
|
35
|
+
input_voltage: WaveformTrace | None = None,
|
|
36
|
+
input_current: WaveformTrace | None = None,
|
|
37
|
+
report: str | None = None,
|
|
38
|
+
) -> dict[str, Any]:
|
|
39
|
+
"""Comprehensive power consumption analysis.
|
|
40
|
+
|
|
41
|
+
Analyzes power consumption from voltage and current measurements:
|
|
42
|
+
- Instantaneous power calculation
|
|
43
|
+
- Average, RMS, and peak power
|
|
44
|
+
- Energy consumption
|
|
45
|
+
- Efficiency (if input power provided)
|
|
46
|
+
- Power profile generation
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
voltage: Output voltage trace.
|
|
50
|
+
current: Output current trace.
|
|
51
|
+
input_voltage: Optional input voltage for efficiency calculation.
|
|
52
|
+
input_current: Optional input current for efficiency calculation.
|
|
53
|
+
report: Optional path to save HTML report.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Dictionary containing:
|
|
57
|
+
- power_trace: WaveformTrace of instantaneous power P(t)
|
|
58
|
+
- average_power: Mean power in watts
|
|
59
|
+
- output_power_avg: Average output power (same as average_power)
|
|
60
|
+
- output_power_rms: RMS output power in watts
|
|
61
|
+
- peak_power: Maximum power in watts
|
|
62
|
+
- min_power: Minimum power in watts
|
|
63
|
+
- energy: Total energy in joules
|
|
64
|
+
- duration: Measurement duration in seconds
|
|
65
|
+
- efficiency: Efficiency percentage (if input provided)
|
|
66
|
+
- power_loss: Power loss in watts (if input provided)
|
|
67
|
+
- input_power_avg: Average input power (if input provided)
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
AnalysisError: If traces have incompatible sample rates or lengths.
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
>>> voltage = osc.load('vout.wfm')
|
|
74
|
+
>>> current = osc.load('iout.wfm')
|
|
75
|
+
>>> result = osc.power_analysis(voltage, current)
|
|
76
|
+
>>> print(f"Average: {result['average_power']*1e3:.2f} mW")
|
|
77
|
+
>>> print(f"Peak: {result['peak_power']*1e3:.2f} mW")
|
|
78
|
+
>>> print(f"Energy: {result['energy']*1e6:.2f} µJ")
|
|
79
|
+
|
|
80
|
+
References:
|
|
81
|
+
IEC 61000-4-7: Harmonics and interharmonics measurement
|
|
82
|
+
IEEE 1459-2010: Definitions for measurement of electric power
|
|
83
|
+
"""
|
|
84
|
+
# Import power analysis functions
|
|
85
|
+
from oscura.analyzers.power.basic import (
|
|
86
|
+
instantaneous_power,
|
|
87
|
+
power_statistics,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Validate traces
|
|
91
|
+
if voltage.metadata.sample_rate != current.metadata.sample_rate:
|
|
92
|
+
# Would need interpolation in real implementation
|
|
93
|
+
raise AnalysisError(
|
|
94
|
+
"Voltage and current traces must have same sample rate. "
|
|
95
|
+
f"Got {voltage.metadata.sample_rate} and {current.metadata.sample_rate}"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Calculate instantaneous power
|
|
99
|
+
power_trace = instantaneous_power(voltage, current)
|
|
100
|
+
|
|
101
|
+
# Calculate power statistics
|
|
102
|
+
stats = power_statistics(power_trace)
|
|
103
|
+
|
|
104
|
+
# Build result with output power
|
|
105
|
+
result = {
|
|
106
|
+
"power_trace": power_trace,
|
|
107
|
+
"average_power": stats["average"],
|
|
108
|
+
"output_power_avg": stats["average"],
|
|
109
|
+
"output_power_rms": stats["rms"],
|
|
110
|
+
"peak_power": stats["peak"],
|
|
111
|
+
"min_power": stats.get("min", np.min(power_trace.data)),
|
|
112
|
+
"energy": stats["energy"],
|
|
113
|
+
"duration": stats["duration"],
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Calculate efficiency if input provided
|
|
117
|
+
if input_voltage is not None and input_current is not None:
|
|
118
|
+
input_power_trace = instantaneous_power(input_voltage, input_current)
|
|
119
|
+
input_stats = power_statistics(input_power_trace)
|
|
120
|
+
|
|
121
|
+
input_power_avg = input_stats["average"]
|
|
122
|
+
output_power_avg = stats["average"]
|
|
123
|
+
|
|
124
|
+
if input_power_avg > 0:
|
|
125
|
+
efficiency = (output_power_avg / input_power_avg) * 100.0
|
|
126
|
+
power_loss = input_power_avg - output_power_avg
|
|
127
|
+
else:
|
|
128
|
+
efficiency = 0.0
|
|
129
|
+
power_loss = 0.0
|
|
130
|
+
|
|
131
|
+
result["efficiency"] = efficiency
|
|
132
|
+
result["power_loss"] = power_loss
|
|
133
|
+
result["input_power_avg"] = input_power_avg
|
|
134
|
+
|
|
135
|
+
# Generate report if requested
|
|
136
|
+
if report is not None:
|
|
137
|
+
_generate_power_report(result, report)
|
|
138
|
+
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _generate_power_report(result: dict[str, Any], output_path: str) -> None:
|
|
143
|
+
"""Generate HTML report for power analysis.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
result: Power analysis result dictionary.
|
|
147
|
+
output_path: Path to save HTML report.
|
|
148
|
+
"""
|
|
149
|
+
html = f"""
|
|
150
|
+
<html>
|
|
151
|
+
<head><title>Power Analysis Report</title></head>
|
|
152
|
+
<body>
|
|
153
|
+
<h1>Power Analysis Report</h1>
|
|
154
|
+
<h2>Power Statistics</h2>
|
|
155
|
+
<table>
|
|
156
|
+
<tr><th>Parameter</th><th>Value</th><th>Units</th></tr>
|
|
157
|
+
<tr><td>Average Power</td><td>{result["average_power"] * 1e3:.3f}</td><td>mW</td></tr>
|
|
158
|
+
<tr><td>RMS Power</td><td>{result["output_power_rms"] * 1e3:.3f}</td><td>mW</td></tr>
|
|
159
|
+
<tr><td>Peak Power</td><td>{result["peak_power"] * 1e3:.3f}</td><td>mW</td></tr>
|
|
160
|
+
<tr><td>Total Energy</td><td>{result["energy"] * 1e6:.3f}</td><td>µJ</td></tr>
|
|
161
|
+
<tr><td>Duration</td><td>{result["duration"] * 1e3:.3f}</td><td>ms</td></tr>
|
|
162
|
+
"""
|
|
163
|
+
if "efficiency" in result:
|
|
164
|
+
html += f"""
|
|
165
|
+
<tr><td>Efficiency</td><td>{result["efficiency"]:.1f}</td><td>%</td></tr>
|
|
166
|
+
<tr><td>Input Power</td><td>{result["input_power_avg"] * 1e3:.3f}</td><td>mW</td></tr>
|
|
167
|
+
<tr><td>Power Loss</td><td>{result["power_loss"] * 1e3:.3f}</td><td>mW</td></tr>
|
|
168
|
+
"""
|
|
169
|
+
html += """
|
|
170
|
+
</table>
|
|
171
|
+
</body>
|
|
172
|
+
</html>
|
|
173
|
+
"""
|
|
174
|
+
with open(output_path, "w") as f:
|
|
175
|
+
f.write(html)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
__all__ = ["power_analysis"]
|