oscura 0.5.0__py3-none-any.whl → 0.6.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 +169 -167
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/classification.py +659 -0
- oscura/analyzers/digital/__init__.py +0 -48
- oscura/analyzers/digital/edges.py +325 -65
- oscura/analyzers/digital/extraction.py +0 -195
- oscura/analyzers/digital/quality.py +293 -166
- oscura/analyzers/digital/timing.py +260 -115
- oscura/analyzers/digital/timing_numba.py +334 -0
- oscura/analyzers/entropy.py +605 -0
- oscura/analyzers/eye/diagram.py +176 -109
- oscura/analyzers/eye/metrics.py +5 -5
- oscura/analyzers/jitter/__init__.py +6 -4
- oscura/analyzers/jitter/ber.py +52 -52
- oscura/analyzers/jitter/classification.py +156 -0
- oscura/analyzers/jitter/decomposition.py +163 -113
- oscura/analyzers/jitter/spectrum.py +80 -64
- oscura/analyzers/ml/__init__.py +39 -0
- oscura/analyzers/ml/features.py +600 -0
- oscura/analyzers/ml/signal_classifier.py +604 -0
- oscura/analyzers/packet/daq.py +246 -158
- oscura/analyzers/packet/parser.py +12 -1
- oscura/analyzers/packet/payload.py +50 -2110
- oscura/analyzers/packet/payload_analysis.py +361 -181
- oscura/analyzers/packet/payload_patterns.py +133 -70
- oscura/analyzers/packet/stream.py +84 -23
- oscura/analyzers/patterns/__init__.py +26 -5
- oscura/analyzers/patterns/anomaly_detection.py +908 -0
- oscura/analyzers/patterns/clustering.py +169 -108
- oscura/analyzers/patterns/clustering_optimized.py +227 -0
- oscura/analyzers/patterns/discovery.py +1 -1
- oscura/analyzers/patterns/matching.py +581 -197
- oscura/analyzers/patterns/pattern_mining.py +778 -0
- oscura/analyzers/patterns/periodic.py +121 -38
- oscura/analyzers/patterns/sequences.py +175 -78
- oscura/analyzers/power/conduction.py +1 -1
- oscura/analyzers/power/soa.py +6 -6
- oscura/analyzers/power/switching.py +250 -110
- oscura/analyzers/protocol/__init__.py +17 -1
- oscura/analyzers/protocols/__init__.py +1 -22
- oscura/analyzers/protocols/base.py +6 -6
- oscura/analyzers/protocols/ble/__init__.py +38 -0
- oscura/analyzers/protocols/ble/analyzer.py +809 -0
- oscura/analyzers/protocols/ble/uuids.py +288 -0
- oscura/analyzers/protocols/can.py +257 -127
- oscura/analyzers/protocols/can_fd.py +107 -80
- oscura/analyzers/protocols/flexray.py +139 -80
- oscura/analyzers/protocols/hdlc.py +93 -58
- oscura/analyzers/protocols/i2c.py +247 -106
- oscura/analyzers/protocols/i2s.py +138 -86
- oscura/analyzers/protocols/industrial/__init__.py +40 -0
- oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
- oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
- oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
- oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
- oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
- oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
- oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
- oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
- oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
- oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
- oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
- oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
- oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
- oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
- oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
- oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
- oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
- oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
- oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
- oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
- oscura/analyzers/protocols/jtag.py +180 -98
- oscura/analyzers/protocols/lin.py +219 -114
- oscura/analyzers/protocols/manchester.py +4 -4
- oscura/analyzers/protocols/onewire.py +253 -149
- oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
- oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
- oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
- oscura/analyzers/protocols/spi.py +192 -95
- oscura/analyzers/protocols/swd.py +321 -167
- oscura/analyzers/protocols/uart.py +267 -125
- oscura/analyzers/protocols/usb.py +235 -131
- oscura/analyzers/side_channel/power.py +17 -12
- oscura/analyzers/signal/__init__.py +15 -0
- oscura/analyzers/signal/timing_analysis.py +1086 -0
- oscura/analyzers/signal_integrity/__init__.py +4 -1
- oscura/analyzers/signal_integrity/sparams.py +2 -19
- oscura/analyzers/spectral/chunked.py +129 -60
- oscura/analyzers/spectral/chunked_fft.py +300 -94
- oscura/analyzers/spectral/chunked_wavelet.py +100 -80
- oscura/analyzers/statistical/checksum.py +376 -217
- oscura/analyzers/statistical/classification.py +229 -107
- oscura/analyzers/statistical/entropy.py +78 -53
- oscura/analyzers/statistics/correlation.py +407 -211
- oscura/analyzers/statistics/outliers.py +2 -2
- oscura/analyzers/statistics/streaming.py +30 -5
- oscura/analyzers/validation.py +216 -101
- oscura/analyzers/waveform/measurements.py +9 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
- oscura/analyzers/waveform/spectral.py +500 -228
- oscura/api/__init__.py +31 -5
- oscura/api/dsl/__init__.py +582 -0
- oscura/{dsl → api/dsl}/commands.py +43 -76
- oscura/{dsl → api/dsl}/interpreter.py +26 -51
- oscura/{dsl → api/dsl}/parser.py +107 -77
- oscura/{dsl → api/dsl}/repl.py +2 -2
- oscura/api/dsl.py +1 -1
- oscura/{integrations → api/integrations}/__init__.py +1 -1
- oscura/{integrations → api/integrations}/llm.py +201 -102
- oscura/api/operators.py +3 -3
- oscura/api/optimization.py +144 -30
- oscura/api/rest_server.py +921 -0
- oscura/api/server/__init__.py +17 -0
- oscura/api/server/dashboard.py +850 -0
- oscura/api/server/static/README.md +34 -0
- oscura/api/server/templates/base.html +181 -0
- oscura/api/server/templates/export.html +120 -0
- oscura/api/server/templates/home.html +284 -0
- oscura/api/server/templates/protocols.html +58 -0
- oscura/api/server/templates/reports.html +43 -0
- oscura/api/server/templates/session_detail.html +89 -0
- oscura/api/server/templates/sessions.html +83 -0
- oscura/api/server/templates/waveforms.html +73 -0
- oscura/automotive/__init__.py +8 -1
- oscura/automotive/can/__init__.py +10 -0
- oscura/automotive/can/checksum.py +3 -1
- oscura/automotive/can/dbc_generator.py +590 -0
- oscura/automotive/can/message_wrapper.py +121 -74
- oscura/automotive/can/patterns.py +98 -21
- oscura/automotive/can/session.py +292 -56
- oscura/automotive/can/state_machine.py +6 -3
- oscura/automotive/can/stimulus_response.py +97 -75
- oscura/automotive/dbc/__init__.py +10 -2
- oscura/automotive/dbc/generator.py +84 -56
- oscura/automotive/dbc/parser.py +6 -6
- oscura/automotive/dtc/data.json +2763 -0
- oscura/automotive/dtc/database.py +2 -2
- oscura/automotive/flexray/__init__.py +31 -0
- oscura/automotive/flexray/analyzer.py +504 -0
- oscura/automotive/flexray/crc.py +185 -0
- oscura/automotive/flexray/fibex.py +449 -0
- oscura/automotive/j1939/__init__.py +45 -8
- oscura/automotive/j1939/analyzer.py +605 -0
- oscura/automotive/j1939/spns.py +326 -0
- oscura/automotive/j1939/transport.py +306 -0
- oscura/automotive/lin/__init__.py +47 -0
- oscura/automotive/lin/analyzer.py +612 -0
- oscura/automotive/loaders/blf.py +13 -2
- oscura/automotive/loaders/csv_can.py +143 -72
- oscura/automotive/loaders/dispatcher.py +50 -2
- oscura/automotive/loaders/mdf.py +86 -45
- oscura/automotive/loaders/pcap.py +111 -61
- oscura/automotive/uds/__init__.py +4 -0
- oscura/automotive/uds/analyzer.py +725 -0
- oscura/automotive/uds/decoder.py +140 -58
- oscura/automotive/uds/models.py +7 -1
- oscura/automotive/visualization.py +1 -1
- oscura/cli/analyze.py +348 -0
- oscura/cli/batch.py +142 -122
- oscura/cli/benchmark.py +275 -0
- oscura/cli/characterize.py +137 -82
- oscura/cli/compare.py +224 -131
- oscura/cli/completion.py +250 -0
- oscura/cli/config_cmd.py +361 -0
- oscura/cli/decode.py +164 -87
- oscura/cli/export.py +286 -0
- oscura/cli/main.py +115 -31
- oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
- oscura/{onboarding → cli/onboarding}/help.py +80 -58
- oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
- oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
- oscura/cli/progress.py +147 -0
- oscura/cli/shell.py +157 -135
- oscura/cli/validate_cmd.py +204 -0
- oscura/cli/visualize.py +158 -0
- oscura/convenience.py +125 -79
- oscura/core/__init__.py +4 -2
- oscura/core/backend_selector.py +3 -3
- oscura/core/cache.py +126 -15
- oscura/core/cancellation.py +1 -1
- oscura/{config → core/config}/__init__.py +20 -11
- oscura/{config → core/config}/defaults.py +1 -1
- oscura/{config → core/config}/loader.py +7 -5
- oscura/{config → core/config}/memory.py +5 -5
- oscura/{config → core/config}/migration.py +1 -1
- oscura/{config → core/config}/pipeline.py +99 -23
- oscura/{config → core/config}/preferences.py +1 -1
- oscura/{config → core/config}/protocol.py +3 -3
- oscura/{config → core/config}/schema.py +426 -272
- oscura/{config → core/config}/settings.py +1 -1
- oscura/{config → core/config}/thresholds.py +195 -153
- oscura/core/correlation.py +5 -6
- oscura/core/cross_domain.py +0 -2
- oscura/core/debug.py +9 -5
- oscura/{extensibility → core/extensibility}/docs.py +158 -70
- oscura/{extensibility → core/extensibility}/extensions.py +160 -76
- oscura/{extensibility → core/extensibility}/logging.py +1 -1
- oscura/{extensibility → core/extensibility}/measurements.py +1 -1
- oscura/{extensibility → core/extensibility}/plugins.py +1 -1
- oscura/{extensibility → core/extensibility}/templates.py +73 -3
- oscura/{extensibility → core/extensibility}/validation.py +1 -1
- oscura/core/gpu_backend.py +11 -7
- oscura/core/log_query.py +101 -11
- oscura/core/logging.py +126 -54
- oscura/core/logging_advanced.py +5 -5
- oscura/core/memory_limits.py +108 -70
- oscura/core/memory_monitor.py +2 -2
- oscura/core/memory_progress.py +7 -7
- oscura/core/memory_warnings.py +1 -1
- oscura/core/numba_backend.py +13 -13
- oscura/{plugins → core/plugins}/__init__.py +9 -9
- oscura/{plugins → core/plugins}/base.py +7 -7
- oscura/{plugins → core/plugins}/cli.py +3 -3
- oscura/{plugins → core/plugins}/discovery.py +186 -106
- oscura/{plugins → core/plugins}/lifecycle.py +1 -1
- oscura/{plugins → core/plugins}/manager.py +7 -7
- oscura/{plugins → core/plugins}/registry.py +3 -3
- oscura/{plugins → core/plugins}/versioning.py +1 -1
- oscura/core/progress.py +16 -1
- oscura/core/provenance.py +8 -2
- oscura/{schemas → core/schemas}/__init__.py +2 -2
- oscura/core/schemas/bus_configuration.json +322 -0
- oscura/core/schemas/device_mapping.json +182 -0
- oscura/core/schemas/packet_format.json +418 -0
- oscura/core/schemas/protocol_definition.json +363 -0
- oscura/core/types.py +4 -0
- oscura/core/uncertainty.py +3 -3
- oscura/correlation/__init__.py +52 -0
- oscura/correlation/multi_protocol.py +811 -0
- oscura/discovery/auto_decoder.py +117 -35
- oscura/discovery/comparison.py +191 -86
- oscura/discovery/quality_validator.py +155 -68
- oscura/discovery/signal_detector.py +196 -79
- oscura/export/__init__.py +18 -20
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- oscura/export/wireshark/README.md +15 -15
- oscura/export/wireshark/generator.py +1 -1
- oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
- oscura/export/wireshark_dissector.py +746 -0
- oscura/guidance/wizard.py +207 -111
- oscura/hardware/__init__.py +19 -0
- oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
- oscura/{acquisition → hardware/acquisition}/file.py +2 -2
- oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
- oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
- oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
- oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
- oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
- oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
- oscura/hardware/firmware/__init__.py +29 -0
- oscura/hardware/firmware/pattern_recognition.py +874 -0
- oscura/hardware/hal_detector.py +736 -0
- oscura/hardware/security/__init__.py +37 -0
- oscura/hardware/security/side_channel_detector.py +1126 -0
- oscura/inference/__init__.py +4 -0
- oscura/inference/active_learning/README.md +7 -7
- oscura/inference/active_learning/observation_table.py +4 -1
- oscura/inference/alignment.py +216 -123
- oscura/inference/bayesian.py +113 -33
- oscura/inference/crc_reverse.py +101 -55
- oscura/inference/logic.py +6 -2
- oscura/inference/message_format.py +342 -183
- oscura/inference/protocol.py +95 -44
- oscura/inference/protocol_dsl.py +180 -82
- oscura/inference/signal_intelligence.py +1439 -706
- oscura/inference/spectral.py +99 -57
- oscura/inference/state_machine.py +810 -158
- oscura/inference/stream.py +270 -110
- oscura/iot/__init__.py +34 -0
- oscura/iot/coap/__init__.py +32 -0
- oscura/iot/coap/analyzer.py +668 -0
- oscura/iot/coap/options.py +212 -0
- oscura/iot/lorawan/__init__.py +21 -0
- oscura/iot/lorawan/crypto.py +206 -0
- oscura/iot/lorawan/decoder.py +801 -0
- oscura/iot/lorawan/mac_commands.py +341 -0
- oscura/iot/mqtt/__init__.py +27 -0
- oscura/iot/mqtt/analyzer.py +999 -0
- oscura/iot/mqtt/properties.py +315 -0
- oscura/iot/zigbee/__init__.py +31 -0
- oscura/iot/zigbee/analyzer.py +615 -0
- oscura/iot/zigbee/security.py +153 -0
- oscura/iot/zigbee/zcl.py +349 -0
- oscura/jupyter/display.py +125 -45
- oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
- oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
- oscura/jupyter/exploratory/fuzzy.py +746 -0
- oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
- oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
- oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
- oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
- oscura/jupyter/exploratory/sync.py +612 -0
- oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
- oscura/jupyter/magic.py +4 -4
- oscura/{ui → jupyter/ui}/__init__.py +2 -2
- oscura/{ui → jupyter/ui}/formatters.py +3 -3
- oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
- oscura/loaders/__init__.py +171 -63
- oscura/loaders/binary.py +88 -1
- oscura/loaders/chipwhisperer.py +153 -137
- oscura/loaders/configurable.py +208 -86
- oscura/loaders/csv_loader.py +458 -215
- oscura/loaders/hdf5_loader.py +278 -119
- oscura/loaders/lazy.py +87 -54
- oscura/loaders/mmap_loader.py +1 -1
- oscura/loaders/numpy_loader.py +253 -116
- oscura/loaders/pcap.py +226 -151
- oscura/loaders/rigol.py +110 -49
- oscura/loaders/sigrok.py +201 -78
- oscura/loaders/tdms.py +81 -58
- oscura/loaders/tektronix.py +291 -174
- oscura/loaders/touchstone.py +182 -87
- oscura/loaders/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -7
- oscura/reporting/analyze.py +352 -146
- oscura/reporting/argument_preparer.py +69 -14
- oscura/reporting/auto_report.py +97 -61
- oscura/reporting/batch.py +131 -58
- oscura/reporting/chart_selection.py +57 -45
- oscura/reporting/comparison.py +63 -17
- oscura/reporting/content/executive.py +76 -24
- oscura/reporting/core_formats/multi_format.py +11 -8
- oscura/reporting/engine.py +312 -158
- oscura/reporting/enhanced_reports.py +949 -0
- oscura/reporting/export.py +86 -43
- oscura/reporting/formatting/numbers.py +69 -42
- oscura/reporting/html.py +139 -58
- oscura/reporting/index.py +137 -65
- oscura/reporting/output.py +158 -67
- oscura/reporting/pdf.py +67 -102
- oscura/reporting/plots.py +191 -112
- oscura/reporting/sections.py +88 -47
- oscura/reporting/standards.py +104 -61
- oscura/reporting/summary_generator.py +75 -55
- oscura/reporting/tables.py +138 -54
- oscura/reporting/templates/enhanced/protocol_re.html +525 -0
- oscura/reporting/templates/index.md +13 -13
- oscura/sessions/__init__.py +14 -23
- oscura/sessions/base.py +3 -3
- oscura/sessions/blackbox.py +106 -10
- oscura/sessions/generic.py +2 -2
- oscura/sessions/legacy.py +783 -0
- oscura/side_channel/__init__.py +63 -0
- oscura/side_channel/dpa.py +1025 -0
- oscura/utils/__init__.py +15 -1
- oscura/utils/autodetect.py +1 -5
- oscura/utils/bitwise.py +118 -0
- oscura/{builders → utils/builders}/__init__.py +1 -1
- oscura/{comparison → utils/comparison}/__init__.py +6 -6
- oscura/{comparison → utils/comparison}/compare.py +202 -101
- oscura/{comparison → utils/comparison}/golden.py +83 -63
- oscura/{comparison → utils/comparison}/limits.py +313 -89
- oscura/{comparison → utils/comparison}/mask.py +151 -45
- oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
- oscura/{comparison → utils/comparison}/visualization.py +147 -89
- oscura/{component → utils/component}/__init__.py +3 -3
- oscura/{component → utils/component}/impedance.py +122 -58
- oscura/{component → utils/component}/reactive.py +165 -168
- oscura/{component → utils/component}/transmission_line.py +3 -3
- oscura/{filtering → utils/filtering}/__init__.py +6 -6
- oscura/{filtering → utils/filtering}/base.py +1 -1
- oscura/{filtering → utils/filtering}/convenience.py +2 -2
- oscura/{filtering → utils/filtering}/design.py +169 -93
- oscura/{filtering → utils/filtering}/filters.py +2 -2
- oscura/{filtering → utils/filtering}/introspection.py +2 -2
- oscura/utils/geometry.py +31 -0
- oscura/utils/imports.py +184 -0
- oscura/utils/lazy.py +1 -1
- oscura/{math → utils/math}/__init__.py +2 -2
- oscura/{math → utils/math}/arithmetic.py +114 -48
- oscura/{math → utils/math}/interpolation.py +139 -106
- oscura/utils/memory.py +129 -66
- oscura/utils/memory_advanced.py +92 -9
- oscura/utils/memory_extensions.py +10 -8
- oscura/{optimization → utils/optimization}/__init__.py +1 -1
- oscura/{optimization → utils/optimization}/search.py +2 -2
- oscura/utils/performance/__init__.py +58 -0
- oscura/utils/performance/caching.py +889 -0
- oscura/utils/performance/lsh_clustering.py +333 -0
- oscura/utils/performance/memory_optimizer.py +699 -0
- oscura/utils/performance/optimizations.py +675 -0
- oscura/utils/performance/parallel.py +654 -0
- oscura/utils/performance/profiling.py +661 -0
- oscura/{pipeline → utils/pipeline}/base.py +1 -1
- oscura/{pipeline → utils/pipeline}/composition.py +11 -3
- oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
- oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
- oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
- oscura/{search → utils/search}/__init__.py +3 -3
- oscura/{search → utils/search}/anomaly.py +188 -58
- oscura/utils/search/context.py +294 -0
- oscura/{search → utils/search}/pattern.py +138 -10
- oscura/utils/serial.py +51 -0
- oscura/utils/storage/__init__.py +61 -0
- oscura/utils/storage/database.py +1166 -0
- oscura/{streaming → utils/streaming}/chunked.py +302 -143
- oscura/{streaming → utils/streaming}/progressive.py +1 -1
- oscura/{streaming → utils/streaming}/realtime.py +3 -2
- oscura/{triggering → utils/triggering}/__init__.py +6 -6
- oscura/{triggering → utils/triggering}/base.py +6 -6
- oscura/{triggering → utils/triggering}/edge.py +2 -2
- oscura/{triggering → utils/triggering}/pattern.py +2 -2
- oscura/{triggering → utils/triggering}/pulse.py +115 -74
- oscura/{triggering → utils/triggering}/window.py +2 -2
- oscura/utils/validation.py +32 -0
- oscura/validation/__init__.py +121 -0
- oscura/{compliance → validation/compliance}/__init__.py +5 -5
- oscura/{compliance → validation/compliance}/advanced.py +5 -5
- oscura/{compliance → validation/compliance}/masks.py +1 -1
- oscura/{compliance → validation/compliance}/reporting.py +127 -53
- oscura/{compliance → validation/compliance}/testing.py +114 -52
- oscura/validation/compliance_tests.py +915 -0
- oscura/validation/fuzzer.py +990 -0
- oscura/validation/grammar_tests.py +596 -0
- oscura/validation/grammar_validator.py +904 -0
- oscura/validation/hil_testing.py +977 -0
- oscura/{quality → validation/quality}/__init__.py +4 -4
- oscura/{quality → validation/quality}/ensemble.py +251 -171
- oscura/{quality → validation/quality}/explainer.py +3 -3
- oscura/{quality → validation/quality}/scoring.py +1 -1
- oscura/{quality → validation/quality}/warnings.py +4 -4
- oscura/validation/regression_suite.py +808 -0
- oscura/validation/replay.py +788 -0
- oscura/{testing → validation/testing}/__init__.py +2 -2
- oscura/{testing → validation/testing}/synthetic.py +5 -5
- oscura/visualization/__init__.py +9 -0
- oscura/visualization/accessibility.py +1 -1
- oscura/visualization/annotations.py +64 -67
- oscura/visualization/colors.py +7 -7
- oscura/visualization/digital.py +180 -81
- oscura/visualization/eye.py +236 -85
- oscura/visualization/interactive.py +320 -143
- oscura/visualization/jitter.py +587 -247
- oscura/visualization/layout.py +169 -134
- oscura/visualization/optimization.py +103 -52
- oscura/visualization/palettes.py +1 -1
- oscura/visualization/power.py +427 -211
- oscura/visualization/power_extended.py +626 -297
- oscura/visualization/presets.py +2 -0
- oscura/visualization/protocols.py +495 -181
- oscura/visualization/render.py +79 -63
- oscura/visualization/reverse_engineering.py +171 -124
- oscura/visualization/signal_integrity.py +460 -279
- oscura/visualization/specialized.py +190 -100
- oscura/visualization/spectral.py +670 -255
- oscura/visualization/thumbnails.py +166 -137
- oscura/visualization/waveform.py +150 -63
- oscura/workflows/__init__.py +3 -0
- oscura/{batch → workflows/batch}/__init__.py +5 -5
- oscura/{batch → workflows/batch}/advanced.py +150 -75
- oscura/workflows/batch/aggregate.py +531 -0
- oscura/workflows/batch/analyze.py +236 -0
- oscura/{batch → workflows/batch}/logging.py +2 -2
- oscura/{batch → workflows/batch}/metrics.py +1 -1
- oscura/workflows/complete_re.py +1144 -0
- oscura/workflows/compliance.py +44 -54
- oscura/workflows/digital.py +197 -51
- oscura/workflows/legacy/__init__.py +12 -0
- oscura/{workflow → workflows/legacy}/dag.py +4 -1
- oscura/workflows/multi_trace.py +9 -9
- oscura/workflows/power.py +42 -62
- oscura/workflows/protocol.py +82 -49
- oscura/workflows/reverse_engineering.py +351 -150
- oscura/workflows/signal_integrity.py +157 -82
- oscura-0.6.0.dist-info/METADATA +643 -0
- oscura-0.6.0.dist-info/RECORD +590 -0
- oscura/analyzers/digital/ic_database.py +0 -498
- oscura/analyzers/digital/timing_paths.py +0 -339
- oscura/analyzers/digital/vintage.py +0 -377
- oscura/analyzers/digital/vintage_result.py +0 -148
- oscura/analyzers/protocols/parallel_bus.py +0 -449
- oscura/batch/aggregate.py +0 -300
- oscura/batch/analyze.py +0 -139
- oscura/dsl/__init__.py +0 -73
- oscura/exceptions.py +0 -59
- oscura/exploratory/fuzzy.py +0 -513
- oscura/exploratory/sync.py +0 -384
- oscura/export/wavedrom.py +0 -430
- oscura/exporters/__init__.py +0 -94
- oscura/exporters/csv.py +0 -303
- oscura/exporters/exporters.py +0 -44
- oscura/exporters/hdf5.py +0 -217
- oscura/exporters/html_export.py +0 -701
- oscura/exporters/json_export.py +0 -338
- oscura/exporters/markdown_export.py +0 -367
- oscura/exporters/matlab_export.py +0 -354
- oscura/exporters/npz_export.py +0 -219
- oscura/exporters/spice_export.py +0 -210
- oscura/exporters/vintage_logic_csv.py +0 -247
- oscura/reporting/vintage_logic_report.py +0 -523
- oscura/search/context.py +0 -149
- oscura/session/__init__.py +0 -34
- oscura/session/annotations.py +0 -289
- oscura/session/history.py +0 -313
- oscura/session/session.py +0 -520
- oscura/visualization/digital_advanced.py +0 -718
- oscura/visualization/figure_manager.py +0 -156
- oscura/workflow/__init__.py +0 -13
- oscura-0.5.0.dist-info/METADATA +0 -407
- oscura-0.5.0.dist-info/RECORD +0 -486
- /oscura/core/{config.py → config/legacy.py} +0 -0
- /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
- /oscura/{extensibility → core/extensibility}/registry.py +0 -0
- /oscura/{plugins → core/plugins}/isolation.py +0 -0
- /oscura/{builders → utils/builders}/signal_builder.py +0 -0
- /oscura/{optimization → utils/optimization}/parallel.py +0 -0
- /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
- /oscura/{streaming → utils/streaming}/__init__.py +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -45,6 +45,142 @@ __all__ = [
|
|
|
45
45
|
]
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
def _normalize_efficiency_values(
|
|
49
|
+
efficiency_values: NDArray[np.floating[Any]],
|
|
50
|
+
efficiency_sets: list[NDArray[np.floating[Any]]] | None,
|
|
51
|
+
) -> tuple[NDArray[np.floating[Any]], list[NDArray[np.floating[Any]]] | None]:
|
|
52
|
+
"""Normalize efficiency values to percentage (0-100).
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
efficiency_values: Efficiency values (0-100 or 0-1).
|
|
56
|
+
efficiency_sets: List of efficiency arrays or None.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Tuple of (normalized_efficiency, normalized_sets).
|
|
60
|
+
"""
|
|
61
|
+
if np.max(efficiency_values) <= 1.0:
|
|
62
|
+
efficiency_values = efficiency_values * 100
|
|
63
|
+
if efficiency_sets is not None:
|
|
64
|
+
efficiency_sets = [e * 100 for e in efficiency_sets]
|
|
65
|
+
|
|
66
|
+
return efficiency_values, efficiency_sets
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _plot_multi_efficiency_curves(
|
|
70
|
+
ax: Axes,
|
|
71
|
+
load_values: NDArray[np.floating[Any]],
|
|
72
|
+
v_in_values: list[float],
|
|
73
|
+
efficiency_sets: list[NDArray[np.floating[Any]]],
|
|
74
|
+
show_peak: bool,
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Plot multiple efficiency curves for different input voltages.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
ax: Matplotlib axes to plot on.
|
|
80
|
+
load_values: Load current or power array.
|
|
81
|
+
v_in_values: List of input voltages.
|
|
82
|
+
efficiency_sets: List of efficiency arrays.
|
|
83
|
+
show_peak: Show peak efficiency markers.
|
|
84
|
+
"""
|
|
85
|
+
colors = ["#3498DB", "#E74C3C", "#27AE60", "#9B59B6", "#F39C12"]
|
|
86
|
+
|
|
87
|
+
for i, (v_in, eff) in enumerate(zip(v_in_values, efficiency_sets, strict=False)):
|
|
88
|
+
color = colors[i % len(colors)]
|
|
89
|
+
ax.plot(load_values, eff, "-", linewidth=2, color=color, label=f"Vin = {v_in}V")
|
|
90
|
+
|
|
91
|
+
if show_peak:
|
|
92
|
+
peak_idx = np.argmax(eff)
|
|
93
|
+
ax.plot(load_values[peak_idx], eff[peak_idx], "o", color=color, markersize=8)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _plot_single_efficiency_curve(
|
|
97
|
+
ax: Axes,
|
|
98
|
+
load_values: NDArray[np.floating[Any]],
|
|
99
|
+
efficiency_values: NDArray[np.floating[Any]],
|
|
100
|
+
load_unit: str,
|
|
101
|
+
show_peak: bool,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Plot single efficiency curve with peak annotation.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
ax: Matplotlib axes to plot on.
|
|
107
|
+
load_values: Load current or power array.
|
|
108
|
+
efficiency_values: Efficiency values in %.
|
|
109
|
+
load_unit: Load axis unit.
|
|
110
|
+
show_peak: Show peak efficiency annotation.
|
|
111
|
+
"""
|
|
112
|
+
ax.plot(load_values, efficiency_values, "-", linewidth=2.5, color="#3498DB", label="Efficiency")
|
|
113
|
+
|
|
114
|
+
if show_peak:
|
|
115
|
+
peak_idx = np.argmax(efficiency_values)
|
|
116
|
+
peak_load = load_values[peak_idx]
|
|
117
|
+
peak_eff = efficiency_values[peak_idx]
|
|
118
|
+
ax.plot(peak_load, peak_eff, "o", color="#E74C3C", markersize=10, zorder=5)
|
|
119
|
+
ax.annotate(
|
|
120
|
+
f"Peak: {peak_eff:.1f}%\n@ {peak_load:.2f} {load_unit}",
|
|
121
|
+
xy=(peak_load, peak_eff),
|
|
122
|
+
xytext=(15, -15),
|
|
123
|
+
textcoords="offset points",
|
|
124
|
+
fontsize=9,
|
|
125
|
+
ha="left",
|
|
126
|
+
bbox={"boxstyle": "round,pad=0.3", "facecolor": "white", "alpha": 0.9},
|
|
127
|
+
arrowprops={"arrowstyle": "->", "connectionstyle": "arc3,rad=0.2"},
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _format_efficiency_plot(
|
|
132
|
+
ax: Axes,
|
|
133
|
+
load_values: NDArray[np.floating[Any]],
|
|
134
|
+
efficiency_values: NDArray[np.floating[Any]],
|
|
135
|
+
efficiency_sets: list[NDArray[np.floating[Any]]] | None,
|
|
136
|
+
load_unit: str,
|
|
137
|
+
target_efficiency: float | None,
|
|
138
|
+
title: str | None,
|
|
139
|
+
) -> None:
|
|
140
|
+
"""Format efficiency plot axes and labels.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
ax: Matplotlib axes to format.
|
|
144
|
+
load_values: Load current or power array.
|
|
145
|
+
efficiency_values: Efficiency values in %.
|
|
146
|
+
efficiency_sets: List of efficiency arrays or None.
|
|
147
|
+
load_unit: Load axis unit.
|
|
148
|
+
target_efficiency: Target efficiency line.
|
|
149
|
+
title: Plot title.
|
|
150
|
+
"""
|
|
151
|
+
# Target efficiency line
|
|
152
|
+
if target_efficiency is not None:
|
|
153
|
+
ax.axhline(
|
|
154
|
+
target_efficiency,
|
|
155
|
+
color="#E74C3C",
|
|
156
|
+
linestyle="--",
|
|
157
|
+
linewidth=1.5,
|
|
158
|
+
label=f"Target: {target_efficiency}%",
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Fill area under curve
|
|
162
|
+
ax.fill_between(
|
|
163
|
+
load_values,
|
|
164
|
+
0,
|
|
165
|
+
efficiency_values if efficiency_sets is None else efficiency_sets[0],
|
|
166
|
+
alpha=0.1,
|
|
167
|
+
color="#3498DB",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Labels and formatting
|
|
171
|
+
ax.set_xlabel(f"Load ({load_unit})", fontsize=11)
|
|
172
|
+
ax.set_ylabel("Efficiency (%)", fontsize=11)
|
|
173
|
+
ax.set_ylim(0, 100)
|
|
174
|
+
ax.set_xlim(load_values[0], load_values[-1])
|
|
175
|
+
ax.grid(True, alpha=0.3)
|
|
176
|
+
ax.legend(loc="best")
|
|
177
|
+
|
|
178
|
+
if title:
|
|
179
|
+
ax.set_title(title, fontsize=12, fontweight="bold")
|
|
180
|
+
else:
|
|
181
|
+
ax.set_title("Converter Efficiency vs Load", fontsize=12, fontweight="bold")
|
|
182
|
+
|
|
183
|
+
|
|
48
184
|
def plot_efficiency_curve(
|
|
49
185
|
load_values: NDArray[np.floating[Any]],
|
|
50
186
|
efficiency_values: NDArray[np.floating[Any]],
|
|
@@ -90,6 +226,7 @@ def plot_efficiency_curve(
|
|
|
90
226
|
if not HAS_MATPLOTLIB:
|
|
91
227
|
raise ImportError("matplotlib is required for visualization")
|
|
92
228
|
|
|
229
|
+
# Figure/axes creation
|
|
93
230
|
if ax is None:
|
|
94
231
|
fig, ax = plt.subplots(figsize=figsize)
|
|
95
232
|
else:
|
|
@@ -98,78 +235,246 @@ def plot_efficiency_curve(
|
|
|
98
235
|
raise ValueError("Axes must have an associated figure")
|
|
99
236
|
fig = cast("Figure", fig_temp)
|
|
100
237
|
|
|
101
|
-
#
|
|
102
|
-
|
|
103
|
-
efficiency_values
|
|
104
|
-
|
|
105
|
-
efficiency_sets = [e * 100 for e in efficiency_sets]
|
|
106
|
-
|
|
107
|
-
# Color palette for multiple curves
|
|
108
|
-
colors = ["#3498DB", "#E74C3C", "#27AE60", "#9B59B6", "#F39C12"]
|
|
238
|
+
# Data preparation/validation
|
|
239
|
+
efficiency_values, efficiency_sets = _normalize_efficiency_values(
|
|
240
|
+
efficiency_values, efficiency_sets
|
|
241
|
+
)
|
|
109
242
|
|
|
243
|
+
# Plotting/rendering
|
|
110
244
|
if v_in_values is not None and efficiency_sets is not None:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
245
|
+
_plot_multi_efficiency_curves(ax, load_values, v_in_values, efficiency_sets, show_peak)
|
|
246
|
+
else:
|
|
247
|
+
_plot_single_efficiency_curve(ax, load_values, efficiency_values, load_unit, show_peak)
|
|
248
|
+
|
|
249
|
+
# Annotation/labeling and layout/formatting
|
|
250
|
+
_format_efficiency_plot(
|
|
251
|
+
ax, load_values, efficiency_values, efficiency_sets, load_unit, target_efficiency, title
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
fig.tight_layout()
|
|
255
|
+
|
|
256
|
+
if save_path is not None:
|
|
257
|
+
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
258
|
+
|
|
259
|
+
if show:
|
|
260
|
+
plt.show()
|
|
261
|
+
|
|
262
|
+
return fig
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _determine_time_scale(time: NDArray[np.floating[Any]], time_unit: str) -> tuple[str, float]:
|
|
266
|
+
"""Determine time axis scale and multiplier.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
time: Time array in seconds.
|
|
270
|
+
time_unit: Requested time unit ("auto" or specific).
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Tuple of (unit_name, multiplier).
|
|
274
|
+
"""
|
|
275
|
+
if time_unit != "auto":
|
|
276
|
+
time_mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9}.get(time_unit, 1.0)
|
|
277
|
+
return time_unit, time_mult
|
|
278
|
+
|
|
279
|
+
max_time = np.max(time)
|
|
280
|
+
if max_time < 1e-6:
|
|
281
|
+
return "us", 1e6
|
|
282
|
+
elif max_time < 1e-3:
|
|
283
|
+
return "ms", 1e3
|
|
119
284
|
else:
|
|
120
|
-
|
|
285
|
+
return "s", 1.0
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _plot_voltage_current_panel(
|
|
289
|
+
ax: Axes,
|
|
290
|
+
time_scaled: NDArray[np.floating[Any]],
|
|
291
|
+
voltage: NDArray[np.floating[Any]],
|
|
292
|
+
current: NDArray[np.floating[Any]] | None,
|
|
293
|
+
v_label: str,
|
|
294
|
+
i_label: str,
|
|
295
|
+
v_color: str,
|
|
296
|
+
i_color: str,
|
|
297
|
+
panel_title: str,
|
|
298
|
+
) -> None:
|
|
299
|
+
"""Plot voltage and current on dual-axis panel.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
ax: Matplotlib axes for voltage.
|
|
303
|
+
time_scaled: Scaled time array.
|
|
304
|
+
voltage: Voltage waveform.
|
|
305
|
+
current: Current waveform (optional).
|
|
306
|
+
v_label: Voltage axis label.
|
|
307
|
+
i_label: Current axis label.
|
|
308
|
+
v_color: Voltage plot color.
|
|
309
|
+
i_color: Current plot color.
|
|
310
|
+
panel_title: Panel title.
|
|
311
|
+
"""
|
|
312
|
+
ax.plot(time_scaled, voltage, v_color, linewidth=1.5)
|
|
313
|
+
ax.set_ylabel(v_label, color=v_color, fontsize=10)
|
|
314
|
+
ax.tick_params(axis="y", labelcolor=v_color)
|
|
315
|
+
ax.grid(True, alpha=0.3)
|
|
316
|
+
|
|
317
|
+
if current is not None:
|
|
318
|
+
ax2 = ax.twinx()
|
|
319
|
+
ax2.plot(time_scaled, current, i_color, linewidth=1.5)
|
|
320
|
+
ax2.set_ylabel(i_label, color=i_color, fontsize=10)
|
|
321
|
+
ax2.tick_params(axis="y", labelcolor=i_color)
|
|
322
|
+
|
|
323
|
+
ax.set_title(panel_title, fontsize=10, fontweight="bold", loc="left")
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _plot_power_panel(
|
|
327
|
+
ax: Axes,
|
|
328
|
+
time_scaled: NDArray[np.floating[Any]],
|
|
329
|
+
v_in: NDArray[np.floating[Any]] | None,
|
|
330
|
+
i_in: NDArray[np.floating[Any]] | None,
|
|
331
|
+
v_out: NDArray[np.floating[Any]] | None,
|
|
332
|
+
i_out: NDArray[np.floating[Any]] | None,
|
|
333
|
+
) -> None:
|
|
334
|
+
"""Plot instantaneous power panel.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
ax: Matplotlib axes.
|
|
338
|
+
time_scaled: Scaled time array.
|
|
339
|
+
v_in: Input voltage (optional).
|
|
340
|
+
i_in: Input current (optional).
|
|
341
|
+
v_out: Output voltage (optional).
|
|
342
|
+
i_out: Output current (optional).
|
|
343
|
+
"""
|
|
344
|
+
if v_in is not None and i_in is not None:
|
|
345
|
+
p_in = v_in * i_in
|
|
121
346
|
ax.plot(
|
|
122
|
-
|
|
347
|
+
time_scaled,
|
|
348
|
+
p_in,
|
|
349
|
+
"#3498DB",
|
|
350
|
+
linewidth=1.5,
|
|
351
|
+
label=f"P_in (avg: {np.mean(p_in):.2f}W)",
|
|
123
352
|
)
|
|
124
353
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
f"Peak: {peak_eff:.1f}%\n@ {peak_load:.2f} {load_unit}",
|
|
132
|
-
xy=(peak_load, peak_eff),
|
|
133
|
-
xytext=(15, -15),
|
|
134
|
-
textcoords="offset points",
|
|
135
|
-
fontsize=9,
|
|
136
|
-
ha="left",
|
|
137
|
-
bbox={"boxstyle": "round,pad=0.3", "facecolor": "white", "alpha": 0.9},
|
|
138
|
-
arrowprops={"arrowstyle": "->", "connectionstyle": "arc3,rad=0.2"},
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
# Target efficiency line
|
|
142
|
-
if target_efficiency is not None:
|
|
143
|
-
ax.axhline(
|
|
144
|
-
target_efficiency,
|
|
145
|
-
color="#E74C3C",
|
|
146
|
-
linestyle="--",
|
|
354
|
+
if v_out is not None and i_out is not None:
|
|
355
|
+
p_out = v_out * i_out
|
|
356
|
+
ax.plot(
|
|
357
|
+
time_scaled,
|
|
358
|
+
p_out,
|
|
359
|
+
"#27AE60",
|
|
147
360
|
linewidth=1.5,
|
|
148
|
-
label=f"
|
|
361
|
+
label=f"P_out (avg: {np.mean(p_out):.2f}W)",
|
|
149
362
|
)
|
|
150
363
|
|
|
151
|
-
|
|
152
|
-
ax.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
364
|
+
ax.set_ylabel("Power (W)", fontsize=10)
|
|
365
|
+
ax.set_title("Instantaneous Power", fontsize=10, fontweight="bold", loc="left")
|
|
366
|
+
ax.legend(loc="upper right", fontsize=9)
|
|
367
|
+
ax.grid(True, alpha=0.3)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _setup_power_waveform_figure(
|
|
371
|
+
figsize: tuple[float, float],
|
|
372
|
+
v_in: NDArray[np.floating[Any]] | None,
|
|
373
|
+
v_out: NDArray[np.floating[Any]] | None,
|
|
374
|
+
show_power: bool,
|
|
375
|
+
) -> tuple[Figure, list[Axes]]:
|
|
376
|
+
"""Setup figure and axes for power waveform plot.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
figsize: Figure size.
|
|
380
|
+
v_in: Input voltage (optional).
|
|
381
|
+
v_out: Output voltage (optional).
|
|
382
|
+
show_power: Show power panel.
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
Tuple of (figure, axes_list).
|
|
386
|
+
"""
|
|
387
|
+
n_plots = sum(
|
|
388
|
+
[
|
|
389
|
+
v_in is not None,
|
|
390
|
+
v_out is not None,
|
|
391
|
+
show_power and (v_in is not None or v_out is not None),
|
|
392
|
+
]
|
|
158
393
|
)
|
|
394
|
+
if n_plots == 0:
|
|
395
|
+
raise ValueError("At least one voltage waveform must be provided")
|
|
159
396
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
ax.set_ylim(0, 100)
|
|
164
|
-
ax.set_xlim(load_values[0], load_values[-1])
|
|
165
|
-
ax.grid(True, alpha=0.3)
|
|
166
|
-
ax.legend(loc="best")
|
|
397
|
+
fig, axes = plt.subplots(n_plots, 1, figsize=figsize, sharex=True)
|
|
398
|
+
if n_plots == 1:
|
|
399
|
+
axes = [axes]
|
|
167
400
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
401
|
+
return fig, axes
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _plot_power_waveform_panels(
|
|
405
|
+
axes: list[Axes],
|
|
406
|
+
time_scaled: NDArray[np.floating[Any]],
|
|
407
|
+
v_in: NDArray[np.floating[Any]] | None,
|
|
408
|
+
i_in: NDArray[np.floating[Any]] | None,
|
|
409
|
+
v_out: NDArray[np.floating[Any]] | None,
|
|
410
|
+
i_out: NDArray[np.floating[Any]] | None,
|
|
411
|
+
show_power: bool,
|
|
412
|
+
) -> None:
|
|
413
|
+
"""Plot all voltage/current panels.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
axes: List of axes to plot on.
|
|
417
|
+
time_scaled: Scaled time array.
|
|
418
|
+
v_in: Input voltage (optional).
|
|
419
|
+
i_in: Input current (optional).
|
|
420
|
+
v_out: Output voltage (optional).
|
|
421
|
+
i_out: Output current (optional).
|
|
422
|
+
show_power: Show power panel.
|
|
423
|
+
"""
|
|
424
|
+
ax_idx = 0
|
|
172
425
|
|
|
426
|
+
if v_in is not None:
|
|
427
|
+
_plot_voltage_current_panel(
|
|
428
|
+
axes[ax_idx],
|
|
429
|
+
time_scaled,
|
|
430
|
+
v_in,
|
|
431
|
+
i_in,
|
|
432
|
+
"V_in (V)",
|
|
433
|
+
"I_in (A)",
|
|
434
|
+
"#3498DB",
|
|
435
|
+
"#E74C3C",
|
|
436
|
+
"Input",
|
|
437
|
+
)
|
|
438
|
+
ax_idx += 1
|
|
439
|
+
|
|
440
|
+
if v_out is not None:
|
|
441
|
+
_plot_voltage_current_panel(
|
|
442
|
+
axes[ax_idx],
|
|
443
|
+
time_scaled,
|
|
444
|
+
v_out,
|
|
445
|
+
i_out,
|
|
446
|
+
"V_out (V)",
|
|
447
|
+
"I_out (A)",
|
|
448
|
+
"#27AE60",
|
|
449
|
+
"#9B59B6",
|
|
450
|
+
"Output",
|
|
451
|
+
)
|
|
452
|
+
ax_idx += 1
|
|
453
|
+
|
|
454
|
+
if show_power:
|
|
455
|
+
_plot_power_panel(axes[ax_idx], time_scaled, v_in, i_in, v_out, i_out)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _finalize_power_waveform_plot(
|
|
459
|
+
fig: Figure,
|
|
460
|
+
axes: list[Axes],
|
|
461
|
+
time_unit: str,
|
|
462
|
+
title: str | None,
|
|
463
|
+
save_path: str | Path | None,
|
|
464
|
+
show: bool,
|
|
465
|
+
) -> None:
|
|
466
|
+
"""Finalize power waveform plot formatting and save.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
fig: Matplotlib figure.
|
|
470
|
+
axes: List of axes.
|
|
471
|
+
time_unit: Time axis unit.
|
|
472
|
+
title: Plot title.
|
|
473
|
+
save_path: Save path.
|
|
474
|
+
show: Display plot.
|
|
475
|
+
"""
|
|
476
|
+
axes[-1].set_xlabel(f"Time ({time_unit})", fontsize=11)
|
|
477
|
+
fig.suptitle(title if title else "Power Converter Waveforms", fontsize=14, fontweight="bold")
|
|
173
478
|
fig.tight_layout()
|
|
174
479
|
|
|
175
480
|
if save_path is not None:
|
|
@@ -178,8 +483,6 @@ def plot_efficiency_curve(
|
|
|
178
483
|
if show:
|
|
179
484
|
plt.show()
|
|
180
485
|
|
|
181
|
-
return fig
|
|
182
|
-
|
|
183
486
|
|
|
184
487
|
def plot_power_waveforms(
|
|
185
488
|
time: NDArray[np.floating[Any]],
|
|
@@ -219,120 +522,178 @@ def plot_power_waveforms(
|
|
|
219
522
|
if not HAS_MATPLOTLIB:
|
|
220
523
|
raise ImportError("matplotlib is required for visualization")
|
|
221
524
|
|
|
222
|
-
#
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
525
|
+
# Setup: determine layout and prepare axes
|
|
526
|
+
fig, axes = _setup_power_waveform_figure(figsize, v_in, v_out, show_power)
|
|
527
|
+
time_unit, time_mult = _determine_time_scale(time, time_unit)
|
|
528
|
+
time_scaled = time * time_mult
|
|
529
|
+
|
|
530
|
+
# Processing: plot data panels
|
|
531
|
+
_plot_power_waveform_panels(axes, time_scaled, v_in, i_in, v_out, i_out, show_power)
|
|
532
|
+
|
|
533
|
+
# Formatting: finalize and save
|
|
534
|
+
_finalize_power_waveform_plot(fig, axes, time_unit, title, save_path, show)
|
|
535
|
+
|
|
536
|
+
return fig
|
|
232
537
|
|
|
233
|
-
fig, axes = plt.subplots(n_plots, 1, figsize=figsize, sharex=True)
|
|
234
|
-
if n_plots == 1:
|
|
235
|
-
axes = [axes]
|
|
236
538
|
|
|
237
|
-
|
|
539
|
+
def _determine_time_unit_and_multiplier(
|
|
540
|
+
time: NDArray[np.floating[Any]], time_unit: str
|
|
541
|
+
) -> tuple[str, float]:
|
|
542
|
+
"""Determine time unit and multiplier for time axis scaling.
|
|
543
|
+
|
|
544
|
+
Args:
|
|
545
|
+
time: Time array in seconds.
|
|
546
|
+
time_unit: Requested time unit ("auto" or specific unit).
|
|
547
|
+
|
|
548
|
+
Returns:
|
|
549
|
+
Tuple of (time_unit, time_multiplier).
|
|
550
|
+
"""
|
|
238
551
|
if time_unit == "auto":
|
|
239
552
|
max_time = np.max(time)
|
|
240
553
|
if max_time < 1e-6:
|
|
241
|
-
|
|
242
|
-
time_mult = 1e6
|
|
554
|
+
return "us", 1e6
|
|
243
555
|
elif max_time < 1e-3:
|
|
244
|
-
|
|
245
|
-
time_mult = 1e3
|
|
556
|
+
return "ms", 1e3
|
|
246
557
|
else:
|
|
247
|
-
|
|
248
|
-
time_mult = 1.0
|
|
558
|
+
return "s", 1.0
|
|
249
559
|
else:
|
|
250
560
|
time_mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9}.get(time_unit, 1.0)
|
|
561
|
+
return time_unit, time_mult
|
|
251
562
|
|
|
252
|
-
time_scaled = time * time_mult
|
|
253
563
|
|
|
254
|
-
|
|
564
|
+
def _calculate_ripple_metrics(
|
|
565
|
+
voltage: NDArray[np.floating[Any]],
|
|
566
|
+
) -> tuple[float, NDArray[np.floating[Any]], float, float]:
|
|
567
|
+
"""Calculate DC level and AC ripple metrics.
|
|
255
568
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
ax = axes[ax_idx]
|
|
259
|
-
ax.plot(time_scaled, v_in, "#3498DB", linewidth=1.5, label="V_in")
|
|
260
|
-
ax.set_ylabel("V_in (V)", color="#3498DB", fontsize=10)
|
|
261
|
-
ax.tick_params(axis="y", labelcolor="#3498DB")
|
|
262
|
-
ax.grid(True, alpha=0.3)
|
|
263
|
-
|
|
264
|
-
if i_in is not None:
|
|
265
|
-
ax2 = ax.twinx()
|
|
266
|
-
ax2.plot(time_scaled, i_in, "#E74C3C", linewidth=1.5, label="I_in")
|
|
267
|
-
ax2.set_ylabel("I_in (A)", color="#E74C3C", fontsize=10)
|
|
268
|
-
ax2.tick_params(axis="y", labelcolor="#E74C3C")
|
|
269
|
-
|
|
270
|
-
ax.set_title("Input", fontsize=10, fontweight="bold", loc="left")
|
|
271
|
-
ax_idx += 1
|
|
569
|
+
Args:
|
|
570
|
+
voltage: Voltage waveform array.
|
|
272
571
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if i_out is not None:
|
|
282
|
-
ax2 = ax.twinx()
|
|
283
|
-
ax2.plot(time_scaled, i_out, "#9B59B6", linewidth=1.5, label="I_out")
|
|
284
|
-
ax2.set_ylabel("I_out (A)", color="#9B59B6", fontsize=10)
|
|
285
|
-
ax2.tick_params(axis="y", labelcolor="#9B59B6")
|
|
286
|
-
|
|
287
|
-
ax.set_title("Output", fontsize=10, fontweight="bold", loc="left")
|
|
288
|
-
ax_idx += 1
|
|
572
|
+
Returns:
|
|
573
|
+
Tuple of (dc_level, ac_ripple, ripple_pp, ripple_rms).
|
|
574
|
+
"""
|
|
575
|
+
dc_level = float(np.mean(voltage))
|
|
576
|
+
ac_ripple = voltage - dc_level
|
|
577
|
+
ripple_pp = float(np.ptp(ac_ripple))
|
|
578
|
+
ripple_rms = float(np.std(ac_ripple))
|
|
579
|
+
return dc_level, ac_ripple, ripple_pp, ripple_rms
|
|
289
580
|
|
|
290
|
-
# Power panel
|
|
291
|
-
if show_power:
|
|
292
|
-
ax = axes[ax_idx]
|
|
293
|
-
|
|
294
|
-
if v_in is not None and i_in is not None:
|
|
295
|
-
p_in = v_in * i_in
|
|
296
|
-
ax.plot(
|
|
297
|
-
time_scaled,
|
|
298
|
-
p_in,
|
|
299
|
-
"#3498DB",
|
|
300
|
-
linewidth=1.5,
|
|
301
|
-
label=f"P_in (avg: {np.mean(p_in):.2f}W)",
|
|
302
|
-
)
|
|
303
|
-
|
|
304
|
-
if v_out is not None and i_out is not None:
|
|
305
|
-
p_out = v_out * i_out
|
|
306
|
-
ax.plot(
|
|
307
|
-
time_scaled,
|
|
308
|
-
p_out,
|
|
309
|
-
"#27AE60",
|
|
310
|
-
linewidth=1.5,
|
|
311
|
-
label=f"P_out (avg: {np.mean(p_out):.2f}W)",
|
|
312
|
-
)
|
|
313
|
-
|
|
314
|
-
ax.set_ylabel("Power (W)", fontsize=10)
|
|
315
|
-
ax.set_title("Instantaneous Power", fontsize=10, fontweight="bold", loc="left")
|
|
316
|
-
ax.legend(loc="upper right", fontsize=9)
|
|
317
|
-
ax.grid(True, alpha=0.3)
|
|
318
|
-
|
|
319
|
-
# X-axis label on bottom
|
|
320
|
-
axes[-1].set_xlabel(f"Time ({time_unit})", fontsize=11)
|
|
321
581
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
582
|
+
def _plot_dc_coupled_waveform(
|
|
583
|
+
ax: Axes,
|
|
584
|
+
time_scaled: NDArray[np.floating[Any]],
|
|
585
|
+
voltage: NDArray[np.floating[Any]],
|
|
586
|
+
dc_level: float,
|
|
587
|
+
) -> None:
|
|
588
|
+
"""Plot DC-coupled waveform with DC level indicator.
|
|
326
589
|
|
|
327
|
-
|
|
590
|
+
Args:
|
|
591
|
+
ax: Matplotlib axes to plot on.
|
|
592
|
+
time_scaled: Scaled time array.
|
|
593
|
+
voltage: Voltage waveform.
|
|
594
|
+
dc_level: DC level value.
|
|
595
|
+
"""
|
|
596
|
+
ax.plot(time_scaled, voltage, "#3498DB", linewidth=1)
|
|
597
|
+
ax.axhline(
|
|
598
|
+
dc_level, color="#E74C3C", linestyle="--", linewidth=1.5, label=f"DC: {dc_level:.3f}V"
|
|
599
|
+
)
|
|
600
|
+
ax.set_ylabel("Voltage (V)", fontsize=10)
|
|
601
|
+
ax.set_title("DC-Coupled Waveform", fontsize=10, fontweight="bold", loc="left")
|
|
602
|
+
ax.legend(loc="upper right", fontsize=9)
|
|
603
|
+
ax.grid(True, alpha=0.3)
|
|
328
604
|
|
|
329
|
-
if save_path is not None:
|
|
330
|
-
fig.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
331
605
|
|
|
332
|
-
|
|
333
|
-
|
|
606
|
+
def _plot_ac_ripple_waveform(
|
|
607
|
+
ax: Axes,
|
|
608
|
+
time_scaled: NDArray[np.floating[Any]],
|
|
609
|
+
ac_ripple: NDArray[np.floating[Any]],
|
|
610
|
+
ripple_pp: float,
|
|
611
|
+
ripple_rms: float,
|
|
612
|
+
) -> None:
|
|
613
|
+
"""Plot AC-coupled ripple waveform with peak-to-peak annotation.
|
|
614
|
+
|
|
615
|
+
Args:
|
|
616
|
+
ax: Matplotlib axes to plot on.
|
|
617
|
+
time_scaled: Scaled time array.
|
|
618
|
+
ac_ripple: AC ripple waveform.
|
|
619
|
+
ripple_pp: Peak-to-peak ripple voltage.
|
|
620
|
+
ripple_rms: RMS ripple voltage.
|
|
621
|
+
"""
|
|
622
|
+
ax.plot(time_scaled, ac_ripple * 1e3, "#27AE60", linewidth=1) # Convert to mV
|
|
623
|
+
ax.axhline(0, color="gray", linestyle="-", linewidth=0.5)
|
|
624
|
+
|
|
625
|
+
# Mark peak-to-peak
|
|
626
|
+
max_idx = int(np.argmax(ac_ripple))
|
|
627
|
+
min_idx = int(np.argmin(ac_ripple))
|
|
628
|
+
ax.annotate(
|
|
629
|
+
"",
|
|
630
|
+
xy=(time_scaled[max_idx], ac_ripple[max_idx] * 1e3),
|
|
631
|
+
xytext=(time_scaled[min_idx], ac_ripple[min_idx] * 1e3),
|
|
632
|
+
arrowprops={"arrowstyle": "<->", "color": "#E74C3C", "lw": 1.5},
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
ax.set_ylabel("Ripple (mV)", fontsize=10)
|
|
636
|
+
ax.set_title(
|
|
637
|
+
f"AC Ripple (pk-pk: {ripple_pp * 1e3:.2f}mV, RMS: {ripple_rms * 1e3:.2f}mV)",
|
|
638
|
+
fontsize=10,
|
|
639
|
+
fontweight="bold",
|
|
640
|
+
loc="left",
|
|
641
|
+
)
|
|
642
|
+
ax.grid(True, alpha=0.3)
|
|
334
643
|
|
|
335
|
-
|
|
644
|
+
|
|
645
|
+
def _plot_ripple_spectrum(
|
|
646
|
+
ax: Axes,
|
|
647
|
+
ac_ripple: NDArray[np.floating[Any]],
|
|
648
|
+
sample_rate: float,
|
|
649
|
+
) -> None:
|
|
650
|
+
"""Plot ripple frequency spectrum.
|
|
651
|
+
|
|
652
|
+
Args:
|
|
653
|
+
ax: Matplotlib axes to plot on.
|
|
654
|
+
ac_ripple: AC ripple waveform.
|
|
655
|
+
sample_rate: Sample rate in Hz.
|
|
656
|
+
"""
|
|
657
|
+
n_fft = len(ac_ripple)
|
|
658
|
+
freq = np.fft.rfftfreq(n_fft, 1 / sample_rate)
|
|
659
|
+
fft_mag = np.abs(np.fft.rfft(ac_ripple)) / n_fft * 2
|
|
660
|
+
fft_db = 20 * np.log10(fft_mag + 1e-12)
|
|
661
|
+
|
|
662
|
+
# Find dominant ripple frequency
|
|
663
|
+
peak_idx = int(np.argmax(fft_mag[1:])) + 1 # Skip DC
|
|
664
|
+
peak_freq = freq[peak_idx]
|
|
665
|
+
|
|
666
|
+
# Plot in kHz
|
|
667
|
+
freq_khz = freq / 1e3
|
|
668
|
+
ax.plot(freq_khz, fft_db, "#9B59B6", linewidth=1)
|
|
669
|
+
ax.plot(
|
|
670
|
+
freq_khz[peak_idx],
|
|
671
|
+
fft_db[peak_idx],
|
|
672
|
+
"ro",
|
|
673
|
+
markersize=8,
|
|
674
|
+
label=f"Peak: {peak_freq / 1e3:.1f}kHz",
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
ax.set_ylabel("Magnitude (dB)", fontsize=10)
|
|
678
|
+
ax.set_xlabel("Frequency (kHz)", fontsize=10)
|
|
679
|
+
ax.set_title("Ripple Spectrum", fontsize=10, fontweight="bold", loc="left")
|
|
680
|
+
ax.set_xlim(0, min(freq_khz[-1], sample_rate / 2e3))
|
|
681
|
+
ax.legend(loc="upper right", fontsize=9)
|
|
682
|
+
ax.grid(True, alpha=0.3)
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def _estimate_sample_rate(time: NDArray[np.floating[Any]]) -> float:
|
|
686
|
+
"""Estimate sample rate from time array.
|
|
687
|
+
|
|
688
|
+
Args:
|
|
689
|
+
time: Time array in seconds.
|
|
690
|
+
|
|
691
|
+
Returns:
|
|
692
|
+
Estimated sample rate in Hz.
|
|
693
|
+
"""
|
|
694
|
+
if len(time) > 1:
|
|
695
|
+
return float(1 / (time[1] - time[0]))
|
|
696
|
+
return 1e6 # Default 1 MHz
|
|
336
697
|
|
|
337
698
|
|
|
338
699
|
def plot_ripple_waveform(
|
|
@@ -361,16 +722,21 @@ def plot_ripple_waveform(
|
|
|
361
722
|
ax: Matplotlib axes (creates multi-panel if None).
|
|
362
723
|
figsize: Figure size.
|
|
363
724
|
title: Plot title.
|
|
364
|
-
time_unit: Time axis unit.
|
|
725
|
+
time_unit: Time axis unit ("auto", "s", "ms", "us", "ns").
|
|
365
726
|
show_dc: Show DC-coupled waveform.
|
|
366
727
|
show_ac: Show AC-coupled ripple.
|
|
367
728
|
show_spectrum: Show ripple spectrum.
|
|
368
|
-
sample_rate: Sample rate for FFT (
|
|
729
|
+
sample_rate: Sample rate for FFT (estimated if None).
|
|
369
730
|
show: Display plot.
|
|
370
731
|
save_path: Save path.
|
|
371
732
|
|
|
372
733
|
Returns:
|
|
373
734
|
Matplotlib Figure object.
|
|
735
|
+
|
|
736
|
+
Example:
|
|
737
|
+
>>> time = np.linspace(0, 1e-3, 1000) # 1ms capture
|
|
738
|
+
>>> voltage = 5.0 + 0.01 * np.sin(2 * np.pi * 100e3 * time) # 5V + 10mV ripple
|
|
739
|
+
>>> fig = plot_ripple_waveform(time, voltage, show_spectrum=True)
|
|
374
740
|
"""
|
|
375
741
|
if not HAS_MATPLOTLIB:
|
|
376
742
|
raise ImportError("matplotlib is required for visualization")
|
|
@@ -383,107 +749,33 @@ def plot_ripple_waveform(
|
|
|
383
749
|
if n_plots == 1:
|
|
384
750
|
axes = [axes]
|
|
385
751
|
|
|
386
|
-
#
|
|
387
|
-
|
|
388
|
-
max_time = np.max(time)
|
|
389
|
-
if max_time < 1e-6:
|
|
390
|
-
time_unit = "us"
|
|
391
|
-
time_mult = 1e6
|
|
392
|
-
elif max_time < 1e-3:
|
|
393
|
-
time_unit = "ms"
|
|
394
|
-
time_mult = 1e3
|
|
395
|
-
else:
|
|
396
|
-
time_unit = "s"
|
|
397
|
-
time_mult = 1.0
|
|
398
|
-
else:
|
|
399
|
-
time_mult = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9}.get(time_unit, 1.0)
|
|
400
|
-
|
|
752
|
+
# Determine time scaling
|
|
753
|
+
time_unit, time_mult = _determine_time_unit_and_multiplier(time, time_unit)
|
|
401
754
|
time_scaled = time * time_mult
|
|
402
755
|
|
|
403
|
-
# Calculate
|
|
404
|
-
dc_level =
|
|
405
|
-
ac_ripple = voltage - dc_level
|
|
406
|
-
ripple_pp = np.ptp(ac_ripple)
|
|
407
|
-
ripple_rms = np.std(ac_ripple)
|
|
756
|
+
# Calculate ripple metrics
|
|
757
|
+
dc_level, ac_ripple, ripple_pp, ripple_rms = _calculate_ripple_metrics(voltage)
|
|
408
758
|
|
|
409
759
|
ax_idx = 0
|
|
410
760
|
|
|
411
|
-
# DC-coupled
|
|
761
|
+
# Plot DC-coupled waveform
|
|
412
762
|
if show_dc:
|
|
413
|
-
|
|
414
|
-
ax.plot(time_scaled, voltage, "#3498DB", linewidth=1)
|
|
415
|
-
ax.axhline(
|
|
416
|
-
dc_level, color="#E74C3C", linestyle="--", linewidth=1.5, label=f"DC: {dc_level:.3f}V"
|
|
417
|
-
)
|
|
418
|
-
ax.set_ylabel("Voltage (V)", fontsize=10)
|
|
419
|
-
ax.set_title("DC-Coupled Waveform", fontsize=10, fontweight="bold", loc="left")
|
|
420
|
-
ax.legend(loc="upper right", fontsize=9)
|
|
421
|
-
ax.grid(True, alpha=0.3)
|
|
763
|
+
_plot_dc_coupled_waveform(axes[ax_idx], time_scaled, voltage, dc_level)
|
|
422
764
|
ax_idx += 1
|
|
423
765
|
|
|
424
|
-
# AC-coupled
|
|
766
|
+
# Plot AC-coupled ripple
|
|
425
767
|
if show_ac:
|
|
426
|
-
|
|
427
|
-
ax.plot(time_scaled, ac_ripple * 1e3, "#27AE60", linewidth=1) # Convert to mV
|
|
428
|
-
ax.axhline(0, color="gray", linestyle="-", linewidth=0.5)
|
|
429
|
-
|
|
430
|
-
# Mark peak-to-peak
|
|
431
|
-
max_idx = np.argmax(ac_ripple)
|
|
432
|
-
min_idx = np.argmin(ac_ripple)
|
|
433
|
-
ax.annotate(
|
|
434
|
-
"",
|
|
435
|
-
xy=(time_scaled[max_idx], ac_ripple[max_idx] * 1e3),
|
|
436
|
-
xytext=(time_scaled[min_idx], ac_ripple[min_idx] * 1e3),
|
|
437
|
-
arrowprops={"arrowstyle": "<->", "color": "#E74C3C", "lw": 1.5},
|
|
438
|
-
)
|
|
439
|
-
|
|
440
|
-
ax.set_ylabel("Ripple (mV)", fontsize=10)
|
|
441
|
-
ax.set_title(
|
|
442
|
-
f"AC Ripple (pk-pk: {ripple_pp * 1e3:.2f}mV, RMS: {ripple_rms * 1e3:.2f}mV)",
|
|
443
|
-
fontsize=10,
|
|
444
|
-
fontweight="bold",
|
|
445
|
-
loc="left",
|
|
446
|
-
)
|
|
447
|
-
ax.grid(True, alpha=0.3)
|
|
768
|
+
_plot_ac_ripple_waveform(axes[ax_idx], time_scaled, ac_ripple, ripple_pp, ripple_rms)
|
|
448
769
|
ax_idx += 1
|
|
449
770
|
|
|
450
|
-
#
|
|
771
|
+
# Plot ripple spectrum
|
|
451
772
|
if show_spectrum:
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
if sample_rate is None:
|
|
455
|
-
# Estimate from time array
|
|
456
|
-
sample_rate = 1 / (time[1] - time[0]) if len(time) > 1 else 1e6
|
|
457
|
-
|
|
458
|
-
n_fft = len(voltage)
|
|
459
|
-
freq = np.fft.rfftfreq(n_fft, 1 / sample_rate)
|
|
460
|
-
fft_mag = np.abs(np.fft.rfft(ac_ripple)) / n_fft * 2
|
|
461
|
-
fft_db = 20 * np.log10(fft_mag + 1e-12)
|
|
462
|
-
|
|
463
|
-
# Find dominant ripple frequency
|
|
464
|
-
peak_idx = np.argmax(fft_mag[1:]) + 1 # Skip DC
|
|
465
|
-
peak_freq = freq[peak_idx]
|
|
466
|
-
|
|
467
|
-
# Plot in kHz
|
|
468
|
-
freq_khz = freq / 1e3
|
|
469
|
-
ax.plot(freq_khz, fft_db, "#9B59B6", linewidth=1)
|
|
470
|
-
ax.plot(
|
|
471
|
-
freq_khz[peak_idx],
|
|
472
|
-
fft_db[peak_idx],
|
|
473
|
-
"ro",
|
|
474
|
-
markersize=8,
|
|
475
|
-
label=f"Peak: {peak_freq / 1e3:.1f}kHz",
|
|
476
|
-
)
|
|
477
|
-
|
|
478
|
-
ax.set_ylabel("Magnitude (dB)", fontsize=10)
|
|
479
|
-
ax.set_xlabel("Frequency (kHz)", fontsize=10)
|
|
480
|
-
ax.set_title("Ripple Spectrum", fontsize=10, fontweight="bold", loc="left")
|
|
481
|
-
ax.set_xlim(0, min(freq_khz[-1], sample_rate / 2e3))
|
|
482
|
-
ax.legend(loc="upper right", fontsize=9)
|
|
483
|
-
ax.grid(True, alpha=0.3)
|
|
773
|
+
sr = sample_rate if sample_rate is not None else _estimate_sample_rate(time)
|
|
774
|
+
_plot_ripple_spectrum(axes[ax_idx], ac_ripple, sr)
|
|
484
775
|
else:
|
|
485
776
|
axes[-1].set_xlabel(f"Time ({time_unit})", fontsize=11)
|
|
486
777
|
|
|
778
|
+
# Finalize figure
|
|
487
779
|
if title:
|
|
488
780
|
fig.suptitle(title, fontsize=14, fontweight="bold")
|
|
489
781
|
else:
|
|
@@ -500,6 +792,90 @@ def plot_ripple_waveform(
|
|
|
500
792
|
return fig
|
|
501
793
|
|
|
502
794
|
|
|
795
|
+
def _create_loss_autopct_formatter(
|
|
796
|
+
show_watts: bool, total_loss: float
|
|
797
|
+
) -> str | Callable[[float], str]:
|
|
798
|
+
"""Create autopct formatter for pie chart labels.
|
|
799
|
+
|
|
800
|
+
Args:
|
|
801
|
+
show_watts: Whether to show watt values.
|
|
802
|
+
total_loss: Total loss in watts.
|
|
803
|
+
|
|
804
|
+
Returns:
|
|
805
|
+
Format string or callable for autopct.
|
|
806
|
+
"""
|
|
807
|
+
if show_watts:
|
|
808
|
+
|
|
809
|
+
def autopct_func(pct: float) -> str:
|
|
810
|
+
watts = pct / 100 * total_loss
|
|
811
|
+
return f"{pct:.1f}%\n({watts * 1e3:.1f}mW)"
|
|
812
|
+
|
|
813
|
+
return autopct_func
|
|
814
|
+
return "%1.1f%%"
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def _create_loss_pie_chart(
|
|
818
|
+
ax: Axes,
|
|
819
|
+
labels: list[str],
|
|
820
|
+
values: list[float],
|
|
821
|
+
colors: list[str],
|
|
822
|
+
autopct_val: str | Callable[[float], str],
|
|
823
|
+
) -> tuple[Any, ...]:
|
|
824
|
+
"""Create pie chart with loss breakdown.
|
|
825
|
+
|
|
826
|
+
Args:
|
|
827
|
+
ax: Matplotlib axes.
|
|
828
|
+
labels: Loss type labels.
|
|
829
|
+
values: Loss values.
|
|
830
|
+
colors: Color palette.
|
|
831
|
+
autopct_val: Autopct formatter.
|
|
832
|
+
|
|
833
|
+
Returns:
|
|
834
|
+
Pie chart result tuple.
|
|
835
|
+
"""
|
|
836
|
+
return ax.pie(
|
|
837
|
+
values,
|
|
838
|
+
labels=labels,
|
|
839
|
+
autopct=autopct_val,
|
|
840
|
+
colors=colors[: len(labels)],
|
|
841
|
+
startangle=90,
|
|
842
|
+
explode=[0.02] * len(labels),
|
|
843
|
+
shadow=True,
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
def _format_loss_pie_chart(
|
|
848
|
+
ax: Axes, pie_result: tuple[Any, ...], total_loss: float, title: str | None
|
|
849
|
+
) -> None:
|
|
850
|
+
"""Format pie chart styling and annotations.
|
|
851
|
+
|
|
852
|
+
Args:
|
|
853
|
+
ax: Matplotlib axes.
|
|
854
|
+
pie_result: Result from ax.pie.
|
|
855
|
+
total_loss: Total loss value.
|
|
856
|
+
title: Chart title.
|
|
857
|
+
"""
|
|
858
|
+
# Style autotexts if available
|
|
859
|
+
if len(pie_result) >= 3:
|
|
860
|
+
autotexts = pie_result[2]
|
|
861
|
+
for autotext in autotexts:
|
|
862
|
+
autotext.set_fontsize(9)
|
|
863
|
+
autotext.set_fontweight("bold")
|
|
864
|
+
|
|
865
|
+
# Add total loss annotation
|
|
866
|
+
ax.text(
|
|
867
|
+
0,
|
|
868
|
+
-1.3,
|
|
869
|
+
f"Total Loss: {total_loss * 1e3:.1f}mW ({total_loss:.3f}W)",
|
|
870
|
+
ha="center",
|
|
871
|
+
fontsize=11,
|
|
872
|
+
fontweight="bold",
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
ax.set_aspect("equal")
|
|
876
|
+
ax.set_title(title if title else "Power Loss Breakdown", fontsize=12, fontweight="bold", pad=20)
|
|
877
|
+
|
|
878
|
+
|
|
503
879
|
def plot_loss_breakdown(
|
|
504
880
|
loss_values: dict[str, float],
|
|
505
881
|
*,
|
|
@@ -539,6 +915,7 @@ def plot_loss_breakdown(
|
|
|
539
915
|
if not HAS_MATPLOTLIB:
|
|
540
916
|
raise ImportError("matplotlib is required for visualization")
|
|
541
917
|
|
|
918
|
+
# Setup: create figure and extract data
|
|
542
919
|
if ax is None:
|
|
543
920
|
fig, ax = plt.subplots(figsize=figsize)
|
|
544
921
|
else:
|
|
@@ -550,8 +927,6 @@ def plot_loss_breakdown(
|
|
|
550
927
|
labels = list(loss_values.keys())
|
|
551
928
|
values = list(loss_values.values())
|
|
552
929
|
total_loss = sum(values)
|
|
553
|
-
|
|
554
|
-
# Color palette
|
|
555
930
|
colors = [
|
|
556
931
|
"#3498DB",
|
|
557
932
|
"#E74C3C",
|
|
@@ -563,58 +938,12 @@ def plot_loss_breakdown(
|
|
|
563
938
|
"#95A5A6",
|
|
564
939
|
]
|
|
565
940
|
|
|
566
|
-
#
|
|
567
|
-
autopct_val
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
def autopct_func(pct: float) -> str:
|
|
571
|
-
watts = pct / 100 * total_loss
|
|
572
|
-
return f"{pct:.1f}%\n({watts * 1e3:.1f}mW)"
|
|
573
|
-
|
|
574
|
-
autopct_val = autopct_func
|
|
575
|
-
else:
|
|
576
|
-
autopct_val = "%1.1f%%"
|
|
577
|
-
|
|
578
|
-
pie_result = ax.pie(
|
|
579
|
-
values,
|
|
580
|
-
labels=labels,
|
|
581
|
-
autopct=autopct_val, # type: ignore[arg-type]
|
|
582
|
-
colors=colors[: len(labels)],
|
|
583
|
-
startangle=90,
|
|
584
|
-
explode=[0.02] * len(labels),
|
|
585
|
-
shadow=True,
|
|
586
|
-
)
|
|
587
|
-
# ax.pie returns (wedges, texts, autotexts) when autopct is provided
|
|
588
|
-
# Unpack with length check for type safety
|
|
589
|
-
if len(pie_result) >= 3:
|
|
590
|
-
_wedges = pie_result[0]
|
|
591
|
-
_texts = pie_result[1]
|
|
592
|
-
autotexts = pie_result[2]
|
|
593
|
-
else:
|
|
594
|
-
autotexts = []
|
|
595
|
-
|
|
596
|
-
# Style autotexts
|
|
597
|
-
for autotext in autotexts:
|
|
598
|
-
autotext.set_fontsize(9)
|
|
599
|
-
autotext.set_fontweight("bold")
|
|
600
|
-
|
|
601
|
-
# Add total loss annotation
|
|
602
|
-
ax.text(
|
|
603
|
-
0,
|
|
604
|
-
-1.3,
|
|
605
|
-
f"Total Loss: {total_loss * 1e3:.1f}mW ({total_loss:.3f}W)",
|
|
606
|
-
ha="center",
|
|
607
|
-
fontsize=11,
|
|
608
|
-
fontweight="bold",
|
|
609
|
-
)
|
|
610
|
-
|
|
611
|
-
ax.set_aspect("equal")
|
|
612
|
-
|
|
613
|
-
if title:
|
|
614
|
-
ax.set_title(title, fontsize=12, fontweight="bold", pad=20)
|
|
615
|
-
else:
|
|
616
|
-
ax.set_title("Power Loss Breakdown", fontsize=12, fontweight="bold", pad=20)
|
|
941
|
+
# Processing: create pie chart
|
|
942
|
+
autopct_val = _create_loss_autopct_formatter(show_watts, total_loss)
|
|
943
|
+
pie_result = _create_loss_pie_chart(ax, labels, values, colors, autopct_val)
|
|
617
944
|
|
|
945
|
+
# Result building: format and finalize
|
|
946
|
+
_format_loss_pie_chart(ax, pie_result, total_loss, title)
|
|
618
947
|
fig.tight_layout()
|
|
619
948
|
|
|
620
949
|
if save_path is not None:
|