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,598 @@
|
|
|
1
|
+
"""Reactive component extraction for Oscura.
|
|
2
|
+
|
|
3
|
+
This module provides capacitance and inductance measurement from
|
|
4
|
+
waveform data, including parasitic extraction.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.component import measure_capacitance, measure_inductance
|
|
9
|
+
>>> C = measure_capacitance(voltage_trace, current_trace)
|
|
10
|
+
>>> L = measure_inductance(voltage_trace, current_trace)
|
|
11
|
+
|
|
12
|
+
References:
|
|
13
|
+
IEEE 181-2011: Standard for Transitional Waveform Definitions
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import TYPE_CHECKING, Any, Literal, cast
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
from oscura.core.exceptions import AnalysisError, InsufficientDataError
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from numpy.typing import NDArray
|
|
27
|
+
|
|
28
|
+
from oscura.core.types import WaveformTrace
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class CapacitanceMeasurement:
|
|
33
|
+
"""Result of capacitance measurement.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
capacitance: Measured capacitance in Farads.
|
|
37
|
+
esr: Equivalent Series Resistance in ohms.
|
|
38
|
+
method: Measurement method used.
|
|
39
|
+
confidence: Confidence in measurement (0-1).
|
|
40
|
+
statistics: Additional measurement statistics.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
capacitance: float
|
|
44
|
+
esr: float = 0.0
|
|
45
|
+
method: str = ""
|
|
46
|
+
confidence: float = 1.0
|
|
47
|
+
statistics: dict = field(default_factory=dict) # type: ignore[type-arg]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class InductanceMeasurement:
|
|
52
|
+
"""Result of inductance measurement.
|
|
53
|
+
|
|
54
|
+
Attributes:
|
|
55
|
+
inductance: Measured inductance in Henrys.
|
|
56
|
+
dcr: DC Resistance in ohms.
|
|
57
|
+
q_factor: Quality factor at measurement frequency.
|
|
58
|
+
method: Measurement method used.
|
|
59
|
+
confidence: Confidence in measurement (0-1).
|
|
60
|
+
statistics: Additional measurement statistics.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
inductance: float
|
|
64
|
+
dcr: float = 0.0
|
|
65
|
+
q_factor: float | None = None
|
|
66
|
+
method: str = ""
|
|
67
|
+
confidence: float = 1.0
|
|
68
|
+
statistics: dict = field(default_factory=dict) # type: ignore[type-arg]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class ParasiticExtraction:
|
|
73
|
+
"""Result of parasitic parameter extraction.
|
|
74
|
+
|
|
75
|
+
Attributes:
|
|
76
|
+
capacitance: Parasitic capacitance in Farads.
|
|
77
|
+
inductance: Parasitic inductance in Henrys.
|
|
78
|
+
resistance: Parasitic resistance in ohms.
|
|
79
|
+
model_type: Equivalent circuit model type.
|
|
80
|
+
resonant_freq: Self-resonant frequency (if applicable).
|
|
81
|
+
fit_quality: Quality of model fit (R-squared).
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
capacitance: float
|
|
85
|
+
inductance: float
|
|
86
|
+
resistance: float
|
|
87
|
+
model_type: Literal["series_RLC", "parallel_RLC", "pi", "tee"]
|
|
88
|
+
resonant_freq: float | None = None
|
|
89
|
+
fit_quality: float = 0.0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def measure_capacitance(
|
|
93
|
+
voltage_trace: WaveformTrace,
|
|
94
|
+
current_trace: WaveformTrace | None = None,
|
|
95
|
+
*,
|
|
96
|
+
method: Literal["charge", "slope", "frequency"] = "charge",
|
|
97
|
+
resistance: float | None = None,
|
|
98
|
+
) -> CapacitanceMeasurement:
|
|
99
|
+
"""Measure capacitance from voltage/current waveforms.
|
|
100
|
+
|
|
101
|
+
Calculates capacitance using the relationship C = Q/V or C = I/(dV/dt).
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
voltage_trace: Voltage waveform across capacitor.
|
|
105
|
+
current_trace: Current waveform through capacitor (optional for
|
|
106
|
+
some methods).
|
|
107
|
+
method: Measurement method:
|
|
108
|
+
- "charge": C = integral(I*dt) / delta_V
|
|
109
|
+
- "slope": C = I / (dV/dt)
|
|
110
|
+
- "frequency": Extract from RC time constant
|
|
111
|
+
resistance: Known resistance for frequency method.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
CapacitanceMeasurement with capacitance value.
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
AnalysisError: If measurement conditions are not met.
|
|
118
|
+
InsufficientDataError: If insufficient samples are provided.
|
|
119
|
+
|
|
120
|
+
Example:
|
|
121
|
+
>>> C = measure_capacitance(voltage, current)
|
|
122
|
+
>>> print(f"C = {C.capacitance * 1e12:.1f} pF")
|
|
123
|
+
|
|
124
|
+
References:
|
|
125
|
+
COMP-002
|
|
126
|
+
"""
|
|
127
|
+
voltage = voltage_trace.data.astype(np.float64)
|
|
128
|
+
sample_rate = voltage_trace.metadata.sample_rate
|
|
129
|
+
dt = 1.0 / sample_rate
|
|
130
|
+
|
|
131
|
+
if len(voltage) < 10:
|
|
132
|
+
raise InsufficientDataError(
|
|
133
|
+
"Capacitance measurement requires at least 10 samples",
|
|
134
|
+
required=10,
|
|
135
|
+
available=len(voltage),
|
|
136
|
+
analysis_type="capacitance",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if method == "charge" and current_trace is not None:
|
|
140
|
+
# C = Q / V = integral(I*dt) / delta_V
|
|
141
|
+
current = current_trace.data.astype(np.float64)
|
|
142
|
+
min_len = min(len(voltage), len(current))
|
|
143
|
+
voltage = voltage[:min_len]
|
|
144
|
+
current = current[:min_len]
|
|
145
|
+
|
|
146
|
+
# Integrate current to get charge
|
|
147
|
+
charge = np.cumsum(current) * dt
|
|
148
|
+
delta_v = np.max(voltage) - np.min(voltage)
|
|
149
|
+
|
|
150
|
+
if delta_v > 1e-10:
|
|
151
|
+
delta_q = np.max(charge) - np.min(charge)
|
|
152
|
+
capacitance = delta_q / delta_v
|
|
153
|
+
else:
|
|
154
|
+
raise AnalysisError("Voltage change too small for capacitance measurement")
|
|
155
|
+
|
|
156
|
+
# Estimate ESR from phase relationship
|
|
157
|
+
esr = _estimate_esr(voltage, current, sample_rate)
|
|
158
|
+
|
|
159
|
+
return CapacitanceMeasurement(
|
|
160
|
+
capacitance=float(abs(capacitance)),
|
|
161
|
+
esr=esr,
|
|
162
|
+
method="charge_integration",
|
|
163
|
+
confidence=0.9,
|
|
164
|
+
statistics={
|
|
165
|
+
"delta_v": delta_v,
|
|
166
|
+
"delta_q": delta_q,
|
|
167
|
+
"num_samples": min_len,
|
|
168
|
+
},
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
elif method == "slope" and current_trace is not None:
|
|
172
|
+
# C = I / (dV/dt)
|
|
173
|
+
current = current_trace.data.astype(np.float64)
|
|
174
|
+
min_len = min(len(voltage), len(current))
|
|
175
|
+
voltage = voltage[:min_len]
|
|
176
|
+
current = current[:min_len]
|
|
177
|
+
|
|
178
|
+
# Calculate dV/dt
|
|
179
|
+
dv_dt = np.diff(voltage) / dt
|
|
180
|
+
|
|
181
|
+
# Find region where dV/dt is significant
|
|
182
|
+
significant_mask = np.abs(dv_dt) > np.max(np.abs(dv_dt)) * 0.1
|
|
183
|
+
if np.sum(significant_mask) < 5:
|
|
184
|
+
raise AnalysisError("Insufficient voltage slope for capacitance measurement")
|
|
185
|
+
|
|
186
|
+
# Use corresponding current values
|
|
187
|
+
current_for_slope = current[:-1][significant_mask]
|
|
188
|
+
dv_dt_significant = dv_dt[significant_mask]
|
|
189
|
+
|
|
190
|
+
# C = I / (dV/dt)
|
|
191
|
+
capacitance_values = current_for_slope / dv_dt_significant
|
|
192
|
+
capacitance = float(np.median(np.abs(capacitance_values)))
|
|
193
|
+
|
|
194
|
+
return CapacitanceMeasurement(
|
|
195
|
+
capacitance=capacitance,
|
|
196
|
+
method="slope",
|
|
197
|
+
confidence=0.85,
|
|
198
|
+
statistics={
|
|
199
|
+
"num_valid_points": int(np.sum(significant_mask)),
|
|
200
|
+
"capacitance_std": float(np.std(np.abs(capacitance_values))),
|
|
201
|
+
},
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
elif method == "frequency":
|
|
205
|
+
# Extract from RC time constant
|
|
206
|
+
if resistance is None:
|
|
207
|
+
raise AnalysisError("Resistance value required for frequency method")
|
|
208
|
+
|
|
209
|
+
# Find time constant from step response
|
|
210
|
+
tau = _extract_time_constant(voltage, sample_rate)
|
|
211
|
+
|
|
212
|
+
# C = tau / R
|
|
213
|
+
capacitance = tau / resistance
|
|
214
|
+
|
|
215
|
+
return CapacitanceMeasurement(
|
|
216
|
+
capacitance=float(capacitance),
|
|
217
|
+
method="time_constant",
|
|
218
|
+
confidence=0.8,
|
|
219
|
+
statistics={
|
|
220
|
+
"time_constant": tau,
|
|
221
|
+
"resistance": resistance,
|
|
222
|
+
},
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
else:
|
|
226
|
+
raise AnalysisError(f"Method '{method}' requires current_trace or resistance parameter")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def measure_inductance(
|
|
230
|
+
voltage_trace: WaveformTrace,
|
|
231
|
+
current_trace: WaveformTrace | None = None,
|
|
232
|
+
*,
|
|
233
|
+
method: Literal["flux", "slope", "frequency"] = "slope",
|
|
234
|
+
resistance: float | None = None,
|
|
235
|
+
) -> InductanceMeasurement:
|
|
236
|
+
"""Measure inductance from voltage/current waveforms.
|
|
237
|
+
|
|
238
|
+
Calculates inductance using the relationship V = L * dI/dt.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
voltage_trace: Voltage waveform across inductor.
|
|
242
|
+
current_trace: Current waveform through inductor (optional for
|
|
243
|
+
some methods).
|
|
244
|
+
method: Measurement method:
|
|
245
|
+
- "flux": L = integral(V*dt) / delta_I
|
|
246
|
+
- "slope": L = V / (dI/dt)
|
|
247
|
+
- "frequency": Extract from RL time constant
|
|
248
|
+
resistance: Known resistance for frequency method.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
InductanceMeasurement with inductance value.
|
|
252
|
+
|
|
253
|
+
Raises:
|
|
254
|
+
AnalysisError: If measurement conditions are not met.
|
|
255
|
+
InsufficientDataError: If insufficient samples are provided.
|
|
256
|
+
|
|
257
|
+
Example:
|
|
258
|
+
>>> L = measure_inductance(voltage, current)
|
|
259
|
+
>>> print(f"L = {L.inductance * 1e6:.1f} uH")
|
|
260
|
+
|
|
261
|
+
References:
|
|
262
|
+
COMP-003
|
|
263
|
+
"""
|
|
264
|
+
voltage = voltage_trace.data.astype(np.float64)
|
|
265
|
+
sample_rate = voltage_trace.metadata.sample_rate
|
|
266
|
+
dt = 1.0 / sample_rate
|
|
267
|
+
|
|
268
|
+
if len(voltage) < 10:
|
|
269
|
+
raise InsufficientDataError(
|
|
270
|
+
"Inductance measurement requires at least 10 samples",
|
|
271
|
+
required=10,
|
|
272
|
+
available=len(voltage),
|
|
273
|
+
analysis_type="inductance",
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
if method == "flux" and current_trace is not None:
|
|
277
|
+
# L = flux / I = integral(V*dt) / delta_I
|
|
278
|
+
current = current_trace.data.astype(np.float64)
|
|
279
|
+
min_len = min(len(voltage), len(current))
|
|
280
|
+
voltage = voltage[:min_len]
|
|
281
|
+
current = current[:min_len]
|
|
282
|
+
|
|
283
|
+
# Integrate voltage to get flux linkage
|
|
284
|
+
flux = np.cumsum(voltage) * dt
|
|
285
|
+
delta_i = np.max(current) - np.min(current)
|
|
286
|
+
|
|
287
|
+
if delta_i > 1e-10:
|
|
288
|
+
delta_flux = np.max(flux) - np.min(flux)
|
|
289
|
+
inductance = delta_flux / delta_i
|
|
290
|
+
else:
|
|
291
|
+
raise AnalysisError("Current change too small for inductance measurement")
|
|
292
|
+
|
|
293
|
+
# Estimate DCR from steady-state
|
|
294
|
+
dcr = _estimate_dcr(voltage, current)
|
|
295
|
+
|
|
296
|
+
return InductanceMeasurement(
|
|
297
|
+
inductance=float(abs(inductance)),
|
|
298
|
+
dcr=dcr,
|
|
299
|
+
method="flux_integration",
|
|
300
|
+
confidence=0.9,
|
|
301
|
+
statistics={
|
|
302
|
+
"delta_i": delta_i,
|
|
303
|
+
"delta_flux": delta_flux,
|
|
304
|
+
"num_samples": min_len,
|
|
305
|
+
},
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
elif method == "slope" and current_trace is not None:
|
|
309
|
+
# L = V / (dI/dt)
|
|
310
|
+
current = current_trace.data.astype(np.float64)
|
|
311
|
+
min_len = min(len(voltage), len(current))
|
|
312
|
+
voltage = voltage[:min_len]
|
|
313
|
+
current = current[:min_len]
|
|
314
|
+
|
|
315
|
+
# Calculate dI/dt
|
|
316
|
+
di_dt = np.diff(current) / dt
|
|
317
|
+
|
|
318
|
+
# Find region where dI/dt is significant
|
|
319
|
+
significant_mask = np.abs(di_dt) > np.max(np.abs(di_dt)) * 0.1
|
|
320
|
+
if np.sum(significant_mask) < 5:
|
|
321
|
+
raise AnalysisError("Insufficient current slope for inductance measurement")
|
|
322
|
+
|
|
323
|
+
# Use corresponding voltage values
|
|
324
|
+
voltage_for_slope = voltage[:-1][significant_mask]
|
|
325
|
+
di_dt_significant = di_dt[significant_mask]
|
|
326
|
+
|
|
327
|
+
# L = V / (dI/dt)
|
|
328
|
+
inductance_values = voltage_for_slope / di_dt_significant
|
|
329
|
+
inductance = float(np.median(np.abs(inductance_values)))
|
|
330
|
+
|
|
331
|
+
return InductanceMeasurement(
|
|
332
|
+
inductance=inductance,
|
|
333
|
+
method="slope",
|
|
334
|
+
confidence=0.85,
|
|
335
|
+
statistics={
|
|
336
|
+
"num_valid_points": int(np.sum(significant_mask)),
|
|
337
|
+
"inductance_std": float(np.std(np.abs(inductance_values))),
|
|
338
|
+
},
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
elif method == "frequency":
|
|
342
|
+
# Extract from RL time constant
|
|
343
|
+
if resistance is None:
|
|
344
|
+
raise AnalysisError("Resistance value required for frequency method")
|
|
345
|
+
|
|
346
|
+
# Find time constant from step response
|
|
347
|
+
tau = _extract_time_constant(voltage, sample_rate)
|
|
348
|
+
|
|
349
|
+
# L = tau * R
|
|
350
|
+
inductance = tau * resistance
|
|
351
|
+
|
|
352
|
+
return InductanceMeasurement(
|
|
353
|
+
inductance=float(inductance),
|
|
354
|
+
method="time_constant",
|
|
355
|
+
confidence=0.8,
|
|
356
|
+
statistics={
|
|
357
|
+
"time_constant": tau,
|
|
358
|
+
"resistance": resistance,
|
|
359
|
+
},
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
else:
|
|
363
|
+
raise AnalysisError(f"Method '{method}' requires current_trace or resistance parameter")
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def extract_parasitics(
|
|
367
|
+
voltage_trace: WaveformTrace,
|
|
368
|
+
current_trace: WaveformTrace,
|
|
369
|
+
*,
|
|
370
|
+
model: Literal["series_RLC", "parallel_RLC"] = "series_RLC",
|
|
371
|
+
frequency_range: tuple[float, float] | None = None,
|
|
372
|
+
) -> ParasiticExtraction:
|
|
373
|
+
"""Extract parasitic R, L, C parameters from impedance measurement.
|
|
374
|
+
|
|
375
|
+
Fits an equivalent circuit model to measured voltage/current data
|
|
376
|
+
to extract parasitic component values.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
voltage_trace: Voltage waveform.
|
|
380
|
+
current_trace: Current waveform.
|
|
381
|
+
model: Equivalent circuit model type.
|
|
382
|
+
frequency_range: Frequency range for analysis (Hz).
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
ParasiticExtraction with R, L, C values.
|
|
386
|
+
|
|
387
|
+
Raises:
|
|
388
|
+
AnalysisError: If measurement conditions are not met.
|
|
389
|
+
InsufficientDataError: If insufficient samples are provided.
|
|
390
|
+
|
|
391
|
+
Example:
|
|
392
|
+
>>> params = extract_parasitics(voltage, current)
|
|
393
|
+
>>> print(f"C = {params.capacitance*1e12:.1f}pF, L = {params.inductance*1e9:.1f}nH")
|
|
394
|
+
|
|
395
|
+
References:
|
|
396
|
+
COMP-004
|
|
397
|
+
"""
|
|
398
|
+
voltage = voltage_trace.data.astype(np.float64)
|
|
399
|
+
current = current_trace.data.astype(np.float64)
|
|
400
|
+
sample_rate = voltage_trace.metadata.sample_rate
|
|
401
|
+
|
|
402
|
+
min_len = min(len(voltage), len(current))
|
|
403
|
+
voltage = voltage[:min_len]
|
|
404
|
+
current = current[:min_len]
|
|
405
|
+
|
|
406
|
+
if min_len < 100:
|
|
407
|
+
raise InsufficientDataError(
|
|
408
|
+
"Parasitic extraction requires at least 100 samples",
|
|
409
|
+
required=100,
|
|
410
|
+
available=min_len,
|
|
411
|
+
analysis_type="parasitic_extraction",
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
# Compute impedance in frequency domain
|
|
415
|
+
from scipy.fft import fft, fftfreq
|
|
416
|
+
|
|
417
|
+
V_fft = fft(voltage)
|
|
418
|
+
I_fft = fft(current)
|
|
419
|
+
|
|
420
|
+
freqs = fftfreq(min_len, 1 / sample_rate)
|
|
421
|
+
|
|
422
|
+
# Use only positive frequencies
|
|
423
|
+
pos_mask = freqs > 0
|
|
424
|
+
freqs = freqs[pos_mask]
|
|
425
|
+
Z_fft = V_fft[pos_mask] / (I_fft[pos_mask] + 1e-20)
|
|
426
|
+
|
|
427
|
+
# Apply frequency range filter
|
|
428
|
+
if frequency_range is not None:
|
|
429
|
+
freq_mask = (freqs >= frequency_range[0]) & (freqs <= frequency_range[1])
|
|
430
|
+
freqs = freqs[freq_mask]
|
|
431
|
+
Z_fft = Z_fft[freq_mask]
|
|
432
|
+
|
|
433
|
+
if len(freqs) < 10:
|
|
434
|
+
raise AnalysisError("Insufficient frequency points for parasitic extraction")
|
|
435
|
+
|
|
436
|
+
# Fit RLC model to impedance data
|
|
437
|
+
if model == "series_RLC":
|
|
438
|
+
R, L, C = _fit_series_rlc(freqs, Z_fft)
|
|
439
|
+
else: # parallel_RLC
|
|
440
|
+
R, L, C = _fit_parallel_rlc(freqs, Z_fft)
|
|
441
|
+
|
|
442
|
+
# Calculate resonant frequency
|
|
443
|
+
f_res = 1 / (2 * np.pi * np.sqrt(L * C)) if L > 0 and C > 0 else None
|
|
444
|
+
|
|
445
|
+
# Calculate fit quality
|
|
446
|
+
Z_model = _calculate_rlc_impedance(freqs, R, L, C, model)
|
|
447
|
+
ss_res = np.sum(np.abs(Z_fft - Z_model) ** 2)
|
|
448
|
+
ss_tot = np.sum(np.abs(Z_fft - np.mean(Z_fft)) ** 2)
|
|
449
|
+
r_squared = 1 - ss_res / (ss_tot + 1e-20)
|
|
450
|
+
|
|
451
|
+
return ParasiticExtraction(
|
|
452
|
+
capacitance=float(C),
|
|
453
|
+
inductance=float(L),
|
|
454
|
+
resistance=float(R),
|
|
455
|
+
model_type=model,
|
|
456
|
+
resonant_freq=f_res,
|
|
457
|
+
fit_quality=float(max(0, r_squared)),
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _extract_time_constant(data: NDArray[np.float64], sample_rate: float) -> float:
|
|
462
|
+
"""Extract time constant from step response."""
|
|
463
|
+
# Normalize data
|
|
464
|
+
data_min = np.min(data)
|
|
465
|
+
data_max = np.max(data)
|
|
466
|
+
if data_max - data_min < 1e-10:
|
|
467
|
+
return 1 / sample_rate # Return minimum time constant
|
|
468
|
+
|
|
469
|
+
normalized = (data - data_min) / (data_max - data_min)
|
|
470
|
+
|
|
471
|
+
# Find 63.2% point (time constant)
|
|
472
|
+
target = 0.632
|
|
473
|
+
idx_raw = np.argmax(normalized >= target)
|
|
474
|
+
idx = int(idx_raw)
|
|
475
|
+
if idx == 0:
|
|
476
|
+
idx = int(len(data) // 2)
|
|
477
|
+
|
|
478
|
+
tau = float(idx) / sample_rate
|
|
479
|
+
min_tau = 1 / sample_rate
|
|
480
|
+
return float(tau if tau > min_tau else min_tau)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _estimate_esr(
|
|
484
|
+
voltage: NDArray[np.float64],
|
|
485
|
+
current: NDArray[np.float64],
|
|
486
|
+
sample_rate: float,
|
|
487
|
+
) -> float:
|
|
488
|
+
"""Estimate ESR from voltage/current phase relationship."""
|
|
489
|
+
# Use correlation to estimate resistive component
|
|
490
|
+
# ESR causes in-phase voltage drop
|
|
491
|
+
correlation = np.correlate(voltage - np.mean(voltage), current - np.mean(current))
|
|
492
|
+
if np.std(current) > 1e-10:
|
|
493
|
+
esr = np.max(correlation) / (len(current) * np.var(current))
|
|
494
|
+
return float(abs(esr))
|
|
495
|
+
return 0.0
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _estimate_dcr(voltage: NDArray[np.float64], current: NDArray[np.float64]) -> float:
|
|
499
|
+
"""Estimate DC resistance from steady-state V/I."""
|
|
500
|
+
# Use last 10% of data as steady-state
|
|
501
|
+
n = len(voltage)
|
|
502
|
+
start = int(0.9 * n)
|
|
503
|
+
v_ss = np.mean(voltage[start:])
|
|
504
|
+
i_ss = np.mean(current[start:])
|
|
505
|
+
if abs(i_ss) > 1e-10:
|
|
506
|
+
return float(v_ss / i_ss)
|
|
507
|
+
return 0.0
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _fit_series_rlc(
|
|
511
|
+
freqs: NDArray[np.float64], Z: NDArray[np.complex128]
|
|
512
|
+
) -> tuple[float, float, float]:
|
|
513
|
+
"""Fit series RLC model to impedance data."""
|
|
514
|
+
omega = 2 * np.pi * freqs
|
|
515
|
+
|
|
516
|
+
# Initial estimates
|
|
517
|
+
R_init = float(np.real(np.mean(Z)))
|
|
518
|
+
# From imaginary part at low/high frequency
|
|
519
|
+
L_init = float(np.abs(np.imag(Z[-1])) / omega[-1]) if len(omega) > 0 else 1e-9
|
|
520
|
+
C_init = 1e-12
|
|
521
|
+
|
|
522
|
+
def model(omega: NDArray[np.float64], R: float, L: float, C: float) -> NDArray[np.complex128]:
|
|
523
|
+
return R + 1j * (omega * L - 1 / (omega * C + 1e-20))
|
|
524
|
+
|
|
525
|
+
try:
|
|
526
|
+
# Fit real and imaginary parts separately
|
|
527
|
+
np.real(Z)
|
|
528
|
+
np.imag(Z)
|
|
529
|
+
|
|
530
|
+
# Simple optimization
|
|
531
|
+
from scipy.optimize import minimize
|
|
532
|
+
|
|
533
|
+
def objective(params: NDArray[np.float64]) -> np.floating[Any]: # type: ignore[name-defined]
|
|
534
|
+
R, L, C = params
|
|
535
|
+
Z_model = model(omega, R, L, C)
|
|
536
|
+
return float(np.sum(np.abs(Z - Z_model) ** 2)) # type: ignore[return-value]
|
|
537
|
+
|
|
538
|
+
result = minimize(
|
|
539
|
+
objective,
|
|
540
|
+
[R_init, L_init, C_init],
|
|
541
|
+
bounds=[(1e-6, 1e6), (1e-15, 1e-3), (1e-15, 1e-3)],
|
|
542
|
+
method="L-BFGS-B",
|
|
543
|
+
)
|
|
544
|
+
return tuple(result.x)
|
|
545
|
+
except Exception:
|
|
546
|
+
return (R_init, L_init, C_init)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _fit_parallel_rlc(
|
|
550
|
+
freqs: NDArray[np.float64], Z: NDArray[np.complex128]
|
|
551
|
+
) -> tuple[float, float, float]:
|
|
552
|
+
"""Fit parallel RLC model to impedance data."""
|
|
553
|
+
# Convert to admittance
|
|
554
|
+
Y = 1 / (Z + 1e-20)
|
|
555
|
+
omega = 2 * np.pi * freqs
|
|
556
|
+
|
|
557
|
+
# Initial estimates
|
|
558
|
+
G_init = float(np.real(np.mean(Y)))
|
|
559
|
+
R_init = 1 / G_init if G_init > 1e-10 else 1e3
|
|
560
|
+
C_init = float(np.abs(np.imag(Y[-1])) / omega[-1]) if len(omega) > 0 else 1e-12
|
|
561
|
+
L_init = 1e-9
|
|
562
|
+
|
|
563
|
+
try:
|
|
564
|
+
from scipy.optimize import minimize
|
|
565
|
+
|
|
566
|
+
def objective(params: NDArray[np.float64]) -> np.floating[Any]: # type: ignore[name-defined]
|
|
567
|
+
R, L, C = params
|
|
568
|
+
Y_model = 1 / R + 1j * omega * C + 1 / (1j * omega * L + 1e-20)
|
|
569
|
+
Z_model = 1 / (Y_model + 1e-20)
|
|
570
|
+
return float(np.sum(np.abs(Z - Z_model) ** 2)) # type: ignore[return-value]
|
|
571
|
+
|
|
572
|
+
result = minimize(
|
|
573
|
+
objective,
|
|
574
|
+
[R_init, L_init, C_init],
|
|
575
|
+
bounds=[(1e-6, 1e9), (1e-15, 1e-3), (1e-15, 1e-3)],
|
|
576
|
+
method="L-BFGS-B",
|
|
577
|
+
)
|
|
578
|
+
return tuple(result.x)
|
|
579
|
+
except Exception:
|
|
580
|
+
return (R_init, L_init, C_init)
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _calculate_rlc_impedance(
|
|
584
|
+
freqs: NDArray[np.float64],
|
|
585
|
+
R: float,
|
|
586
|
+
L: float,
|
|
587
|
+
C: float,
|
|
588
|
+
model: str,
|
|
589
|
+
) -> NDArray[np.complex128]:
|
|
590
|
+
"""Calculate impedance of RLC circuit."""
|
|
591
|
+
omega = 2 * np.pi * freqs
|
|
592
|
+
|
|
593
|
+
if model == "series_RLC":
|
|
594
|
+
Z: NDArray[np.complex128] = R + 1j * (omega * L - 1 / (omega * C + 1e-20))
|
|
595
|
+
return Z
|
|
596
|
+
else: # parallel_RLC
|
|
597
|
+
Y = 1 / R + 1j * omega * C + 1 / (1j * omega * L + 1e-20)
|
|
598
|
+
return cast("NDArray[np.complex128]", 1 / (Y + 1e-20))
|